diff --git a/docs/decisions.md b/docs/decisions.md index 304c3ba..387ecef 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -323,6 +323,66 @@ Format léger : une décision = un titre + contexte + choix + raison. même fichier SQLite (par défaut `/crcore.db`) ; le préfixe isole proprement. +## 2026-06-09 — Refonte permissions + modèle admin/chef/joueur + +- **Choix** : chaque sous-commande `/core team ` a sa propre permission + `crcore.team.`. Trois niveaux fonctionnels : + - **Admin** (permission seule, cible une team par argument) : `create`, + `delete`, `setleader`, `score`. + - **Chef** (permission + check chef dans `execute()`) : `add`, `remove`, + `transfer`, `visibility`, `setspawn`. + - **Joueur** (permission seule, défaut « tout le monde » côté LuckPerms si + voulu) : `join`, `leave`, `info`, `list`, `top`. +- **Aliases courts supprimés** : `c` (create), `i` (info), `t` (team), `j` + (join), `vis` (visibility), `disband`/`dissolve` (delete), `kick`/`expel` + (remove), etc. Plus que les noms longs. Raison : réduire la friction + d'apprentissage et la confusion (les game plugins ont leurs propres noms, + l'aliasing devient un bruit). +- **`delete` devient admin** : `/core team delete ` (au lieu de + l'ancien `/core team delete` qui ciblait l'équipe du chef). Cohérent avec + `create` qui est aussi admin. + +## 2026-06-09 — Team peut être leaderless + +- **Choix** : `Team.leaderId` devient nullable. `getLeaderId()` renvoie + `Optional`, `getLeader()` renvoie `Optional`. Nouveau + `hasLeader()` et `isLeader(UUID)` pour les checks. +- **Raison** : le modèle admin requiert qu'on puisse créer une équipe sans + chef et l'assigner ensuite via {@code setLeader}. Avant, créer une équipe + imposait de connaître l'UUID du chef. +- **Constructeurs ajoutés** : + - `new Team(id, name, tag, color)` — leaderless, PRIVATE + - `new Team(id, name, tag, color, visibility)` — leaderless avec visibilité + - Les constructeurs avec leaderId acceptent maintenant `null`. +- **`Team.setLeader(playerId)`** : assigne un chef à n'importe quel moment. + Si la team a déjà un chef, il est démis en MEMBER. Si le nouveau n'est + pas membre, il est auto-ajouté. +- **`Team.transferLeadership(playerId)`** : conserve sa sémantique stricte + (chef→chef, membre déjà existant). Lève `IllegalStateException` si la team + est leaderless. Utilisé par la commande `/core team transfer` (chef). +- **`TeamLeadershipTransferEvent.getOldLeaderId()`** renvoie maintenant + `Optional` (vide si la team était leaderless avant l'opération). +- **Schéma SQLite** : la colonne `crcore_teams.leader_id` n'a plus la + contrainte `NOT NULL`. Migration automatique sur nouvelle base — pour les + bases existantes, ALTER TABLE manuel ou suppression du fichier + (les bases d'event sont jetables). + +## 2026-06-09 — Nouvelle commande `/core team setleader` + +- **Choix** : ajout de `TeamSetLeaderSubCommand` (`/core team setleader + `). Permission `crcore.team.setleader`. Délègue à + `TeamService.setLeader(...)`. +- **Différence avec `/core team transfer`** : + - `transfer` : action **chef**, cible son équipe, le nouveau chef doit déjà + être membre. + - `setleader` : action **admin**, cible n'importe quelle équipe, le nouveau + chef peut être non-membre (auto-ajouté). +- **Use cases couverts** : + - Admin assigne un chef à une équipe leaderless fraîchement créée. + - Admin remplace le chef d'une équipe (le membre target est déjà dans + l'équipe ou pas — peu importe). + - Admin "promote member up to leader" (cas explicitement demandé). + ## 2026-06-09 — Bascule Java 16 → Java 11 (révision) - **Révision** de la décision "Java 16" du 2026-06-08. diff --git a/docs/diagrams/builtin-commands-diagram.puml b/docs/diagrams/builtin-commands-diagram.puml index cf5db85..249dacc 100644 --- a/docs/diagrams/builtin-commands-diagram.puml +++ b/docs/diagrams/builtin-commands-diagram.puml @@ -1,5 +1,5 @@ @startuml builtin-commands-diagram -title CR-Core — Default /core team commands +title CR-Core — Default /core team commands (admin / chef / joueur) skinparam classAttributeIconSize 0 hide empty members @@ -13,7 +13,6 @@ package "fr.luc.crcore.command.builtin" { class CoreCommand { + CoreCommand(teamSvc, playerSvc) - # registerDefaults(): void } CoreCommand --|> BaseCommand @@ -29,44 +28,93 @@ package "fr.luc.crcore.command.builtin" { + {static} teamByName(service): ArgumentType } - class TeamCreateSubCommand { - + execute(ctx): CommandResult + ' === ADMIN commands (permission seule) === + package "admin" <> { + class TeamCreateSubCommand { + perm: crcore.team.create + args: name, tag, color, [leader] + } + class TeamDeleteSubCommand { + perm: crcore.team.delete + args: + } + class TeamSetLeaderSubCommand { + perm: crcore.team.setleader + args: + } + class TeamScoreSubCommand { + perm: crcore.team.score + args: + } + } + + ' === CHEF commands (permission + check chef) === + package "chef" <> { + class TeamAddSubCommand { + perm: crcore.team.add + args: + } + class TeamRemoveSubCommand { + perm: crcore.team.remove + args: + } + class TeamTransferSubCommand { + perm: crcore.team.transfer + args: + } + class TeamVisibilitySubCommand { + perm: crcore.team.visibility + args: + } + class TeamSetSpawnSubCommand { + perm: crcore.team.setspawn + } + } + + ' === PLAYER commands === + package "player" <> { + class TeamJoinSubCommand { + perm: crcore.team.join + args: + } + class TeamLeaveSubCommand { + perm: crcore.team.leave + } + class TeamInfoSubCommand { + perm: crcore.team.info + args: [team] + } + class TeamListSubCommand { + perm: crcore.team.list + } + class TeamTopSubCommand { + perm: crcore.team.top + args: [score] + } } - class TeamDeleteSubCommand - class TeamAddSubCommand - class TeamRemoveSubCommand - class TeamJoinSubCommand - class TeamLeaveSubCommand - class TeamInfoSubCommand - class TeamListSubCommand - class TeamTransferSubCommand - class TeamVisibilitySubCommand - class TeamScoreSubCommand - class TeamTopSubCommand - class TeamSetSpawnSubCommand TeamCreateSubCommand --|> SubCommand TeamDeleteSubCommand --|> SubCommand + TeamSetLeaderSubCommand --|> SubCommand + TeamScoreSubCommand --|> SubCommand TeamAddSubCommand --|> SubCommand TeamRemoveSubCommand --|> SubCommand + TeamTransferSubCommand --|> SubCommand + TeamVisibilitySubCommand --|> SubCommand + TeamSetSpawnSubCommand --|> SubCommand TeamJoinSubCommand --|> SubCommand TeamLeaveSubCommand --|> SubCommand TeamInfoSubCommand --|> SubCommand TeamListSubCommand --|> SubCommand - TeamTransferSubCommand --|> SubCommand - TeamVisibilitySubCommand --|> SubCommand - TeamScoreSubCommand --|> SubCommand TeamTopSubCommand --|> SubCommand - TeamSetSpawnSubCommand --|> SubCommand CoreCommand "1" *-- "1" TeamGroupSubCommand : contains - TeamGroupSubCommand "1" *-- "13" SubCommand : contains + TeamGroupSubCommand "1" *-- "14" SubCommand : contains } } -note right of CoreCommand - Le plugin de jeu downstream - remplace une feuille avec : +note bottom of TeamGroupSubCommand + Override d'une feuille : core.getCoreCommand() .findSubCommand("team") .replaceSubCommand("create", diff --git a/docs/diagrams/team-class-diagram.puml b/docs/diagrams/team-class-diagram.puml index 5b20f02..bcbae73 100644 --- a/docs/diagrams/team-class-diagram.puml +++ b/docs/diagrams/team-class-diagram.puml @@ -101,17 +101,24 @@ package "fr.luc.crcore.team" { - name: String - tag: String - color: TeamColor - - leaderId: UUID + - leaderId: UUID *(nullable)* - visibility: TeamVisibility - members: Set - scores: Map - spawnPoint: Location -- + + Team(id, name, tag, color) ' leaderless, PRIVATE + + Team(id, name, tag, color, visibility) ' leaderless + + Team(id, name, tag, color, leaderId) ' with leader, PRIVATE + + Team(id, name, tag, color, leaderId, visibility) + -- + getName(): String + getTag(): String + getColor(): TeamColor - + getLeaderId(): UUID - + getLeader(): TeamMember + + getLeaderId(): Optional + + getLeader(): Optional + + hasLeader(): boolean + + isLeader(playerId): boolean + getVisibility(): TeamVisibility + setVisibility(v): void + isPublic(): boolean @@ -121,7 +128,8 @@ package "fr.luc.crcore.team" { + size(): int + addMember(playerId): TeamMember + removeMember(playerId): boolean - + transferLeadership(newLeaderId): void + + transferLeadership(newLeaderId): void ' strict: chef→chef + + setLeader(newLeaderId): void ' permissive: assign -- + getScore(name): int + hasScore(name): boolean @@ -157,12 +165,16 @@ package "fr.luc.crcore.team" { } interface TeamService { - + createTeam(name, tag, color, leaderId, [visibility]): Team + + createTeam(name, tag, color): Team ' leaderless, PRIVATE + + createTeam(name, tag, color, visibility): Team ' leaderless + + createTeam(name, tag, color, leaderId): Team + + createTeam(name, tag, color, leaderId, visibility): Team + dissolveTeam(teamId): boolean + addMember(teamId, playerId): boolean + removeMember(teamId, playerId): boolean + joinTeam(teamId, playerId): boolean - + transferLeadership(teamId, newLeaderId): boolean + + transferLeadership(teamId, newLeaderId): boolean ' strict: chef→chef + + setLeader(teamId, newLeaderId): boolean ' permissive (admin) + setVisibility(teamId, visibility): void -- + addScore(teamId, name, delta): int diff --git a/docs/features.md b/docs/features.md index d9878a5..8512cee 100644 --- a/docs/features.md +++ b/docs/features.md @@ -30,7 +30,7 @@ Architecture des domaines : | `name` | `String` | Nom lisible. Unique (case-insensitive). | | `tag` | `String` | Tag court (le « # »). Unique. Affiché entre `[# … ]`. | | `color` | `TeamColor` | Couleur associée (enum). | -| `leaderId` | `UUID` | Identifiant du joueur **chef d'équipe**. | +| `leaderId` | `UUID` *(nullable)* | Identifiant du joueur **chef d'équipe**. Peut être `null` (équipe leaderless — typique après création par admin). | | `visibility` | `TeamVisibility` | `PUBLIC` (les joueurs peuvent rejoindre) ou `PRIVATE` (seul le chef ajoute). Défaut : `PRIVATE`. | | `members` | `Set` | Ensemble des membres (inclut le chef). | @@ -64,13 +64,16 @@ Architecture des domaines : | Opération | Description | |---|---| -| `createTeam(name, tag, color, leaderId)` | Crée une équipe `PRIVATE` avec ce joueur comme chef. Échoue si nom/tag/joueur déjà pris. | -| `createTeam(name, tag, color, leaderId, visibility)` | Surcharge : permet de créer directement en `PUBLIC` ou `PRIVATE`. | +| `createTeam(name, tag, color)` | Crée une équipe **leaderless** en `PRIVATE`. | +| `createTeam(name, tag, color, visibility)` | Crée une équipe **leaderless** avec la visibilité spécifiée. | +| `createTeam(name, tag, color, leaderId)` | Crée une équipe `PRIVATE` avec ce joueur comme chef. | +| `createTeam(name, tag, color, leaderId, visibility)` | Surcharge complète. `leaderId` peut être `null` (équivalent leaderless). | | `dissolveTeam(teamId)` | Supprime l'équipe. | | `addMember(teamId, playerId)` | **Action du chef** : ajoute un joueur comme `MEMBER` (marche en PUBLIC comme en PRIVATE). | | `removeMember(teamId, playerId)` | Retire un joueur (interdit sur le chef). | | `joinTeam(teamId, playerId)` | **Action du joueur** : auto-rejoindre une équipe `PUBLIC`. Lève `TeamAccessException` si la team est `PRIVATE` ou si le joueur est déjà dans une équipe. | -| `transferLeadership(teamId, newLeaderId)` | Le nouveau chef doit déjà être membre. | +| `transferLeadership(teamId, newLeaderId)` | Transfert chef→chef strict : le nouveau chef doit être membre, et la team doit avoir un chef actuel. | +| `setLeader(teamId, newLeaderId)` | Plus permissif : fonctionne sur team leaderless **et** auto-ajoute le joueur s'il n'est pas membre. C'est l'opération admin. | | `setVisibility(teamId, visibility)` | Change la visibilité (typiquement appelée par le chef). | | `addScore(teamId, name, delta)` / `setScore` / `getScore` / `resetScore` / `resetAllScores` | Gestion des scores (voir section Scores). | | `getRankingByScore(name)` / `getGlobalRanking()` / `getTopRankingByScore(name, n)` / `getTopGlobalRanking(n)` | Classements (voir section Classements). | @@ -326,54 +329,65 @@ Voir [setup.md](setup.md#utilisation-depuis-un-plugin-de-jeu). ## 4. Commandes built-in `/core team ...` -**Statut** : 13 sous-commandes prêtes à l'emploi, branchées par +**Statut** : 14 sous-commandes prêtes à l'emploi, branchées par `CRCore.enable()`. Chaque sous-commande vit dans `fr.luc.crcore.command.builtin.team` et est substituable individuellement par sous-classe ou via `replaceSubCommand`. +**Pas d'aliases courts** : les commandes ont leur nom long uniquement +(`/core team create` et pas `/core team c`). Les aliases ont été retirés +pour réduire la friction de découverte et la confusion. + ### Arborescence ``` -/core (CoreCommand, BaseCommand racine, aliases: cr, crcore) -└── team (TeamGroupSubCommand, alias: t) - ├── create [visibility] — créer une équipe - ├── delete — dissoudre son équipe (chef) - ├── add — chef ajoute un membre - ├── remove — chef retire un membre - ├── join — auto-join sur team PUBLIC - ├── leave — quitter son équipe - ├── info [name] — infos d'une équipe - ├── list — liste toutes les équipes - ├── transfer — transfert de leadership - ├── visibility — changer la visibilité - ├── score — [admin] modifier un score - ├── top [score] — classement (par score ou global) - └── setspawn — chef définit le spawn +/core (CoreCommand) +└── team (TeamGroupSubCommand) + ├── create [leader] [admin] créer (chef optionnel) + ├── delete [admin] dissoudre une équipe + ├── setleader [admin] (re)assigner le chef + ├── score [admin] modifier un score + ├── add [chef] ajouter à son équipe + ├── remove [chef] retirer de son équipe + ├── transfer [chef] transférer leadership + ├── visibility [chef] changer visibilité + ├── setspawn [chef] définir le spawn + ├── join [joueur] rejoindre PUBLIC + ├── leave [joueur] quitter son équipe + ├── info [team] [joueur] infos + ├── list [joueur] toutes les équipes + └── top [score] [joueur] classement ``` -### Aliases supplémentaires (équivalents Bukkit) +### Permissions -| Sous-commande | Aliases | -|---|---| -| `create` | `c`, `new` | -| `delete` | `disband`, `dissolve` | -| `add` | `invite` | -| `remove` | `kick`, `expel` | -| `join` | `j` | -| `leave` | `quit` | -| `info` | `i` | -| `list` | `ls` | -| `visibility` | `vis` | -| `top` | `ranking`, `leaderboard` | -| `setspawn` | `spawn` | +Chaque sous-commande a une permission `crcore.team.`. Modèle à 3 niveaux : -### Permissions par défaut +| Niveau | Commandes | Comportement | +|---|---|---| +| **Admin** | `create`, `delete`, `setleader`, `score` | Permission seule (pas de check chef). Cible une team via argument. | +| **Chef** | `add`, `remove`, `transfer`, `visibility`, `setspawn` | Permission **ET** check chef de sa propre équipe en plus. Cible la team de l'exécutant. | +| **Joueur** | `join`, `leave`, `info`, `list`, `top` | Permission seule (à granter par défaut côté LuckPerms si on veut que tout le monde y ait accès). | | Sous-commande | Permission | |---|---| -| `create` | `crcore.team.create` | -| `score` | `crcore.team.score.modify` (admin) | -| autres | aucune (gated par appartenance / rôle de chef) | +| `create` | `crcore.team.create` | +| `delete` | `crcore.team.delete` | +| `setleader` | `crcore.team.setleader` | +| `score` | `crcore.team.score` | +| `add` | `crcore.team.add` | +| `remove` | `crcore.team.remove` | +| `transfer` | `crcore.team.transfer` | +| `visibility` | `crcore.team.visibility` | +| `setspawn` | `crcore.team.setspawn` | +| `join` | `crcore.team.join` | +| `leave` | `crcore.team.leave` | +| `info` | `crcore.team.info` | +| `list` | `crcore.team.list` | +| `top` | `crcore.team.top` | + +Le plugin de jeu ou le serveur configure les défauts via LuckPerms / Bukkit +permissions plugin. ### Override d'une sous-commande par défaut diff --git a/src/main/java/fr/luc/crcore/command/builtin/CoreCommand.java b/src/main/java/fr/luc/crcore/command/builtin/CoreCommand.java index a4d5466..5492913 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/CoreCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/CoreCommand.java @@ -33,7 +33,7 @@ public class CoreCommand extends BaseCommand { protected final PlayerProfileService playerProfileService; public CoreCommand(TeamService teamService, PlayerProfileService playerProfileService) { - super("core", "cr", "crcore"); + super("core"); this.teamService = Objects.requireNonNull(teamService, "teamService"); this.playerProfileService = Objects.requireNonNull(playerProfileService, "playerProfileService"); description("Commandes du noyau CR-Core"); 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 index 85945bd..c5218fc 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamAddSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamAddSubCommand.java @@ -21,9 +21,10 @@ public class TeamAddSubCommand extends SubCommand { protected final TeamService service; public TeamAddSubCommand(TeamService service) { - super("add", "invite"); + super("add"); this.service = Objects.requireNonNull(service, "service"); description("Ajouter un joueur à son équipe (chef uniquement)"); + permission("crcore.team.add"); playerOnly(); argument("player", ArgumentTypes.ONLINE_PLAYER); } @@ -37,7 +38,7 @@ public class TeamAddSubCommand extends SubCommand { if (team == null) { return CommandResult.failure("Vous n'appartenez à aucune équipe."); } - if (!team.getLeaderId().equals(executor.getUniqueId())) { + if (!team.isLeader(executor.getUniqueId())) { return CommandResult.failure("Seul le chef peut ajouter des membres."); } if (service.getTeamOfPlayer(target.getUniqueId()).isPresent()) { diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamCreateSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamCreateSubCommand.java index 78a8a9a..14d20b0 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamCreateSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamCreateSubCommand.java @@ -8,45 +8,58 @@ import fr.luc.crcore.team.Team; import fr.luc.crcore.team.TeamColor; import fr.luc.crcore.team.TeamException; import fr.luc.crcore.team.TeamService; -import fr.luc.crcore.team.TeamVisibility; import org.bukkit.entity.Player; import java.util.Objects; +import java.util.Optional; +import java.util.UUID; /** - * {@code /core team create [visibility]} + * {@code /core team create [leader]} * - *

