feat: initial CR-Core library (team + player + command framework)
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>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
# Documentation CR-Core
|
||||
|
||||
Ce dossier est la **source de vérité** du projet. Toutes les décisions, idées,
|
||||
mécaniques, règles et spécifications du noyau sont consignées ici.
|
||||
|
||||
## Objectif du projet
|
||||
|
||||
**CR-Core** est une **librairie Maven** réutilisable pour construire des plugins
|
||||
Minecraft. Elle fournit :
|
||||
|
||||
- des abstractions communes (`Identifiable`, `Named`, `ScoreHolder`,
|
||||
`AbstractEntity`, `Repository<T>`) ;
|
||||
- un **domaine équipes** clé en main (`Team`, `TeamMember`, `TeamService`,
|
||||
`TeamRepository`) — visibilité, scores, classements, spawn, overridable ;
|
||||
- un **domaine profils joueurs** (`PlayerProfile`, `PlayerProfileService`) —
|
||||
scores nommés par joueur, classements individuels ;
|
||||
- un **framework de commandes** (`BaseCommand`, `SubCommand`, `ArgumentType`,
|
||||
tab-completion intégrée).
|
||||
|
||||
Les plugins de jeu (futur `CitesPlugin`, BedWars, etc.) déclarent CR-Core en
|
||||
dépendance Maven `provided` (ou shadent la lib) et l'utilisent directement.
|
||||
|
||||
- Cible runtime : **Minecraft 1.16.5** (API Paper).
|
||||
- Build : Maven, Java 16.
|
||||
|
||||
## Structure de la documentation
|
||||
|
||||
- `README.md` — Ce fichier. Vue d'ensemble et index.
|
||||
- `setup.md` — Build, intégration dans un plugin de jeu, exemple d'usage.
|
||||
- `features.md` — Domaines fonctionnels (team, command).
|
||||
- `decisions.md` — Journal des décisions importantes (ADR léger).
|
||||
- `diagrams/` — Diagrammes PlantUML (`.puml`).
|
||||
|
||||
## Diagrammes
|
||||
|
||||
| Fichier | Type | Sujet |
|
||||
|---|---|---|
|
||||
| [team-class-diagram.puml](diagrams/team-class-diagram.puml) | Classe | Domaine Team + abstractions communes |
|
||||
| [team-create-sequence.puml](diagrams/team-create-sequence.puml) | Séquence | Création d'une équipe |
|
||||
| [team-join-sequence.puml](diagrams/team-join-sequence.puml) | Séquence | Auto-join sur une équipe publique |
|
||||
| [team-create-activity.puml](diagrams/team-create-activity.puml) | Activité | Flux de validation à la création |
|
||||
| [player-class-diagram.puml](diagrams/player-class-diagram.puml) | Classe | Domaine Player (profils + scores + classements) |
|
||||
| [command-class-diagram.puml](diagrams/command-class-diagram.puml) | Classe | Framework de commandes |
|
||||
|
||||
## Conventions
|
||||
|
||||
- Code : **anglais standard**, séparation stricte interfaces / enums / classes
|
||||
abstraites / classes concrètes / exceptions.
|
||||
- Doc & messages joueur : **français**.
|
||||
- Package racine : `fr.luc.crcore`.
|
||||
- Classes du noyau **non-`final`**, méthodes-clés `protected`, hooks
|
||||
`onBefore…`/`onAfter…` et factories `new…` pour l'override.
|
||||
|
||||
## Contribuer à la doc
|
||||
|
||||
Chaque nouvelle idée, règle, commande ou contrainte discutée doit être ajoutée
|
||||
au fichier approprié, **avant ou pendant** l'implémentation. La doc précède le
|
||||
code.
|
||||
@@ -0,0 +1,234 @@
|
||||
# Journal des décisions
|
||||
|
||||
Format léger : une décision = un titre + contexte + choix + raison.
|
||||
|
||||
## 2026-06-08 — Cible Minecraft 1.16.5 / Paper
|
||||
|
||||
- **Choix** : utiliser l'API Paper 1.16.5 (`paper-api`) plutôt que Spigot.
|
||||
- **Raison** : Paper est un sur-ensemble de Spigot, expose plus d'API utiles
|
||||
pour les évènements, et reste compatible avec un serveur Spigot 1.16.5.
|
||||
- **Conséquence** : un serveur Paper 1.16.5 est recommandé pour le test.
|
||||
|
||||
## 2026-06-08 — Java 16
|
||||
|
||||
- **Choix** : `maven.compiler.source/target = 16`.
|
||||
- **Raison** : Paper 1.16.5 tourne avec un JDK 8–16. Java 16 donne accès aux
|
||||
records, pattern matching simple, etc., tout en restant exécutable sur un
|
||||
serveur 1.16.5.
|
||||
|
||||
## 2026-06-08 — `docs/` = source de vérité
|
||||
|
||||
- **Choix** : toutes les décisions, règles de gameplay, commandes et idées
|
||||
doivent être notées dans `docs/` avant ou pendant l'implémentation.
|
||||
- **Raison** : éviter la dérive entre intention et code, garder une trace
|
||||
partageable des échanges.
|
||||
|
||||
## 2026-06-08 — Code en anglais standard
|
||||
|
||||
- **Choix** : tout le code (classes, méthodes, attributs, variables) est écrit
|
||||
en anglais. La doc utilisateur (`docs/`, messages joueurs) reste en français.
|
||||
- **Raison** : conventions standards du monde Java/Bukkit, lisibilité par tout
|
||||
développeur, cohérence avec l'API Paper.
|
||||
|
||||
## 2026-06-08 — Architecture en couches pour le domaine
|
||||
|
||||
- **Choix** : séparer chaque domaine fonctionnel (ex. `team`) en :
|
||||
- **Interfaces** d'abstraction (ex. `Identifiable`, `Named`, `Repository<T>`,
|
||||
`TeamRepository`, `TeamService`).
|
||||
- **Enums** pour les ensembles fermés (`TeamRole`, `TeamColor`).
|
||||
- **Classe abstraite** commune `AbstractEntity` (gère `id` + `equals/hashCode`).
|
||||
- **Classes concrètes** : entités (`Team`, `TeamMember`), implémentations
|
||||
(`InMemoryTeamRepository`, `TeamServiceImpl`).
|
||||
- **Exceptions** dédiées avec hiérarchie (`TeamException` ➜
|
||||
`TeamAlreadyExistsException`, `TeamNotFoundException`).
|
||||
- **Raison** : testabilité (mocker une interface), évolutivité (changer le
|
||||
backend de persistance sans toucher au service), lisibilité.
|
||||
|
||||
## 2026-06-08 — Persistence en mémoire pour démarrer
|
||||
|
||||
- **Choix** : `InMemoryTeamRepository` (Map<UUID, Team>) comme première
|
||||
implémentation.
|
||||
- **Raison** : permet d'avancer sur le gameplay sans dépendre d'un schéma de
|
||||
stockage. À remplacer par une implémentation YAML/SQLite/Postgres plus tard
|
||||
sans toucher au service.
|
||||
|
||||
## 2026-06-08 — `Team` = entité mutable, `TeamMember` = quasi-immutable
|
||||
|
||||
- **Choix** : `Team` mute (ajouts/retraits de membres, transfert de leadership)
|
||||
; `TeamMember` est immuable, sa transition de rôle passe par `withRole(...)`
|
||||
qui renvoie une nouvelle instance.
|
||||
- **Raison** : un `TeamMember` est identifié par son `playerId` ; il est plus
|
||||
simple de raisonner sur des états successifs en remplaçant l'instance. Le
|
||||
`Set<TeamMember>` reste cohérent car `equals/hashCode` ne dépend que du
|
||||
`playerId`.
|
||||
|
||||
## 2026-06-08 — Renommage du projet : CitesPlugin ➜ CR-Core
|
||||
|
||||
- **Choix** : le projet devient **CR-Core**, un plugin "noyau" réutilisable.
|
||||
Les anciens packages `fr.luc.citesplugin.*` sont déplacés sous `fr.luc.crcore.*`.
|
||||
L'ancien `CitesPlugin` (le jeu lui-même) deviendra un futur plugin séparé qui
|
||||
déclarera `depend: [CR-Core]`.
|
||||
- **Raison** : centraliser les briques transverses (équipes, futurs scores,
|
||||
profils joueurs, etc.) pour pouvoir les réutiliser sur plusieurs plugins de
|
||||
jeu sans dupliquer le code.
|
||||
- **Conséquence** : downstream consomme CR-Core soit via la façade statique
|
||||
`fr.luc.crcore.CR` (ex. `CR.teams()`), soit via le `ServicesManager` Bukkit
|
||||
(`getServicesManager().load(TeamService.class)`).
|
||||
|
||||
## 2026-06-08 — Distribution : CR-Core devient une librairie Maven pure (révision)
|
||||
|
||||
- **Révision** des décisions précédentes "plugin Bukkit autonome" et
|
||||
"architecture en modules (PluginModule / ModuleRegistry)".
|
||||
- **Choix** : CR-Core n'est plus un plugin Bukkit, c'est une **librairie**
|
||||
(`jar`) sans `plugin.yml` ni `JavaPlugin`. Chaque plugin de jeu consomme la
|
||||
lib en dépendance Maven, instancie lui-même ses services (`new TeamServiceImpl(...)`)
|
||||
et garde son propre registre.
|
||||
- **Raison** : la complexité du système de modules + façade statique +
|
||||
`ServicesManager` n'apporte rien quand on est dans un contexte d'events
|
||||
ponctuels où chaque jeu vit dans sa propre session. La lib est nettement plus
|
||||
simple à utiliser et à tester. Le partage d'état entre jeux n'est pas un
|
||||
besoin réel pour les events entre amis.
|
||||
- **Conséquence** : suppression de `CRCorePlugin`, `CR` (façade), `plugin.yml`,
|
||||
`PluginModule`, `AbstractModule`, `ModuleRegistry`, `TeamModule`. Suppression
|
||||
aussi de `maven-shade-plugin` côté core (c'est le plugin de jeu qui décide
|
||||
de shader ou non).
|
||||
|
||||
## 2026-06-08 — Overridabilité par défaut
|
||||
|
||||
- **Choix** : toutes les classes du noyau sont conçues pour être étendues.
|
||||
Pas de `final` sur les classes ; méthodes-clés en `protected` ; factories
|
||||
`newXxx(...)` pour substituer des sous-classes ; hooks `onBeforeXxx` /
|
||||
`onAfterXxx` autour des opérations importantes.
|
||||
- **Exemples sur `TeamServiceImpl`** : `newTeam`, `validateName`, `validateTag`,
|
||||
`validateLeader`, `onBeforeSave`, `onAfterCreate`, `onBeforeDissolve`,
|
||||
`onAfterDissolve`, `onMemberAdded`, `onMemberRemoved`,
|
||||
`onLeadershipTransferred`. Sur `Team` : `newMember`.
|
||||
- **Raison** : le noyau doit fournir un comportement par défaut "qui marche",
|
||||
mais chaque jeu doit pouvoir greffer ses propres règles (logging, persistance
|
||||
custom, validations supplémentaires, hooks d'events Bukkit) sans réécrire
|
||||
toute la classe.
|
||||
|
||||
## 2026-06-08 — Framework de commandes intégré
|
||||
|
||||
- **Choix** : CR-Core fournit `Command` (interface), `AbstractCommand`
|
||||
(classe abstraite avec tous les builders), `BaseCommand` (top-level
|
||||
Bukkit-aware, conteneur de sous-commandes), `SubCommand` (feuille), plus
|
||||
`CommandContext`, `CommandResult`, `ArgumentType<T>` et un jeu de types
|
||||
built-in (`STRING`, `INTEGER`, `DOUBLE`, `BOOLEAN`, `ONLINE_PLAYER`,
|
||||
`enumOf(...)`, `choice(...)`).
|
||||
- **Raison** : chaque plugin de jeu aura ses commandes ; mutualiser le routage
|
||||
args[0]→sous-commande, les checks permission / player-only, le parsing des
|
||||
arguments et la tab-completion évite de redévelopper le même squelette à
|
||||
chaque fois.
|
||||
- **Non-choix** : pas d'annotations (`@Command`, `@Argument`) — la résolution
|
||||
par réflexion ajouterait une dépendance d'outillage et compliquerait le
|
||||
debug. L'API builder reste assez concise.
|
||||
- **Découplage** : le framework ne contient pas de commande "team" prête à
|
||||
l'emploi. C'est au plugin de jeu de définir ses commandes en utilisant les
|
||||
briques. Ça permet à chaque jeu d'avoir ses propres permissions, messages,
|
||||
et règles métier.
|
||||
|
||||
## 2026-06-08 — Visibilité publique / privée des équipes
|
||||
|
||||
- **Choix** : ajout d'un enum `TeamVisibility { PUBLIC, PRIVATE }` porté par
|
||||
`Team`. Une équipe `PUBLIC` peut être rejointe par un joueur via
|
||||
`TeamService.joinTeam(teamId, playerId)` ; une équipe `PRIVATE` ne peut
|
||||
recevoir des membres que via `addMember` appelé par le chef.
|
||||
- **Défaut** : `PRIVATE` à la création (le chef garde le contrôle ; il faut
|
||||
une action explicite pour ouvrir l'équipe au public). Une surcharge
|
||||
`createTeam(..., visibility)` permet de créer directement en `PUBLIC`.
|
||||
- **Nouvelle exception** : `TeamAccessException extends TeamException` —
|
||||
levée quand un auto-join est refusé (team privée, ou joueur déjà dans une
|
||||
équipe, ou refus custom dans `validateJoinable`).
|
||||
- **Nouveaux hooks** : `validateJoinable(team, playerId)`,
|
||||
`onPlayerJoined(team, member)`, `onVisibilityChanged(team, oldV, newV)`.
|
||||
- **Décision écartée pour l'instant** : un mode `INVITE_ONLY` avec un système
|
||||
d'invitations pendantes (Player A invite Player B → B accepte). Pas
|
||||
indispensable pour démarrer ; le chef peut déjà ajouter directement via
|
||||
`addMember`. À reconsidérer si le besoin remonte.
|
||||
|
||||
## 2026-06-08 — Scores nommés (Map<String, Integer>) plutôt qu'un score unique
|
||||
|
||||
- **Choix** : chaque équipe porte un `Map<String, Integer>` de scores nommés
|
||||
(`"kills"`, `"objectives"`, `"global"`, …) plutôt qu'un seul `int score`.
|
||||
- **Raison** : tous les jeux n'ont pas la même métrique. BedWars a "beds_broken"
|
||||
+ "final_kills" ; un mode Capture the Flag a "flags" + "kills" ; un mode
|
||||
simple peut n'utiliser que `"global"`. Un Map évite d'imposer un schéma fixe
|
||||
et reste compact pour les cas mono-score.
|
||||
- **Conséquence** : les noms de scores sont libres et non typés au niveau du
|
||||
noyau ; chaque jeu choisit ses propres noms. Pour de la sûreté, un jeu peut
|
||||
exposer un enum ou des constantes côté plugin.
|
||||
- **Type** : `Integer` plutôt que `Long` ou `Double`. Suffisant pour des
|
||||
scores de match (limite ~2 milliards). Si un jeu a besoin de Long ou Double,
|
||||
il peut wrapper et stocker un encodage custom ; ou bien on étendra l'API
|
||||
plus tard.
|
||||
|
||||
## 2026-06-08 — Classements : ranking par score + ranking global (= somme)
|
||||
|
||||
- **Choix** : `TeamService` expose `getRankingByScore(scoreName)` et
|
||||
`getGlobalRanking()`. Le ranking global est calculé comme la **somme** de
|
||||
tous les scores nommés de chaque équipe.
|
||||
- **Raison** : couvre les deux cas usuels (« qui a le plus de kills ? » et
|
||||
« qui a la meilleure perf globale ? ») sans imposer de pondération par
|
||||
défaut. Un jeu qui veut une formule custom (pondérée, ratio, …) override
|
||||
`rank(ToIntFunction<Team>)` ou ajoute une méthode de service dans sa propre
|
||||
sous-classe.
|
||||
- **Format du résultat** : `record TeamRanking(int rank, Team team, int score)`.
|
||||
Le `rank` est 1-based, le tri est descendant sur le score, le tiebreaker est
|
||||
alphabétique (case-insensitive) sur le nom de l'équipe.
|
||||
- **Records** : choix d'un record Java 16 plutôt qu'une classe immutable
|
||||
manuelle — moins de boilerplate, `equals/hashCode/toString` gratuits. Les
|
||||
records étant `final`, un jeu qui veut un type custom devra wrapper et
|
||||
override `newRanking(...)` au niveau du service.
|
||||
|
||||
## 2026-06-08 — Spawn point par équipe (Bukkit Location)
|
||||
|
||||
- **Choix** : chaque `Team` peut avoir un `Location` Bukkit optionnel comme
|
||||
point de spawn. Stocké en mémoire pour l'instant.
|
||||
- **Clonage défensif** : `getSpawnPoint()` retourne un `Optional<Location>` où
|
||||
la `Location` est **clonée** ; idem à l'entrée dans `setSpawnPoint`.
|
||||
`Location` étant mutable côté Bukkit, ça évite que du code externe modifie
|
||||
accidentellement le spawn en faisant `team.getSpawnPoint().get().setX(...)`.
|
||||
- **Persistance différée** : `Location` n'est pas trivialement sérialisable
|
||||
(référence au `World`). On utilisera `ConfigurationSerializable` quand on
|
||||
branchera un repo fichier ; pour l'instant, le `InMemoryTeamRepository`
|
||||
s'en moque.
|
||||
- **Pas de téléport intégré** : le noyau ne fournit pas `teleportToSpawn(...)`.
|
||||
C'est au plugin de jeu d'enchaîner `player.teleport(team.getSpawnPoint())`
|
||||
s'il veut. La lib reste purement "data + règles".
|
||||
|
||||
## 2026-06-08 — Scores joueurs : domaine `player` indépendant du domaine `team`
|
||||
|
||||
- **Choix** : ajout d'un domaine `fr.luc.crcore.player` complet et parallèle au
|
||||
domaine `team` : `PlayerProfile` (entité identifiée par l'UUID Bukkit),
|
||||
`PlayerProfileService`, `PlayerProfileRepository`,
|
||||
`InMemoryPlayerProfileRepository`, `PlayerRanking` (record),
|
||||
`PlayerException` / `PlayerProfileNotFoundException`.
|
||||
- **Pourquoi pas sur `TeamMember`** : `TeamMember` est immuable (transitions de
|
||||
rôle via `withRole`), et un joueur peut changer/quitter une équipe — son
|
||||
profil doit persister. Mettre les scores sur `TeamMember` aurait couplé la
|
||||
durée de vie du score à l'appartenance à l'équipe.
|
||||
- **Auto-création** : `addScore` / `setScore` créent le profil automatiquement
|
||||
s'il n'existe pas (`getOrCreateProfile(playerId)`). Pas besoin d'appeler
|
||||
explicitement un `register(playerId)` avant de tracker un score.
|
||||
- **Symétrie** : les noms de méthodes, hooks et factories reflètent
|
||||
exactement le domaine team (`newProfile`/`newTeam`,
|
||||
`newRanking`/`newRanking`, `rank(scoreFn)`, `onScoreChanged`,
|
||||
`getRankingByScore`, `getGlobalRanking`, etc.).
|
||||
|
||||
## 2026-06-08 — Interface `ScoreHolder` mutualisée
|
||||
|
||||
- **Choix** : extraction d'une interface `fr.luc.crcore.common.ScoreHolder`
|
||||
qui déclare le contrat de scoring (`getScore`, `addScore`, `setScore`,
|
||||
`resetScore`, `getTotalScore`, etc.). `Team` et `PlayerProfile`
|
||||
l'implémentent.
|
||||
- **Raison** : documenter le contrat et permettre du code générique côté
|
||||
plugin de jeu (ex. `ScoreHolder.getTotalScore()` traité uniformément pour
|
||||
l'affichage). Pas de classe abstraite partagée pour éviter le couplage
|
||||
serré (Team et PlayerProfile ont des cycles de vie très différents) ; on
|
||||
reste sur deux implémentations indépendantes mais avec un contrat commun.
|
||||
- **Pas de `Scoreboard` aggregate** : envisagé un composant `Scoreboard`
|
||||
composé dans Team et PlayerProfile, mais ça aurait imposé une indirection
|
||||
pour ~8 méthodes simples. Choix actuel : duplication contrôlée des
|
||||
implémentations (Map + getters/setters), interface commune pour le
|
||||
contrat.
|
||||
@@ -0,0 +1,132 @@
|
||||
@startuml command-class-diagram
|
||||
title CR-Core — Command framework (class diagram)
|
||||
|
||||
skinparam classAttributeIconSize 0
|
||||
hide empty members
|
||||
|
||||
package "fr.luc.crcore.command" {
|
||||
|
||||
interface Command {
|
||||
+ getName(): String
|
||||
+ getAliases(): List<String>
|
||||
+ getPermission(): String
|
||||
+ isPlayerOnly(): boolean
|
||||
+ getDescription(): String
|
||||
+ execute(ctx: CommandContext): CommandResult
|
||||
+ tabComplete(sender, argIndex, partial): List<String>
|
||||
+ matches(label: String): boolean
|
||||
}
|
||||
|
||||
abstract class AbstractCommand {
|
||||
- name: String
|
||||
- aliases: List<String>
|
||||
- permission: String
|
||||
- playerOnly: boolean
|
||||
- description: String
|
||||
- usage: String
|
||||
- arguments: List<ArgumentDef>
|
||||
# addAlias(...): void
|
||||
# permission(p): void
|
||||
# playerOnly(): void
|
||||
# description(d): void
|
||||
# usage(u): void
|
||||
# argument(name, type): void
|
||||
# optionalArgument(name, type): void
|
||||
# buildContext(sender, label, subArgs): CommandContext
|
||||
+ getRequiredArgumentCount(): int
|
||||
+ getTotalArgumentCount(): int
|
||||
+ getUsage(): String
|
||||
}
|
||||
|
||||
abstract class BaseCommand {
|
||||
- subCommandsByName: Map<String, SubCommand>
|
||||
- subCommandsByAlias: Map<String, SubCommand>
|
||||
# addSubCommand(sub: SubCommand): void
|
||||
+ findSubCommand(label: String): Optional<SubCommand>
|
||||
+ getSubCommands(): Collection<SubCommand>
|
||||
# execute(ctx): CommandResult
|
||||
+ onCommand(sender, cmd, label, args): boolean
|
||||
+ onTabComplete(sender, cmd, alias, args): List<String>
|
||||
# checkAccess(sender, target): boolean
|
||||
# handleResult(sender, result): void
|
||||
}
|
||||
|
||||
abstract class SubCommand {
|
||||
+ {abstract} execute(ctx: CommandContext): CommandResult
|
||||
}
|
||||
|
||||
class CommandContext {
|
||||
- sender: CommandSender
|
||||
- label: String
|
||||
- rawArgs: String[]
|
||||
- parsedArgs: Map<String, Object>
|
||||
+ getSender(): CommandSender
|
||||
+ isPlayer(): boolean
|
||||
+ getPlayer(): Optional<Player>
|
||||
+ requirePlayer(): Player
|
||||
+ get(name: String): T
|
||||
+ getOptional(name): Optional<T>
|
||||
+ has(name): boolean
|
||||
+ reply(msg): void
|
||||
}
|
||||
|
||||
class CommandResult {
|
||||
- type: Type
|
||||
- message: String
|
||||
+ getType(): Type
|
||||
+ getMessage(): String
|
||||
+ isSuccess(): boolean
|
||||
+ {static} success(): CommandResult
|
||||
+ {static} success(msg): CommandResult
|
||||
+ {static} failure(msg): CommandResult
|
||||
+ {static} invalidUsage(): CommandResult
|
||||
+ {static} noPermission(): CommandResult
|
||||
+ {static} playerOnly(): CommandResult
|
||||
}
|
||||
|
||||
enum "CommandResult.Type" as ResultType {
|
||||
SUCCESS
|
||||
FAILURE
|
||||
INVALID_USAGE
|
||||
NO_PERMISSION
|
||||
PLAYER_ONLY
|
||||
}
|
||||
|
||||
class CommandException
|
||||
|
||||
interface "ArgumentType<T>" as ArgumentType {
|
||||
+ parse(input: String): T
|
||||
+ suggestions(sender, partial): List<String>
|
||||
}
|
||||
|
||||
class ArgumentTypes << (S, #FFC107) static >> {
|
||||
+ {static} STRING: ArgumentType<String>
|
||||
+ {static} INTEGER: ArgumentType<Integer>
|
||||
+ {static} DOUBLE: ArgumentType<Double>
|
||||
+ {static} BOOLEAN: ArgumentType<Boolean>
|
||||
+ {static} ONLINE_PLAYER: ArgumentType<Player>
|
||||
+ {static} enumOf(type): ArgumentType<E>
|
||||
+ {static} choice(choices): ArgumentType<String>
|
||||
}
|
||||
|
||||
class ArgumentDef << (P, #BBBBBB) package-private >> {
|
||||
- name: String
|
||||
- type: ArgumentType<?>
|
||||
- required: boolean
|
||||
}
|
||||
|
||||
AbstractCommand ..|> Command
|
||||
BaseCommand --|> AbstractCommand
|
||||
SubCommand --|> AbstractCommand
|
||||
BaseCommand "1" o-- "*" SubCommand : subCommands
|
||||
AbstractCommand "1" *-- "*" ArgumentDef : arguments
|
||||
ArgumentDef --> ArgumentType
|
||||
CommandResult +-- ResultType
|
||||
CommandException --|> RuntimeException
|
||||
|
||||
BaseCommand ..> CommandContext : creates
|
||||
AbstractCommand ..> CommandContext : creates
|
||||
SubCommand ..> CommandResult : returns
|
||||
}
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,110 @@
|
||||
@startuml player-class-diagram
|
||||
title CR-Core — Player domain (class diagram)
|
||||
|
||||
skinparam classAttributeIconSize 0
|
||||
hide empty members
|
||||
|
||||
' === Common abstractions ===
|
||||
|
||||
package "fr.luc.crcore.common" {
|
||||
|
||||
interface Identifiable {
|
||||
+ getId(): UUID
|
||||
}
|
||||
|
||||
interface ScoreHolder {
|
||||
+ getScore(name): int
|
||||
+ hasScore(name): boolean
|
||||
+ getScores(): Map<String, Integer>
|
||||
+ getTotalScore(): int
|
||||
+ addScore(name, delta): int
|
||||
+ setScore(name, value): int
|
||||
+ resetScore(name): boolean
|
||||
+ resetAllScores(): void
|
||||
}
|
||||
|
||||
abstract class AbstractEntity {
|
||||
- id: UUID
|
||||
+ getId(): UUID
|
||||
}
|
||||
|
||||
interface "Repository<T extends Identifiable>" as Repository {
|
||||
+ save(entity: T): T
|
||||
+ findById(id): Optional<T>
|
||||
+ findAll(): Collection<T>
|
||||
+ delete(id): boolean
|
||||
}
|
||||
|
||||
AbstractEntity ..|> Identifiable
|
||||
}
|
||||
|
||||
' === Player domain ===
|
||||
|
||||
package "fr.luc.crcore.player" {
|
||||
|
||||
class PlayerProfile {
|
||||
- scores: Map<String, Integer>
|
||||
+ PlayerProfile(playerId: UUID)
|
||||
+ getPlayerId(): UUID
|
||||
}
|
||||
|
||||
class PlayerRanking <<record>> {
|
||||
+ rank: int
|
||||
+ profile: PlayerProfile
|
||||
+ score: int
|
||||
}
|
||||
|
||||
interface PlayerProfileRepository
|
||||
|
||||
class InMemoryPlayerProfileRepository {
|
||||
- profiles: Map<UUID, PlayerProfile>
|
||||
}
|
||||
|
||||
interface PlayerProfileService {
|
||||
+ getOrCreateProfile(playerId): PlayerProfile
|
||||
+ getProfile(playerId): Optional<PlayerProfile>
|
||||
+ deleteProfile(playerId): boolean
|
||||
+ getAllProfiles(): Collection<PlayerProfile>
|
||||
--
|
||||
+ addScore(playerId, name, delta): int
|
||||
+ setScore(playerId, name, value): int
|
||||
+ getScore(playerId, name): int
|
||||
+ resetScore(playerId, name): boolean
|
||||
+ resetAllScores(playerId): void
|
||||
--
|
||||
+ getRankingByScore(name): List<PlayerRanking>
|
||||
+ getGlobalRanking(): List<PlayerRanking>
|
||||
+ getTopRankingByScore(name, limit): List<PlayerRanking>
|
||||
+ getTopGlobalRanking(limit): List<PlayerRanking>
|
||||
}
|
||||
|
||||
class PlayerProfileServiceImpl {
|
||||
- repository: PlayerProfileRepository
|
||||
--
|
||||
# newProfile(playerId): PlayerProfile
|
||||
# newRanking(rank, profile, score): PlayerRanking
|
||||
# rank(scoreFn): List<PlayerRanking>
|
||||
--
|
||||
# onProfileCreated(profile): void
|
||||
# onProfileDeleted(profile): void
|
||||
# onScoreChanged(profile, name, oldV, newV): void
|
||||
}
|
||||
|
||||
class PlayerException
|
||||
class PlayerProfileNotFoundException
|
||||
|
||||
PlayerProfile --|> AbstractEntity
|
||||
PlayerProfile ..|> ScoreHolder
|
||||
PlayerProfileRepository --|> Repository
|
||||
InMemoryPlayerProfileRepository ..|> PlayerProfileRepository
|
||||
PlayerProfileServiceImpl ..|> PlayerProfileService
|
||||
|
||||
PlayerRanking --> PlayerProfile
|
||||
PlayerProfileServiceImpl o--> PlayerProfileRepository
|
||||
PlayerProfileService ..> PlayerRanking : produces
|
||||
|
||||
PlayerException --|> RuntimeException
|
||||
PlayerProfileNotFoundException --|> PlayerException
|
||||
}
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,239 @@
|
||||
@startuml team-class-diagram
|
||||
title CR-Core — Team domain (class diagram)
|
||||
|
||||
skinparam classAttributeIconSize 0
|
||||
hide empty members
|
||||
|
||||
' === Common abstractions ===
|
||||
|
||||
package "fr.luc.crcore.common" {
|
||||
|
||||
interface Identifiable {
|
||||
+ getId(): UUID
|
||||
}
|
||||
|
||||
interface Named {
|
||||
+ getName(): String
|
||||
}
|
||||
|
||||
interface ScoreHolder {
|
||||
+ getScore(name): int
|
||||
+ hasScore(name): boolean
|
||||
+ getScores(): Map<String, Integer>
|
||||
+ getTotalScore(): int
|
||||
+ addScore(name, delta): int
|
||||
+ setScore(name, value): int
|
||||
+ resetScore(name): boolean
|
||||
+ resetAllScores(): void
|
||||
}
|
||||
|
||||
abstract class AbstractEntity {
|
||||
- id: UUID
|
||||
+ AbstractEntity(id: UUID)
|
||||
+ getId(): UUID
|
||||
+ equals(o: Object): boolean
|
||||
+ hashCode(): int
|
||||
}
|
||||
|
||||
interface "Repository<T extends Identifiable>" as Repository {
|
||||
+ save(entity: T): T
|
||||
+ findById(id: UUID): Optional<T>
|
||||
+ findAll(): Collection<T>
|
||||
+ delete(id: UUID): boolean
|
||||
}
|
||||
|
||||
AbstractEntity ..|> Identifiable
|
||||
}
|
||||
|
||||
' === Team domain ===
|
||||
|
||||
package "fr.luc.crcore.team" {
|
||||
|
||||
enum TeamRole {
|
||||
LEADER
|
||||
MEMBER
|
||||
--
|
||||
+ isLeader(): boolean
|
||||
}
|
||||
|
||||
enum TeamVisibility {
|
||||
PUBLIC
|
||||
PRIVATE
|
||||
--
|
||||
+ isPublic(): boolean
|
||||
+ isPrivate(): boolean
|
||||
}
|
||||
|
||||
enum TeamColor {
|
||||
RED
|
||||
BLUE
|
||||
GREEN
|
||||
YELLOW
|
||||
AQUA
|
||||
LIGHT_PURPLE
|
||||
GOLD
|
||||
WHITE
|
||||
BLACK
|
||||
DARK_BLUE
|
||||
DARK_GREEN
|
||||
DARK_AQUA
|
||||
DARK_RED
|
||||
DARK_PURPLE
|
||||
DARK_GRAY
|
||||
GRAY
|
||||
--
|
||||
+ getChatColor(): ChatColor
|
||||
+ getDyeColor(): DyeColor
|
||||
+ getDisplayName(): String
|
||||
}
|
||||
|
||||
class TeamMember {
|
||||
- role: TeamRole
|
||||
- joinedAt: Instant
|
||||
+ getPlayerId(): UUID
|
||||
+ getRole(): TeamRole
|
||||
+ getJoinedAt(): Instant
|
||||
+ isLeader(): boolean
|
||||
+ withRole(role: TeamRole): TeamMember
|
||||
}
|
||||
|
||||
class Team {
|
||||
- name: String
|
||||
- tag: String
|
||||
- color: TeamColor
|
||||
- leaderId: UUID
|
||||
- visibility: TeamVisibility
|
||||
- members: Set<TeamMember>
|
||||
- scores: Map<String, Integer>
|
||||
- spawnPoint: Location
|
||||
--
|
||||
+ getName(): String
|
||||
+ getTag(): String
|
||||
+ getColor(): TeamColor
|
||||
+ getLeaderId(): UUID
|
||||
+ getLeader(): TeamMember
|
||||
+ getVisibility(): TeamVisibility
|
||||
+ setVisibility(v): void
|
||||
+ isPublic(): boolean
|
||||
+ getMembers(): Set<TeamMember>
|
||||
+ getMember(playerId): Optional<TeamMember>
|
||||
+ hasMember(playerId): boolean
|
||||
+ size(): int
|
||||
+ addMember(playerId): TeamMember
|
||||
+ removeMember(playerId): boolean
|
||||
+ transferLeadership(newLeaderId): void
|
||||
--
|
||||
+ getScore(name): int
|
||||
+ hasScore(name): boolean
|
||||
+ getScores(): Map<String, Integer>
|
||||
+ getTotalScore(): int
|
||||
+ addScore(name, delta): int
|
||||
+ setScore(name, value): int
|
||||
+ resetScore(name): boolean
|
||||
+ resetAllScores(): void
|
||||
--
|
||||
+ getSpawnPoint(): Optional<Location>
|
||||
+ hasSpawnPoint(): boolean
|
||||
+ setSpawnPoint(loc): void
|
||||
+ clearSpawnPoint(): void
|
||||
--
|
||||
# newMember(playerId, role): TeamMember
|
||||
}
|
||||
|
||||
class TeamRanking <<record>> {
|
||||
+ rank: int
|
||||
+ team: Team
|
||||
+ score: int
|
||||
}
|
||||
|
||||
interface TeamRepository {
|
||||
+ findByName(name: String): Optional<Team>
|
||||
+ findByTag(tag: String): Optional<Team>
|
||||
+ findByMember(playerId: UUID): Optional<Team>
|
||||
}
|
||||
|
||||
class InMemoryTeamRepository {
|
||||
- teams: Map<UUID, Team>
|
||||
}
|
||||
|
||||
interface TeamService {
|
||||
+ createTeam(name, tag, color, leaderId, [visibility]): Team
|
||||
+ dissolveTeam(teamId): boolean
|
||||
+ addMember(teamId, playerId): boolean
|
||||
+ removeMember(teamId, playerId): boolean
|
||||
+ joinTeam(teamId, playerId): boolean
|
||||
+ transferLeadership(teamId, newLeaderId): boolean
|
||||
+ setVisibility(teamId, visibility): void
|
||||
--
|
||||
+ addScore(teamId, name, delta): int
|
||||
+ setScore(teamId, name, value): int
|
||||
+ getScore(teamId, name): int
|
||||
+ resetScore(teamId, name): boolean
|
||||
+ resetAllScores(teamId): void
|
||||
--
|
||||
+ getRankingByScore(name): List<TeamRanking>
|
||||
+ getGlobalRanking(): List<TeamRanking>
|
||||
+ getTopRankingByScore(name, limit): List<TeamRanking>
|
||||
+ getTopGlobalRanking(limit): List<TeamRanking>
|
||||
--
|
||||
+ setSpawnPoint(teamId, loc): void
|
||||
+ clearSpawnPoint(teamId): void
|
||||
+ getSpawnPoint(teamId): Optional<Location>
|
||||
--
|
||||
+ getTeam / getTeamByName / getTeamByTag / getTeamOfPlayer
|
||||
+ getAllTeams(): Collection<Team>
|
||||
}
|
||||
|
||||
class TeamServiceImpl {
|
||||
- repository: TeamRepository
|
||||
--
|
||||
# newTeam(...): Team
|
||||
# newRanking(rank, team, score): TeamRanking
|
||||
# rank(scoreFn): List<TeamRanking>
|
||||
--
|
||||
# validateName(name): void
|
||||
# validateTag(tag): void
|
||||
# validateLeader(leaderId): void
|
||||
# validateJoinable(team, playerId): void
|
||||
--
|
||||
# onBeforeSave(team): void
|
||||
# onAfterCreate(team): void
|
||||
# onBeforeDissolve(team): void
|
||||
# onAfterDissolve(team): void
|
||||
# onMemberAdded(team, member): void
|
||||
# onMemberRemoved(team, playerId): void
|
||||
# onPlayerJoined(team, member): void
|
||||
# onLeadershipTransferred(team, oldId, newId): void
|
||||
# onVisibilityChanged(team, oldV, newV): void
|
||||
# onScoreChanged(team, name, oldV, newV): void
|
||||
# onSpawnPointChanged(team, oldLoc, newLoc): void
|
||||
}
|
||||
|
||||
class TeamException
|
||||
class TeamAlreadyExistsException
|
||||
class TeamNotFoundException
|
||||
class TeamAccessException
|
||||
|
||||
TeamMember --|> AbstractEntity
|
||||
Team --|> AbstractEntity
|
||||
Team ..|> Named
|
||||
Team ..|> ScoreHolder
|
||||
TeamRepository --|> Repository
|
||||
InMemoryTeamRepository ..|> TeamRepository
|
||||
TeamServiceImpl ..|> TeamService
|
||||
|
||||
Team "1" o-- "1..*" TeamMember : members
|
||||
Team --> TeamColor : color
|
||||
Team --> TeamVisibility : visibility
|
||||
TeamMember --> TeamRole : role
|
||||
TeamRanking --> Team
|
||||
TeamServiceImpl o--> TeamRepository
|
||||
TeamService ..> TeamRanking : produces
|
||||
|
||||
TeamException --|> RuntimeException
|
||||
TeamAlreadyExistsException --|> TeamException
|
||||
TeamNotFoundException --|> TeamException
|
||||
TeamAccessException --|> TeamException
|
||||
}
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,40 @@
|
||||
@startuml team-create-activity
|
||||
title CR-Core — Create Team (activity diagram)
|
||||
|
||||
start
|
||||
|
||||
:Player runs /team create <name> <tag> <color> [visibility];
|
||||
|
||||
if (Name already in use?) then (yes)
|
||||
:Reply "team name already taken";
|
||||
stop
|
||||
else (no)
|
||||
endif
|
||||
|
||||
if (Tag already in use?) then (yes)
|
||||
:Reply "team tag already taken";
|
||||
stop
|
||||
else (no)
|
||||
endif
|
||||
|
||||
if (Player already in a team?) then (yes)
|
||||
:Reply "you already belong to a team";
|
||||
stop
|
||||
else (no)
|
||||
endif
|
||||
|
||||
if (Color valid?) then (no)
|
||||
:Reply "unknown color";
|
||||
stop
|
||||
else (yes)
|
||||
endif
|
||||
|
||||
:Create Team(id = randomUUID, name, tag, color, leaderId = playerId, visibility);
|
||||
note right: visibility defaults to PRIVATE\nif not specified
|
||||
:Add player as TeamMember(role = LEADER);
|
||||
:Persist via TeamRepository.save(team);
|
||||
:Reply "team created";
|
||||
|
||||
stop
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,69 @@
|
||||
@startuml team-create-sequence
|
||||
title CR-Core — Create Team via command framework (sequence diagram)
|
||||
|
||||
actor Player
|
||||
participant "BaseCommand\n(TeamCommand)" as Base
|
||||
participant "SubCommand\n(TeamCreateSub)" as Sub
|
||||
participant "TeamService" as Service
|
||||
participant "TeamRepository" as Repo
|
||||
participant "Team" as Team
|
||||
|
||||
Player -> Base : /team create <name> <tag> <color>
|
||||
activate Base
|
||||
|
||||
Base -> Base : route args[0]="create" → TeamCreateSub
|
||||
Base -> Base : check permission + playerOnly
|
||||
Base -> Sub : buildContext(sender, label, subArgs)
|
||||
activate Sub
|
||||
Sub -> Sub : parse name (STRING) / tag (STRING) / color (enumOf TeamColor)
|
||||
Sub --> Base : CommandContext
|
||||
deactivate Sub
|
||||
|
||||
Base -> Sub : execute(ctx)
|
||||
activate Sub
|
||||
|
||||
Sub -> Service : createTeam(name, tag, color, playerId)
|
||||
activate Service
|
||||
|
||||
Service -> Service : validateName(name)
|
||||
Service -> Repo : findByName(name)
|
||||
activate Repo
|
||||
Repo --> Service : Optional.empty()
|
||||
deactivate Repo
|
||||
|
||||
Service -> Service : validateTag(tag)
|
||||
Service -> Repo : findByTag(tag)
|
||||
activate Repo
|
||||
Repo --> Service : Optional.empty()
|
||||
deactivate Repo
|
||||
|
||||
Service -> Service : validateLeader(playerId)
|
||||
Service -> Repo : findByMember(playerId)
|
||||
activate Repo
|
||||
Repo --> Service : Optional.empty()
|
||||
deactivate Repo
|
||||
|
||||
Service -> Team : newTeam(UUID.randomUUID(), name, tag, color, playerId, PRIVATE)
|
||||
activate Team
|
||||
Team -> Team : add newMember(playerId, LEADER)
|
||||
Team --> Service : team
|
||||
deactivate Team
|
||||
|
||||
Service -> Service : onBeforeSave(team)
|
||||
Service -> Repo : save(team)
|
||||
activate Repo
|
||||
Repo --> Service : team
|
||||
deactivate Repo
|
||||
Service -> Service : onAfterCreate(team)
|
||||
|
||||
Service --> Sub : team
|
||||
deactivate Service
|
||||
|
||||
Sub --> Base : CommandResult.success("Team created")
|
||||
deactivate Sub
|
||||
|
||||
Base -> Base : handleResult → sender.sendMessage(green text)
|
||||
Base --> Player : "Team <name> created"
|
||||
deactivate Base
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,62 @@
|
||||
@startuml team-join-sequence
|
||||
title CR-Core — Player joins a public team (sequence diagram)
|
||||
|
||||
actor Player
|
||||
participant "BaseCommand\n(TeamCommand)" as Base
|
||||
participant "SubCommand\n(TeamJoinSub)" as Sub
|
||||
participant "TeamService" as Service
|
||||
participant "TeamRepository" as Repo
|
||||
participant "Team" as Team
|
||||
|
||||
Player -> Base : /team join <name>
|
||||
activate Base
|
||||
|
||||
Base -> Base : route args[0]="join" → TeamJoinSub
|
||||
Base -> Sub : execute(ctx)
|
||||
activate Sub
|
||||
|
||||
Sub -> Service : getTeamByName(name)
|
||||
activate Service
|
||||
Service -> Repo : findByName(name)
|
||||
activate Repo
|
||||
Repo --> Service : Optional<Team>
|
||||
deactivate Repo
|
||||
Service --> Sub : Optional<Team>
|
||||
deactivate Service
|
||||
|
||||
alt team not found
|
||||
Sub --> Base : CommandResult.failure("Team not found")
|
||||
else team found
|
||||
Sub -> Service : joinTeam(team.id, playerId)
|
||||
activate Service
|
||||
|
||||
Service -> Service : validateJoinable(team, playerId)
|
||||
alt team is PRIVATE
|
||||
Service --> Sub : throw TeamAccessException
|
||||
Sub --> Base : CommandResult.failure(ex.message)
|
||||
else player already in a team
|
||||
Service --> Sub : throw TeamAccessException
|
||||
Sub --> Base : CommandResult.failure(ex.message)
|
||||
else allowed
|
||||
Service -> Team : addMember(playerId)
|
||||
activate Team
|
||||
Team --> Service : member
|
||||
deactivate Team
|
||||
Service -> Repo : save(team)
|
||||
activate Repo
|
||||
Repo --> Service : team
|
||||
deactivate Repo
|
||||
Service -> Service : onMemberAdded(team, member)
|
||||
Service -> Service : onPlayerJoined(team, member)
|
||||
Service --> Sub : true
|
||||
Sub --> Base : CommandResult.success("Joined " + team.name)
|
||||
end
|
||||
deactivate Service
|
||||
end
|
||||
|
||||
deactivate Sub
|
||||
Base -> Base : handleResult → sender.sendMessage
|
||||
Base --> Player : reply
|
||||
deactivate Base
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,312 @@
|
||||
# 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, ...)_
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
# Setup technique
|
||||
|
||||
## Stack
|
||||
|
||||
- **Type** : librairie Java (`jar`) — pas un plugin Bukkit
|
||||
- **artifactId Maven** : `CR-Core`
|
||||
- **Build** : Maven
|
||||
- **Java** : 16
|
||||
- **API serveur (provided)** : Paper 1.16.5
|
||||
(`com.destroystokyo.paper:paper-api:1.16.5-R0.1-SNAPSHOT`)
|
||||
- **Package racine** : `fr.luc.crcore`
|
||||
|
||||
## Dépôts Maven
|
||||
|
||||
- `papermc` — https://repo.papermc.io/repository/maven-public/
|
||||
- `spigot-repo` — https://hub.spigotmc.org/nexus/content/repositories/snapshots/
|
||||
- `sonatype` — https://oss.sonatype.org/content/groups/public/
|
||||
|
||||
## Build & install local
|
||||
|
||||
```bash
|
||||
mvn clean install
|
||||
```
|
||||
|
||||
Cela publie `fr.luc:CR-Core:1.0-SNAPSHOT` dans le repo Maven local `~/.m2/`,
|
||||
prêt à être consommé par les plugins de jeu.
|
||||
|
||||
## Utilisation depuis un plugin de jeu
|
||||
|
||||
Dans le `pom.xml` du plugin de jeu :
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>fr.luc</groupId>
|
||||
<artifactId>CR-Core</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
> Scope `compile` (et non `provided`) **si** le plugin de jeu shade CR-Core dans
|
||||
> son propre jar (recommandé pour de la pure librairie). Pensez à utiliser un
|
||||
> `<relocation>` dans le `maven-shade-plugin` du plugin de jeu pour éviter les
|
||||
> conflits si plusieurs plugins shadent CR-Core sur le même serveur.
|
||||
|
||||
Côté code du plugin de jeu :
|
||||
|
||||
```java
|
||||
public final class MyGamePlugin extends JavaPlugin {
|
||||
|
||||
private TeamService teamService;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
// Instancie le service team (chaque plugin a son propre registre)
|
||||
this.teamService = new TeamServiceImpl(new InMemoryTeamRepository());
|
||||
|
||||
// Enregistre une commande basée sur le framework de CR-Core
|
||||
getCommand("team").setExecutor(new TeamCommand(teamService));
|
||||
getCommand("team").setTabCompleter(new TeamCommand(teamService));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Exemple minimal de commande :
|
||||
|
||||
```java
|
||||
public class TeamCommand extends BaseCommand {
|
||||
|
||||
public TeamCommand(TeamService service) {
|
||||
super("team", "teams", "t");
|
||||
description("Manage teams");
|
||||
addSubCommand(new TeamCreateSub(service));
|
||||
addSubCommand(new TeamInfoSub(service));
|
||||
}
|
||||
}
|
||||
|
||||
public class TeamCreateSub extends SubCommand {
|
||||
|
||||
private final TeamService service;
|
||||
|
||||
public TeamCreateSub(TeamService service) {
|
||||
super("create", "c", "new");
|
||||
this.service = service;
|
||||
description("Create a new team");
|
||||
permission("mygame.team.create");
|
||||
playerOnly();
|
||||
argument("name", ArgumentTypes.STRING);
|
||||
argument("tag", ArgumentTypes.STRING);
|
||||
argument("color", ArgumentTypes.enumOf(TeamColor.class));
|
||||
}
|
||||
|
||||
@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");
|
||||
try {
|
||||
Team team = service.createTeam(name, tag, color, player.getUniqueId());
|
||||
return CommandResult.success("Équipe " + team.getName() + " créée !");
|
||||
} catch (TeamException ex) {
|
||||
return CommandResult.failure(ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Et dans le `plugin.yml` du plugin de jeu :
|
||||
|
||||
```yaml
|
||||
name: MyGame
|
||||
main: fr.luc.mygame.MyGamePlugin
|
||||
version: 1.0
|
||||
api-version: 1.16
|
||||
commands:
|
||||
team:
|
||||
description: Manage teams
|
||||
aliases: [teams, t]
|
||||
```
|
||||
|
||||
## Override
|
||||
|
||||
Toute classe du noyau peut être étendue :
|
||||
|
||||
```java
|
||||
public class LoggingTeamService extends TeamServiceImpl {
|
||||
|
||||
public LoggingTeamService(TeamRepository repo) { super(repo); }
|
||||
|
||||
@Override
|
||||
protected void onAfterCreate(Team team) {
|
||||
getPlugin().getLogger().info("Team created: " + team.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Team newTeam(UUID id, String name, String tag, TeamColor color, UUID leaderId) {
|
||||
return new MyCustomTeam(id, name, tag, color, leaderId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Arborescence du projet
|
||||
|
||||
```
|
||||
CitesPlugin/ # dossier IntelliJ (renommer plus tard si voulu)
|
||||
├── pom.xml
|
||||
├── GEMINI.md
|
||||
├── docs/ # source de vérité
|
||||
│ ├── README.md
|
||||
│ ├── setup.md
|
||||
│ ├── features.md
|
||||
│ ├── decisions.md
|
||||
│ └── diagrams/
|
||||
│ ├── team-class-diagram.puml
|
||||
│ ├── team-create-sequence.puml
|
||||
│ ├── team-create-activity.puml
|
||||
│ └── command-class-diagram.puml
|
||||
└── src/main/java/fr/luc/crcore/
|
||||
├── common/ # abstractions partagées
|
||||
│ ├── Identifiable.java # interface
|
||||
│ ├── Named.java # interface
|
||||
│ ├── ScoreHolder.java # interface (impl. par Team et PlayerProfile)
|
||||
│ ├── AbstractEntity.java # abstract class
|
||||
│ └── Repository.java # interface
|
||||
├── command/ # framework de commandes
|
||||
│ ├── Command.java # interface
|
||||
│ ├── AbstractCommand.java # base partagée
|
||||
│ ├── BaseCommand.java # commande top-level (Bukkit-aware)
|
||||
│ ├── SubCommand.java # sous-commande
|
||||
│ ├── CommandContext.java
|
||||
│ ├── CommandResult.java
|
||||
│ ├── CommandException.java
|
||||
│ ├── ArgumentType.java
|
||||
│ ├── ArgumentTypes.java # STRING, INTEGER, BOOLEAN, ONLINE_PLAYER, enumOf, choice
|
||||
│ └── ArgumentDef.java # package-private
|
||||
├── team/ # domaine team
|
||||
│ ├── Team.java
|
||||
│ ├── TeamMember.java
|
||||
│ ├── TeamRole.java # enum
|
||||
│ ├── TeamColor.java # enum
|
||||
│ ├── TeamVisibility.java # enum (PUBLIC / PRIVATE)
|
||||
│ ├── TeamRanking.java # record (rank, team, score)
|
||||
│ ├── TeamRepository.java # interface
|
||||
│ ├── InMemoryTeamRepository.java # impl
|
||||
│ ├── TeamService.java # interface
|
||||
│ ├── TeamServiceImpl.java # impl avec hooks overridables
|
||||
│ ├── TeamException.java
|
||||
│ ├── TeamAlreadyExistsException.java
|
||||
│ ├── TeamNotFoundException.java
|
||||
│ └── TeamAccessException.java # auto-join refusé
|
||||
└── player/ # domaine player
|
||||
├── PlayerProfile.java # entité, scores par joueur
|
||||
├── PlayerRanking.java # record (rank, profile, score)
|
||||
├── PlayerProfileRepository.java # interface
|
||||
├── InMemoryPlayerProfileRepository.java
|
||||
├── PlayerProfileService.java # interface
|
||||
├── PlayerProfileServiceImpl.java # impl avec hooks overridables
|
||||
├── PlayerException.java
|
||||
└── PlayerProfileNotFoundException.java
|
||||
```
|
||||
|
||||
> **Note IntelliJ** : le dossier physique s'appelle encore `CitesPlugin/`. Pour
|
||||
> le renommer en `CR-Core/`, fermer IntelliJ, renommer, rouvrir. Le `pom.xml`
|
||||
> et les packages sont déjà à jour, le nom du dossier n'a aucun impact sur le
|
||||
> build.
|
||||
Reference in New Issue
Block a user