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
@@ -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");
@@ -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()) {
@@ -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 <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 :
* {@link TeamVisibility#PRIVATE}.
* <p><b>Admin uniquement</b>. Crée une équipe en {@link
* 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 {
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.<TeamVisibility>getOptional("visibility")
.orElse(TeamVisibility.PRIVATE);
Optional<Player> 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());
}
@@ -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 <team>}
*
* <p>Dissout l'équipe de l'exécutant. Réservé au chef. Pas d'argument :
* l'équipe ciblée est déduite du joueur.
*
* <p>Aliases : {@code disband}, {@code dissolve}.
* <p><b>Admin uniquement</b>. 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.");
}
@@ -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));
@@ -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));
}
@@ -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));
}
@@ -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 <player>, ou dissolvez avec /core team delete.");
}
@@ -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
@@ -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.");
}
@@ -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;
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());
@@ -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);
}
@@ -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")
@@ -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);
@@ -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
);
+82 -8
View File
@@ -40,21 +40,39 @@ public class Team extends AbstractEntity implements Named, ScoreHolder {
private TeamVisibility visibility;
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) {
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<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() {
@@ -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<TeamMember> getLeader() {
if (leaderId == null) return Optional.empty();
return getMember(leaderId);
}
public Set<TeamMember> 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 <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) {
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} :
* <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 ----
public int getScore(String scoreName) {
@@ -25,8 +25,19 @@ public interface TeamService {
// ---- 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);
/**
* 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 ----
@@ -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");
@@ -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.
*
* <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 {
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<UUID> getOldLeaderId() {
return Optional.ofNullable(oldLeaderId);
}
public UUID getNewLeaderId() {
return newLeaderId;
}
@Override
public HandlerList getHandlers() {