Crée une équipe dont l'exécutant devient le chef. Visibilité par défaut : - * {@link TeamVisibility#PRIVATE}. + *

Admin uniquement. Crée une équipe en {@link + * fr.luc.crcore.team.TeamVisibility#PRIVATE} par défaut. + * + *

Le chef est optionnel : + *

    + *
  • Sans argument {@code leader} → équipe leaderless. L'admin assignera + * plus tard via {@code /core team setleader}.
  • + *
  • Avec argument {@code leader} (nom d'un joueur connecté) → ce joueur + * devient chef et membre de l'équipe.
  • + *
+ * + *

La visibilité (PUBLIC/PRIVATE) se change ensuite via {@code /core team + * visibility} (action du chef). */ public class TeamCreateSubCommand extends SubCommand { protected final TeamService service; public TeamCreateSubCommand(TeamService service) { - super("create", "c", "new"); + super("create"); this.service = Objects.requireNonNull(service, "service"); - description("Créer une équipe"); + description("Créer une équipe (admin)"); 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)); + optionalArgument("leader", ArgumentTypes.ONLINE_PLAYER); } @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.getOptional("visibility") - .orElse(TeamVisibility.PRIVATE); + Optional leaderOpt = ctx.getOptional("leader"); + UUID leaderId = leaderOpt.map(Player::getUniqueId).orElse(null); try { - Team team = service.createTeam(name, tag, color, player.getUniqueId(), visibility); - return CommandResult.success("Équipe " + team.getName() + " [#" + team.getTag() + "] créée."); + Team team = service.createTeam(name, tag, color, leaderId); + String suffix = leaderOpt.isPresent() + ? " (chef : " + leaderOpt.get().getName() + ")" + : " (sans chef)"; + return CommandResult.success("Équipe " + team.getName() + " [#" + team.getTag() + "] créée" + suffix + "."); } catch (TeamException ex) { return CommandResult.failure(ex.getMessage()); } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamDeleteSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamDeleteSubCommand.java index f98eac9..249ef32 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamDeleteSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamDeleteSubCommand.java @@ -5,40 +5,30 @@ 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 delete} + * {@code /core team delete } * - *

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}. + *

