ffc77c4213
Pure Maven library for CR Minecraft game plugins, targeting Paper 1.16.5. Common abstractions (fr.luc.crcore.common): Identifiable, Named, ScoreHolder, AbstractEntity, Repository<T>. Team domain (fr.luc.crcore.team): Team entity with name/tag/color/leader/ visibility (PUBLIC|PRIVATE)/members/scores/spawn point, TeamMember, TeamRole/TeamColor/TeamVisibility enums, TeamRanking record, TeamService with overridable hooks (factories, validations, lifecycle events), in-memory repository, dedicated exception hierarchy. Player domain (fr.luc.crcore.player): PlayerProfile with named scores per player, PlayerProfileService with auto-creation, individual rankings, exception hierarchy. Both Team and PlayerProfile implement ScoreHolder. Command framework (fr.luc.crcore.command): Command interface, AbstractCommand base, BaseCommand (CommandExecutor + TabCompleter), SubCommand, CommandContext, CommandResult, ArgumentType<T> + ArgumentTypes catalogue (STRING, INTEGER, DOUBLE, BOOLEAN, ONLINE_PLAYER, enumOf, choice). Docs (docs/) is the single source of truth: README, setup, features, decisions log, and 6 PlantUML diagrams (team class/sequence/activity/join, player class, command class). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
313 lines
14 KiB
Markdown
313 lines
14 KiB
Markdown
# Domaines fonctionnels
|
|
|
|
CR-Core est une librairie. Chaque domaine est autonome ; le plugin de jeu
|
|
downstream pioche ce qu'il utilise.
|
|
|
|
---
|
|
|
|
## 1. Domaine Team
|
|
|
|
**Statut** : modèle + service implémenté (repository en mémoire). Overridable
|
|
étape par étape via hooks et factories.
|
|
|
|
### Définition d'une équipe (`Team`)
|
|
|
|
| Attribut | Type | Description |
|
|
|---|---|---|
|
|
| `id` | `UUID` | Identifiant interne unique, généré automatiquement. |
|
|
| `name` | `String` | Nom lisible. Unique (case-insensitive). |
|
|
| `tag` | `String` | Tag court (le « # »). Unique. Affiché entre `[# … ]`. |
|
|
| `color` | `TeamColor` | Couleur associée (enum). |
|
|
| `leaderId` | `UUID` | Identifiant du joueur **chef d'équipe**. |
|
|
| `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). |
|
|
|
|
### Membre (`TeamMember`)
|
|
|
|
| Attribut | Type | Description |
|
|
|---|---|---|
|
|
| `playerId` | `UUID` | UUID du joueur Bukkit (= `id` de l'entité). |
|
|
| `role` | `TeamRole` | `LEADER` ou `MEMBER`. |
|
|
| `joinedAt` | `Instant` | Date d'entrée. |
|
|
|
|
### Enums
|
|
|
|
- **`TeamRole`** : `LEADER`, `MEMBER`.
|
|
- **`TeamVisibility`** : `PUBLIC`, `PRIVATE`. Méthodes `isPublic()`, `isPrivate()`.
|
|
- **`TeamColor`** : 16 valeurs, chacune expose `ChatColor`, `DyeColor`,
|
|
`displayName`.
|
|
|
|
### Règles d'intégrité
|
|
|
|
1. Une équipe a **exactement un chef** à tout instant.
|
|
2. Un joueur appartient à **au plus une équipe** (au sein du registre du
|
|
plugin de jeu — chaque plugin a son propre registre).
|
|
3. `name` et `tag` sont uniques (case-insensitive) dans le registre.
|
|
4. Le chef ne peut pas être retiré sans `transferLeadership` préalable.
|
|
5. Une équipe `PRIVATE` ne peut être rejointe que via `addMember` (action du
|
|
chef) ; une équipe `PUBLIC` peut être rejointe via `joinTeam` (action du
|
|
joueur lui-même).
|
|
|
|
### Opérations (`TeamService`)
|
|
|
|
| Opération | Description |
|
|
|---|---|
|
|
| `createTeam(name, tag, color, leaderId)` | Crée une équipe `PRIVATE` avec ce joueur comme chef. Échoue si nom/tag/joueur déjà pris. |
|
|
| `createTeam(name, tag, color, leaderId, visibility)` | Surcharge : permet de créer directement en `PUBLIC` ou `PRIVATE`. |
|
|
| `dissolveTeam(teamId)` | Supprime l'équipe. |
|
|
| `addMember(teamId, playerId)` | **Action du chef** : ajoute un joueur comme `MEMBER` (marche en PUBLIC comme en PRIVATE). |
|
|
| `removeMember(teamId, playerId)` | Retire un joueur (interdit sur le chef). |
|
|
| `joinTeam(teamId, playerId)` | **Action du joueur** : auto-rejoindre une équipe `PUBLIC`. Lève `TeamAccessException` si la team est `PRIVATE` ou si le joueur est déjà dans une équipe. |
|
|
| `transferLeadership(teamId, newLeaderId)` | Le nouveau chef doit déjà être membre. |
|
|
| `setVisibility(teamId, visibility)` | Change la visibilité (typiquement appelée par le chef). |
|
|
| `addScore(teamId, name, delta)` / `setScore` / `getScore` / `resetScore` / `resetAllScores` | Gestion des scores (voir section Scores). |
|
|
| `getRankingByScore(name)` / `getGlobalRanking()` / `getTopRankingByScore(name, n)` / `getTopGlobalRanking(n)` | Classements (voir section Classements). |
|
|
| `setSpawnPoint(teamId, loc)` / `clearSpawnPoint(teamId)` / `getSpawnPoint(teamId)` | Point de spawn (voir section Spawn). |
|
|
| `getTeam` / `getTeamByName` / `getTeamByTag` / `getTeamOfPlayer` | Recherches. |
|
|
| `getAllTeams()` | Toutes les équipes. |
|
|
|
|
### Scores
|
|
|
|
Chaque équipe porte une `Map<String, Integer>` de scores nommés. Le nom du
|
|
score est libre (`"kills"`, `"objectives"`, `"global"`, …). Un jeu qui n'a
|
|
besoin que d'un seul score peut utiliser un nom unique (typiquement
|
|
`"global"`).
|
|
|
|
| Opération sur `Team` | Description |
|
|
|---|---|
|
|
| `getScore(name)` | Valeur courante (0 si jamais set). |
|
|
| `hasScore(name)` | `true` si le score a été initialisé au moins une fois. |
|
|
| `getScores()` | Map immuable de tous les scores. |
|
|
| `getTotalScore()` | Somme de tous les scores (utilisée pour le classement global). |
|
|
| `addScore(name, delta)` | Incrémente (ou décrémente avec un delta négatif), renvoie la nouvelle valeur. |
|
|
| `setScore(name, value)` | Affecte une valeur absolue. |
|
|
| `resetScore(name)` | Supprime un score (revient à 0). |
|
|
| `resetAllScores()` | Vide tous les scores. |
|
|
|
|
Côté `TeamService` : mêmes opérations préfixées du `teamId` (`addScore(teamId,
|
|
name, delta)` etc.). Hook `onScoreChanged(team, name, oldValue, newValue)`
|
|
appelé uniquement quand la valeur change réellement.
|
|
|
|
### Classements (rankings)
|
|
|
|
Le service expose deux types de classements :
|
|
|
|
| Méthode | Description |
|
|
|---|---|
|
|
| `getRankingByScore(name)` | Classement par un score précis (descendant). |
|
|
| `getGlobalRanking()` | Classement par la somme des scores de chaque équipe. |
|
|
| `getTopRankingByScore(name, n)` | Top N par score. |
|
|
| `getTopGlobalRanking(n)` | Top N global. |
|
|
|
|
Le résultat est une `List<TeamRanking>` (record Java 16) avec `rank` (1-based),
|
|
`team` et `score`. Tiebreaker : ordre alphabétique sur le nom de l'équipe
|
|
(insensible à la casse).
|
|
|
|
La méthode `protected rank(ToIntFunction<Team>)` est exposée pour permettre à
|
|
une sous-classe de créer ses propres classements (ex. score pondéré, ratio
|
|
kills/deaths, score borné). La factory `newRanking(rank, team, score)`
|
|
permet de retourner un type custom étendant `TeamRanking` (les records étant
|
|
final, on peut wrapper plutôt qu'étendre).
|
|
|
|
### Spawn point
|
|
|
|
Chaque équipe peut avoir un `Location` Bukkit comme point de spawn. Optionnel
|
|
(défaut : pas de spawn défini).
|
|
|
|
| Opération sur `Team` | Description |
|
|
|---|---|
|
|
| `getSpawnPoint()` | `Optional<Location>` (clonée — modifier l'objet retourné n'affecte pas l'équipe). |
|
|
| `hasSpawnPoint()` | `true` si un spawn a été défini. |
|
|
| `setSpawnPoint(location)` | Définit le spawn (cloné à l'entrée). `null` accepté = clear. |
|
|
| `clearSpawnPoint()` | Supprime le spawn. |
|
|
|
|
Côté `TeamService` : `setSpawnPoint(teamId, location)`,
|
|
`clearSpawnPoint(teamId)`, `getSpawnPoint(teamId)`. Hook
|
|
`onSpawnPointChanged(team, oldLocation, newLocation)`.
|
|
|
|
> **Persistance** : `Location` référence un `World` Bukkit, donc ce n'est pas
|
|
> trivialement sérialisable. Pour l'instant on stocke en mémoire ; quand on
|
|
> branchera une persistance fichier, on utilisera `Location.serialize()` /
|
|
> `deserialize()` de l'API Bukkit (`ConfigurationSerializable`).
|
|
|
|
### Hooks d'override (sur `TeamServiceImpl`)
|
|
|
|
| Hook | Quand |
|
|
|---|---|
|
|
| `newTeam(id, name, tag, color, leaderId, visibility)` | Factory — instancier une sous-classe de `Team`. |
|
|
| `newRanking(rank, team, score)` | Factory — wrapper / sous-classe de `TeamRanking`. |
|
|
| `rank(scoreFn)` | Logique de tri du classement (override pour pondération, tiebreaker custom, etc.). |
|
|
| `validateName(name)` | Avant création — règles custom sur le nom. |
|
|
| `validateTag(tag)` | Avant création — règles custom sur le tag. |
|
|
| `validateLeader(leaderId)` | Avant création — règles custom sur l'éligibilité du chef. |
|
|
| `validateJoinable(team, playerId)` | Avant `joinTeam` — règles custom. |
|
|
| `onBeforeSave(team)` | Juste avant le `repository.save`. |
|
|
| `onAfterCreate(team)` | Juste après une création réussie. |
|
|
| `onBeforeDissolve(team)` / `onAfterDissolve(team)` | Autour de la dissolution. |
|
|
| `onMemberAdded(team, member)` | Après ajout d'un membre (via `addMember` OU `joinTeam`). |
|
|
| `onMemberRemoved(team, playerId)` | Après retrait d'un membre. |
|
|
| `onPlayerJoined(team, member)` | Spécifique à `joinTeam` (en plus de `onMemberAdded`). |
|
|
| `onLeadershipTransferred(team, oldId, newId)` | Après transfert. |
|
|
| `onVisibilityChanged(team, oldV, newV)` | Après changement de visibilité. |
|
|
| `onScoreChanged(team, scoreName, oldV, newV)` | Après changement effectif d'un score. |
|
|
| `onSpawnPointChanged(team, oldLoc, newLoc)` | Après changement du point de spawn. |
|
|
|
|
Côté `Team`, factory `newMember(playerId, role)` pour utiliser un `TeamMember`
|
|
custom dans une sous-classe.
|
|
|
|
### Exceptions
|
|
|
|
- `TeamException` (base, `RuntimeException`).
|
|
- `TeamAlreadyExistsException` : nom, tag ou joueur déjà pris.
|
|
- `TeamNotFoundException` : équipe introuvable.
|
|
- `TeamAccessException` : auto-join refusé (team `PRIVATE`, joueur déjà dans
|
|
une équipe, ou règle custom dans `validateJoinable`).
|
|
|
|
### Persistance
|
|
|
|
`InMemoryTeamRepository` (Map<UUID, Team>) par défaut. Le contrat
|
|
`TeamRepository extends Repository<Team>` permet de brancher YAML / SQLite /
|
|
Postgres sans toucher au service.
|
|
|
|
### Diagrammes
|
|
|
|
- Classes : [team-class-diagram.puml](diagrams/team-class-diagram.puml)
|
|
- Séquence création : [team-create-sequence.puml](diagrams/team-create-sequence.puml)
|
|
- Séquence auto-join : [team-join-sequence.puml](diagrams/team-join-sequence.puml)
|
|
- Activité création : [team-create-activity.puml](diagrams/team-create-activity.puml)
|
|
|
|
---
|
|
|
|
## 2. Profils joueurs et scores individuels
|
|
|
|
**Statut** : modèle + service implémenté (en mémoire). Symétrique au domaine
|
|
Team pour tout ce qui touche aux scores et classements.
|
|
|
|
### Pourquoi un domaine séparé
|
|
|
|
Les scores joueur vivent en dehors de l'équipe : un joueur peut changer
|
|
d'équipe, en quitter une, en rejoindre une autre — son profil et ses scores
|
|
persistent. Le domaine `player` est indépendant du domaine `team` ; libre au
|
|
plugin de jeu de les combiner (ex. score d'équipe = somme des scores des
|
|
membres).
|
|
|
|
### `PlayerProfile`
|
|
|
|
| Attribut | Type | Description |
|
|
|---|---|---|
|
|
| `id` | `UUID` | UUID Bukkit du joueur (= `id` de l'entité). |
|
|
| `scores` | `Map<String, Integer>` | Scores nommés (`"kills"`, `"deaths"`, `"global"`, …). |
|
|
|
|
`PlayerProfile implements ScoreHolder` — la même interface que `Team`. Toutes
|
|
les méthodes de scoring (`getScore`, `addScore`, `setScore`, `resetScore`,
|
|
`resetAllScores`, `getTotalScore`, `getScores`, `hasScore`) sont identiques
|
|
à celles de `Team`.
|
|
|
|
### `PlayerProfileService`
|
|
|
|
| Opération | Description |
|
|
|---|---|
|
|
| `getOrCreateProfile(playerId)` | Retourne le profil existant ou en crée un. Utilisé en interne par les méthodes de scoring (auto-création à la première écriture). |
|
|
| `getProfile(playerId)` | `Optional<PlayerProfile>` sans création. |
|
|
| `deleteProfile(playerId)` | Supprime un profil. |
|
|
| `getAllProfiles()` | Tous les profils. |
|
|
| `addScore` / `setScore` / `getScore` / `resetScore` / `resetAllScores` | Identiques au service Team mais par `playerId`. |
|
|
| `getRankingByScore(name)` / `getGlobalRanking()` / `getTopRankingByScore(name, n)` / `getTopGlobalRanking(n)` | Classements de joueurs. |
|
|
|
|
### `PlayerRanking`
|
|
|
|
Record Java 16 : `record PlayerRanking(int rank, PlayerProfile profile, int score)`.
|
|
Mêmes règles que `TeamRanking` (rank 1-based, tri descendant, tiebreaker par
|
|
UUID pour rester déterministe).
|
|
|
|
### Hooks d'override (sur `PlayerProfileServiceImpl`)
|
|
|
|
| Hook | Quand |
|
|
|---|---|
|
|
| `newProfile(playerId)` | Factory — instancier une sous-classe de `PlayerProfile`. |
|
|
| `newRanking(rank, profile, score)` | Factory — sous-classe de `PlayerRanking`. |
|
|
| `rank(scoreFn)` | Logique de tri (override pour pondération, tiebreaker custom, etc.). |
|
|
| `onProfileCreated(profile)` | Après création (lazy ou explicite). |
|
|
| `onProfileDeleted(profile)` | Après suppression. |
|
|
| `onScoreChanged(profile, name, oldV, newV)` | Après changement effectif d'un score. |
|
|
|
|
### Exceptions
|
|
|
|
- `PlayerException` (base, `RuntimeException`).
|
|
- `PlayerProfileNotFoundException` : profil introuvable (peu utilisé côté
|
|
service car la plupart des opérations auto-créent).
|
|
|
|
### Diagrammes
|
|
|
|
- Classes : [player-class-diagram.puml](diagrams/player-class-diagram.puml)
|
|
|
|
---
|
|
|
|
## 3. Framework de commandes
|
|
|
|
**Statut** : framework implémenté. Pas de commande Team intégrée — c'est au
|
|
plugin de jeu de définir ses commandes en utilisant les briques fournies.
|
|
|
|
### Architecture
|
|
|
|
- **`Command`** (interface) — contrat partagé : `getName()`, `getAliases()`,
|
|
`getPermission()`, `isPlayerOnly()`, `getDescription()`, `execute(ctx)`,
|
|
`tabComplete(sender, argIndex, partial)`, `matches(label)`.
|
|
- **`AbstractCommand`** (abstract, implémente `Command`) — porte tous les
|
|
champs : nom, aliases, permission, player-only, description, usage,
|
|
arguments. Méthodes builder en `protected` : `addAlias`, `permission`,
|
|
`playerOnly`, `description`, `usage`, `argument`, `optionalArgument`.
|
|
- **`BaseCommand extends AbstractCommand`** — implémente aussi
|
|
`CommandExecutor` et `TabCompleter` de Bukkit. Conteneur de `SubCommand`,
|
|
fait le routage `args[0]` → sous-commande, gère permissions, player-only,
|
|
invalid usage, affichage de l'aide par défaut.
|
|
- **`SubCommand extends AbstractCommand`** — sous-commande sans logique
|
|
Bukkit. La méthode abstraite `execute(CommandContext)` est à implémenter.
|
|
|
|
### Types d'arguments (`ArgumentTypes`)
|
|
|
|
| Constante / factory | Type produit | Tab-complete |
|
|
|---|---|---|
|
|
| `STRING` | `String` | (aucun) |
|
|
| `INTEGER` | `Integer` | (aucun) |
|
|
| `DOUBLE` | `Double` | (aucun) |
|
|
| `BOOLEAN` | `Boolean` | `true` / `false` |
|
|
| `ONLINE_PLAYER` | `Player` | joueurs connectés |
|
|
| `enumOf(Enum.class)` | l'enum | toutes les valeurs |
|
|
| `choice("a", "b", …)` | `String` | les choix |
|
|
|
|
Un `ArgumentType<T>` custom = une classe qui implémente `parse(String): T` et
|
|
optionnellement `suggestions(sender, partial): List<String>`.
|
|
|
|
### Résultats (`CommandResult`)
|
|
|
|
`SUCCESS`, `FAILURE`, `INVALID_USAGE`, `NO_PERMISSION`, `PLAYER_ONLY` — chacun
|
|
avec un message optionnel. Factories statiques `CommandResult.success(...)`,
|
|
`failure(...)`, `invalidUsage(...)`.
|
|
|
|
Rendus automatiquement par `BaseCommand.handleResult` avec un code couleur
|
|
standard (vert succès, rouge erreur). Override `handleResult` pour personnaliser.
|
|
|
|
### Contexte (`CommandContext`)
|
|
|
|
Wrapper passé à `execute()` :
|
|
|
|
- `getSender()`, `isPlayer()`, `getPlayer()`, `requirePlayer()`
|
|
- `get(name)` — récupère un argument parsé typé (`String name = ctx.get("name")`)
|
|
- `getOptional(name)` / `has(name)`
|
|
- `reply(message)` — raccourci `sender.sendMessage`
|
|
|
|
### Exemple complet
|
|
|
|
Voir [setup.md](setup.md#utilisation-depuis-un-plugin-de-jeu).
|
|
|
|
### Diagramme
|
|
|
|
- Classes : [command-class-diagram.puml](diagrams/command-class-diagram.puml)
|
|
|
|
---
|
|
|
|
## Backlog / idées de futurs domaines
|
|
|
|
_(à remplir — ex. score, profil joueur, gestion d'event/round, ...)_
|