feat: chef commands moved to admin + PlaceholderAPI integration

Chef → admin: the chef role no longer grants any command privilege.
All team-management subcommands now take <team> as an argument and are
gated by their crcore.team.<action> permission only:
- add <team> <player>
- remove <team> <player>
- transfer <team> <player>
- visibility <team> <PUBLIC|PRIVATE>
- setspawn <team> (still player-only — needs admin's location)

The LEADER role is kept in the data model (Team / TeamMember) and remains
usable by game plugins via the API, but does not unlock any default
command. Future work can re-introduce chef-specific commands if needed.

PlaceholderAPI: auto-detected at CRCore.enable(). If the PAPI plugin is
present on the server, CRCorePlaceholderExpansion registers automatically;
otherwise the lib runs without it (no NoClassDefFoundError thanks to the
indirection through doRegisterPlaceholderHook).

Placeholders exposed:
- Team: %crcore_team%, %crcore_team_name/tag/color/color_chat/size/
  visibility/leader_name/total_score%, %crcore_team_score_<name>%
- Player: %crcore_player_score_<name>%, %crcore_player_score_total%

Dependency: me.clip:placeholderapi:2.11.6, scope provided. New repo:
https://repo.extendedclip.com/content/repositories/placeholderapi/.

docs/features.md, decisions.md and the builtin-commands diagram updated to
reflect the simpler admin/player two-tier model and the PAPI section.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Antone Barbaud
2026-06-09 15:05:02 +02:00
parent 002fefdc02
commit 8b7cad3fce
12 changed files with 376 additions and 108 deletions
+46
View File
@@ -367,6 +367,52 @@ Format léger : une décision = un titre + contexte + choix + raison.
bases existantes, ALTER TABLE manuel ou suppression du fichier bases existantes, ALTER TABLE manuel ou suppression du fichier
(les bases d'event sont jetables). (les bases d'event sont jetables).
## 2026-06-09 — Toutes les commandes "chef" deviennent admin (révision)
- **Révision** de la décision "Refonte permissions + modèle admin/chef/joueur"
prise plus tôt aujourd'hui.
- **Choix** : le rôle chef n'apporte plus aucun privilège de commande pour
l'instant. Toutes les opérations de gestion d'équipe (`add`, `remove`,
`transfer`, `visibility`, `setspawn`) deviennent **admin** :
- Signature avec `<team>` en argument (au lieu d'implicite "ma team").
- Permission `crcore.team.<action>` requise.
- Plus de check `isLeader(...)` dans `execute()`.
- **Raison** : le user a explicitement décidé que pour l'instant le chef
n'a pas plus de privilèges qu'un joueur lambda côté commandes. Le rôle
`LEADER` reste dans le modèle de données (utile pour les game plugins qui
pourraient l'exploiter via l'API, ou pour de futures commandes), mais il
ne gate plus rien au niveau du framework de commandes.
- **Conséquences** :
- `TeamRemoveSubCommand` : refuse de retirer le chef (l'admin doit
`setleader` d'abord). Pas un check chef, juste une garde de cohérence.
- `TeamTransferSubCommand` : devient l'équivalent admin "strict" de
`setleader` (membre existant uniquement). Les deux cohabitent ; doc dit
quand préférer l'un ou l'autre.
- `TeamSetSpawnSubCommand` : reste `playerOnly` car nécessite la
`Location` de l'exécutant — mais c'est désormais l'admin qui se place
à l'endroit voulu et tape `/core team setspawn <team>`.
## 2026-06-09 — Intégration PlaceholderAPI (optionnelle, auto-détectée)
- **Choix** : `CRCore.enable()` détecte la présence du plugin PlaceholderAPI
via `pluginManager.getPlugin("PlaceholderAPI")` et enregistre
automatiquement `CRCorePlaceholderExpansion` si présent. Aucune action
requise côté plugin de jeu.
- **Dépendance Maven** : `me.clip:placeholderapi:2.11.6` en scope
`provided` (depuis `https://repo.extendedclip.com/...`). Le jar PAPI
n'est PAS embarqué — c'est un plugin runtime indépendant.
- **Indirection de chargement** : la méthode privée
`doRegisterPlaceholderHook()` isole la référence à
`CRCorePlaceholderExpansion`. Si PAPI est absent, la méthode n'est jamais
appelée et le bytecode référençant `me.clip.placeholderapi.*` n'est pas
vérifié → pas de `NoClassDefFoundError`.
- **Placeholders exposés** :
- Team : `%crcore_team%`, `%crcore_team_name/tag/color/color_chat/size/`
`visibility/leader_name/total_score%`, `%crcore_team_score_<name>%`
- Player : `%crcore_player_score_<name>%`, `%crcore_player_score_total%`
- **Override** : `CRCore.registerPlaceholderHook()` est `protected` — une
sous-classe peut ajouter des placeholders ou skipper la hook.
## 2026-06-09 — Nouvelle commande `/core team setleader` ## 2026-06-09 — Nouvelle commande `/core team setleader`
- **Choix** : ajout de `TeamSetLeaderSubCommand` (`/core team setleader - **Choix** : ajout de `TeamSetLeaderSubCommand` (`/core team setleader
+15 -15
View File
@@ -1,5 +1,5 @@
@startuml builtin-commands-diagram @startuml builtin-commands-diagram
title CR-Core — Default /core team commands (admin / chef / joueur) title CR-Core — Default /core team commands (admin / joueur)
skinparam classAttributeIconSize 0 skinparam classAttributeIconSize 0
hide empty members hide empty members
@@ -11,16 +11,13 @@ package "fr.luc.crcore.command" {
package "fr.luc.crcore.command.builtin" { package "fr.luc.crcore.command.builtin" {
class CoreCommand { class CoreCommand
+ CoreCommand(teamSvc, playerSvc)
}
CoreCommand --|> BaseCommand CoreCommand --|> BaseCommand
package "fr.luc.crcore.command.builtin.team" { package "fr.luc.crcore.command.builtin.team" {
class TeamGroupSubCommand { class TeamGroupSubCommand {
+ TeamGroupSubCommand(service) + TeamGroupSubCommand(service)
# registerDefaults(): void
} }
TeamGroupSubCommand --|> SubCommand TeamGroupSubCommand --|> SubCommand
@@ -28,7 +25,7 @@ package "fr.luc.crcore.command.builtin" {
+ {static} teamByName(service): ArgumentType<Team> + {static} teamByName(service): ArgumentType<Team>
} }
' === ADMIN commands (permission seule) === ' ─── ADMIN commands (permission seule, team par argument) ───
package "admin" <<Rectangle>> { package "admin" <<Rectangle>> {
class TeamCreateSubCommand { class TeamCreateSubCommand {
perm: crcore.team.create perm: crcore.team.create
@@ -46,32 +43,30 @@ package "fr.luc.crcore.command.builtin" {
perm: crcore.team.score perm: crcore.team.score
args: <team> <name> <add|set> <value> args: <team> <name> <add|set> <value>
} }
}
' === CHEF commands (permission + check chef) ===
package "chef" <<Rectangle>> {
class TeamAddSubCommand { class TeamAddSubCommand {
perm: crcore.team.add perm: crcore.team.add
args: <player> args: <team> <player>
} }
class TeamRemoveSubCommand { class TeamRemoveSubCommand {
perm: crcore.team.remove perm: crcore.team.remove
args: <player> args: <team> <player>
} }
class TeamTransferSubCommand { class TeamTransferSubCommand {
perm: crcore.team.transfer perm: crcore.team.transfer
args: <player> args: <team> <player>
} }
class TeamVisibilitySubCommand { class TeamVisibilitySubCommand {
perm: crcore.team.visibility perm: crcore.team.visibility
args: <vis> args: <team> <vis>
} }
class TeamSetSpawnSubCommand { class TeamSetSpawnSubCommand {
perm: crcore.team.setspawn perm: crcore.team.setspawn
args: <team>
playerOnly
} }
} }
' === PLAYER commands === ' ─── PLAYER commands ───
package "player" <<Rectangle>> { package "player" <<Rectangle>> {
class TeamJoinSubCommand { class TeamJoinSubCommand {
perm: crcore.team.join perm: crcore.team.join
@@ -114,6 +109,11 @@ package "fr.luc.crcore.command.builtin" {
} }
note bottom of TeamGroupSubCommand note bottom of TeamGroupSubCommand
Le rôle LEADER reste dans le modèle Team
(utilisable par les game plugins via l'API)
mais n'accorde aucun privilège de commande
dans le set par défaut.
Override d'une feuille : Override d'une feuille :
core.getCoreCommand() core.getCoreCommand()
.findSubCommand("team") .findSubCommand("team")
+73 -15
View File
@@ -335,8 +335,14 @@ Voir [setup.md](setup.md#utilisation-depuis-un-plugin-de-jeu).
sous-classe ou via `replaceSubCommand`. sous-classe ou via `replaceSubCommand`.
**Pas d'aliases courts** : les commandes ont leur nom long uniquement **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 (`/core team create` et pas `/core team c`).
pour réduire la friction de découverte et la confusion.
**Modèle simplifié à 2 rôles** : toutes les opérations de gestion d'équipe
sont **admin** (perm requise + team passée en argument). Les opérations
joueur (`join`, `leave`, `info`, `list`, `top`) sont gated par permission
mais ne nécessitent pas le rôle chef. Le rôle `LEADER` reste présent dans
le modèle de données (utilisable par les game plugins via l'API) mais
n'accorde aucun privilège de commande pour l'instant.
### Arborescence ### Arborescence
@@ -347,12 +353,12 @@ pour réduire la friction de découverte et la confusion.
├── delete <team> [admin] dissoudre une équipe ├── delete <team> [admin] dissoudre une équipe
├── setleader <team> <player> [admin] (re)assigner le chef ├── setleader <team> <player> [admin] (re)assigner le chef
├── score <team> <name> <add|set> <value> [admin] modifier un score ├── score <team> <name> <add|set> <value> [admin] modifier un score
├── add <player> [chef] ajouter à son équipe ├── add <team> <player> [admin] ajouter un joueur
├── remove <player> [chef] retirer de son équipe ├── remove <team> <player> [admin] retirer un joueur
├── transfer <player> [chef] transférer leadership ├── transfer <team> <player> [admin] transfert chef→membre existant
├── visibility <PUBLIC|PRIVATE> [chef] changer visibilité ├── visibility <team> <PUBLIC|PRIVATE> [admin] changer visibilité
├── setspawn [chef] définir le spawn ├── setspawn <team> [admin] spawn à la position de l'admin
├── join <team> [joueur] rejoindre PUBLIC ├── join <team> [joueur] rejoindre une PUBLIC
├── leave [joueur] quitter son équipe ├── leave [joueur] quitter son équipe
├── info [team] [joueur] infos ├── info [team] [joueur] infos
├── list [joueur] toutes les équipes ├── list [joueur] toutes les équipes
@@ -361,13 +367,12 @@ pour réduire la friction de découverte et la confusion.
### Permissions ### Permissions
Chaque sous-commande a une permission `crcore.team.<action>`. Modèle à 3 niveaux : Chaque sous-commande a une permission `crcore.team.<action>` :
| Niveau | Commandes | Comportement | | Niveau | Commandes |
|---|---|---| |---|---|
| **Admin** | `create`, `delete`, `setleader`, `score` | Permission seule (pas de check chef). Cible une team via argument. | | **Admin** | `create`, `delete`, `setleader`, `score`, `add`, `remove`, `transfer`, `visibility`, `setspawn` |
| **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` |
| **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 |
|---|---| |---|---|
@@ -512,7 +517,60 @@ db.table("my_kills")
--- ---
## 7. Bootstrap `CRCore` ## 7. Intégration PlaceholderAPI (optionnelle)
**Statut** : implémentée. Auto-détectée par `CRCore.enable()` — si le plugin
**PlaceholderAPI** est installé sur le serveur, les placeholders `%crcore_*%`
sont enregistrés automatiquement. Si PAPI est absent, la lib reste
fonctionnelle, juste sans placeholders.
### Placeholders Team
Renvoient vides si le joueur n'est dans aucune équipe.
| Placeholder | Renvoie | Exemple |
|---|---|---|
| `%crcore_team%` | récap formaté coloré | `§c[#WOLF] Wolves` |
| `%crcore_team_name%` | nom de l'équipe | `Wolves` |
| `%crcore_team_tag%` | tag court | `WOLF` |
| `%crcore_team_color%` | nom de la couleur | `Red` |
| `%crcore_team_color_chat%` | code couleur ChatColor | `§c` |
| `%crcore_team_size%` | nombre de membres | `5` |
| `%crcore_team_visibility%` | `PUBLIC` ou `PRIVATE` | `PRIVATE` |
| `%crcore_team_leader_name%` | nom du chef (vide si leaderless) | `Alice` |
| `%crcore_team_total_score%` | somme des scores de l'équipe | `42` |
| `%crcore_team_score_<name>%` | score nommé de l'équipe | `%crcore_team_score_kills%``12` |
### Placeholders Player
| Placeholder | Renvoie |
|---|---|
| `%crcore_player_score_<name>%` | score nommé du joueur (0 si pas set) |
| `%crcore_player_score_total%` | somme de tous les scores du joueur |
### Usage côté plugin de jeu / config
Pas d'action à faire côté plugin de jeu — la hook s'enregistre toute seule.
Les placeholders sont disponibles partout où PAPI les résout (scoreboard,
tablist, chat, hologrammes via DecentHolograms, etc.) :
```yaml
# Exemple de scoreboard config (FeatherBoard / Scoreboard plugin)
lines:
- "&aÉquipe : %crcore_team%"
- "&aChef : %crcore_team_leader_name%"
- "&aKills : %crcore_player_score_kills% (total équipe %crcore_team_score_kills%)"
```
### Override
`CRCore.registerPlaceholderHook()` est `protected`. Override dans une
sous-classe de `CRCore` pour ajouter ses propres placeholders ou désactiver
la hook.
---
## 8. Bootstrap `CRCore`
**Statut** : implémenté. Point d'entrée unique pour les plugins de jeu. **Statut** : implémenté. Point d'entrée unique pour les plugins de jeu.
+1
View File
@@ -5,6 +5,7 @@
- **Type** : librairie Java (`jar`) — pas un plugin Bukkit - **Type** : librairie Java (`jar`) — pas un plugin Bukkit
- **artifactId Maven** : `CR-Core` - **artifactId Maven** : `CR-Core`
- **Build** : Maven, Java 11 - **Build** : Maven, Java 11
- **Intégrations optionnelles** : PlaceholderAPI (auto-détectée si installée)
- **API serveur (provided)** : Paper 1.16.5 - **API serveur (provided)** : Paper 1.16.5
- **SQLite (compile)** : `org.xerial:sqlite-jdbc:3.45.3.0` - **SQLite (compile)** : `org.xerial:sqlite-jdbc:3.45.3.0`
- **Package racine** : `fr.luc.crcore` - **Package racine** : `fr.luc.crcore`
+16
View File
@@ -37,6 +37,10 @@
<id>sonatype</id> <id>sonatype</id>
<url>https://oss.sonatype.org/content/groups/public/</url> <url>https://oss.sonatype.org/content/groups/public/</url>
</repository> </repository>
<repository>
<id>placeholderapi</id>
<url>https://repo.extendedclip.com/content/repositories/placeholderapi/</url>
</repository>
</repositories> </repositories>
<dependencies> <dependencies>
@@ -58,6 +62,18 @@
<version>${sqlite.version}</version> <version>${sqlite.version}</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<!--
PlaceholderAPI — intégration OPTIONNELLE. Scope provided : si PAPI
est installé sur le serveur, on enregistre nos placeholders ; sinon,
la hook reste dormante. Le plugin de jeu n'a pas besoin de l'ajouter
lui-même.
-->
<dependency>
<groupId>me.clip</groupId>
<artifactId>placeholderapi</artifactId>
<version>2.11.6</version>
<scope>provided</scope>
</dependency>
</dependencies> </dependencies>
<!-- <!--
+32
View File
@@ -117,11 +117,43 @@ public class CRCore {
this.coreCommand = buildCoreCommand(teamService, playerProfileService); this.coreCommand = buildCoreCommand(teamService, playerProfileService);
registerCommand(); registerCommand();
registerPlaceholderHook();
plugin.getLogger().info("CR-Core activé."); plugin.getLogger().info("CR-Core activé.");
enabled = true; enabled = true;
return this; return this;
} }
/**
* Enregistre l'expansion PlaceholderAPI {@code %crcore_*%} si le plugin
* PAPI est installé sur le serveur. Si absent, no-op silencieux — la
* lib reste fonctionnelle sans.
*
* <p>Le chargement de la classe d'expansion est différé via une indirection
* (méthode {@code doRegisterPlaceholderHook}) pour que le bytecode
* référençant {@code me.clip.placeholderapi.*} ne soit pas vérifié si
* PAPI n'est pas présent.
*/
protected void registerPlaceholderHook() {
if (plugin.getServer().getPluginManager().getPlugin("PlaceholderAPI") == null) {
return;
}
try {
doRegisterPlaceholderHook();
plugin.getLogger().info("PlaceholderAPI détecté — placeholders %crcore_*% enregistrés.");
} catch (Throwable t) {
plugin.getLogger().warning(
"Échec d'enregistrement des placeholders PAPI : " + t.getMessage());
}
}
/** Indirection pour différer le chargement des classes PAPI (cf. {@link #registerPlaceholderHook}). */
private void doRegisterPlaceholderHook() {
new fr.luc.crcore.placeholder.CRCorePlaceholderExpansion(
teamService, playerProfileService, plugin.getDescription().getVersion()
).register();
}
/** Libère les ressources (ferme la DB notamment). Idempotent. */ /** Libère les ressources (ferme la DB notamment). Idempotent. */
public void disable() { public void disable() {
if (!enabled) return; if (!enabled) return;
@@ -11,10 +11,10 @@ import org.bukkit.entity.Player;
import java.util.Objects; import java.util.Objects;
/** /**
* {@code /core team add <player>} * {@code /core team add <team> <player>}
* *
* <p>Le chef ajoute un joueur à son équipe. Marche que la team soit PUBLIC ou * <p><b>Admin uniquement</b>. Ajoute un joueur connecté à l'équipe spécifiée,
* PRIVATE — c'est une action chef, pas un auto-join. * quelle que soit sa visibilité.
*/ */
public class TeamAddSubCommand extends SubCommand { public class TeamAddSubCommand extends SubCommand {
@@ -23,24 +23,16 @@ public class TeamAddSubCommand extends SubCommand {
public TeamAddSubCommand(TeamService service) { public TeamAddSubCommand(TeamService service) {
super("add"); 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 à une équipe (admin)");
permission("crcore.team.add"); permission("crcore.team.add");
playerOnly(); argument("team", TeamArgumentTypes.teamByName(service));
argument("player", ArgumentTypes.ONLINE_PLAYER); argument("player", ArgumentTypes.ONLINE_PLAYER);
} }
@Override @Override
public CommandResult execute(CommandContext ctx) { public CommandResult execute(CommandContext ctx) {
Player executor = ctx.requirePlayer(); Team team = ctx.get("team");
Player target = ctx.get("player"); Player target = ctx.get("player");
Team team = service.getTeamOfPlayer(executor.getUniqueId()).orElse(null);
if (team == null) {
return CommandResult.failure("Vous n'appartenez à aucune équipe.");
}
if (!team.isLeader(executor.getUniqueId())) {
return CommandResult.failure("Seul le chef peut ajouter des membres.");
}
if (service.getTeamOfPlayer(target.getUniqueId()).isPresent()) { if (service.getTeamOfPlayer(target.getUniqueId()).isPresent()) {
return CommandResult.failure(target.getName() + " est déjà dans une équipe."); return CommandResult.failure(target.getName() + " est déjà dans une équipe.");
} }
@@ -8,15 +8,15 @@ import fr.luc.crcore.team.Team;
import fr.luc.crcore.team.TeamService; import fr.luc.crcore.team.TeamService;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import java.util.Objects; import java.util.Objects;
/** /**
* {@code /core team remove <player>} * {@code /core team remove <team> <player>}
* *
* <p>Le chef retire un membre. Accepte les joueurs offline (utilise leur nom * <p><b>Admin uniquement</b>. Retire un joueur (online ou offline) de
* pour résoudre l'UUID via {@link Bukkit#getOfflinePlayer(String)}). * l'équipe spécifiée. Refuse si le joueur ciblé est le chef — l'admin doit
* d'abord transférer ou réassigner via {@code setleader}.
*/ */
public class TeamRemoveSubCommand extends SubCommand { public class TeamRemoveSubCommand extends SubCommand {
@@ -25,34 +25,27 @@ public class TeamRemoveSubCommand extends SubCommand {
public TeamRemoveSubCommand(TeamService service) { public TeamRemoveSubCommand(TeamService service) {
super("remove"); 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 d'une équipe (admin)");
permission("crcore.team.remove"); permission("crcore.team.remove");
playerOnly(); argument("team", TeamArgumentTypes.teamByName(service));
argument("player", ArgumentTypes.STRING); argument("player", ArgumentTypes.STRING);
} }
@Override @Override
public CommandResult execute(CommandContext ctx) { public CommandResult execute(CommandContext ctx) {
Player executor = ctx.requirePlayer(); Team team = ctx.get("team");
String targetName = ctx.get("player"); String targetName = ctx.get("player");
Team team = service.getTeamOfPlayer(executor.getUniqueId()).orElse(null);
if (team == null) {
return CommandResult.failure("Vous n'appartenez à aucune équipe.");
}
if (!team.isLeader(executor.getUniqueId())) {
return CommandResult.failure("Seul le chef peut retirer des membres.");
}
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
OfflinePlayer target = Bukkit.getOfflinePlayer(targetName); OfflinePlayer target = Bukkit.getOfflinePlayer(targetName);
if (target.getUniqueId().equals(executor.getUniqueId())) {
return CommandResult.failure("Pour quitter l'équipe en tant que chef, transférez d'abord le leadership.");
}
if (!team.hasMember(target.getUniqueId())) { if (!team.hasMember(target.getUniqueId())) {
return CommandResult.failure(targetName + " n'est pas dans votre équipe."); return CommandResult.failure(targetName + " n'est pas dans l'équipe " + team.getName() + ".");
}
if (team.isLeader(target.getUniqueId())) {
return CommandResult.failure(
"Impossible de retirer le chef. Réassignez-le d'abord via /core team setleader.");
} }
service.removeMember(team.getId(), target.getUniqueId()); service.removeMember(team.getId(), target.getUniqueId());
return CommandResult.success(targetName + " retiré de l'équipe."); return CommandResult.success(targetName + " retiré de l'équipe " + team.getName() + ".");
} }
} }
@@ -10,9 +10,11 @@ import org.bukkit.entity.Player;
import java.util.Objects; import java.util.Objects;
/** /**
* {@code /core team setspawn} * {@code /core team setspawn <team>}
* *
* <p>Définit le point de spawn de l'équipe à la position courante du chef. * <p><b>Admin uniquement, player-only</b>. Définit le point de spawn de
* l'équipe spécifiée à la position courante de l'admin (où il/elle se trouve).
* Doit être lancé en jeu — la console n'a pas de location.
*/ */
public class TeamSetSpawnSubCommand extends SubCommand { public class TeamSetSpawnSubCommand extends SubCommand {
@@ -21,22 +23,17 @@ public class TeamSetSpawnSubCommand extends SubCommand {
public TeamSetSpawnSubCommand(TeamService service) { public TeamSetSpawnSubCommand(TeamService service) {
super("setspawn"); 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 d'une équipe (admin, en jeu)");
permission("crcore.team.setspawn"); permission("crcore.team.setspawn");
playerOnly(); playerOnly();
argument("team", TeamArgumentTypes.teamByName(service));
} }
@Override @Override
public CommandResult execute(CommandContext ctx) { public CommandResult execute(CommandContext ctx) {
Player player = ctx.requirePlayer(); Player admin = ctx.requirePlayer();
Team team = service.getTeamOfPlayer(player.getUniqueId()).orElse(null); Team team = ctx.get("team");
if (team == null) { service.setSpawnPoint(team.getId(), admin.getLocation());
return CommandResult.failure("Vous n'appartenez à aucune équipe."); return CommandResult.success("Spawn de " + team.getName() + " défini à votre position.");
}
if (!team.isLeader(player.getUniqueId())) {
return CommandResult.failure("Seul le chef peut définir le spawn.");
}
service.setSpawnPoint(team.getId(), player.getLocation());
return CommandResult.success("Spawn de l'équipe défini à votre position.");
} }
} }
@@ -8,14 +8,19 @@ import fr.luc.crcore.team.Team;
import fr.luc.crcore.team.TeamService; import fr.luc.crcore.team.TeamService;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import java.util.Objects; import java.util.Objects;
/** /**
* {@code /core team transfer <player>} * {@code /core team transfer <team> <player>}
* *
* <p>Le chef transmet son rôle à un autre membre existant de son équipe. * <p><b>Admin uniquement</b>. Transfère le rôle de chef à un membre existant
* de l'équipe. <i>Strict</i> : le joueur cible doit déjà être membre, et
* l'équipe doit avoir un chef actuel.
*
* <p>Pour un cas plus permissif (assigner un chef sur une équipe leaderless,
* ou auto-ajouter un non-membre comme chef), utiliser
* {@code /core team setleader}.
*/ */
public class TeamTransferSubCommand extends SubCommand { public class TeamTransferSubCommand extends SubCommand {
@@ -24,29 +29,29 @@ 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 (chef uniquement)"); description("Transférer le rôle de chef à un membre (admin, strict)");
permission("crcore.team.transfer"); permission("crcore.team.transfer");
playerOnly(); argument("team", TeamArgumentTypes.teamByName(service));
argument("player", ArgumentTypes.STRING); argument("player", ArgumentTypes.STRING);
} }
@Override @Override
public CommandResult execute(CommandContext ctx) { public CommandResult execute(CommandContext ctx) {
Player executor = ctx.requirePlayer(); Team team = ctx.get("team");
String targetName = ctx.get("player"); String targetName = ctx.get("player");
Team team = service.getTeamOfPlayer(executor.getUniqueId()).orElse(null);
if (team == null) {
return CommandResult.failure("Vous n'appartenez à aucune équipe.");
}
if (!team.isLeader(executor.getUniqueId())) {
return CommandResult.failure("Seul le chef peut transférer le leadership.");
}
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
OfflinePlayer target = Bukkit.getOfflinePlayer(targetName); OfflinePlayer target = Bukkit.getOfflinePlayer(targetName);
if (!team.hasMember(target.getUniqueId())) { if (!team.hasMember(target.getUniqueId())) {
return CommandResult.failure(targetName + " n'est pas dans votre équipe."); return CommandResult.failure(
targetName + " n'est pas membre de " + team.getName() +
" — utilisez /core team setleader pour un cas plus général.");
} }
try {
service.transferLeadership(team.getId(), target.getUniqueId()); service.transferLeadership(team.getId(), target.getUniqueId());
return CommandResult.success("Leadership transféré à " + targetName + "."); } catch (IllegalStateException ex) {
return CommandResult.failure(ex.getMessage());
}
return CommandResult.success(targetName + " est désormais chef de " + team.getName() + ".");
} }
} }
@@ -7,15 +7,14 @@ 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 fr.luc.crcore.team.TeamVisibility; import fr.luc.crcore.team.TeamVisibility;
import org.bukkit.entity.Player;
import java.util.Objects; import java.util.Objects;
/** /**
* {@code /core team visibility <PUBLIC|PRIVATE>} * {@code /core team visibility <team> <PUBLIC|PRIVATE>}
* *
* <p>Le chef change la visibilité de son équipe. PUBLIC = les autres joueurs * <p><b>Admin uniquement</b>. Change la visibilité d'une équipe. PUBLIC permet
* peuvent rejoindre avec {@code /core team join}. * aux joueurs de la rejoindre avec {@code /core team join}.
*/ */
public class TeamVisibilitySubCommand extends SubCommand { public class TeamVisibilitySubCommand extends SubCommand {
@@ -24,24 +23,17 @@ public class TeamVisibilitySubCommand extends SubCommand {
public TeamVisibilitySubCommand(TeamService service) { public TeamVisibilitySubCommand(TeamService service) {
super("visibility"); super("visibility");
this.service = Objects.requireNonNull(service, "service"); this.service = Objects.requireNonNull(service, "service");
description("Changer la visibilité de son équipe (chef uniquement)"); description("Changer la visibilité d'une équipe (admin)");
permission("crcore.team.visibility"); permission("crcore.team.visibility");
playerOnly(); argument("team", TeamArgumentTypes.teamByName(service));
argument("visibility", ArgumentTypes.enumOf(TeamVisibility.class)); argument("visibility", ArgumentTypes.enumOf(TeamVisibility.class));
} }
@Override @Override
public CommandResult execute(CommandContext ctx) { public CommandResult execute(CommandContext ctx) {
Player player = ctx.requirePlayer(); Team team = ctx.get("team");
TeamVisibility visibility = ctx.get("visibility"); TeamVisibility visibility = ctx.get("visibility");
Team team = service.getTeamOfPlayer(player.getUniqueId()).orElse(null);
if (team == null) {
return CommandResult.failure("Vous n'appartenez à aucune équipe.");
}
if (!team.isLeader(player.getUniqueId())) {
return CommandResult.failure("Seul le chef peut changer la visibilité.");
}
service.setVisibility(team.getId(), visibility); service.setVisibility(team.getId(), visibility);
return CommandResult.success("Visibilité réglée sur " + visibility + "."); return CommandResult.success("Visibilité de " + team.getName() + " réglée sur " + visibility + ".");
} }
} }
@@ -0,0 +1,136 @@
package fr.luc.crcore.placeholder;
import fr.luc.crcore.player.PlayerProfileService;
import fr.luc.crcore.team.Team;
import fr.luc.crcore.team.TeamService;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import java.util.Objects;
import java.util.Optional;
/**
* Expansion PlaceholderAPI exposant les données CR-Core (équipe et profil
* joueur) via des placeholders {@code %crcore_*%}.
*
* <h2>Placeholders Team</h2>
* <ul>
* <li>{@code %crcore_team%} — récap formaté avec couleur :
* {@code §c[#WOLF] Wolves}</li>
* <li>{@code %crcore_team_name%} — nom de l'équipe</li>
* <li>{@code %crcore_team_tag%} — tag court</li>
* <li>{@code %crcore_team_color%} — nom de la couleur ({@code Red},
* {@code Blue}, …)</li>
* <li>{@code %crcore_team_color_chat%} — code couleur ChatColor (§c, §9, …)</li>
* <li>{@code %crcore_team_size%} — nombre de membres</li>
* <li>{@code %crcore_team_visibility%} — {@code PUBLIC} ou {@code PRIVATE}</li>
* <li>{@code %crcore_team_leader_name%} — nom du chef (vide si leaderless)</li>
* <li>{@code %crcore_team_score_<name>%} — score nommé de l'équipe
* (ex. {@code %crcore_team_score_kills%})</li>
* </ul>
*
* <h2>Placeholders Player</h2>
* <ul>
* <li>{@code %crcore_player_score_<name>%} — score nommé du joueur
* (ex. {@code %crcore_player_score_kills%})</li>
* <li>{@code %crcore_player_score_total%} — somme de tous les scores du joueur</li>
* </ul>
*
* <p>Si le joueur n'est dans aucune équipe, tous les {@code %crcore_team_*%}
* renvoient une chaîne vide. Si le placeholder est inconnu, on renvoie
* {@code null} → PAPI laisse le placeholder brut.
*/
public class CRCorePlaceholderExpansion extends PlaceholderExpansion {
private final TeamService teamService;
private final PlayerProfileService playerProfileService;
private final String version;
public CRCorePlaceholderExpansion(TeamService teamService,
PlayerProfileService playerProfileService,
String version) {
this.teamService = Objects.requireNonNull(teamService, "teamService");
this.playerProfileService = Objects.requireNonNull(playerProfileService, "playerProfileService");
this.version = Objects.requireNonNullElse(version, "1.0");
}
@Override
public String getIdentifier() {
return "crcore";
}
@Override
public String getAuthor() {
return "luc";
}
@Override
public String getVersion() {
return version;
}
/** Garde l'enregistrement vivant à travers les {@code /papi reload}. */
@Override
public boolean persist() {
return true;
}
@Override
public String onPlaceholderRequest(Player player, String params) {
if (player == null || params == null) return "";
String p = params.toLowerCase();
// Player scores : %crcore_player_score_<name>% / %crcore_player_score_total%
if (p.startsWith("player_score_")) {
String scoreName = p.substring("player_score_".length());
if (scoreName.equals("total")) {
return playerProfileService.getProfile(player.getUniqueId())
.map(profile -> String.valueOf(profile.getTotalScore()))
.orElse("0");
}
return String.valueOf(playerProfileService.getScore(player.getUniqueId(), scoreName));
}
// Team placeholders : récap %crcore_team% ou détail %crcore_team_*%
Optional<Team> teamOpt = teamService.getTeamOfPlayer(player.getUniqueId());
if (p.equals("team")) {
return teamOpt.map(team ->
team.getColor().getChatColor() + "[#" + team.getTag() + "] " + team.getName()
).orElse("");
}
if (p.startsWith("team_")) {
if (teamOpt.isEmpty()) return "";
Team team = teamOpt.get();
String key = p.substring("team_".length());
switch (key) {
case "name": return team.getName();
case "tag": return team.getTag();
case "color": return team.getColor().getDisplayName();
case "color_chat": return team.getColor().getChatColor().toString();
case "size": return String.valueOf(team.size());
case "visibility": return team.getVisibility().name();
case "leader_name": return resolveLeaderName(team);
case "total_score": return String.valueOf(team.getTotalScore());
}
if (key.startsWith("score_")) {
String scoreName = key.substring("score_".length());
return String.valueOf(team.getScore(scoreName));
}
}
// Placeholder inconnu — laisse PAPI le rendre brut.
return null;
}
private String resolveLeaderName(Team team) {
return team.getLeaderId()
.map(id -> {
OfflinePlayer leader = Bukkit.getOfflinePlayer(id);
String name = leader.getName();
return name != null ? name : "";
})
.orElse("");
}
}