Admin uniquement. Dissout l'équipe spécifiée. Aucun check de chef + * — l'action est gated par la permission {@code crcore.team.delete}. */ public class TeamDeleteSubCommand extends SubCommand { protected final TeamService service; public TeamDeleteSubCommand(TeamService service) { - super("delete", "disband", "dissolve"); + super("delete"); this.service = Objects.requireNonNull(service, "service"); - description("Dissoudre son équipe (chef uniquement)"); - playerOnly(); + description("Dissoudre une équipe (admin)"); + permission("crcore.team.delete"); + argument("team", TeamArgumentTypes.teamByName(service)); } @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."); - } + Team team = ctx.get("team"); 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 index dfb3d22..2fcf268 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamGroupSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamGroupSubCommand.java @@ -23,7 +23,7 @@ public class TeamGroupSubCommand extends SubCommand { protected final TeamService service; public TeamGroupSubCommand(TeamService service) { - super("team", "t"); + super("team"); this.service = Objects.requireNonNull(service, "service"); description("Gestion des équipes"); registerDefaults(); @@ -43,6 +43,7 @@ public class TeamGroupSubCommand extends SubCommand { addSubCommand(new TeamInfoSubCommand(service)); addSubCommand(new TeamListSubCommand(service)); addSubCommand(new TeamTransferSubCommand(service)); + addSubCommand(new TeamSetLeaderSubCommand(service)); addSubCommand(new TeamVisibilitySubCommand(service)); addSubCommand(new TeamScoreSubCommand(service)); addSubCommand(new TeamTopSubCommand(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 index 019186f..773a482 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamInfoSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamInfoSubCommand.java @@ -24,9 +24,10 @@ public class TeamInfoSubCommand extends SubCommand { protected final TeamService service; public TeamInfoSubCommand(TeamService service) { - super("info", "i"); + super("info"); this.service = Objects.requireNonNull(service, "service"); description("Afficher les infos d'une équipe"); + permission("crcore.team.info"); optionalArgument("name", TeamArgumentTypes.teamByName(service)); } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamJoinSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamJoinSubCommand.java index a4d572b..f0a0493 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamJoinSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamJoinSubCommand.java @@ -22,9 +22,10 @@ public class TeamJoinSubCommand extends SubCommand { protected final TeamService service; public TeamJoinSubCommand(TeamService service) { - super("join", "j"); + super("join"); this.service = Objects.requireNonNull(service, "service"); description("Rejoindre une équipe publique"); + permission("crcore.team.join"); playerOnly(); argument("name", TeamArgumentTypes.teamByName(service)); } 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 index f6cb6d9..2a469e8 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamLeaveSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamLeaveSubCommand.java @@ -20,9 +20,10 @@ public class TeamLeaveSubCommand extends SubCommand { protected final TeamService service; public TeamLeaveSubCommand(TeamService service) { - super("leave", "quit"); + super("leave"); this.service = Objects.requireNonNull(service, "service"); description("Quitter son équipe"); + permission("crcore.team.leave"); playerOnly(); } @@ -33,7 +34,7 @@ public class TeamLeaveSubCommand extends SubCommand { if (team == null) { return CommandResult.failure("Vous n'appartenez à aucune équipe."); } - if (team.getLeaderId().equals(player.getUniqueId())) { + if (team.isLeader(player.getUniqueId())) { return CommandResult.failure( "Vous êtes le chef. Transférez le leadership avec /core team transfer , ou dissolvez avec /core team delete."); } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamListSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamListSubCommand.java index f318dd9..611a708 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamListSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamListSubCommand.java @@ -21,9 +21,10 @@ public class TeamListSubCommand extends SubCommand { protected final TeamService service; public TeamListSubCommand(TeamService service) { - super("list", "ls"); + super("list"); this.service = Objects.requireNonNull(service, "service"); description("Lister toutes les équipes"); + permission("crcore.team.list"); } @Override diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamRemoveSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamRemoveSubCommand.java index 858b2cf..ca03038 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamRemoveSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamRemoveSubCommand.java @@ -23,9 +23,10 @@ public class TeamRemoveSubCommand extends SubCommand { protected final TeamService service; public TeamRemoveSubCommand(TeamService service) { - super("remove", "kick", "expel"); + super("remove"); this.service = Objects.requireNonNull(service, "service"); description("Retirer un joueur de son équipe (chef uniquement)"); + permission("crcore.team.remove"); playerOnly(); argument("player", ArgumentTypes.STRING); } @@ -39,7 +40,7 @@ public class TeamRemoveSubCommand extends SubCommand { if (team == null) { return CommandResult.failure("Vous n'appartenez à aucune équipe."); } - if (!team.getLeaderId().equals(executor.getUniqueId())) { + if (!team.isLeader(executor.getUniqueId())) { return CommandResult.failure("Seul le chef peut retirer des membres."); } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamSetLeaderSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamSetLeaderSubCommand.java new file mode 100644 index 0000000..c6d53ba --- /dev/null +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamSetLeaderSubCommand.java @@ -0,0 +1,53 @@ +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 setleader } + * + *

Admin uniquement. Assigne un joueur comme chef d'une équipe : + *

    + *
  • Si l'équipe est leaderless → le joueur devient chef (auto-ajouté + * comme membre s'il ne l'est pas).
  • + *
  • Si l'équipe a déjà un chef → l'ancien chef est démis en simple + * membre, le nouveau prend le rôle.
  • + *
  • Si {@code } n'est pas encore membre → il est auto-ajouté + * à l'équipe en tant que chef.
  • + *
+ * + *

L'évènement {@code TeamLeadershipTransferEvent} est tiré dans tous les + * cas (avec {@code oldLeaderId} vide si la team était leaderless). + */ +public class TeamSetLeaderSubCommand extends SubCommand { + + protected final TeamService service; + + public TeamSetLeaderSubCommand(TeamService service) { + super("setleader"); + this.service = Objects.requireNonNull(service, "service"); + description("Assigner / changer le chef d'une équipe (admin)"); + permission("crcore.team.setleader"); + argument("team", TeamArgumentTypes.teamByName(service)); + argument("player", ArgumentTypes.ONLINE_PLAYER); + } + + @Override + public CommandResult execute(CommandContext ctx) { + Team team = ctx.get("team"); + Player target = ctx.get("player"); + + boolean changed = service.setLeader(team.getId(), target.getUniqueId()); + if (!changed) { + return CommandResult.success(target.getName() + " est déjà chef de " + team.getName() + "."); + } + return CommandResult.success(target.getName() + " est désormais chef de " + team.getName() + "."); + } +} 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 index 0f122a3..7eabae4 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamSetSpawnSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamSetSpawnSubCommand.java @@ -19,9 +19,10 @@ public class TeamSetSpawnSubCommand extends SubCommand { protected final TeamService service; public TeamSetSpawnSubCommand(TeamService service) { - super("setspawn", "spawn"); + super("setspawn"); this.service = Objects.requireNonNull(service, "service"); description("Définir le point de spawn de l'équipe (chef uniquement)"); + permission("crcore.team.setspawn"); playerOnly(); } @@ -32,7 +33,7 @@ public class TeamSetSpawnSubCommand extends SubCommand { if (team == null) { return CommandResult.failure("Vous n'appartenez à aucune équipe."); } - if (!team.getLeaderId().equals(player.getUniqueId())) { + if (!team.isLeader(player.getUniqueId())) { return CommandResult.failure("Seul le chef peut définir le spawn."); } service.setSpawnPoint(team.getId(), player.getLocation()); 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 index 5853030..67b2808 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamTopSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamTopSubCommand.java @@ -28,10 +28,11 @@ public class TeamTopSubCommand extends SubCommand { } public TeamTopSubCommand(TeamService service, int limit) { - super("top", "ranking", "leaderboard"); + super("top"); this.service = Objects.requireNonNull(service, "service"); this.limit = limit; description("Classement des équipes"); + permission("crcore.team.top"); optionalArgument("score", ArgumentTypes.STRING); } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamTransferSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamTransferSubCommand.java index 7b30357..e807bf8 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamTransferSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamTransferSubCommand.java @@ -24,7 +24,8 @@ public class TeamTransferSubCommand extends SubCommand { public TeamTransferSubCommand(TeamService service) { super("transfer"); this.service = Objects.requireNonNull(service, "service"); - description("Transférer le rôle de chef à un autre membre"); + description("Transférer le rôle de chef à un autre membre (chef uniquement)"); + permission("crcore.team.transfer"); playerOnly(); argument("player", ArgumentTypes.STRING); } @@ -37,7 +38,7 @@ public class TeamTransferSubCommand extends SubCommand { if (team == null) { return CommandResult.failure("Vous n'appartenez à aucune équipe."); } - if (!team.getLeaderId().equals(executor.getUniqueId())) { + if (!team.isLeader(executor.getUniqueId())) { return CommandResult.failure("Seul le chef peut transférer le leadership."); } @SuppressWarnings("deprecation") 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 index 697e10e..6fbbf34 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamVisibilitySubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamVisibilitySubCommand.java @@ -22,9 +22,10 @@ public class TeamVisibilitySubCommand extends SubCommand { protected final TeamService service; public TeamVisibilitySubCommand(TeamService service) { - super("visibility", "vis"); + super("visibility"); this.service = Objects.requireNonNull(service, "service"); - description("Changer la visibilité de son équipe"); + description("Changer la visibilité de son équipe (chef uniquement)"); + permission("crcore.team.visibility"); playerOnly(); argument("visibility", ArgumentTypes.enumOf(TeamVisibility.class)); } @@ -37,7 +38,7 @@ public class TeamVisibilitySubCommand extends SubCommand { if (team == null) { return CommandResult.failure("Vous n'appartenez à aucune équipe."); } - if (!team.getLeaderId().equals(player.getUniqueId())) { + if (!team.isLeader(player.getUniqueId())) { return CommandResult.failure("Seul le chef peut changer la visibilité."); } service.setVisibility(team.getId(), visibility); diff --git a/src/main/java/fr/luc/crcore/team/SqliteTeamRepository.java b/src/main/java/fr/luc/crcore/team/SqliteTeamRepository.java index 0359003..c4b280b 100644 --- a/src/main/java/fr/luc/crcore/team/SqliteTeamRepository.java +++ b/src/main/java/fr/luc/crcore/team/SqliteTeamRepository.java @@ -46,7 +46,7 @@ public class SqliteTeamRepository extends InMemoryTeamRepository { .column("name", ColumnType.TEXT).notNull().unique() .column("tag", ColumnType.TEXT).notNull().unique() .column("color", ColumnType.TEXT).notNull() - .column("leader_id", ColumnType.UUID).notNull() + .column("leader_id", ColumnType.UUID) // nullable — équipe leaderless autorisée .column("visibility", ColumnType.TEXT).notNull() .column("spawn_world", ColumnType.TEXT) .column("spawn_x", ColumnType.REAL) @@ -83,7 +83,7 @@ public class SqliteTeamRepository extends InMemoryTeamRepository { rs.getString("name"), rs.getString("tag"), TeamColor.valueOf(rs.getString("color")), - UUID.fromString(rs.getString("leader_id")), + rs.getString("leader_id") == null ? null : UUID.fromString(rs.getString("leader_id")), TeamVisibility.valueOf(rs.getString("visibility")), rs.getString("spawn_world"), (Double) rs.getObject("spawn_x"), @@ -109,9 +109,9 @@ public class SqliteTeamRepository extends InMemoryTeamRepository { // 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); - } + // Skip le leader — il est déjà ajouté par le constructeur de Team. + if (row.leaderId != null && m.playerId.equals(row.leaderId)) continue; + team.addMember(m.playerId); } // Scores db.query( @@ -174,7 +174,7 @@ public class SqliteTeamRepository extends InMemoryTeamRepository { " 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(), + team.getLeaderId().orElse(null), team.getVisibility(), spawnWorld, spawnX, spawnY, spawnZ, spawnYaw, spawnPitch ); diff --git a/src/main/java/fr/luc/crcore/team/Team.java b/src/main/java/fr/luc/crcore/team/Team.java index 43f596c..a962fff 100644 --- a/src/main/java/fr/luc/crcore/team/Team.java +++ b/src/main/java/fr/luc/crcore/team/Team.java @@ -40,21 +40,39 @@ public class Team extends AbstractEntity implements Named, ScoreHolder { private TeamVisibility visibility; private Location spawnPoint; + /** Crée une équipe sans chef, visibilité {@link TeamVisibility#PRIVATE}. */ + public Team(UUID id, String name, String tag, TeamColor color) { + this(id, name, tag, color, null, TeamVisibility.PRIVATE); + } + + /** Crée une équipe sans chef avec la visibilité spécifiée. */ + public Team(UUID id, String name, String tag, TeamColor color, TeamVisibility visibility) { + this(id, name, tag, color, null, visibility); + } + + /** Crée une équipe avec chef, visibilité {@link TeamVisibility#PRIVATE}. */ public Team(UUID id, String name, String tag, TeamColor color, UUID leaderId) { this(id, name, tag, color, leaderId, TeamVisibility.PRIVATE); } + /** + * Crée une équipe. {@code leaderId} peut être {@code null} pour créer une + * équipe leaderless (cas typique : l'admin crée une équipe et assignera le + * chef plus tard via {@code setLeader}). + */ public Team(UUID id, String name, String tag, TeamColor color, UUID leaderId, TeamVisibility visibility) { super(id); this.name = Objects.requireNonNull(name, "name"); this.tag = Objects.requireNonNull(tag, "tag"); this.color = Objects.requireNonNull(color, "color"); - this.leaderId = Objects.requireNonNull(leaderId, "leaderId"); this.visibility = Objects.requireNonNull(visibility, "visibility"); + this.leaderId = leaderId; // nullable this.members = new HashSet<>(); this.scores = new HashMap<>(); - this.members.add(newMember(leaderId, TeamRole.LEADER)); + if (leaderId != null) { + this.members.add(newMember(leaderId, TeamRole.LEADER)); + } } /** Override to instantiate a custom TeamMember subclass. */ @@ -75,8 +93,19 @@ public class Team extends AbstractEntity implements Named, ScoreHolder { return color; } - public UUID getLeaderId() { - return leaderId; + /** L'UUID du chef si la team en a un, sinon {@link Optional#empty()}. */ + public Optional getLeaderId() { + return Optional.ofNullable(leaderId); + } + + /** {@code true} si la team a un chef défini. */ + public boolean hasLeader() { + return leaderId != null; + } + + /** {@code true} si {@code playerId} est l'UUID du chef actuel. */ + public boolean isLeader(UUID playerId) { + return leaderId != null && leaderId.equals(playerId); } public TeamVisibility getVisibility() { @@ -91,9 +120,10 @@ public class Team extends AbstractEntity implements Named, ScoreHolder { return visibility.isPublic(); } - public TeamMember getLeader() { - return getMember(leaderId).orElseThrow( - () -> new IllegalStateException("Team has no leader: " + getId())); + /** Le {@link TeamMember} chef si la team en a un, sinon {@link Optional#empty()}. */ + public Optional getLeader() { + if (leaderId == null) return Optional.empty(); + return getMember(leaderId); } public Set getMembers() { @@ -135,15 +165,26 @@ public class Team extends AbstractEntity implements Named, ScoreHolder { return members.removeIf(member -> member.getPlayerId().equals(playerId)); } + /** + * Transfert classique du leadership : le nouveau chef doit déjà être + * membre de l'équipe, et l'équipe doit avoir un chef actuel. + * + *

Pour un cas plus général (équipe leaderless, ou nouveau chef non + * encore membre), utiliser {@link #setLeader(UUID)}. + */ public void transferLeadership(UUID newLeaderId) { Objects.requireNonNull(newLeaderId, "newLeaderId"); + if (leaderId == null) { + throw new IllegalStateException( + "Cannot transfer leadership on a leaderless team — use setLeader instead."); + } if (newLeaderId.equals(leaderId)) { return; } TeamMember newLeader = getMember(newLeaderId).orElseThrow( () -> new IllegalArgumentException( "New leader must already be a member of the team.")); - TeamMember oldLeader = getLeader(); + TeamMember oldLeader = getLeader().orElseThrow(); members.remove(oldLeader); members.remove(newLeader); members.add(oldLeader.withRole(TeamRole.MEMBER)); @@ -151,6 +192,39 @@ public class Team extends AbstractEntity implements Named, ScoreHolder { this.leaderId = newLeaderId; } + /** + * Assigne un chef à l'équipe, plus flexible que {@link #transferLeadership} : + *

    + *
  • Si la team est leaderless → ajoute {@code playerId} comme chef + * (en tant que membre s'il ne l'est pas déjà).
  • + *
  • Si la team a déjà un chef → démet l'ancien en {@link TeamRole#MEMBER}, + * promeut {@code playerId} en {@link TeamRole#LEADER} (auto-ajout + * si pas membre).
  • + *
+ */ + public void setLeader(UUID newLeaderId) { + Objects.requireNonNull(newLeaderId, "newLeaderId"); + if (newLeaderId.equals(leaderId)) { + return; + } + // Démet l'ancien chef si présent. + if (leaderId != null) { + TeamMember oldLeader = getLeader().orElseThrow(); + members.remove(oldLeader); + members.add(oldLeader.withRole(TeamRole.MEMBER)); + } + // Ajoute ou promeut le nouveau chef. + Optional existing = getMember(newLeaderId); + if (existing.isPresent()) { + TeamMember newLeader = existing.get(); + members.remove(newLeader); + members.add(newLeader.withRole(TeamRole.LEADER)); + } else { + members.add(newMember(newLeaderId, TeamRole.LEADER)); + } + this.leaderId = newLeaderId; + } + // ---- Scores ---- public int getScore(String scoreName) { diff --git a/src/main/java/fr/luc/crcore/team/TeamService.java b/src/main/java/fr/luc/crcore/team/TeamService.java index 1d7fc44..fde3591 100644 --- a/src/main/java/fr/luc/crcore/team/TeamService.java +++ b/src/main/java/fr/luc/crcore/team/TeamService.java @@ -25,8 +25,19 @@ public interface TeamService { // ---- Lifecycle ---- + /** Crée une équipe sans chef, visibilité PRIVATE. */ + Team createTeam(String name, String tag, TeamColor color); + + /** Crée une équipe sans chef avec la visibilité spécifiée. */ + Team createTeam(String name, String tag, TeamColor color, TeamVisibility visibility); + + /** Crée une équipe avec chef, visibilité PRIVATE. */ Team createTeam(String name, String tag, TeamColor color, UUID leaderId); + /** + * Crée une équipe. {@code leaderId} peut être {@code null} (équipe + * leaderless — l'admin assignera plus tard via {@link #setLeader}). + */ Team createTeam(String name, String tag, TeamColor color, UUID leaderId, TeamVisibility visibility); @@ -42,6 +53,17 @@ public interface TeamService { boolean transferLeadership(UUID teamId, UUID newLeaderId); + /** + * Assigne un chef à l'équipe (admin). Plus permissif que + * {@link #transferLeadership} : accepte un nouveau chef qui n'est pas + * encore membre (il est auto-ajouté), et fonctionne aussi sur une équipe + * leaderless. + * + * @return {@code true} si le chef a changé, {@code false} si + * {@code newLeaderId} était déjà le chef actuel. + */ + boolean setLeader(UUID teamId, UUID newLeaderId); + void setVisibility(UUID teamId, TeamVisibility visibility); // ---- Scores ---- diff --git a/src/main/java/fr/luc/crcore/team/TeamServiceImpl.java b/src/main/java/fr/luc/crcore/team/TeamServiceImpl.java index e1032e3..a9126dd 100644 --- a/src/main/java/fr/luc/crcore/team/TeamServiceImpl.java +++ b/src/main/java/fr/luc/crcore/team/TeamServiceImpl.java @@ -27,6 +27,16 @@ public class TeamServiceImpl implements TeamService { // ---- Lifecycle ---- + @Override + public Team createTeam(String name, String tag, TeamColor color) { + return createTeam(name, tag, color, null, TeamVisibility.PRIVATE); + } + + @Override + public Team createTeam(String name, String tag, TeamColor color, TeamVisibility visibility) { + return createTeam(name, tag, color, null, visibility); + } + @Override public Team createTeam(String name, String tag, TeamColor color, UUID leaderId) { return createTeam(name, tag, color, leaderId, TeamVisibility.PRIVATE); @@ -37,7 +47,9 @@ public class TeamServiceImpl implements TeamService { TeamVisibility visibility) { validateName(name); validateTag(tag); - validateLeader(leaderId); + if (leaderId != null) { + validateLeader(leaderId); + } Objects.requireNonNull(visibility, "visibility"); Team team = newTeam(UUID.randomUUID(), name, tag, color, leaderId, visibility); @@ -102,13 +114,25 @@ public class TeamServiceImpl implements TeamService { public boolean transferLeadership(UUID teamId, UUID newLeaderId) { Objects.requireNonNull(newLeaderId, "newLeaderId"); Team team = requireTeam(teamId); - UUID oldLeaderId = team.getLeaderId(); + UUID oldLeaderId = team.getLeaderId().orElse(null); team.transferLeadership(newLeaderId); repository.save(team); onLeadershipTransferred(team, oldLeaderId, newLeaderId); return true; } + @Override + public boolean setLeader(UUID teamId, UUID newLeaderId) { + Objects.requireNonNull(newLeaderId, "newLeaderId"); + Team team = requireTeam(teamId); + UUID oldLeaderId = team.getLeaderId().orElse(null); + if (newLeaderId.equals(oldLeaderId)) return false; + team.setLeader(newLeaderId); + repository.save(team); + onLeadershipTransferred(team, oldLeaderId, newLeaderId); + return true; + } + @Override public void setVisibility(UUID teamId, TeamVisibility visibility) { Objects.requireNonNull(visibility, "visibility"); diff --git a/src/main/java/fr/luc/crcore/team/event/TeamLeadershipTransferEvent.java b/src/main/java/fr/luc/crcore/team/event/TeamLeadershipTransferEvent.java index 0714f60..16b10f0 100644 --- a/src/main/java/fr/luc/crcore/team/event/TeamLeadershipTransferEvent.java +++ b/src/main/java/fr/luc/crcore/team/event/TeamLeadershipTransferEvent.java @@ -4,9 +4,16 @@ import fr.luc.crcore.team.Team; import org.bukkit.event.HandlerList; import java.util.Objects; +import java.util.Optional; import java.util.UUID; -/** Déclenché après un transfert de leadership. {@link #getOldLeaderId()} et {@link #getNewLeaderId()} renvoient les UUID des deux joueurs. */ +/** + * Déclenché après changement du chef d'une équipe. + * + *

{@code oldLeaderId} peut être {@code null} si l'équipe était leaderless + * avant l'opération (cas typique : admin assigne un premier chef après + * création via {@code setLeader}). {@code newLeaderId} est toujours non-null. + */ public class TeamLeadershipTransferEvent extends TeamEvent { private static final HandlerList HANDLERS = new HandlerList(); @@ -16,12 +23,18 @@ public class TeamLeadershipTransferEvent extends TeamEvent { public TeamLeadershipTransferEvent(Team team, UUID oldLeaderId, UUID newLeaderId) { super(team); - this.oldLeaderId = Objects.requireNonNull(oldLeaderId, "oldLeaderId"); + this.oldLeaderId = oldLeaderId; // nullable this.newLeaderId = Objects.requireNonNull(newLeaderId, "newLeaderId"); } - public UUID getOldLeaderId() { return oldLeaderId; } - public UUID getNewLeaderId() { return newLeaderId; } + /** L'ancien chef. Vide si l'équipe était leaderless avant. */ + public Optional getOldLeaderId() { + return Optional.ofNullable(oldLeaderId); + } + + public UUID getNewLeaderId() { + return newLeaderId; + } @Override public HandlerList getHandlers() {