feat: admin/chef/player permission model + leaderless teams + setleader

Commands:
- Drop all short aliases (c/i/t/j/vis/disband/...) — long names only.
- Every /core team <action> now has a crcore.team.<action> permission.
- Three-tier model:
  * Admin (perm only): create, delete, setleader, score
  * Chef (perm + chef-check in execute): add, remove, transfer, visibility, setspawn
  * Player (perm): join, leave, info, list, top
- delete now takes <team> as argument (admin); no more chef-disband shortcut.

New /core team setleader <team> <player>:
- Admin assigns or replaces a team's leader.
- More permissive than transfer: target may not yet be a member (auto-add),
  and works on leaderless teams.

Leaderless teams:
- Team.leaderId is now nullable.
- getLeaderId() and getLeader() return Optional<...>.
- hasLeader() and isLeader(UUID) helpers added.
- New constructors Team(id, name, tag, color [, visibility]) for leaderless.
- TeamService.createTeam overloads without leaderId.
- TeamService.setLeader(teamId, playerId): assigns/replaces leader (auto-adds
  as member if needed). Fires TeamLeadershipTransferEvent with optional old.
- TeamLeadershipTransferEvent.getOldLeaderId() returns Optional<UUID>.
- SqliteTeamRepository: leader_id column no longer NOT NULL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Antone Barbaud
2026-06-09 14:56:07 +02:00
parent 5bd6e227d3
commit 002fefdc02
24 changed files with 472 additions and 138 deletions
+60
View File
@@ -323,6 +323,66 @@ Format léger : une décision = un titre + contexte + choix + raison.
même fichier SQLite (par défaut `<dataFolder>/crcore.db`) ; le préfixe même fichier SQLite (par défaut `<dataFolder>/crcore.db`) ; le préfixe
isole proprement. isole proprement.
## 2026-06-09 — Refonte permissions + modèle admin/chef/joueur
- **Choix** : chaque sous-commande `/core team <action>` a sa propre permission
`crcore.team.<action>`. 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 <team>` (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<UUID>`, `getLeader()` renvoie `Optional<TeamMember>`. 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<UUID>` (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
<team> <player>`). 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) ## 2026-06-09 — Bascule Java 16 → Java 11 (révision)
- **Révision** de la décision "Java 16" du 2026-06-08. - **Révision** de la décision "Java 16" du 2026-06-08.
+72 -24
View File
@@ -1,5 +1,5 @@
@startuml builtin-commands-diagram @startuml builtin-commands-diagram
title CR-Core — Default /core team commands title CR-Core — Default /core team commands (admin / chef / joueur)
skinparam classAttributeIconSize 0 skinparam classAttributeIconSize 0
hide empty members hide empty members
@@ -13,7 +13,6 @@ package "fr.luc.crcore.command.builtin" {
class CoreCommand { class CoreCommand {
+ CoreCommand(teamSvc, playerSvc) + CoreCommand(teamSvc, playerSvc)
# registerDefaults(): void
} }
CoreCommand --|> BaseCommand CoreCommand --|> BaseCommand
@@ -29,44 +28,93 @@ package "fr.luc.crcore.command.builtin" {
+ {static} teamByName(service): ArgumentType<Team> + {static} teamByName(service): ArgumentType<Team>
} }
class TeamCreateSubCommand { ' === ADMIN commands (permission seule) ===
+ execute(ctx): CommandResult package "admin" <<Rectangle>> {
class TeamCreateSubCommand {
perm: crcore.team.create
args: name, tag, color, [leader]
}
class TeamDeleteSubCommand {
perm: crcore.team.delete
args: <team>
}
class TeamSetLeaderSubCommand {
perm: crcore.team.setleader
args: <team> <player>
}
class TeamScoreSubCommand {
perm: crcore.team.score
args: <team> <name> <add|set> <value>
}
}
' === CHEF commands (permission + check chef) ===
package "chef" <<Rectangle>> {
class TeamAddSubCommand {
perm: crcore.team.add
args: <player>
}
class TeamRemoveSubCommand {
perm: crcore.team.remove
args: <player>
}
class TeamTransferSubCommand {
perm: crcore.team.transfer
args: <player>
}
class TeamVisibilitySubCommand {
perm: crcore.team.visibility
args: <vis>
}
class TeamSetSpawnSubCommand {
perm: crcore.team.setspawn
}
}
' === PLAYER commands ===
package "player" <<Rectangle>> {
class TeamJoinSubCommand {
perm: crcore.team.join
args: <team>
}
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 TeamCreateSubCommand --|> SubCommand
TeamDeleteSubCommand --|> SubCommand TeamDeleteSubCommand --|> SubCommand
TeamSetLeaderSubCommand --|> SubCommand
TeamScoreSubCommand --|> SubCommand
TeamAddSubCommand --|> SubCommand TeamAddSubCommand --|> SubCommand
TeamRemoveSubCommand --|> SubCommand TeamRemoveSubCommand --|> SubCommand
TeamTransferSubCommand --|> SubCommand
TeamVisibilitySubCommand --|> SubCommand
TeamSetSpawnSubCommand --|> SubCommand
TeamJoinSubCommand --|> SubCommand TeamJoinSubCommand --|> SubCommand
TeamLeaveSubCommand --|> SubCommand TeamLeaveSubCommand --|> SubCommand
TeamInfoSubCommand --|> SubCommand TeamInfoSubCommand --|> SubCommand
TeamListSubCommand --|> SubCommand TeamListSubCommand --|> SubCommand
TeamTransferSubCommand --|> SubCommand
TeamVisibilitySubCommand --|> SubCommand
TeamScoreSubCommand --|> SubCommand
TeamTopSubCommand --|> SubCommand TeamTopSubCommand --|> SubCommand
TeamSetSpawnSubCommand --|> SubCommand
CoreCommand "1" *-- "1" TeamGroupSubCommand : contains CoreCommand "1" *-- "1" TeamGroupSubCommand : contains
TeamGroupSubCommand "1" *-- "13" SubCommand : contains TeamGroupSubCommand "1" *-- "14" SubCommand : contains
} }
} }
note right of CoreCommand note bottom of TeamGroupSubCommand
Le plugin de jeu downstream Override d'une feuille :
remplace une feuille avec :
core.getCoreCommand() core.getCoreCommand()
.findSubCommand("team") .findSubCommand("team")
.replaceSubCommand("create", .replaceSubCommand("create",
+18 -6
View File
@@ -101,17 +101,24 @@ package "fr.luc.crcore.team" {
- name: String - name: String
- tag: String - tag: String
- color: TeamColor - color: TeamColor
- leaderId: UUID - leaderId: UUID *(nullable)*
- visibility: TeamVisibility - visibility: TeamVisibility
- members: Set<TeamMember> - members: Set<TeamMember>
- scores: Map<String, Integer> - scores: Map<String, Integer>
- spawnPoint: Location - 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 + getName(): String
+ getTag(): String + getTag(): String
+ getColor(): TeamColor + getColor(): TeamColor
+ getLeaderId(): UUID + getLeaderId(): Optional<UUID>
+ getLeader(): TeamMember + getLeader(): Optional<TeamMember>
+ hasLeader(): boolean
+ isLeader(playerId): boolean
+ getVisibility(): TeamVisibility + getVisibility(): TeamVisibility
+ setVisibility(v): void + setVisibility(v): void
+ isPublic(): boolean + isPublic(): boolean
@@ -121,7 +128,8 @@ package "fr.luc.crcore.team" {
+ size(): int + size(): int
+ addMember(playerId): TeamMember + addMember(playerId): TeamMember
+ removeMember(playerId): boolean + removeMember(playerId): boolean
+ transferLeadership(newLeaderId): void + transferLeadership(newLeaderId): void ' strict: chef→chef
+ setLeader(newLeaderId): void ' permissive: assign
-- --
+ getScore(name): int + getScore(name): int
+ hasScore(name): boolean + hasScore(name): boolean
@@ -157,12 +165,16 @@ package "fr.luc.crcore.team" {
} }
interface TeamService { 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 + dissolveTeam(teamId): boolean
+ addMember(teamId, playerId): boolean + addMember(teamId, playerId): boolean
+ removeMember(teamId, playerId): boolean + removeMember(teamId, playerId): boolean
+ joinTeam(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 + setVisibility(teamId, visibility): void
-- --
+ addScore(teamId, name, delta): int + addScore(teamId, name, delta): int
+52 -38
View File
@@ -30,7 +30,7 @@ Architecture des domaines :
| `name` | `String` | Nom lisible. Unique (case-insensitive). | | `name` | `String` | Nom lisible. Unique (case-insensitive). |
| `tag` | `String` | Tag court (le « # »). Unique. Affiché entre `[# … ]`. | | `tag` | `String` | Tag court (le « # »). Unique. Affiché entre `[# … ]`. |
| `color` | `TeamColor` | Couleur associée (enum). | | `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`. | | `visibility` | `TeamVisibility` | `PUBLIC` (les joueurs peuvent rejoindre) ou `PRIVATE` (seul le chef ajoute). Défaut : `PRIVATE`. |
| `members` | `Set<TeamMember>` | Ensemble des membres (inclut le chef). | | `members` | `Set<TeamMember>` | Ensemble des membres (inclut le chef). |
@@ -64,13 +64,16 @@ Architecture des domaines :
| Opération | Description | | 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)` | Crée une équipe **leaderless** en `PRIVATE`. |
| `createTeam(name, tag, color, leaderId, visibility)` | Surcharge : permet de créer directement en `PUBLIC` ou `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. | | `dissolveTeam(teamId)` | Supprime l'équipe. |
| `addMember(teamId, playerId)` | **Action du chef** : ajoute un joueur comme `MEMBER` (marche en PUBLIC comme en PRIVATE). | | `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). | | `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. | | `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). | | `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). | | `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). | | `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 ...` ## 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 `CRCore.enable()`. Chaque sous-commande vit dans
`fr.luc.crcore.command.builtin.team` et est substituable individuellement par `fr.luc.crcore.command.builtin.team` et est substituable individuellement par
sous-classe ou via `replaceSubCommand`. 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 ### Arborescence
``` ```
/core (CoreCommand, BaseCommand racine, aliases: cr, crcore) /core (CoreCommand)
└── team (TeamGroupSubCommand, alias: t) └── team (TeamGroupSubCommand)
├── create <name> <tag> <color> [visibility] — créer une équipe ├── create <name> <tag> <color> [leader] [admin] créer (chef optionnel)
├── delete dissoudre son équipe (chef) ├── delete <team> [admin] dissoudre une équipe
├── add <player> — chef ajoute un membre ├── setleader <team> <player> [admin] (re)assigner le chef
├── remove <player> — chef retire un membre ├── score <team> <name> <add|set> <value> [admin] modifier un score
├── join <name> — auto-join sur team PUBLIC ├── add <player> [chef] ajouter à son équipe
├── leave — quitter son équipe ├── remove <player> [chef] retirer de son équipe
├── info [name] — infos d'une équipe ├── transfer <player> [chef] transférer leadership
├── list — liste toutes les équipes ├── visibility <PUBLIC|PRIVATE> [chef] changer visibilité
├── transfer <player> — transfert de leadership ├── setspawn [chef] définir le spawn
├── visibility <PUBLIC|PRIVATE> — changer la visibilité ├── join <team> [joueur] rejoindre PUBLIC
├── score <team> <name> <add|set> <value> — [admin] modifier un score ├── leave [joueur] quitter son équipe
├── top [score] — classement (par score ou global) ├── info [team] [joueur] infos
── setspawn — chef définit le spawn ── list [joueur] toutes les équipes
└── top [score] [joueur] classement
``` ```
### Aliases supplémentaires (équivalents Bukkit) ### Permissions
| Sous-commande | Aliases | Chaque sous-commande a une permission `crcore.team.<action>`. Modèle à 3 niveaux :
|---|---|
| `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` |
### 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 | | Sous-commande | Permission |
|---|---| |---|---|
| `create` | `crcore.team.create` | | `create` | `crcore.team.create` |
| `score` | `crcore.team.score.modify` (admin) | | `delete` | `crcore.team.delete` |
| autres | aucune (gated par appartenance / rôle de chef) | | `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 ### Override d'une sous-commande par défaut
@@ -33,7 +33,7 @@ public class CoreCommand extends BaseCommand {
protected final PlayerProfileService playerProfileService; protected final PlayerProfileService playerProfileService;
public CoreCommand(TeamService teamService, PlayerProfileService playerProfileService) { public CoreCommand(TeamService teamService, PlayerProfileService playerProfileService) {
super("core", "cr", "crcore"); super("core");
this.teamService = Objects.requireNonNull(teamService, "teamService"); this.teamService = Objects.requireNonNull(teamService, "teamService");
this.playerProfileService = Objects.requireNonNull(playerProfileService, "playerProfileService"); this.playerProfileService = Objects.requireNonNull(playerProfileService, "playerProfileService");
description("Commandes du noyau CR-Core"); description("Commandes du noyau CR-Core");
@@ -21,9 +21,10 @@ public class TeamAddSubCommand extends SubCommand {
protected final TeamService service; protected final TeamService service;
public TeamAddSubCommand(TeamService service) { public TeamAddSubCommand(TeamService service) {
super("add", "invite"); super("add");
this.service = Objects.requireNonNull(service, "service"); this.service = Objects.requireNonNull(service, "service");
description("Ajouter un joueur à son équipe (chef uniquement)"); description("Ajouter un joueur à son équipe (chef uniquement)");
permission("crcore.team.add");
playerOnly(); playerOnly();
argument("player", ArgumentTypes.ONLINE_PLAYER); argument("player", ArgumentTypes.ONLINE_PLAYER);
} }
@@ -37,7 +38,7 @@ public class TeamAddSubCommand extends SubCommand {
if (team == null) { if (team == null) {
return CommandResult.failure("Vous n'appartenez à aucune équipe."); 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."); return CommandResult.failure("Seul le chef peut ajouter des membres.");
} }
if (service.getTeamOfPlayer(target.getUniqueId()).isPresent()) { if (service.getTeamOfPlayer(target.getUniqueId()).isPresent()) {
@@ -8,45 +8,58 @@ import fr.luc.crcore.team.Team;
import fr.luc.crcore.team.TeamColor; import fr.luc.crcore.team.TeamColor;
import fr.luc.crcore.team.TeamException; import fr.luc.crcore.team.TeamException;
import fr.luc.crcore.team.TeamService; import fr.luc.crcore.team.TeamService;
import fr.luc.crcore.team.TeamVisibility;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
/** /**
* {@code /core team create <name> <tag> <color> [visibility]} * {@code /core team create <name> <tag> <color> [leader]}
* *
* <p>Crée une équipe dont l'exécutant devient le chef. Visibilité par défaut : * <p><b>Admin uniquement</b>. Crée une équipe en {@link
* {@link TeamVisibility#PRIVATE}. * fr.luc.crcore.team.TeamVisibility#PRIVATE} par défaut.
*
* <p>Le chef est <b>optionnel</b> :
* <ul>
* <li>Sans argument {@code leader} → équipe leaderless. L'admin assignera
* plus tard via {@code /core team setleader}.</li>
* <li>Avec argument {@code leader} (nom d'un joueur connecté) → ce joueur
* devient chef et membre de l'équipe.</li>
* </ul>
*
* <p>La visibilité (PUBLIC/PRIVATE) se change ensuite via {@code /core team
* visibility} (action du chef).
*/ */
public class TeamCreateSubCommand extends SubCommand { public class TeamCreateSubCommand extends SubCommand {
protected final TeamService service; protected final TeamService service;
public TeamCreateSubCommand(TeamService service) { public TeamCreateSubCommand(TeamService service) {
super("create", "c", "new"); super("create");
this.service = Objects.requireNonNull(service, "service"); this.service = Objects.requireNonNull(service, "service");
description("Créer une équipe"); description("Créer une équipe (admin)");
permission("crcore.team.create"); permission("crcore.team.create");
playerOnly();
argument("name", ArgumentTypes.STRING); argument("name", ArgumentTypes.STRING);
argument("tag", ArgumentTypes.STRING); argument("tag", ArgumentTypes.STRING);
argument("color", ArgumentTypes.enumOf(TeamColor.class)); argument("color", ArgumentTypes.enumOf(TeamColor.class));
optionalArgument("visibility", ArgumentTypes.enumOf(TeamVisibility.class)); optionalArgument("leader", ArgumentTypes.ONLINE_PLAYER);
} }
@Override @Override
public CommandResult execute(CommandContext ctx) { public CommandResult execute(CommandContext ctx) {
Player player = ctx.requirePlayer();
String name = ctx.get("name"); String name = ctx.get("name");
String tag = ctx.get("tag"); String tag = ctx.get("tag");
TeamColor color = ctx.get("color"); TeamColor color = ctx.get("color");
TeamVisibility visibility = ctx.<TeamVisibility>getOptional("visibility") Optional<Player> leaderOpt = ctx.getOptional("leader");
.orElse(TeamVisibility.PRIVATE); UUID leaderId = leaderOpt.map(Player::getUniqueId).orElse(null);
try { try {
Team team = service.createTeam(name, tag, color, player.getUniqueId(), visibility); Team team = service.createTeam(name, tag, color, leaderId);
return CommandResult.success("Équipe " + team.getName() + " [#" + team.getTag() + "] créée."); String suffix = leaderOpt.isPresent()
? " (chef : " + leaderOpt.get().getName() + ")"
: " (sans chef)";
return CommandResult.success("Équipe " + team.getName() + " [#" + team.getTag() + "] créée" + suffix + ".");
} catch (TeamException ex) { } catch (TeamException ex) {
return CommandResult.failure(ex.getMessage()); return CommandResult.failure(ex.getMessage());
} }
@@ -5,40 +5,30 @@ import fr.luc.crcore.command.CommandResult;
import fr.luc.crcore.command.SubCommand; import fr.luc.crcore.command.SubCommand;
import fr.luc.crcore.team.Team; import fr.luc.crcore.team.Team;
import fr.luc.crcore.team.TeamService; import fr.luc.crcore.team.TeamService;
import org.bukkit.entity.Player;
import java.util.Objects; import java.util.Objects;
/** /**
* {@code /core team delete} * {@code /core team delete <team>}
* *
* <p>Dissout l'équipe de l'exécutant. Réservé au chef. Pas d'argument : * <p><b>Admin uniquement</b>. Dissout l'équipe spécifiée. Aucun check de chef
* l'équipe ciblée est déduite du joueur. * — l'action est gated par la permission {@code crcore.team.delete}.
*
* <p>Aliases : {@code disband}, {@code dissolve}.
*/ */
public class TeamDeleteSubCommand extends SubCommand { public class TeamDeleteSubCommand extends SubCommand {
protected final TeamService service; protected final TeamService service;
public TeamDeleteSubCommand(TeamService service) { public TeamDeleteSubCommand(TeamService service) {
super("delete", "disband", "dissolve"); super("delete");
this.service = Objects.requireNonNull(service, "service"); this.service = Objects.requireNonNull(service, "service");
description("Dissoudre son équipe (chef uniquement)"); description("Dissoudre une équipe (admin)");
playerOnly(); permission("crcore.team.delete");
argument("team", TeamArgumentTypes.teamByName(service));
} }
@Override @Override
public CommandResult execute(CommandContext ctx) { public CommandResult execute(CommandContext ctx) {
Player player = ctx.requirePlayer(); Team team = ctx.get("team");
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()); service.dissolveTeam(team.getId());
return CommandResult.success("Équipe " + team.getName() + " dissoute."); return CommandResult.success("Équipe " + team.getName() + " dissoute.");
} }
@@ -23,7 +23,7 @@ public class TeamGroupSubCommand extends SubCommand {
protected final TeamService service; protected final TeamService service;
public TeamGroupSubCommand(TeamService service) { public TeamGroupSubCommand(TeamService service) {
super("team", "t"); super("team");
this.service = Objects.requireNonNull(service, "service"); this.service = Objects.requireNonNull(service, "service");
description("Gestion des équipes"); description("Gestion des équipes");
registerDefaults(); registerDefaults();
@@ -43,6 +43,7 @@ public class TeamGroupSubCommand extends SubCommand {
addSubCommand(new TeamInfoSubCommand(service)); addSubCommand(new TeamInfoSubCommand(service));
addSubCommand(new TeamListSubCommand(service)); addSubCommand(new TeamListSubCommand(service));
addSubCommand(new TeamTransferSubCommand(service)); addSubCommand(new TeamTransferSubCommand(service));
addSubCommand(new TeamSetLeaderSubCommand(service));
addSubCommand(new TeamVisibilitySubCommand(service)); addSubCommand(new TeamVisibilitySubCommand(service));
addSubCommand(new TeamScoreSubCommand(service)); addSubCommand(new TeamScoreSubCommand(service));
addSubCommand(new TeamTopSubCommand(service)); addSubCommand(new TeamTopSubCommand(service));
@@ -24,9 +24,10 @@ public class TeamInfoSubCommand extends SubCommand {
protected final TeamService service; protected final TeamService service;
public TeamInfoSubCommand(TeamService service) { public TeamInfoSubCommand(TeamService service) {
super("info", "i"); super("info");
this.service = Objects.requireNonNull(service, "service"); this.service = Objects.requireNonNull(service, "service");
description("Afficher les infos d'une équipe"); description("Afficher les infos d'une équipe");
permission("crcore.team.info");
optionalArgument("name", TeamArgumentTypes.teamByName(service)); optionalArgument("name", TeamArgumentTypes.teamByName(service));
} }
@@ -22,9 +22,10 @@ public class TeamJoinSubCommand extends SubCommand {
protected final TeamService service; protected final TeamService service;
public TeamJoinSubCommand(TeamService service) { public TeamJoinSubCommand(TeamService service) {
super("join", "j"); super("join");
this.service = Objects.requireNonNull(service, "service"); this.service = Objects.requireNonNull(service, "service");
description("Rejoindre une équipe publique"); description("Rejoindre une équipe publique");
permission("crcore.team.join");
playerOnly(); playerOnly();
argument("name", TeamArgumentTypes.teamByName(service)); argument("name", TeamArgumentTypes.teamByName(service));
} }
@@ -20,9 +20,10 @@ public class TeamLeaveSubCommand extends SubCommand {
protected final TeamService service; protected final TeamService service;
public TeamLeaveSubCommand(TeamService service) { public TeamLeaveSubCommand(TeamService service) {
super("leave", "quit"); super("leave");
this.service = Objects.requireNonNull(service, "service"); this.service = Objects.requireNonNull(service, "service");
description("Quitter son équipe"); description("Quitter son équipe");
permission("crcore.team.leave");
playerOnly(); playerOnly();
} }
@@ -33,7 +34,7 @@ public class TeamLeaveSubCommand extends SubCommand {
if (team == null) { if (team == null) {
return CommandResult.failure("Vous n'appartenez à aucune équipe."); return CommandResult.failure("Vous n'appartenez à aucune équipe.");
} }
if (team.getLeaderId().equals(player.getUniqueId())) { if (team.isLeader(player.getUniqueId())) {
return CommandResult.failure( return CommandResult.failure(
"Vous êtes le chef. Transférez le leadership avec /core team transfer <player>, ou dissolvez avec /core team delete."); "Vous êtes le chef. Transférez le leadership avec /core team transfer <player>, ou dissolvez avec /core team delete.");
} }
@@ -21,9 +21,10 @@ public class TeamListSubCommand extends SubCommand {
protected final TeamService service; protected final TeamService service;
public TeamListSubCommand(TeamService service) { public TeamListSubCommand(TeamService service) {
super("list", "ls"); super("list");
this.service = Objects.requireNonNull(service, "service"); this.service = Objects.requireNonNull(service, "service");
description("Lister toutes les équipes"); description("Lister toutes les équipes");
permission("crcore.team.list");
} }
@Override @Override
@@ -23,9 +23,10 @@ public class TeamRemoveSubCommand extends SubCommand {
protected final TeamService service; protected final TeamService service;
public TeamRemoveSubCommand(TeamService service) { public TeamRemoveSubCommand(TeamService service) {
super("remove", "kick", "expel"); super("remove");
this.service = Objects.requireNonNull(service, "service"); this.service = Objects.requireNonNull(service, "service");
description("Retirer un joueur de son équipe (chef uniquement)"); description("Retirer un joueur de son équipe (chef uniquement)");
permission("crcore.team.remove");
playerOnly(); playerOnly();
argument("player", ArgumentTypes.STRING); argument("player", ArgumentTypes.STRING);
} }
@@ -39,7 +40,7 @@ public class TeamRemoveSubCommand extends SubCommand {
if (team == null) { if (team == null) {
return CommandResult.failure("Vous n'appartenez à aucune équipe."); 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."); return CommandResult.failure("Seul le chef peut retirer des membres.");
} }
@@ -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 <team> <player>}
*
* <p><b>Admin uniquement</b>. Assigne un joueur comme chef d'une équipe :
* <ul>
* <li>Si l'équipe est leaderless → le joueur devient chef (auto-ajouté
* comme membre s'il ne l'est pas).</li>
* <li>Si l'équipe a déjà un chef → l'ancien chef est démis en simple
* membre, le nouveau prend le rôle.</li>
* <li>Si {@code <player>} n'est pas encore membre → il est auto-ajouté
* à l'équipe en tant que chef.</li>
* </ul>
*
* <p>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() + ".");
}
}
@@ -19,9 +19,10 @@ public class TeamSetSpawnSubCommand extends SubCommand {
protected final TeamService service; protected final TeamService service;
public TeamSetSpawnSubCommand(TeamService service) { public TeamSetSpawnSubCommand(TeamService service) {
super("setspawn", "spawn"); super("setspawn");
this.service = Objects.requireNonNull(service, "service"); this.service = Objects.requireNonNull(service, "service");
description("Définir le point de spawn de l'équipe (chef uniquement)"); description("Définir le point de spawn de l'équipe (chef uniquement)");
permission("crcore.team.setspawn");
playerOnly(); playerOnly();
} }
@@ -32,7 +33,7 @@ public class TeamSetSpawnSubCommand extends SubCommand {
if (team == null) { if (team == null) {
return CommandResult.failure("Vous n'appartenez à aucune équipe."); 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."); return CommandResult.failure("Seul le chef peut définir le spawn.");
} }
service.setSpawnPoint(team.getId(), player.getLocation()); service.setSpawnPoint(team.getId(), player.getLocation());
@@ -28,10 +28,11 @@ public class TeamTopSubCommand extends SubCommand {
} }
public TeamTopSubCommand(TeamService service, int limit) { public TeamTopSubCommand(TeamService service, int limit) {
super("top", "ranking", "leaderboard"); super("top");
this.service = Objects.requireNonNull(service, "service"); this.service = Objects.requireNonNull(service, "service");
this.limit = limit; this.limit = limit;
description("Classement des équipes"); description("Classement des équipes");
permission("crcore.team.top");
optionalArgument("score", ArgumentTypes.STRING); optionalArgument("score", ArgumentTypes.STRING);
} }
@@ -24,7 +24,8 @@ public class TeamTransferSubCommand extends SubCommand {
public TeamTransferSubCommand(TeamService service) { public TeamTransferSubCommand(TeamService service) {
super("transfer"); super("transfer");
this.service = Objects.requireNonNull(service, "service"); 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(); playerOnly();
argument("player", ArgumentTypes.STRING); argument("player", ArgumentTypes.STRING);
} }
@@ -37,7 +38,7 @@ public class TeamTransferSubCommand extends SubCommand {
if (team == null) { if (team == null) {
return CommandResult.failure("Vous n'appartenez à aucune équipe."); 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."); return CommandResult.failure("Seul le chef peut transférer le leadership.");
} }
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@@ -22,9 +22,10 @@ public class TeamVisibilitySubCommand extends SubCommand {
protected final TeamService service; protected final TeamService service;
public TeamVisibilitySubCommand(TeamService service) { public TeamVisibilitySubCommand(TeamService service) {
super("visibility", "vis"); super("visibility");
this.service = Objects.requireNonNull(service, "service"); 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(); playerOnly();
argument("visibility", ArgumentTypes.enumOf(TeamVisibility.class)); argument("visibility", ArgumentTypes.enumOf(TeamVisibility.class));
} }
@@ -37,7 +38,7 @@ public class TeamVisibilitySubCommand extends SubCommand {
if (team == null) { if (team == null) {
return CommandResult.failure("Vous n'appartenez à aucune équipe."); 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é."); return CommandResult.failure("Seul le chef peut changer la visibilité.");
} }
service.setVisibility(team.getId(), visibility); service.setVisibility(team.getId(), visibility);
@@ -46,7 +46,7 @@ public class SqliteTeamRepository extends InMemoryTeamRepository {
.column("name", ColumnType.TEXT).notNull().unique() .column("name", ColumnType.TEXT).notNull().unique()
.column("tag", ColumnType.TEXT).notNull().unique() .column("tag", ColumnType.TEXT).notNull().unique()
.column("color", ColumnType.TEXT).notNull() .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("visibility", ColumnType.TEXT).notNull()
.column("spawn_world", ColumnType.TEXT) .column("spawn_world", ColumnType.TEXT)
.column("spawn_x", ColumnType.REAL) .column("spawn_x", ColumnType.REAL)
@@ -83,7 +83,7 @@ public class SqliteTeamRepository extends InMemoryTeamRepository {
rs.getString("name"), rs.getString("name"),
rs.getString("tag"), rs.getString("tag"),
TeamColor.valueOf(rs.getString("color")), 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")), TeamVisibility.valueOf(rs.getString("visibility")),
rs.getString("spawn_world"), rs.getString("spawn_world"),
(Double) rs.getObject("spawn_x"), (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. // Le leader est ajouté par le constructeur de Team avec role LEADER.
// On ajoute les autres membres manuellement via addMember (qui les marque MEMBER). // On ajoute les autres membres manuellement via addMember (qui les marque MEMBER).
for (MemberRow m : members) { for (MemberRow m : members) {
if (!m.playerId.equals(row.leaderId)) { // Skip le leader — il est déjà ajouté par le constructeur de Team.
team.addMember(m.playerId); if (row.leaderId != null && m.playerId.equals(row.leaderId)) continue;
} team.addMember(m.playerId);
} }
// Scores // Scores
db.query( db.query(
@@ -174,7 +174,7 @@ public class SqliteTeamRepository extends InMemoryTeamRepository {
" spawn_world, spawn_x, spawn_y, spawn_z, spawn_yaw, spawn_pitch) " + " spawn_world, spawn_x, spawn_y, spawn_z, spawn_yaw, spawn_pitch) " +
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
team.getId(), team.getName(), team.getTag(), team.getColor(), team.getId(), team.getName(), team.getTag(), team.getColor(),
team.getLeaderId(), team.getVisibility(), team.getLeaderId().orElse(null), team.getVisibility(),
spawnWorld, spawnX, spawnY, spawnZ, spawnYaw, spawnPitch spawnWorld, spawnX, spawnY, spawnZ, spawnYaw, spawnPitch
); );
+82 -8
View File
@@ -40,21 +40,39 @@ public class Team extends AbstractEntity implements Named, ScoreHolder {
private TeamVisibility visibility; private TeamVisibility visibility;
private Location spawnPoint; private Location spawnPoint;
/** Crée une équipe <b>sans chef</b>, 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 <b>sans chef</b> 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) { public Team(UUID id, String name, String tag, TeamColor color, UUID leaderId) {
this(id, name, tag, color, leaderId, TeamVisibility.PRIVATE); 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, public Team(UUID id, String name, String tag, TeamColor color, UUID leaderId,
TeamVisibility visibility) { TeamVisibility visibility) {
super(id); super(id);
this.name = Objects.requireNonNull(name, "name"); this.name = Objects.requireNonNull(name, "name");
this.tag = Objects.requireNonNull(tag, "tag"); this.tag = Objects.requireNonNull(tag, "tag");
this.color = Objects.requireNonNull(color, "color"); this.color = Objects.requireNonNull(color, "color");
this.leaderId = Objects.requireNonNull(leaderId, "leaderId");
this.visibility = Objects.requireNonNull(visibility, "visibility"); this.visibility = Objects.requireNonNull(visibility, "visibility");
this.leaderId = leaderId; // nullable
this.members = new HashSet<>(); this.members = new HashSet<>();
this.scores = new HashMap<>(); 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. */ /** Override to instantiate a custom TeamMember subclass. */
@@ -75,8 +93,19 @@ public class Team extends AbstractEntity implements Named, ScoreHolder {
return color; return color;
} }
public UUID getLeaderId() { /** L'UUID du chef si la team en a un, sinon {@link Optional#empty()}. */
return leaderId; public Optional<UUID> 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() { public TeamVisibility getVisibility() {
@@ -91,9 +120,10 @@ public class Team extends AbstractEntity implements Named, ScoreHolder {
return visibility.isPublic(); return visibility.isPublic();
} }
public TeamMember getLeader() { /** Le {@link TeamMember} chef si la team en a un, sinon {@link Optional#empty()}. */
return getMember(leaderId).orElseThrow( public Optional<TeamMember> getLeader() {
() -> new IllegalStateException("Team has no leader: " + getId())); if (leaderId == null) return Optional.empty();
return getMember(leaderId);
} }
public Set<TeamMember> getMembers() { public Set<TeamMember> getMembers() {
@@ -135,15 +165,26 @@ public class Team extends AbstractEntity implements Named, ScoreHolder {
return members.removeIf(member -> member.getPlayerId().equals(playerId)); return members.removeIf(member -> member.getPlayerId().equals(playerId));
} }
/**
* Transfert classique du leadership : le nouveau chef doit <b>déjà</b> être
* membre de l'équipe, et l'équipe doit avoir un chef actuel.
*
* <p>Pour un cas plus général (équipe leaderless, ou nouveau chef non
* encore membre), utiliser {@link #setLeader(UUID)}.
*/
public void transferLeadership(UUID newLeaderId) { public void transferLeadership(UUID newLeaderId) {
Objects.requireNonNull(newLeaderId, "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)) { if (newLeaderId.equals(leaderId)) {
return; return;
} }
TeamMember newLeader = getMember(newLeaderId).orElseThrow( TeamMember newLeader = getMember(newLeaderId).orElseThrow(
() -> new IllegalArgumentException( () -> new IllegalArgumentException(
"New leader must already be a member of the team.")); "New leader must already be a member of the team."));
TeamMember oldLeader = getLeader(); TeamMember oldLeader = getLeader().orElseThrow();
members.remove(oldLeader); members.remove(oldLeader);
members.remove(newLeader); members.remove(newLeader);
members.add(oldLeader.withRole(TeamRole.MEMBER)); members.add(oldLeader.withRole(TeamRole.MEMBER));
@@ -151,6 +192,39 @@ public class Team extends AbstractEntity implements Named, ScoreHolder {
this.leaderId = newLeaderId; this.leaderId = newLeaderId;
} }
/**
* Assigne un chef à l'équipe, plus flexible que {@link #transferLeadership} :
* <ul>
* <li>Si la team est leaderless → ajoute {@code playerId} comme chef
* (en tant que membre s'il ne l'est pas déjà).</li>
* <li>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).</li>
* </ul>
*/
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<TeamMember> 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 ---- // ---- Scores ----
public int getScore(String scoreName) { public int getScore(String scoreName) {
@@ -25,8 +25,19 @@ public interface TeamService {
// ---- Lifecycle ---- // ---- Lifecycle ----
/** Crée une équipe <b>sans chef</b>, visibilité PRIVATE. */
Team createTeam(String name, String tag, TeamColor color);
/** Crée une équipe <b>sans chef</b> 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); 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, Team createTeam(String name, String tag, TeamColor color, UUID leaderId,
TeamVisibility visibility); TeamVisibility visibility);
@@ -42,6 +53,17 @@ public interface TeamService {
boolean transferLeadership(UUID teamId, UUID newLeaderId); 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); void setVisibility(UUID teamId, TeamVisibility visibility);
// ---- Scores ---- // ---- Scores ----
@@ -27,6 +27,16 @@ public class TeamServiceImpl implements TeamService {
// ---- Lifecycle ---- // ---- 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 @Override
public Team createTeam(String name, String tag, TeamColor color, UUID leaderId) { public Team createTeam(String name, String tag, TeamColor color, UUID leaderId) {
return createTeam(name, tag, color, leaderId, TeamVisibility.PRIVATE); return createTeam(name, tag, color, leaderId, TeamVisibility.PRIVATE);
@@ -37,7 +47,9 @@ public class TeamServiceImpl implements TeamService {
TeamVisibility visibility) { TeamVisibility visibility) {
validateName(name); validateName(name);
validateTag(tag); validateTag(tag);
validateLeader(leaderId); if (leaderId != null) {
validateLeader(leaderId);
}
Objects.requireNonNull(visibility, "visibility"); Objects.requireNonNull(visibility, "visibility");
Team team = newTeam(UUID.randomUUID(), name, tag, color, leaderId, 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) { public boolean transferLeadership(UUID teamId, UUID newLeaderId) {
Objects.requireNonNull(newLeaderId, "newLeaderId"); Objects.requireNonNull(newLeaderId, "newLeaderId");
Team team = requireTeam(teamId); Team team = requireTeam(teamId);
UUID oldLeaderId = team.getLeaderId(); UUID oldLeaderId = team.getLeaderId().orElse(null);
team.transferLeadership(newLeaderId); team.transferLeadership(newLeaderId);
repository.save(team); repository.save(team);
onLeadershipTransferred(team, oldLeaderId, newLeaderId); onLeadershipTransferred(team, oldLeaderId, newLeaderId);
return true; 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 @Override
public void setVisibility(UUID teamId, TeamVisibility visibility) { public void setVisibility(UUID teamId, TeamVisibility visibility) {
Objects.requireNonNull(visibility, "visibility"); Objects.requireNonNull(visibility, "visibility");
@@ -4,9 +4,16 @@ import fr.luc.crcore.team.Team;
import org.bukkit.event.HandlerList; import org.bukkit.event.HandlerList;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.UUID; 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.
*
* <p>{@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 { public class TeamLeadershipTransferEvent extends TeamEvent {
private static final HandlerList HANDLERS = new HandlerList(); private static final HandlerList HANDLERS = new HandlerList();
@@ -16,12 +23,18 @@ public class TeamLeadershipTransferEvent extends TeamEvent {
public TeamLeadershipTransferEvent(Team team, UUID oldLeaderId, UUID newLeaderId) { public TeamLeadershipTransferEvent(Team team, UUID oldLeaderId, UUID newLeaderId) {
super(team); super(team);
this.oldLeaderId = Objects.requireNonNull(oldLeaderId, "oldLeaderId"); this.oldLeaderId = oldLeaderId; // nullable
this.newLeaderId = Objects.requireNonNull(newLeaderId, "newLeaderId"); this.newLeaderId = Objects.requireNonNull(newLeaderId, "newLeaderId");
} }
public UUID getOldLeaderId() { return oldLeaderId; } /** L'ancien chef. Vide si l'équipe était leaderless avant. */
public UUID getNewLeaderId() { return newLeaderId; } public Optional<UUID> getOldLeaderId() {
return Optional.ofNullable(oldLeaderId);
}
public UUID getNewLeaderId() {
return newLeaderId;
}
@Override @Override
public HandlerList getHandlers() { public HandlerList getHandlers() {