# 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` | 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` 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` (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)` 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` (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) par défaut. Le contrat `TeamRepository extends Repository` 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` | 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` 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` custom = une classe qui implémente `parse(String): T` et optionnellement `suggestions(sender, partial): List`. ### 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, ...)_