feat: initialisation complète de la bibliothèque betterMcGuis (Fluent DSL, ItemBuilder, PaginatedGui, TabbedGui, AnimatedGui, Patterns ASCII, Moteur d'événements, Sécurité anti-glitch et documentation)

This commit is contained in:
2026-08-23 18:10:49 +02:00
parent b88eaad155
commit 806d1789a1
63 changed files with 6233 additions and 2 deletions
+17
View File
@@ -0,0 +1,17 @@
# Gradle
.gradle/
build/
!gradle/wrapper/gradle-wrapper.jar
# IDE
.idea/
*.iml
*.iws
*.ipr
out/
.vscode/
# OS / Misc
.DS_Store
Thumbs.db
*.log
+23
View File
@@ -0,0 +1,23 @@
# Directives & Architecture du Projet betterMcGuis
## 🎯 Objectif
`betterMcGuis` est une bibliothèque Java moderne, fluide et événementielle pour la création dynamique d'interfaces graphiques (GUIs / Menus interactifs / Inventaires) sur serveurs Minecraft Paper et Spigot (1.20.4+).
## 🌟 Principes Clés
1. **DSL Fluide & Déclaratif** : Création chainable d'inventaires (Coffres 1-6 lignes, Hoppers, Distributeurs, Enclumes).
2. **ItemBuilder Riches** : Prise en charge native de MiniMessage, enchantements, flags, têtes de joueurs, armures teintées, CustomModelData et PDC.
3. **Système de Layouts / Patterns** : Définition visuelle des inventaires par matrice de caractères (ASCII Masks / Patterns).
4. **Types de GUIs Avancés** :
- GUI Simple (`Gui`)
- GUI Paginé (`PaginatedGui`)
- GUI Défilant (`ScrollableGui`)
- GUI à Onglets (`TabbedGui`)
- GUI Animé (`AnimatedGui`)
- GUI avec zones d'interaction / coffre de dépôt (`StorageGui` / `EditableSlots`)
5. **Gestionnaire d'Événements & Cycle de Vie** :
- Événements complets : `GuiOpenEvent`, `GuiCloseEvent`, `GuiClickEvent`, `GuiPageChangeEvent`, `GuiRefreshEvent`.
- Hooks locaux (`.onOpen()`, `.onClose()`, `.onClick()`) et écouteurs globaux annotés `@GuiEventHandler`.
6. **Protection contre les Glitches** :
- Annulation sécurisée des clics, shift-clicks, drag-and-drop et échanges rapides (Hotbar swap).
- Nettoyage automatique des états lors de la déconnexion ou fermeture de l'inventaire.
7. **Indépendance & Testabilité** : Classes métier découplées du moteur Minecraft pour des tests unitaires rapides et fiables.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Luc Rival / GeminiAntigravityCLI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+358 -2
View File
@@ -1,3 +1,359 @@
# betterMcGuis
# 🖼️ betterMcGuis
Bibliotheque Java moderne, fluide et evenementielle pour la creation dynamique de GUIs et menus interactifs Minecraft Paper/Spigot.
[![Java](https://img.shields.io/badge/Java-17%2B%20%2F%2021-orange.svg)](https://www.oracle.com/java/)
[![Paper](https://img.shields.io/badge/Paper-1.20.4%2B-blue.svg)](https://papermc.io/)
[![Kyori Adventure](https://img.shields.io/badge/Adventure-MiniMessage-purple.svg)](https://docs.advntr.dev/)
[![License](https://img.shields.io/badge/License-MIT-green.svg)](#)
Une bibliothèque Java moderne, fluide et événementielle permettant de créer, animer et gérer des interfaces graphiques (GUIs / Menus interactifs / Inventaires) sur serveurs Minecraft Paper et Spigot (1.20.4+).
---
## 📑 Sommaire
- [📚 Guides Détaillés (Dossier `/docs`)](#-guides-détaillés-dossier-docs)
- [🌟 Fonctionnalités](#-fonctionnalités)
- [🏗️ Architecture & Flux d'Interactions](#-architecture--flux-dinteractions)
- [📦 Installation & Configuration](#-installation--configuration)
- [🚀 Guide de Démarrage Rapide](#-guide-de-démarrage-rapide)
- [💡 Exemples de Menus](#-exemples-de-menus)
- [1. Menu Principal Standard (`GuiBuilder`)](#1-menu-principal-standard-guibuilder)
- [2. Menu Paginé Dynamique (`PaginatedGui`)](#2-menu-paginé-dynamique-paginatedgui)
- [3. Motifs ASCII & Grilles de Disposition (`GuiPattern`)](#3-motifs-ascii--grilles-de-disposition-guipattern)
- [4. Menus à Onglets sans Réouverture (`TabbedGui`)](#4-menus-à-onglets-sans-réouverture-tabbedgui)
- [5. Zones de Dépôt / Poubelle (`StorageGui`)](#5-zones-de-dépôt--poubelle-storagegui)
- [💎 Construction d'Items (`ItemBuilder`)](#-construction-ditems-itembuilder)
- [🎯 Système d'Événements & Écouteurs Annotés](#-système-dévénements--écouteurs-annotés)
- [📂 Organisation des Packages](#-organisation-des-packages)
---
## 📚 Guides Détaillés (Dossier `/docs`)
Pour une exploration approfondie de chaque composant, consultez la documentation modulaire :
* 📘 [**01. Guide de Démarrage Rapide**](docs/01_GETTING_STARTED.md) : Installation Gradle/Maven, initialisation dans `JavaPlugin` et cycle de vie.
* 🏗️ [**02. Fluent GUI Builder DSL**](docs/02_GUI_BUILDER_DSL.md) : Méthodes de placement de slots, dimensions, bordures, remplissages et propriétés dynamiques.
* 💎 [**03. ItemBuilder & GuiItem**](docs/03_ITEM_BUILDER_AND_ITEMS.md) : Formatage MiniMessage, têtes personnalisées, armures teintées, sons, cooldowns et `GuiClickContext`.
* 🎨 [**04. Motifs & Masques ASCII**](docs/04_PATTERNS_AND_MASKS.md) : Conception visuelle de grilles par matrices textuelles (`GuiPattern`, `GuiMask`).
* 📚 [**05. Menus Paginés, Onglets & Animations**](docs/05_PAGINATED_AND_ADVANCED_GUIS.md) : Pagination automatique, onglets commutables sans réouverture et frames animées.
* 🎯 [**06. Cycle de Vie & Événements**](docs/06_LIFECYCLE_AND_EVENTS.md) : Hooks locaux (`.onClick()`, `.onClose()`) et écouteurs globaux annotés `@GuiEventHandler`.
* 📦 [**07. Zones de Stockage & Slots Éditables**](docs/07_STORAGE_AND_EDITABLE_SLOTS.md) : Menus de dépôts, restitution d'items et sécurités anti-glitch.
---
## 🌟 Fonctionnalités
- **DSL Fluide & Déclaratif** : Construction rapide et lisible (`BetterMcGuis.builder("Titre", 3)`).
- **Formatage Moderne MiniMessage & Adventure** : Prise en charge native des gradients HEX, balises de style et composants texte.
- **Grilles & Motifs ASCII (`GuiPattern`)** : Dessinez vos interfaces directement en texte sous forme de matrice avec associations de caractères.
- **Pagination Intelligente (`PaginatedGui`)** : Calcul automatique des pages, boutons de navigation dynamiques et indicateurs de progression.
- **Onglets Commutables (`TabbedGui`)** : Basculez entre plusieurs vues de menu instantanément sans fermer la fenêtre du joueur.
- **Animations Cadencées (`AnimatedGui`)** : Lecture de séquences de frames temporelles (titres animés, coffres mystères, carrousels).
- **Protection Anti-Duplication & Sécurité** : Blocage natif et sécurisé des vols d'items, glisser-déposer illégaux, shift-clics et raccourcis de hotbar.
- **Moteur d'Événements Riches** : Bus d'événements global avec priorités et annotations `@GuiEventHandler`.
---
## 🏗️ Architecture & Flux d'Interactions
```mermaid
flowchart TD
Player["Joueur en Jeu"] -->|Effectue un Clic / Drag / Fermeture| BukkitEvt["BukkitGuiEventListener (Sécurité & Interception)"]
BukkitEvt --> HolderCheck{"Inventaire lié à BetterGuiHolder ?"}
HolderCheck -- Non --> Ignore["Ignore l'interaction"]
HolderCheck -- Oui --> SlotTypeCheck{"Localisation du Clic"}
SlotTypeCheck -- Extérieur (-999) --> OutsideHook["gui.handleOutsideClick() & GuiClickEvent"]
SlotTypeCheck -- Inventaire Joueur --> BottomCheck{"Shift-Clic ou Hotbar Swap vers slot protégé ?"}
BottomCheck -- Oui --> BlockShift["event.setCancelled(true) & Blocage"]
BottomCheck -- Non --> BottomHook["gui.handleBottomClick()"]
SlotTypeCheck -- Inventaire Supérieur (GUI) --> EditCheck{"Le Slot est-il Editable ?"}
EditCheck -- Non (Défaut) --> CancelDef["event.setCancelled(true) (Empêche le vol)"]
EditCheck -- Oui --> AllowEdit["event.setCancelled(false) (Autorise le dépôt)"]
CancelDef --> CoolCheck{"Cooldown Actif sur l'Item ?"}
AllowEdit --> CoolCheck
CoolCheck -- Oui --> CoolBlock["Interruption silencieuse"]
CoolCheck -- Non --> SoundTrigger["Déclenchement du Son configuré"]
SoundTrigger --> ItemAction["Exécution de l'Action Métier (item.handleClick)"]
ItemAction --> GuiHook["Exécution du Hook Local (gui.handleClick)"]
GuiHook --> EventBus["Diffusion sur le Bus Global (GuiClickEvent)"]
```
---
## 📦 Installation & Configuration
### Gradle (Kotlin DSL)
```kotlin
repositories {
mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/")
}
dependencies {
implementation("fr.luc:betterMcGuis:1.0.0-SNAPSHOT")
}
```
---
## 🚀 Guide de Démarrage Rapide
### 1. Initialisation dans votre `JavaPlugin`
```java
package fr.luc.monplugin;
import fr.luc.bettermcguis.BetterMcGuis;
import org.bukkit.plugin.java.JavaPlugin;
public class MonPlugin extends JavaPlugin {
private BetterMcGuis guiManager;
@Override
public void onEnable() {
// Initialisation de betterMcGuis (enregistre automatiquement les écouteurs Bukkit)
this.guiManager = BetterMcGuis.create(this);
// Enregistrement d'écouteurs annotés globaux (optionnel)
this.guiManager.registerListeners(new MonEcouteurDeGui());
}
@Override
public void onDisable() {
// Nettoyage complet
if (guiManager != null) {
guiManager.unregisterAll();
}
}
}
```
---
## 💡 Exemples de Menus
### 1. Menu Principal Standard (`GuiBuilder`)
```java
BetterMcGuis.builder()
.title("<gradient:#ff5e62:#ff9966><bold>Menu Principal</bold></gradient>")
.rows(3)
.fillBorder(Material.BLACK_STAINED_GLASS_PANE)
// Bouton central interactif
.item(13, BetterMcGuis.item(Material.NETHER_STAR)
.name("<gold><bold>Récompense Quotidienne</bold></gold>")
.lore(
"<gray>Récupérez votre bonus de connexion !</gray>",
"",
"<yellow>▶ Cliquez pour réclamer</yellow>"
)
.glowing(true)
.asGuiItem(ctx -> {
ctx.playSound(Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.2f);
ctx.replySuccess("Bonus réclamé avec succès !");
ctx.close();
})
)
.build()
.open(player);
```
---
### 2. Menu Paginé Dynamique (`PaginatedGui`)
```java
var shop = BetterMcGuis.paginated()
.title("<gradient:#00c6ff:#0072ff><bold>Boutique du Serveur</bold></gradient>")
.rows(5)
.fillBorder(Material.GRAY_STAINED_GLASS_PANE);
// Ajout de 50 articles dans le catalogue paginé
for (int i = 1; i <= 50; i++) {
int id = i;
shop.addPageItem(BetterMcGuis.item(Material.DIAMOND)
.name("<aqua>Article #" + id + "</aqua>")
.lore("<gray>Prix : <gold>100 $</gold></gray>")
.asGuiItem(ctx -> {
ctx.replySuccess("Achat de l'article #" + id + " validé !");
})
);
}
shop.build().open(player);
```
---
### 3. Motifs ASCII & Grilles de Disposition (`GuiPattern`)
```java
BetterMcGuis.builder("Menu Quêtes", 5)
.pattern(p -> p
.lines(
"#########",
"# 1 2 3 #",
"# #",
"# C #",
"#########"
)
.bindFiller('#', Material.BLACK_STAINED_GLASS_PANE)
.bind('1', BetterMcGuis.item(Material.BOOK).name("<green>Quête Facile</green>").asGuiItem())
.bind('2', BetterMcGuis.item(Material.WRITABLE_BOOK).name("<yellow>Quête Moyenne</yellow>").asGuiItem())
.bind('3', BetterMcGuis.item(Material.ENCHANTED_BOOK).name("<red>Quête Difficile</red>").asGuiItem())
.bind('C', BetterMcGuis.item(Material.BARRIER).name("<red>Fermer</red>").asGuiItem(GuiClickContext::close))
)
.build()
.open(player);
```
---
### 4. Menus à Onglets sans Réouverture (`TabbedGui`)
```java
BetterMcGuis.tabbed("Menu Profil", 4)
// Onglet 1 : Statistiques
.tab("stats", 11, BetterMcGuis.item(Material.PAPER).name("<yellow>Statistiques</yellow>").asGuiItem(), tab -> {
tab.setTabItem("stats", 22, BetterMcGuis.item(Material.EXPERIENCE_BOTTLE).name("<green>Niveau 42</green>").asGuiItem());
})
// Onglet 2 : Récompenses
.tab("rewards", 15, BetterMcGuis.item(Material.CHEST).name("<gold>Récompenses</gold>").asGuiItem(), tab -> {
tab.setTabItem("rewards", 22, BetterMcGuis.item(Material.GOLD_INGOT).name("<gold>Bonus Débloqué</gold>").asGuiItem());
})
.build()
.open(player);
```
---
### 5. Zones de Dépôt / Poubelle (`StorageGui`)
```java
StorageGui trash = new StorageGui("<red><bold>Poubelle Publique</bold></red>", GuiType.CHEST_4_ROWS);
trash.fillBorder(Material.RED_STAINED_GLASS_PANE);
trash.setStorageSlots(SlotRange.interior(4, 9));
trash.returnItemsOnClose(false); // Détruit définitivement les objets déposés
trash.open(player);
```
---
## 💎 Construction d'Items (`ItemBuilder`)
```java
ItemStack item = BetterMcGuis.item(Material.DIAMOND_HELMET)
.name("<gradient:#4facfe:#00f2fe><bold>Casque de Diamant Divin</bold></gradient>")
.lore(
"<gray>Forgé par les anciens dieux.</gray>",
"",
"<yellow>Protection : <aqua>IV</aqua></yellow>"
)
.enchant(Enchantment.PROTECTION_ENVIRONMENTAL, 4)
.unbreakable(true)
.customModelData(2005)
.glowing(true)
.build();
```
---
## 🎯 Système d'Événements & Écouteurs Annotés
```java
package fr.luc.monplugin.listener;
import fr.luc.bettermcguis.event.*;
import fr.luc.bettermcguis.event.annotation.GuiEventHandler;
public class MonEcouteurGlobalGui {
@GuiEventHandler(priority = 10)
public void onGuiClick(GuiClickEvent event) {
var ctx = event.getContext();
System.out.println("[GUI] Clic de " + ctx.getPlayer().getName() + " sur le slot #" + ctx.getSlot());
}
@GuiEventHandler
public void onGuiClose(GuiCloseEvent event) {
System.out.println("[GUI] Fermeture du menu : " + event.getGui().getTitle());
}
}
```
---
## 📂 Organisation des Packages
```
fr.luc.bettermcguis/
├── BetterMcGuis.java (Façade principale & Enregistrement)
├── api/ (Contrats et modèles publics)
│ ├── Gui.java (Interface de base de tout GUI)
│ ├── GuiType.java (Types de coffres 1-6 et types Bukkit)
│ ├── GuiItem.java (Items interactifs, cooldowns, sons, visibilité)
│ ├── GuiClickAction.java (Callback fonctionnel de clic)
│ ├── GuiClickContext.java (Contexte complet de clic)
│ ├── GuiOpenContext.java (Contexte d'ouverture)
│ ├── GuiCloseContext.java (Contexte de fermeture)
│ └── slot/
│ ├── SlotPos.java (Position ligne, colonne)
│ └── SlotRange.java (Bordures, intérieurs, plages)
├── builder/ (Constructeurs fluides)
│ ├── ItemBuilder.java (Création d'ItemStacks riches avec Adventure)
│ ├── GuiBuilder.java (Constructeur de menus standards)
│ ├── PaginatedGuiBuilder.java (Constructeur de menus paginés)
│ ├── TabbedGuiBuilder.java (Constructeur de menus à onglets)
│ ├── AnimatedGuiBuilder.java (Constructeur de menus animés)
│ └── PatternBuilder.java (Constructeur de motifs ASCII)
├── pattern/ (Moteur de disposition par grilles ASCII)
│ ├── GuiPattern.java
│ └── GuiMask.java
├── type/ (Implémentations de GUIs)
│ ├── SimpleGui.java
│ ├── PaginatedGui.java
│ ├── TabbedGui.java
│ ├── AnimatedGui.java
│ └── StorageGui.java
├── animation/ (Moteur d'animation)
│ └── Frame.java
├── event/ (Bus d'événements & Lifecycle)
│ ├── GuiEvent.java
│ ├── CancellableGuiEvent.java
│ ├── GuiOpenEvent.java
│ ├── GuiCloseEvent.java
│ ├── GuiClickEvent.java
│ ├── GuiPageChangeEvent.java
│ ├── GuiRefreshEvent.java
│ ├── GuiEventManager.java
│ ├── GuiEventListener.java
│ └── annotation/
│ └── GuiEventHandler.java
├── holder/ (Liaison avec InventoryHolder de Bukkit)
│ └── BetterGuiHolder.java
├── listener/ (Interception Bukkit & Sécurités anti-glitch)
│ └── BukkitGuiEventListener.java
└── demo/ (Démonstrations et plugin de test)
├── DemoGuiPlugin.java
├── guis/
│ ├── DemoMainMenuGui.java
│ ├── DemoPaginatedShopGui.java
│ └── DemoStorageTrashGui.java
└── listeners/
└── DemoGuiListener.java
```
---
## 📄 Licence
Ce projet est sous licence [MIT](LICENSE).
+77
View File
@@ -0,0 +1,77 @@
plugins {
`java-library`
`maven-publish`
id("com.gradleup.shadow") version "8.3.6"
}
group = "fr.luc"
version = "1.0.0-SNAPSHOT"
repositories {
mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/")
maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/")
maven("https://oss.sonatype.org/content/groups/public/")
maven("https://jitpack.io")
}
dependencies {
// Paper API 1.20.4 (Fournit Bukkit, InventoryHolder et Adventure natif)
compileOnly("io.papermc.paper:paper-api:1.20.4-R0.1-SNAPSHOT")
// Kyori Adventure & MiniMessage
compileOnly("net.kyori:adventure-api:4.17.0")
compileOnly("net.kyori:adventure-text-minimessage:4.17.0")
// Tests unitaires JUnit 5 & Mockito
testImplementation(platform("org.junit:junit-bom:5.10.2"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("org.mockito:mockito-core:5.11.0")
testImplementation("org.mockito:mockito-junit-jupiter:5.11.0")
// Paper API et Adventure dans les tests unitaires
testImplementation("io.papermc.paper:paper-api:1.20.4-R0.1-SNAPSHOT")
testImplementation("net.kyori:adventure-api:4.17.0")
testImplementation("net.kyori:adventure-text-minimessage:4.17.0")
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
withSourcesJar()
withJavadocJar()
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
options.release.set(17)
}
tasks.withType<Javadoc> {
options.encoding = "UTF-8"
(options as StandardJavadocDocletOptions).apply {
addStringOption("Xdoclint:none", "-quiet")
encoding = "UTF-8"
charSet = "UTF-8"
}
}
tasks.shadowJar {
archiveClassifier.set("")
}
tasks.build {
dependsOn(tasks.shadowJar)
}
publishing {
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
groupId = "fr.luc"
artifactId = "betterMcGuis"
version = "1.0.0-SNAPSHOT"
}
}
}
+89
View File
@@ -0,0 +1,89 @@
# 🚀 Guide de Démarrage Rapide - betterMcGuis
Ce guide vous accompagne dans l'installation, la configuration et la création de vos premières interfaces graphiques interactives (GUIs) avec la bibliothèque **betterMcGuis**.
---
## 📦 1. Installation
### Gradle (Kotlin DSL)
Ajoutez les dépôts et la dépendance dans votre fichier `build.gradle.kts` :
```kotlin
repositories {
mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/")
}
dependencies {
// Intégration de betterMcGuis
implementation("fr.luc:betterMcGuis:1.0.0-SNAPSHOT")
}
```
---
## ⚙️ 2. Initialisation dans votre Plugin Bukkit/Paper
Créez une instance de `BetterMcGuis` dans la méthode `onEnable()` de votre plugin et nettoyez-la proprement dans `onDisable()` :
```java
package fr.luc.monplugin;
import fr.luc.bettermcguis.BetterMcGuis;
import org.bukkit.plugin.java.JavaPlugin;
public class MonPlugin extends JavaPlugin {
private BetterMcGuis guiManager;
@Override
public void onEnable() {
// 1. Initialisation du gestionnaire et enregistrement automatique des écouteurs Bukkit
this.guiManager = BetterMcGuis.create(this);
// 2. Enregistrement d'écouteurs d'événements globaux (optionnel)
this.guiManager.registerListeners(new MonEcouteurDeGui());
getLogger().info("betterMcGuis est prêt !");
}
@Override
public void onDisable() {
// 3. Nettoyage des écouteurs et suppression des ressources
if (guiManager != null) {
guiManager.unregisterAll();
}
}
}
```
---
## 💡 3. Votre Premier Menu en 5 Lignes
```java
public void ouvrirMenuBienvenue(Player player) {
BetterMcGuis.builder()
.title("<gradient:#00c6ff:#0072ff><bold>Menu de Bienvenue</bold></gradient>")
.rows(3)
.fillBorder(Material.BLACK_STAINED_GLASS_PANE)
.item(13, BetterMcGuis.item(Material.DIAMOND)
.name("<gold><bold>Réclamer le Cadeau</bold></gold>")
.lore("<gray>Cliquez pour recevoir 1 Diamant gratuit !</gray>")
.glowing(true)
.asGuiItem(ctx -> {
ctx.getPlayer().getInventory().addItem(new ItemStack(Material.DIAMOND));
ctx.replySuccess("Diamant reçu avec succès !");
ctx.close();
})
)
.build()
.open(player);
}
```
---
> 📖 **Étape suivante** : Découvrez la syntaxe complète du constructeur dans [02_GUI_BUILDER_DSL.md](02_GUI_BUILDER_DSL.md).
+94
View File
@@ -0,0 +1,94 @@
# 🏗️ Fluent GUI Builder DSL - betterMcGuis
La création de menus repose sur un pattern **Builder chainable, fluide et modulaire**.
---
## 📑 Sommaire
- [1. Configuration des Dimensions et Types d'Inventaires](#1-configuration-des-dimensions-et-types-dinventaires)
- [2. Placement des Items](#2-placement-des-items)
- [3. Remplissage et Bordures](#3-remplissage-et-bordures)
- [4. Propriétés Dynamiques](#4-propriétés-dynamiques)
- [5. Contrôle du Cycle de Vie et Ouvertures](#5-contrôle-du-cycle-de-vie-et-ouvertures)
---
## 1. Configuration des Dimensions et Types d'Inventaires
Vous pouvez configurer des coffres standards de 1 à 6 lignes, ou des types d'inventaires Bukkit spécifiques :
```java
// Coffre 3 lignes (27 slots)
BetterMcGuis.builder("Titre", 3);
// Types spécifiques via GuiType
BetterMcGuis.builder()
.title("<yellow>Distributeur</yellow>")
.type(GuiType.DISPENSER); // DISPENSER, DROPPER, HOPPER, ANVIL, WORKBENCH, BREWING
```
---
## 2. Placement des Items
Vous pouvez positionner des items selon trois méthodes d'adressage :
```java
var builder = BetterMcGuis.builder("Exemple", 4);
// Par slot absolu (0-indexé, 0 à 35)
builder.item(13, monItem);
// Par coordonnées ligne/colonne (0-indexé, ligne 1, col 4)
builder.item(1, 4, monItem);
// Par SlotPos
builder.item(SlotPos.of(1, 4), monItem);
```
---
## 3. Remplissage et Bordures
Des méthodes utilitaires permettent de remplir instantanément l'inventaire ou ses contours :
```java
// Remplit toutes les bordures extérieures avec du vitrage teinté
builder.fillBorder(Material.GRAY_STAINED_GLASS_PANE);
// Remplit une plage continue de slots
builder.fillRange(SlotRange.of(10, 16), monItem);
// Remplit tout l'inventaire
builder.fill(monItem);
```
---
## 4. Propriétés Dynamiques
Vous pouvez attacher des données arbitraires à l'instance de GUI pour les partager entre callbacks :
```java
Gui gui = BetterMcGuis.builder("Profil", 3)
.property("targetPlayerUuid", target.getUniqueId())
.property("openedAt", System.currentTimeMillis())
.build();
// Récupération typée dans un callback
UUID uuid = gui.getProperty("targetPlayerUuid", UUID.class);
```
---
## 5. Contrôle du Cycle de Vie et Ouvertures
* `gui.open(Player)` : Ouvre l'inventaire pour le joueur.
* `gui.close(Player)` : Ferme l'inventaire.
* `gui.refresh(Player)` : Met à jour visuellement les items sans réouverture.
* `gui.refreshAll()` : Rafraîchit l'affichage pour tous les spectateurs actifs.
* `gui.setTitle("<green>Nouveau Titre</green>")` : Modifie le titre du menu.
---
> 📖 **Étape suivante** : Découvrez la création d'items avec [03_ITEM_BUILDER_AND_ITEMS.md](03_ITEM_BUILDER_AND_ITEMS.md).
+111
View File
@@ -0,0 +1,111 @@
# 💎 ItemBuilder & GuiItem - betterMcGuis
La création et l'interaction avec les items Minecraft sont grandement simplifiées grâce à `ItemBuilder` et `GuiItem`.
---
## 📑 Sommaire
- [1. Création d'ItemStacks avec `ItemBuilder`](#1-création-ditemstacks-avec-itembuilder)
- [2. Prise en Charge de MiniMessage et Kyori Adventure](#2-prise-en-charge-de-minimessage-et-kyori-adventure)
- [3. Têtes Personnalisées, Couleurs et Flags](#3-têtes-personnalisées-couleurs-et-flags)
- [4. L'Objet Interactif `GuiItem`](#4-lobjet-interactif-guiitem)
- [5. Le Contexte de Clic (`GuiClickContext`)](#5-le-contexte-de-clic-guiclickcontext)
---
## 1. Création d'ItemStacks avec `ItemBuilder`
```java
ItemStack epee = ItemBuilder.of(Material.DIAMOND_SWORD)
.name("<gradient:#ff0844:#ffb199><bold>Lame des Enfers</bold></gradient>")
.lore(
"<gray>Une épée forgée dans les profondeurs du Nether.</gray>",
"",
"<yellow>Dégâts : <red>+15</red></yellow>",
"<green>Effet : <gold>Enflamme les cibles</gold></green>"
)
.amount(1)
.enchant(Enchantment.DAMAGE_ALL, 5)
.enchant(Enchantment.FIRE_ASPECT, 2)
.unbreakable(true)
.flags(ItemFlag.HIDE_UNBREAKABLE)
.customModelData(1001)
.build();
```
---
## 2. Prise en Charge de MiniMessage et Kyori Adventure
Tous les noms et lores utilisent nativement les balises MiniMessage :
* `<gradient:#color1:#color2>Texte</gradient>` : Dégradés de couleurs HEX.
* `<bold>`, `<italic>`, `<underlined>`, `<strikethrough>` : Mises en forme.
* `<rainbow>Texte Arc-en-ciel</rainbow>` : Animation multicolore.
* `<green>`, `<red>`, `<gold>`, `<aqua>`, `<gray>`, etc.
---
## 3. Têtes Personnalisées, Couleurs et Flags
```java
// Tête de joueur par pseudo ou UUID
GuiItem tete = ItemBuilder.skull()
.skullOwner("Luc")
.name("<yellow>Profil de Luc</yellow>")
.asGuiItem();
// Armure en cuir teintée
ItemStack armure = ItemBuilder.of(Material.LEATHER_CHESTPLATE)
.color(Color.fromRGB(41, 128, 185))
.build();
// Item brillant sans texte d'enchantement
GuiItem etoile = ItemBuilder.of(Material.NETHER_STAR)
.glowing(true)
.asGuiItem();
```
---
## 4. L'Objet Interactif `GuiItem`
Un `GuiItem` encapsule un `ItemStack` avec des comportements avancés :
```java
GuiItem bouton = ItemBuilder.of(Material.EMERALD)
.name("<green>Confirmer l'achat</green>")
.asGuiItem(ctx -> {
ctx.replySuccess("Achat confirmé !");
})
// Condition de visibilité : seuls les joueurs avec la permission voient cet item
.visibleIf(player -> player.hasPermission("monplugin.vip"))
// Cooldown de 2 secondes entre les clics
.cooldown(Duration.ofSeconds(2))
// Joue un son au joueur
.sound(Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.2f)
// Ferme l'inventaire après le clic
.closeOnClick();
```
---
## 5. Le Contexte de Clic (`GuiClickContext`)
| Méthode | Description |
|---|---|
| `ctx.getPlayer()` | Récupère le joueur ayant cliqué. |
| `ctx.getSlot()` | Récupère l'index du slot absolu cliqué. |
| `ctx.getSlotPos()` | Récupère la position (ligne, colonne). |
| `ctx.getClickType()` | Récupère le type de clic (`LEFT`, `RIGHT`, `SHIFT_LEFT`, etc.). |
| `ctx.isLeftClick()` / `isRightClick()` | Raccourcis booléens pour tester le type de clic. |
| `ctx.isShiftClick()` | Teste si la touche Shift était enfoncée. |
| `ctx.close()` | Ferme l'inventaire actuel. |
| `ctx.refresh()` | Rafraîchit les items du menu. |
| `ctx.playSound(sound, vol, pitch)` | Joue un son de confirmation ou d'erreur. |
| `ctx.reply("<yellow>Message</yellow>")`| Envoie un message formaté en MiniMessage. |
| `ctx.replySuccess(msg)` | Envoie un message de succès préfixé en vert. |
| `ctx.replyError(msg)` | Envoie un message d'erreur préfixé en rouge. |
---
> 📖 **Étape suivante** : Apprenez à concevoir des grilles avec [04_PATTERNS_AND_MASKS.md](04_PATTERNS_AND_MASKS.md).
+82
View File
@@ -0,0 +1,82 @@
# 🎨 Motifs & Masques ASCII - betterMcGuis
La création de mises en page complexes est grandement facilitée par le système de **Motifs (Patterns)** et **Masques (Masks)** textuels.
---
## 📑 Sommaire
- [1. Qu'est-ce qu'un Motif ASCII (`GuiPattern`) ?](#1-quest-ce-quun-motif-ascii-guipattern-)
- [2. Déclaration et Utilisation](#2-déclaration-et-utilisation)
- [3. Masques de Sélection de Slots (`GuiMask`)](#3-masques-de-sélection-de-slots-guimask)
---
## 1. Qu'est-ce qu'un Motif ASCII (`GuiPattern`) ?
Un `GuiPattern` vous permet de dessiner littéralement la disposition visuelle de votre inventaire sous forme de lignes de texte, où chaque caractère représente un type d'item.
---
## 2. Déclaration et Utilisation
```java
GuiPattern pattern = BetterMcGuis.pattern(
"#########",
"# S K Q #",
"# #",
"# C #",
"#########"
)
.bindFiller('#', Material.BLACK_STAINED_GLASS_PANE)
.bind('S', ItemBuilder.of(Material.EMERALD).name("<green>Boutique</green>").asGuiItem(this::ouvrirShop))
.bind('K', ItemBuilder.of(Material.CHEST).name("<gold>Kits</gold>").asGuiItem(this::ouvrirKits))
.bind('Q', ItemBuilder.of(Material.BOOK).name("<aqua>Quêtes</aqua>").asGuiItem(this::ouvrirQuetes))
.bind('C', ItemBuilder.of(Material.BARRIER).name("<red>Fermer</red>").asGuiItem(GuiClickContext::close))
.build();
// Application directe sur le GUI
BetterMcGuis.builder("Menu Principal", 5)
.pattern(pattern)
.build()
.open(player);
```
Vous pouvez aussi définir le motif directement dans le builder de manière fluide :
```java
BetterMcGuis.builder("Menu Compact", 3)
.pattern(p -> p
.lines(
"111111111",
"1 D 1",
"111111111"
)
.bindFiller('1', Material.GRAY_STAINED_GLASS_PANE)
.bind('D', ItemBuilder.of(Material.DIAMOND).name("<aqua>Trésor</aqua>").asGuiItem())
)
.build();
```
---
## 3. Masques de Sélection de Slots (`GuiMask`)
Un `GuiMask` permet de sélectionner des ensembles de slots spécifiques via une matrice binaire (`0` et `1`) pour définir par exemple des zones d'items paginés ou des slots éditables :
```java
GuiMask masqueZone = new GuiMask(
"000000000",
"011111110",
"011111110",
"000000000"
);
// Utilisation pour définir les slots d'un menu paginé
BetterMcGuis.paginated("Boutique", 4)
.itemSlots(masqueZone)
.build();
```
---
> 📖 **Étape suivante** : Découvrez les menus avancés dans [05_PAGINATED_AND_ADVANCED_GUIS.md](05_PAGINATED_AND_ADVANCED_GUIS.md).
+89
View File
@@ -0,0 +1,89 @@
# 📚 Menus Paginés, Onglets & Animations - betterMcGuis
Ce guide explore les types d'inventaires avancés : **PaginatedGui**, **TabbedGui** et **AnimatedGui**.
---
## 📑 Sommaire
- [1. Menus Paginés (`PaginatedGui`)](#1-menus-paginés-paginatedgui)
- [2. Menus à Onglets (`TabbedGui`)](#2-menus-à-onglets-tabbedgui)
- [3. Menus Animés (`AnimatedGui`)](#3-menus-animés-animatedgui)
---
## 1. Menus Paginés (`PaginatedGui`)
Le `PaginatedGui` prend en charge le calcul automatique des pages, la disposition des boutons de navigation et les indicateurs dynamiques.
```java
var shop = BetterMcGuis.paginated("Boutique", 5)
.fillBorder(Material.BLACK_STAINED_GLASS_PANE)
// Personnalisation des boutons de navigation
.previousButton(38, ItemBuilder.of(Material.ARROW).name("<yellow>◀ Page Précédente</yellow>").asGuiItem())
.nextButton(42, ItemBuilder.of(Material.ARROW).name("<yellow>Page Suivante ▶</yellow>").asGuiItem())
// Indicateur de page personnalisée
.pageIndicator(40, (page, total) -> ItemBuilder.of(Material.PAPER)
.name("<gold><bold>Page " + page + " / " + total + "</bold></gold>")
.asGuiItem()
);
// Ajout de 100 items au catalogue
for (int i = 1; i <= 100; i++) {
shop.addPageItem(ItemBuilder.of(Material.GOLD_INGOT)
.name("<yellow>Lingot #" + i + "</yellow>")
.asGuiItem(ctx -> ctx.replySuccess("Achat réussi !"))
);
}
shop.build().open(player);
```
---
## 2. Menus à Onglets (`TabbedGui`)
Permet de basculer instantanément entre plusieurs catégories d'items **sans fermer ni rouvrir l'inventaire** :
```java
BetterMcGuis.tabbed("Menu Multi-Catégories", 4)
// Onglet 1 : Guerrier
.tab("warrior", 11, ItemBuilder.of(Material.IRON_SWORD).name("<red>Guerrier</red>").asGuiItem(), tabGui -> {
tabGui.setTabItem("warrior", 20, ItemBuilder.of(Material.DIAMOND_SWORD).name("<red>Épée Lourde</red>").asGuiItem());
tabGui.setTabItem("warrior", 24, ItemBuilder.of(Material.SHIELD).name("<gray>Bouclier</gray>").asGuiItem());
})
// Onglet 2 : Archer
.tab("archer", 15, ItemBuilder.of(Material.BOW).name("<green>Archer</green>").asGuiItem(), tabGui -> {
tabGui.setTabItem("archer", 22, ItemBuilder.of(Material.CROSSBOW).name("<green>Arbalète Précise</green>").asGuiItem());
})
.build()
.open(player);
```
---
## 3. Menus Animés (`AnimatedGui`)
Permet d'animer des frames d'inventaires cadencées par tick ou secondes :
```java
AnimatedGui animated = BetterMcGuis.animated()
.title("<gold>Ouverture de Coffre Mystère...</gold>")
.rows(3)
// Frame 1
.frame(f -> f.item(13, ItemBuilder.of(Material.CHEST).name("<yellow>Chargement.</yellow>").asGuiItem()))
// Frame 2
.frame(f -> f.item(13, ItemBuilder.of(Material.ENDER_CHEST).name("<gold>Chargement..</gold>").asGuiItem()))
// Frame 3 : Révélation du lot !
.frame(f -> f.item(13, ItemBuilder.of(Material.NETHER_STAR).name("<gradient:#00f2fe:#4facfe>Bravo ! Trésor Obtenu !</gradient>").asGuiItem()))
.loop(false)
.build();
animated.open(player);
animated.startAnimation(monPlugin, Duration.ofMillis(600));
```
---
> 📖 **Étape suivante** : Découvrez le cycle de vie et les événements dans [06_LIFECYCLE_AND_EVENTS.md](06_LIFECYCLE_AND_EVENTS.md).
+91
View File
@@ -0,0 +1,91 @@
# 🎯 Cycle de Vie & Système d'Événements - betterMcGuis
La bibliothèque intègre un moteur d'événements à deux niveaux : **Hooks Locaux** (sur l'instance du menu) et **Bus d'Événements Global** (avec annotations `@GuiEventHandler`).
---
## 📑 Sommaire
- [1. Hooks Locaux sur l'Instance de Menu](#1-hooks-locaux-sur-linstance-de-menu)
- [2. Bus d'Événements Global & Annotations `@GuiEventHandler`](#2-bus-dévénements-global--annotations-guieventhandler)
- [3. Référence Exhaustive des Événements](#3-référence-exhaustive-des-événements)
---
## 1. Hooks Locaux sur l'Instance de Menu
```java
BetterMcGuis.builder("Banque", 3)
.onOpen(ctx -> {
ctx.getPlayer().sendMessage("§aBienvenue dans votre coffre sécurisé.");
})
.onClose(ctx -> {
sauvegarderDonnees(ctx.getPlayer());
})
.onOutsideClick(ctx -> {
// Ferme si le joueur clique en dehors du menu
ctx.close();
})
.onBottomClick(ctx -> {
// Clic dans son propre inventaire
ctx.getPlayer().sendMessage("§7Action détectée dans votre inventaire.");
})
.build();
```
---
## 2. Bus d'Événements Global & Annotations `@GuiEventHandler`
Vous pouvez créer des classes dédiées pour auditer, logger ou sécuriser l'ensemble des GUIs de votre serveur :
```java
package fr.luc.monplugin.listener;
import fr.luc.bettermcguis.event.*;
import fr.luc.bettermcguis.event.annotation.GuiEventHandler;
public class MonEcouteurGlobalGui {
// Écoute TOUS les clics de GUIs avec priorité élevée
@GuiEventHandler(priority = 100)
public void onGlobalClick(GuiClickEvent event) {
var ctx = event.getContext();
System.out.println("[Audit GUI] Joueur : " + ctx.getPlayer().getName() +
" | Slot : #" + ctx.getSlot() +
" | Clic : " + ctx.getClickType());
}
// Écoute UNIQUEMENT les ouvertures de la boutique
@GuiEventHandler(guiTitle = "<gradient:#00c6ff:#0072ff><bold>Boutique Paginée</bold></gradient>")
public void onShopOpen(GuiOpenEvent event) {
event.getPlayer().sendMessage("§eProfitez des promotions du week-end !");
}
// Écoute les fermetures de menus
@GuiEventHandler
public void onClose(GuiCloseEvent event) {
// Traitement global
}
}
```
Pour enregistrer l'écouteur :
```java
guiManager.registerListeners(new MonEcouteurGlobalGui());
```
---
## 3. Référence Exhaustive des Événements
| Classe d'Événement | Description | Annulable ? |
|---|---|:---:|
| `GuiOpenEvent` | Déclenché avant l'ouverture du menu pour un joueur. | ✅ **Oui** |
| `GuiCloseEvent` | Déclenché lorsque le joueur ferme le menu. | ❌ Non |
| `GuiClickEvent` | Déclenché lors de chaque clic (item, slot vide ou extérieur). | ✅ **Oui** |
| `GuiPageChangeEvent` | Déclenché lors d'un changement de page dans un `PaginatedGui`. | ❌ Non |
| `GuiRefreshEvent` | Déclenché lors du rafraîchissement d'un inventaire. | ❌ Non |
---
> 📖 **Étape suivante** : Découvrez les zones de stockage dans [07_STORAGE_AND_EDITABLE_SLOTS.md](07_STORAGE_AND_EDITABLE_SLOTS.md).
+59
View File
@@ -0,0 +1,59 @@
# 📦 Zones de Stockage & Slots Éditables - betterMcGuis
Par défaut, tous les menus créés avec `betterMcGuis` interdisent le vol, le déplacement et le shift-clic d'items pour garantir une sécurité totale contre les duplications de bugs d'inventaires.
Cependant, pour les fonctionnalités nécessitant que les joueurs déposent ou retirent des objets (comme une poubelle, un coffre de recyclage, une enclume ou un hôtel des ventes), `betterMcGuis` propose **les slots éditables** et le **`StorageGui`**.
---
## 📑 Sommaire
- [1. Slots Éditables dans un GUI Standard](#1-slots-éditables-dans-un-gui-standard)
- [2. Menu de Stockage Dédié (`StorageGui`)](#2-menu-de-stockage-dédié-storagegui)
- [3. Sécurité contre les Duplications et Glitches](#3-sécurité-contre-les-duplications-et-glitches)
---
## 1. Slots Éditables dans un GUI Standard
Vous pouvez autoriser les interactions de placement et retrait d'items sur des slots ciblés avec `.editable(...)` :
```java
BetterMcGuis.builder("Boîte de Dépôt", 3)
.fillBorder(Material.BLACK_STAINED_GLASS_PANE)
// Rend le slot 13 (centre) modifiable librement par le joueur
.editable(13)
.build()
.open(player);
```
---
## 2. Menu de Stockage Dédié (`StorageGui`)
`StorageGui` simplifie la gestion d'une zone centrale de dépôt avec restitution automatique des items laissés à la fermeture :
```java
StorageGui trash = new StorageGui("<red>Poubelle</red>", GuiType.CHEST_4_ROWS);
// Remplissage des bordures avec du vitrage
trash.fillBorder(ItemBuilder.filler(Material.RED_STAINED_GLASS_PANE).asGuiItem());
// Définit les slots intérieurs comme zone de dépôt
trash.setStorageSlots(SlotRange.interior(4, 9));
// true = restitue les items au joueur à la fermeture
// false = détruit définitivement les items à la fermeture (idéal poubelle)
trash.returnItemsOnClose(false);
trash.open(player);
```
---
## 3. Sécurité contre les Duplications et Glitches
Le gestionnaire `BukkitGuiEventListener` applique des sécurités strictes :
* **Shift-Clic Protection** : Empêche un joueur de shift-cliquer un item depuis son inventaire vers un slot protégé du menu.
* **Number Key Protection** : Empêche l'échange d'items via les touches numériques (1 à 9 de la hotbar) vers des slots non éditables.
* **Drag-and-Drop Protection** : Bloque le glisser-déposer de curseur sur tous les slots non autorisés.
* **Nettoyage automatique** : En cas de déconnexion ou de fermeture inopinée, les items des zones avec `returnItemsOnClose(true)` sont automatiquement réinsérés dans l'inventaire du joueur ou déposés au sol s'il est plein.
+2
View File
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8
systemProp.file.encoding=UTF-8
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+248
View File
@@ -0,0 +1,248 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
Vendored
+82
View File
@@ -0,0 +1,82 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
+1
View File
@@ -0,0 +1 @@
rootProject.name = "betterMcGuis"
@@ -0,0 +1,167 @@
package fr.luc.bettermcguis;
import fr.luc.bettermcguis.builder.*;
import fr.luc.bettermcguis.event.GuiEventManager;
import fr.luc.bettermcguis.listener.BukkitGuiEventListener;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.event.HandlerList;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import java.util.Objects;
/**
* Point d'entrée principal et gestionnaire central de la bibliothèque **betterMcGuis**.
* Fournit l'accès aux constructeurs fluides de menus (simples, paginés, à onglets, animés),
* au constructeur d'items (ItemBuilder), aux motifs ASCII et au bus d'événements.
*/
public class BetterMcGuis {
private static BetterMcGuis instance;
private final Plugin plugin;
private final GuiEventManager eventManager;
private final BukkitGuiEventListener bukkitListener;
/**
* Initialise une instance de betterMcGuis pour un plugin donné.
*
* @param plugin Le plugin Bukkit/Paper propriétaire.
*/
public BetterMcGuis(Plugin plugin) {
this.plugin = plugin;
this.eventManager = new GuiEventManager();
this.bukkitListener = new BukkitGuiEventListener(this);
if (plugin != null) {
Bukkit.getPluginManager().registerEvents(bukkitListener, plugin);
}
if (instance == null) {
instance = this;
}
}
/**
* Initialise betterMcGuis pour un plugin Bukkit/Paper.
*
* @param plugin Le plugin propriétaire.
* @return L'instance configurée.
*/
public static BetterMcGuis create(Plugin plugin) {
return new BetterMcGuis(plugin);
}
/**
* @return L'instance globale ou par défaut.
*/
public static BetterMcGuis getInstance() {
return instance;
}
/**
* Crée un nouveau constructeur de menu standard ({@link GuiBuilder}).
*/
public static GuiBuilder builder() {
return new GuiBuilder();
}
/**
* Crée un nouveau constructeur de menu avec titre et nombre de lignes.
*/
public static GuiBuilder builder(String title, int rows) {
return new GuiBuilder().title(title).rows(rows);
}
/**
* Crée un nouveau constructeur d'inventaire paginé ({@link PaginatedGuiBuilder}).
*/
public static PaginatedGuiBuilder paginated() {
return new PaginatedGuiBuilder();
}
/**
* Crée un nouveau constructeur d'inventaire paginé avec titre et nombre de lignes.
*/
public static PaginatedGuiBuilder paginated(String title, int rows) {
return (PaginatedGuiBuilder) new PaginatedGuiBuilder().title(title).rows(rows);
}
/**
* Crée un nouveau constructeur de menu à onglets ({@link TabbedGuiBuilder}).
*/
public static TabbedGuiBuilder tabbed() {
return new TabbedGuiBuilder();
}
/**
* Crée un nouveau constructeur de menu à onglets avec titre et nombre de lignes.
*/
public static TabbedGuiBuilder tabbed(String title, int rows) {
return (TabbedGuiBuilder) new TabbedGuiBuilder().title(title).rows(rows);
}
/**
* Crée un nouveau constructeur de menu animé ({@link AnimatedGuiBuilder}).
*/
public static AnimatedGuiBuilder animated() {
return new AnimatedGuiBuilder();
}
/**
* Crée un nouveau constructeur d'ItemStack moderne ({@link ItemBuilder}).
*/
public static ItemBuilder item(Material material) {
return ItemBuilder.of(material);
}
/**
* Crée un constructeur d'item à partir d'un {@link ItemStack} existant.
*/
public static ItemBuilder item(ItemStack itemStack) {
return ItemBuilder.of(itemStack);
}
/**
* Crée un nouveau constructeur de motif / masque ASCII ({@link PatternBuilder}).
*/
public static PatternBuilder pattern(String... lines) {
return new PatternBuilder(lines);
}
/**
* Enregistre un ou plusieurs écouteurs d'événements de GUI annotés avec {@link fr.luc.bettermcguis.event.annotation.GuiEventHandler}.
*
* @param listeners Les objets écouteurs.
*/
public void registerListeners(Object... listeners) {
if (listeners != null) {
for (Object listener : listeners) {
eventManager.registerListeners(listener);
}
}
}
/**
* @return Le gestionnaire de bus d'événements central.
*/
public GuiEventManager getEventManager() {
return eventManager;
}
/**
* @return Le plugin Bukkit associé.
*/
public Plugin getPlugin() {
return plugin;
}
/**
* Désenregistre les écouteurs Bukkit et nettoie toutes les ressources lors de la désactivation du plugin.
*/
public void unregisterAll() {
HandlerList.unregisterAll(bukkitListener);
eventManager.clear();
}
}
@@ -0,0 +1,67 @@
package fr.luc.bettermcguis.animation;
import fr.luc.bettermcguis.api.GuiItem;
import fr.luc.bettermcguis.pattern.GuiPattern;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Représente une image / frame individuelle dans un GUI animé.
*/
public class Frame {
private final Map<Integer, GuiItem> items = new HashMap<>();
private String title;
public Frame() {}
public Frame(String title) {
this.title = title;
}
/**
* Définit le titre de cette frame.
*/
public Frame title(String title) {
this.title = title;
return this;
}
/**
* Définit un item dans cette frame.
*/
public Frame item(int slot, GuiItem item) {
this.items.put(slot, item);
return this;
}
/**
* Applique un motif à cette frame.
*/
public Frame pattern(GuiPattern pattern, int cols, int maxRows) {
if (pattern != null) {
var rows = pattern.getRows();
var bindings = pattern.getItemBindings();
for (int r = 0; r < Math.min(rows.size(), maxRows); r++) {
String line = rows.get(r);
for (int c = 0; c < Math.min(line.length(), cols); c++) {
char ch = line.charAt(c);
if (bindings.containsKey(ch)) {
items.put(r * cols + c, bindings.get(ch));
}
}
}
}
return this;
}
public Map<Integer, GuiItem> getItems() {
return Collections.unmodifiableMap(items);
}
public String getTitle() {
return title;
}
}
@@ -0,0 +1,277 @@
package fr.luc.bettermcguis.api;
import fr.luc.bettermcguis.api.slot.SlotPos;
import fr.luc.bettermcguis.api.slot.SlotRange;
import fr.luc.bettermcguis.pattern.GuiPattern;
import net.kyori.adventure.text.Component;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
/**
* Interface principale définissant le comportement d'un menu d'inventaire interactif (GUI).
*/
public interface Gui {
/**
* @return Le titre actuel du menu au format MiniMessage.
*/
String getTitle();
/**
* @return Le titre actuel sous forme de composant Adventure.
*/
Component getTitleComponent();
/**
* Modifie le titre du menu (met à jour le titre pour les joueurs connectés si supporté).
*
* @param miniMessage Le nouveau titre en MiniMessage.
*/
void setTitle(String miniMessage);
/**
* @return Le type et la géométrie de cet inventaire.
*/
GuiType getType();
/**
* @return Le nombre total de slots de l'inventaire.
*/
default int getSize() {
return getType().getSize();
}
/**
* @return Le nombre de lignes de l'inventaire.
*/
default int getRows() {
return getType().getRows();
}
/**
* @return Le nombre de colonnes de l'inventaire (ex: 9 pour un coffre).
*/
default int getColumns() {
return getType().getColumns();
}
/**
* Place un item dans un slot absolu (0-indexé).
*
* @param slot Le numéro de slot.
* @param item L'item à placer.
*/
void setItem(int slot, GuiItem item);
/**
* Place un item à des coordonnées (ligne, colonne) en 0-indexé.
*
* @param row La ligne (0 à rows - 1).
* @param col La colonne (0 à cols - 1).
* @param item L'item à placer.
*/
default void setItem(int row, int col, GuiItem item) {
setItem(row * getColumns() + col, item);
}
/**
* Place un item à une position {@link SlotPos}.
*
* @param pos La position.
* @param item L'item.
*/
default void setItem(SlotPos pos, GuiItem item) {
if (pos != null) {
setItem(pos.toSlot(getColumns()), item);
}
}
/**
* Récupère le {@link GuiItem} présent à un slot donné.
*
* @param slot Le numéro de slot absolu.
* @return Le GuiItem, ou {@code null} si le slot est vide.
*/
GuiItem getItem(int slot);
/**
* Supprime l'item présent au slot donné.
*
* @param slot Le numéro de slot.
*/
void removeItem(int slot);
/**
* Supprime tous les items de l'inventaire.
*/
void clear();
/**
* Remplit tous les slots vides ou existants avec un item donné.
*
* @param item L'item de remplissage.
*/
void fill(GuiItem item);
/**
* Remplit uniquement les bordures extérieures de l'inventaire.
*
* @param item L'item de bordure.
*/
void fillBorder(GuiItem item);
/**
* Remplit une plage de slots spécifique.
*
* @param range La plage de slots.
* @param item L'item.
*/
void fillRange(SlotRange range, GuiItem item);
/**
* Applique un motif / masque ASCII sur l'inventaire.
*
* @param pattern Le motif à appliquer.
*/
void applyPattern(GuiPattern pattern);
/**
* Ouvre l'inventaire pour un joueur.
*
* @param player Le joueur.
*/
void open(Player player);
/**
* Ferme l'inventaire pour un joueur.
*
* @param player Le joueur.
*/
void close(Player player);
/**
* Rafraîchit l'affichage de l'inventaire pour un joueur actuellement visualisateur.
*
* @param player Le joueur.
*/
void refresh(Player player);
/**
* Rafraîchit l'inventaire pour l'ensemble des joueurs visualisant actuellement ce GUI.
*/
void refreshAll();
/**
* @return L'ensemble des joueurs qui visualisent actuellement ce menu.
*/
Set<Player> getViewers();
/**
* Crée ou met à jour l'inventaire Bukkit sous-jacent pour un joueur.
*
* @param player Le joueur pour lequel l'inventaire est généré.
* @return L'instance {@link Inventory} Bukkit.
*/
Inventory createInventory(Player player);
/**
* Définit si un slot donné est modifiable par le joueur (permet de déposer ou retirer des items).
*
* @param slot Le numéro de slot.
* @param editable true pour autoriser les interactions de déplacement d'items.
*/
void setEditable(int slot, boolean editable);
/**
* @param slot Le numéro de slot.
* @return true si le joueur a le droit de poser ou retirer des items dans ce slot.
*/
boolean isEditable(int slot);
/**
* @return L'ensemble des slots éditables par les joueurs.
*/
Set<Integer> getEditableSlots();
/**
* Stocke une propriété personnalisée dans le GUI.
*
* @param key La clé identifiant la propriété.
* @param value La valeur.
*/
void setProperty(String key, Object value);
/**
* Récupère une propriété typée.
*
* @param key La clé.
* @param type Le type attendu.
* @param <T> Le type générique.
* @return La valeur, ou {@code null}.
*/
<T> T getProperty(String key, Class<T> type);
/**
* Récupère une propriété ou retourne une valeur par défaut.
*/
<T> T getProperty(String key, T defaultValue);
/**
* @return La table complète des propriétés du GUI.
*/
Map<String, Object> getProperties();
/**
* Attache un écouteur déclenché lors de l'ouverture du menu.
*/
Gui onOpen(Consumer<GuiOpenContext> hook);
/**
* Attache un écouteur déclenché lors de la fermeture du menu.
*/
Gui onClose(Consumer<GuiCloseContext> hook);
/**
* Attache un écouteur déclenché lors de n'importe quel clic dans l'inventaire supérieur.
*/
Gui onClick(Consumer<GuiClickContext> hook);
/**
* Attache un écouteur déclenché lors d'un clic en dehors de l'inventaire (extérieur).
*/
Gui onOutsideClick(Consumer<GuiClickContext> hook);
/**
* Attache un écouteur déclenché lors d'un clic dans l'inventaire du joueur (inventaire inférieur).
*/
Gui onBottomClick(Consumer<GuiClickContext> hook);
/**
* Exécute le hook d'ouverture.
*/
void handleOpen(GuiOpenContext context);
/**
* Exécute le hook de fermeture.
*/
void handleClose(GuiCloseContext context);
/**
* Exécute le hook de clic global.
*/
void handleClick(GuiClickContext context);
/**
* Exécute le hook de clic extérieur.
*/
void handleOutsideClick(GuiClickContext context);
/**
* Exécute le hook de clic dans l'inventaire inférieur.
*/
void handleBottomClick(GuiClickContext context);
}
@@ -0,0 +1,15 @@
package fr.luc.bettermcguis.api;
/**
* Interface fonctionnelle déclenchée lors d'un clic sur un item d'un GUI.
*/
@FunctionalInterface
public interface GuiClickAction {
/**
* Exécute l'action associée au clic.
*
* @param context Le contexte complet du clic.
*/
void execute(GuiClickContext context);
}
@@ -0,0 +1,229 @@
package fr.luc.bettermcguis.api;
import fr.luc.bettermcguis.api.slot.SlotPos;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.ItemStack;
import java.util.Objects;
/**
* Contexte transmis lors d'une interaction / clic sur un slot ou item d'un GUI.
*/
public class GuiClickContext {
private final Player player;
private final Gui gui;
private final GuiItem guiItem;
private final int slot;
private final SlotPos slotPos;
private final ClickType clickType;
private final InventoryAction inventoryAction;
private final ItemStack currentItem;
private final ItemStack cursorItem;
private final InventoryClickEvent rawEvent;
private boolean cancelled = true; // Par défaut, les clics sont annulés pour éviter le vol d'items
/**
* Crée un nouveau contexte de clic.
*/
public GuiClickContext(Player player, Gui gui, GuiItem guiItem, int slot, SlotPos slotPos,
ClickType clickType, InventoryAction inventoryAction,
ItemStack currentItem, ItemStack cursorItem, InventoryClickEvent rawEvent) {
this.player = Objects.requireNonNull(player, "player cannot be null");
this.gui = Objects.requireNonNull(gui, "gui cannot be null");
this.guiItem = guiItem;
this.slot = slot;
this.slotPos = slotPos;
this.clickType = clickType;
this.inventoryAction = inventoryAction;
this.currentItem = currentItem;
this.cursorItem = cursorItem;
this.rawEvent = rawEvent;
}
/**
* @return Le joueur ayant effectué le clic.
*/
public Player getPlayer() {
return player;
}
/**
* @return Le GUI dans lequel le clic a eu lieu.
*/
public Gui getGui() {
return gui;
}
/**
* @return L'item {@link GuiItem} cliqué, ou {@code null} si slot vide.
*/
public GuiItem getGuiItem() {
return guiItem;
}
/**
* @return Le numéro de slot absolu (0-indexé).
*/
public int getSlot() {
return slot;
}
/**
* @return La position (ligne, colonne) du slot cliqué.
*/
public SlotPos getSlotPos() {
return slotPos;
}
/**
* @return Le type de clic (ex: LEFT, RIGHT, SHIFT_LEFT, MIDDLE, NUMBER_KEY).
*/
public ClickType getClickType() {
return clickType;
}
/**
* @return L'action d'inventaire Bukkit (ex: PICKUP_ALL, PLACE_ALL).
*/
public InventoryAction getInventoryAction() {
return inventoryAction;
}
/**
* @return L'ItemStack actuellement présent dans le slot cliqué.
*/
public ItemStack getCurrentItem() {
return currentItem;
}
/**
* @return L'ItemStack actuellement tenu sur le curseur de la souris.
*/
public ItemStack getCursorItem() {
return cursorItem;
}
/**
* @return L'événement Bukkit natif {@link InventoryClickEvent}, ou {@code null} lors de tests unitaires.
*/
public InventoryClickEvent getRawEvent() {
return rawEvent;
}
/**
* @return true si le clic est annulé (empêche la prise ou le déplacement d'items).
*/
public boolean isCancelled() {
return cancelled;
}
/**
* Définit si le clic doit être annulé.
*
* @param cancelled true pour bloquer l'action Bukkit native, false pour autoriser la modification du slot.
*/
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
if (rawEvent != null) {
rawEvent.setCancelled(cancelled);
}
}
/**
* @return true s'il s'agit d'un clic gauche (simple ou avec Shift).
*/
public boolean isLeftClick() {
return clickType == ClickType.LEFT || clickType == ClickType.SHIFT_LEFT;
}
/**
* @return true s'il s'agit d'un clic droit (simple ou avec Shift).
*/
public boolean isRightClick() {
return clickType == ClickType.RIGHT || clickType == ClickType.SHIFT_RIGHT;
}
/**
* @return true s'il s'agit d'un shift-clic.
*/
public boolean isShiftClick() {
return clickType == ClickType.SHIFT_LEFT || clickType == ClickType.SHIFT_RIGHT;
}
/**
* Ferme l'inventaire actuel pour le joueur.
*/
public void close() {
player.closeInventory();
}
/**
* Rafraîchit l'affichage du GUI pour le joueur.
*/
public void refresh() {
gui.refresh(player);
}
/**
* Joue un son au joueur.
*
* @param sound Le son Bukkit.
* @param volume Le volume.
* @param pitch La hauteur de ton.
*/
public void playSound(Sound sound, float volume, float pitch) {
if (player != null && sound != null) {
player.playSound(player.getLocation(), sound, volume, pitch);
}
}
/**
* Envoie un message formaté avec MiniMessage au joueur.
*
* @param miniMessage Le texte au format MiniMessage.
*/
public void reply(String miniMessage) {
if (player != null && miniMessage != null) {
player.sendMessage(MiniMessage.miniMessage().deserialize(miniMessage));
}
}
/**
* Envoie un message de succès préfixé en vert.
*/
public void replySuccess(String miniMessage) {
reply("<green>✔ </green>" + (miniMessage != null ? miniMessage : ""));
}
/**
* Envoie un message d'erreur préfixé en rouge.
*/
public void replyError(String miniMessage) {
reply("<red>✖ </red>" + (miniMessage != null ? miniMessage : ""));
}
/**
* Envoie un message d'information préfixé en bleu clair.
*/
public void replyInfo(String miniMessage) {
reply("<aqua> </aqua>" + (miniMessage != null ? miniMessage : ""));
}
/**
* Envoie un composant Kyori Adventure au joueur.
*
* @param component Le composant texte.
*/
public void reply(Component component) {
if (player != null && component != null) {
player.sendMessage(component);
}
}
}
@@ -0,0 +1,34 @@
package fr.luc.bettermcguis.api;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryCloseEvent;
import java.util.Objects;
/**
* Contexte transmis lors de la fermeture d'un GUI par un joueur.
*/
public class GuiCloseContext {
private final Player player;
private final Gui gui;
private final InventoryCloseEvent rawEvent;
public GuiCloseContext(Player player, Gui gui, InventoryCloseEvent rawEvent) {
this.player = Objects.requireNonNull(player, "player cannot be null");
this.gui = Objects.requireNonNull(gui, "gui cannot be null");
this.rawEvent = rawEvent;
}
public Player getPlayer() {
return player;
}
public Gui getGui() {
return gui;
}
public InventoryCloseEvent getRawEvent() {
return rawEvent;
}
}
@@ -0,0 +1,228 @@
package fr.luc.bettermcguis.api;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
/**
* Représente un item interactif ou décoratif placé dans un slot de GUI.
* Encapsule un {@link ItemStack}, des écouteurs de clics, des sons, des conditions de visibilité et des cooldowns.
*/
public class GuiItem {
private final ItemStack itemStack;
private GuiClickAction clickAction;
private Predicate<Player> visibilityCondition;
private boolean closeOnClick = false;
private boolean cancelClick = true;
private Sound clickSound;
private float soundVolume = 1.0f;
private float soundPitch = 1.0f;
private Duration clickCooldown = Duration.ZERO;
private final Map<UUID, Instant> cooldowns = new ConcurrentHashMap<>();
/**
* Crée un GuiItem à partir d'un {@link ItemStack}.
*
* @param itemStack L'item Minecraft sous-jacent.
*/
public GuiItem(ItemStack itemStack) {
this.itemStack = itemStack != null ? itemStack.clone() : new ItemStack(Material.AIR);
}
/**
* Crée un GuiItem avec une action de clic associée.
*
* @param itemStack L'item sous-jacent.
* @param clickAction L'action exécutée lors du clic.
*/
public GuiItem(ItemStack itemStack, GuiClickAction clickAction) {
this(itemStack);
this.clickAction = clickAction;
}
/**
* Fabrique statique à partir d'un {@link ItemStack}.
*/
public static GuiItem of(ItemStack itemStack) {
return new GuiItem(itemStack);
}
/**
* Fabrique statique avec action de clic.
*/
public static GuiItem of(ItemStack itemStack, GuiClickAction clickAction) {
return new GuiItem(itemStack, clickAction);
}
/**
* Fabrique statique à partir d'un matériau simple.
*/
public static GuiItem of(Material material) {
return new GuiItem(new ItemStack(material));
}
/**
* Fabrique statique pour un item décoratif de remplissage.
*/
public static GuiItem filler(Material material) {
ItemStack item = new ItemStack(material);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName(" ");
item.setItemMeta(meta);
}
return new GuiItem(item);
}
/**
* Fabrique statique pour un item vide (AIR).
*/
public static GuiItem empty() {
return new GuiItem(new ItemStack(Material.AIR));
}
/**
* Définit l'action de clic pour cet item.
*
* @param clickAction L'action de clic.
* @return Cette instance de {@link GuiItem} pour chaînage.
*/
public GuiItem onClick(GuiClickAction clickAction) {
this.clickAction = clickAction;
return this;
}
/**
* Conditionne la visibilité de cet item selon le joueur visualisant l'inventaire.
*
* @param condition Le prédicat testant le joueur.
* @return Cette instance.
*/
public GuiItem visibleIf(Predicate<Player> condition) {
this.visibilityCondition = condition;
return this;
}
/**
* Ferme l'inventaire automatiquement dès que le joueur clique sur cet item.
*
* @return Cette instance.
*/
public GuiItem closeOnClick() {
this.closeOnClick = true;
return this;
}
/**
* Joue un son au joueur lors du clic.
*
* @param sound Le son Bukkit.
* @param volume Le volume.
* @param pitch La tonalité.
* @return Cette instance.
*/
public GuiItem sound(Sound sound, float volume, float pitch) {
this.clickSound = sound;
this.soundVolume = volume;
this.soundPitch = pitch;
return this;
}
/**
* Joue un son avec volume et pitch par défaut (1.0f).
*/
public GuiItem sound(Sound sound) {
return sound(sound, 1.0f, 1.0f);
}
/**
* Définit un cooldown entre chaque clic pour un joueur.
*
* @param cooldown La durée de cooldown.
* @return Cette instance.
*/
public GuiItem cooldown(Duration cooldown) {
this.clickCooldown = cooldown != null ? cooldown : Duration.ZERO;
return this;
}
/**
* Indique si l'événement Bukkit de clic doit être annulé (empêche de prendre l'item).
*
* @param cancel true pour annuler (comportement par défaut).
* @return Cette instance.
*/
public GuiItem cancelClick(boolean cancel) {
this.cancelClick = cancel;
return this;
}
/**
* Gère le déclenchement de l'action de clic pour un contexte donné.
*
* @param context Le contexte de clic.
*/
public void handleClick(GuiClickContext context) {
if (cancelClick) {
context.setCancelled(true);
}
Player player = context.getPlayer();
if (player != null && !clickCooldown.isZero()) {
Instant now = Instant.now();
Instant expire = cooldowns.get(player.getUniqueId());
if (expire != null && now.isBefore(expire)) {
// Cooldown actif
return;
}
cooldowns.put(player.getUniqueId(), now.plus(clickCooldown));
}
if (clickSound != null && player != null) {
player.playSound(player.getLocation(), clickSound, soundVolume, soundPitch);
}
if (clickAction != null) {
clickAction.execute(context);
}
if (closeOnClick && player != null) {
context.close();
}
}
/**
* @return L'{@link ItemStack} sous-jacent.
*/
public ItemStack getItemStack() {
return itemStack;
}
/**
* @return true si l'item est visible pour le joueur donné.
*/
public boolean isVisibleFor(Player player) {
if (visibilityCondition == null) {
return true;
}
return visibilityCondition.test(player);
}
/**
* @return L'action de clic attachée.
*/
public GuiClickAction getClickAction() {
return clickAction;
}
}
@@ -0,0 +1,46 @@
package fr.luc.bettermcguis.api;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryOpenEvent;
import java.util.Objects;
/**
* Contexte transmis lors de l'ouverture d'un GUI.
*/
public class GuiOpenContext {
private final Player player;
private final Gui gui;
private final InventoryOpenEvent rawEvent;
private boolean cancelled = false;
public GuiOpenContext(Player player, Gui gui, InventoryOpenEvent rawEvent) {
this.player = Objects.requireNonNull(player, "player cannot be null");
this.gui = Objects.requireNonNull(gui, "gui cannot be null");
this.rawEvent = rawEvent;
}
public Player getPlayer() {
return player;
}
public Gui getGui() {
return gui;
}
public InventoryOpenEvent getRawEvent() {
return rawEvent;
}
public boolean isCancelled() {
return cancelled;
}
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
if (rawEvent != null) {
rawEvent.setCancelled(cancelled);
}
}
}
@@ -0,0 +1,81 @@
package fr.luc.bettermcguis.api;
import org.bukkit.event.inventory.InventoryType;
import java.util.Objects;
/**
* Définit le type et la taille d'un inventaire GUI (Coffre 1 à 6 lignes, Hopper, Distributeur, etc.).
*/
public enum GuiType {
CHEST_1_ROW(9, InventoryType.CHEST, 1),
CHEST_2_ROWS(18, InventoryType.CHEST, 2),
CHEST_3_ROWS(27, InventoryType.CHEST, 3),
CHEST_4_ROWS(36, InventoryType.CHEST, 4),
CHEST_5_ROWS(45, InventoryType.CHEST, 5),
CHEST_6_ROWS(54, InventoryType.CHEST, 6),
HOPPER(5, InventoryType.HOPPER, 1),
DISPENSER(9, InventoryType.DISPENSER, 3),
DROPPER(9, InventoryType.DROPPER, 3),
ANVIL(3, InventoryType.ANVIL, 1),
WORKBENCH(10, InventoryType.WORKBENCH, 3),
BREWING(5, InventoryType.BREWING, 1);
private final int size;
private final InventoryType bukkitType;
private final int rows;
GuiType(int size, InventoryType bukkitType, int rows) {
this.size = size;
this.bukkitType = bukkitType;
this.rows = rows;
}
/**
* @return Le nombre total de slots de l'inventaire.
*/
public int getSize() {
return size;
}
/**
* @return Le type d'inventaire Bukkit correspondant.
*/
public InventoryType getBukkitType() {
return bukkitType;
}
/**
* @return Le nombre de lignes de l'inventaire.
*/
public int getRows() {
return rows;
}
/**
* @return Le nombre de colonnes de l'inventaire (9 pour un coffre, 5 pour hopper, 3 pour dispenser).
*/
public int getColumns() {
return size / rows;
}
/**
* Retourne le type de coffre correspondant au nombre de lignes demandé (1 à 6).
*
* @param rows Le nombre de lignes (1 à 6).
* @return Le type {@link GuiType}.
* @throws IllegalArgumentException si rows n'est pas compris entre 1 et 6.
*/
public static GuiType chest(int rows) {
return switch (rows) {
case 1 -> CHEST_1_ROW;
case 2 -> CHEST_2_ROWS;
case 3 -> CHEST_3_ROWS;
case 4 -> CHEST_4_ROWS;
case 5 -> CHEST_5_ROWS;
case 6 -> CHEST_6_ROWS;
default -> throw new IllegalArgumentException("Nombre de lignes de coffre invalide : " + rows + " (attendu: 1-6)");
};
}
}
@@ -0,0 +1,92 @@
package fr.luc.bettermcguis.api.slot;
import java.util.Objects;
/**
* Représente la position d'un slot dans un inventaire GUI sous forme de coordonnées (ligne, colonne) en index 0.
*/
public class SlotPos {
private final int row;
private final int column;
/**
* Crée une position de slot à partir d'une ligne et d'une colonne (0-indexés).
*
* @param row La ligne (0 à rows - 1).
* @param column La colonne (0 à cols - 1).
*/
public SlotPos(int row, int column) {
if (row < 0 || column < 0) {
throw new IllegalArgumentException("Les coordonnées de slot ne peuvent pas être négatives : (" + row + ", " + column + ")");
}
this.row = row;
this.column = column;
}
/**
* Crée une instance de {@link SlotPos}.
*
* @param row La ligne (0-indexée).
* @param column La colonne (0-indexée).
* @return La position correspondante.
*/
public static SlotPos of(int row, int column) {
return new SlotPos(row, column);
}
/**
* Calcule la position (ligne, colonne) à partir d'un slot brut absolu et du nombre de colonnes par ligne.
*
* @param slot L'index brut du slot (0-indexé).
* @param columns Le nombre de colonnes (ex: 9 pour un coffre).
* @return La position {@link SlotPos}.
*/
public static SlotPos fromSlot(int slot, int columns) {
if (slot < 0 || columns <= 0) {
throw new IllegalArgumentException("Slot ou nombre de colonnes invalide : slot=" + slot + ", cols=" + columns);
}
return new SlotPos(slot / columns, slot % columns);
}
/**
* @return L'index de ligne (0-indexé).
*/
public int getRow() {
return row;
}
/**
* @return L'index de colonne (0-indexé).
*/
public int getColumn() {
return column;
}
/**
* Convertit cette position (ligne, colonne) en index de slot absolu (0-indexé).
*
* @param columns Le nombre de colonnes de l'inventaire (ex: 9 pour un coffre).
* @return Le numéro de slot absolu.
*/
public int toSlot(int columns) {
return (row * columns) + column;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SlotPos slotPos)) return false;
return row == slotPos.row && column == slotPos.column;
}
@Override
public int hashCode() {
return Objects.hash(row, column);
}
@Override
public String toString() {
return "SlotPos(row=" + row + ", col=" + column + ")";
}
}
@@ -0,0 +1,97 @@
package fr.luc.bettermcguis.api.slot;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.IntStream;
/**
* Représente un ensemble ou une plage de slots dans un inventaire GUI.
*/
public class SlotRange {
private final List<Integer> slots;
/**
* Crée une plage avec une liste de slots.
*
* @param slots Les numéros de slots.
*/
public SlotRange(List<Integer> slots) {
this.slots = new ArrayList<>(slots);
}
/**
* Crée une plage continue de slots entre {@code from} et {@code to} inclus.
*
* @param from Slot de début (inclus).
* @param to Slot de fin (inclus).
* @return La plage correspondante.
*/
public static SlotRange of(int from, int to) {
List<Integer> list = new ArrayList<>();
int start = Math.min(from, to);
int end = Math.max(from, to);
for (int i = start; i <= end; i++) {
list.add(i);
}
return new SlotRange(list);
}
/**
* Crée une plage à partir d'une liste explicite de slots.
*
* @param slots Les slots.
* @return La plage correspondante.
*/
public static SlotRange ofSlots(int... slots) {
List<Integer> list = new ArrayList<>();
for (int s : slots) {
list.add(s);
}
return new SlotRange(list);
}
/**
* Crée une plage correspondant aux bordures extérieures d'un inventaire.
*
* @param rows Le nombre de lignes.
* @param cols Le nombre de colonnes (ex: 9).
* @return La plage des slots de bordure.
*/
public static SlotRange border(int rows, int cols) {
List<Integer> list = new ArrayList<>();
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (r == 0 || r == rows - 1 || c == 0 || c == cols - 1) {
list.add(r * cols + c);
}
}
}
return new SlotRange(list);
}
/**
* Crée une plage correspondant à l'intérieur (sans les bordures) d'un inventaire.
*
* @param rows Le nombre de lignes.
* @param cols Le nombre de colonnes.
* @return La plage des slots intérieurs.
*/
public static SlotRange interior(int rows, int cols) {
List<Integer> list = new ArrayList<>();
for (int r = 1; r < rows - 1; r++) {
for (int c = 1; c < cols - 1; c++) {
list.add(r * cols + c);
}
}
return new SlotRange(list);
}
/**
* @return La liste non modifiable des slots de cette plage.
*/
public List<Integer> getSlots() {
return Collections.unmodifiableList(slots);
}
}
@@ -0,0 +1,96 @@
package fr.luc.bettermcguis.builder;
import fr.luc.bettermcguis.animation.Frame;
import fr.luc.bettermcguis.api.GuiType;
import fr.luc.bettermcguis.type.AnimatedGui;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* Constructeur fluide pour concevoir des inventaires animés ({@link AnimatedGui}).
*/
public class AnimatedGuiBuilder extends GuiBuilder {
private final List<Frame> frames = new ArrayList<>();
private boolean loop = true;
public AnimatedGuiBuilder() {}
@Override
public AnimatedGuiBuilder title(String miniMessage) {
super.title(miniMessage);
return this;
}
@Override
public AnimatedGuiBuilder type(GuiType type) {
super.type(type);
return this;
}
@Override
public AnimatedGuiBuilder rows(int rows) {
super.rows(rows);
return this;
}
/**
* Ajoute une frame à l'animation.
*/
public AnimatedGuiBuilder frame(Frame frame) {
this.frames.add(frame);
return this;
}
/**
* Ajoute une frame configurée via une fonction lambda.
*/
public AnimatedGuiBuilder frame(Consumer<Frame> frameConfig) {
Frame f = new Frame();
if (frameConfig != null) {
frameConfig.accept(f);
}
return frame(f);
}
/**
* Active ou désactive la lecture en boucle.
*/
public AnimatedGuiBuilder loop(boolean loop) {
this.loop = loop;
return this;
}
@Override
public AnimatedGui build() {
AnimatedGui gui = new AnimatedGui(title, type);
for (var entry : items.entrySet()) {
gui.setItem(entry.getKey(), entry.getValue());
}
for (var pattern : patterns) {
gui.applyPattern(pattern);
}
for (int s : editableSlots) {
gui.setEditable(s, true);
}
for (var entry : properties.entrySet()) {
gui.setProperty(entry.getKey(), entry.getValue());
}
for (var hook : openHooks) gui.onOpen(hook);
for (var hook : closeHooks) gui.onClose(hook);
for (var hook : clickHooks) gui.onClick(hook);
for (var hook : outsideClickHooks) gui.onOutsideClick(hook);
for (var hook : bottomClickHooks) gui.onBottomClick(hook);
for (Frame frame : frames) {
gui.addFrame(frame);
}
gui.loop(loop);
return gui;
}
}
@@ -0,0 +1,238 @@
package fr.luc.bettermcguis.builder;
import fr.luc.bettermcguis.api.*;
import fr.luc.bettermcguis.api.slot.SlotPos;
import fr.luc.bettermcguis.api.slot.SlotRange;
import fr.luc.bettermcguis.pattern.GuiPattern;
import fr.luc.bettermcguis.type.SimpleGui;
import net.kyori.adventure.text.Component;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import java.util.*;
import java.util.function.Consumer;
/**
* Constructeur fluide pour concevoir des menus d'inventaires interactifs standards ({@link SimpleGui}).
*/
public class GuiBuilder {
protected String title = "Menu";
protected GuiType type = GuiType.CHEST_3_ROWS;
protected final Map<Integer, GuiItem> items = new HashMap<>();
protected final Set<Integer> editableSlots = new HashSet<>();
protected final Map<String, Object> properties = new HashMap<>();
protected final List<GuiPattern> patterns = new ArrayList<>();
protected final List<Consumer<GuiOpenContext>> openHooks = new ArrayList<>();
protected final List<Consumer<GuiCloseContext>> closeHooks = new ArrayList<>();
protected final List<Consumer<GuiClickContext>> clickHooks = new ArrayList<>();
protected final List<Consumer<GuiClickContext>> outsideClickHooks = new ArrayList<>();
protected final List<Consumer<GuiClickContext>> bottomClickHooks = new ArrayList<>();
public GuiBuilder() {}
/**
* Définit le titre du GUI au format MiniMessage.
*/
public GuiBuilder title(String miniMessage) {
this.title = miniMessage;
return this;
}
/**
* Définit le type et les dimensions du GUI.
*/
public GuiBuilder type(GuiType type) {
this.type = Objects.requireNonNull(type, "type cannot be null");
return this;
}
/**
* Définit un inventaire de type coffre avec le nombre de lignes spécifié (1 à 6).
*/
public GuiBuilder rows(int rows) {
this.type = GuiType.chest(rows);
return this;
}
/**
* Place un {@link GuiItem} dans un slot absolu (0-indexé).
*/
public GuiBuilder item(int slot, GuiItem item) {
this.items.put(slot, item);
return this;
}
/**
* Place un {@link ItemStack} dans un slot avec une action optionnelle.
*/
public GuiBuilder item(int slot, ItemStack item, GuiClickAction action) {
return item(slot, GuiItem.of(item, action));
}
/**
* Place un item à des coordonnées (ligne, colonne) en 0-indexé.
*/
public GuiBuilder item(int row, int col, GuiItem item) {
return item(row * type.getColumns() + col, item);
}
/**
* Place un item à une position {@link SlotPos}.
*/
public GuiBuilder item(SlotPos pos, GuiItem item) {
if (pos != null) {
item(pos.toSlot(type.getColumns()), item);
}
return this;
}
/**
* Remplit les bordures extérieures avec un item.
*/
public GuiBuilder fillBorder(GuiItem item) {
for (int slot : SlotRange.border(type.getRows(), type.getColumns()).getSlots()) {
this.items.put(slot, item);
}
return this;
}
/**
* Remplit les bordures extérieures avec un matériau de vitrage.
*/
public GuiBuilder fillBorder(Material material) {
return fillBorder(GuiItem.filler(material));
}
/**
* Remplit une plage de slots.
*/
public GuiBuilder fillRange(SlotRange range, GuiItem item) {
if (range != null) {
for (int slot : range.getSlots()) {
this.items.put(slot, item);
}
}
return this;
}
/**
* Applique un motif / masque ASCII {@link GuiPattern}.
*/
public GuiBuilder pattern(GuiPattern pattern) {
if (pattern != null) {
this.patterns.add(pattern);
}
return this;
}
/**
* Construit et applique un motif via une fonction de configuration de {@link PatternBuilder}.
*/
public GuiBuilder pattern(Consumer<PatternBuilder> patternConfig) {
if (patternConfig != null) {
PatternBuilder builder = new PatternBuilder();
patternConfig.accept(builder);
pattern(builder.build());
}
return this;
}
/**
* Marque un ou plusieurs slots comme éditables par les joueurs.
*/
public GuiBuilder editable(int... slots) {
for (int s : slots) {
this.editableSlots.add(s);
}
return this;
}
/**
* Marque une plage de slots comme éditables.
*/
public GuiBuilder editable(SlotRange range) {
if (range != null) {
this.editableSlots.addAll(range.getSlots());
}
return this;
}
/**
* Définit une propriété personnalisée.
*/
public GuiBuilder property(String key, Object value) {
this.properties.put(key, value);
return this;
}
/**
* Hook exécuté à l'ouverture du GUI.
*/
public GuiBuilder onOpen(Consumer<GuiOpenContext> hook) {
this.openHooks.add(hook);
return this;
}
/**
* Hook exécuté à la fermeture du GUI.
*/
public GuiBuilder onClose(Consumer<GuiCloseContext> hook) {
this.closeHooks.add(hook);
return this;
}
/**
* Hook exécuté lors d'un clic dans le GUI.
*/
public GuiBuilder onClick(Consumer<GuiClickContext> hook) {
this.clickHooks.add(hook);
return this;
}
/**
* Hook exécuté lors d'un clic extérieur.
*/
public GuiBuilder onOutsideClick(Consumer<GuiClickContext> hook) {
this.outsideClickHooks.add(hook);
return this;
}
/**
* Hook exécuté lors d'un clic dans l'inventaire du joueur.
*/
public GuiBuilder onBottomClick(Consumer<GuiClickContext> hook) {
this.bottomClickHooks.add(hook);
return this;
}
/**
* Construit l'instance finale de {@link SimpleGui}.
*
* @return L'instance configurée de {@link SimpleGui}.
*/
public SimpleGui build() {
SimpleGui gui = new SimpleGui(title, type);
for (Map.Entry<Integer, GuiItem> entry : items.entrySet()) {
gui.setItem(entry.getKey(), entry.getValue());
}
for (GuiPattern pattern : patterns) {
gui.applyPattern(pattern);
}
for (int s : editableSlots) {
gui.setEditable(s, true);
}
for (Map.Entry<String, Object> entry : properties.entrySet()) {
gui.setProperty(entry.getKey(), entry.getValue());
}
for (var hook : openHooks) gui.onOpen(hook);
for (var hook : closeHooks) gui.onClose(hook);
for (var hook : clickHooks) gui.onClick(hook);
for (var hook : outsideClickHooks) gui.onOutsideClick(hook);
for (var hook : bottomClickHooks) gui.onBottomClick(hook);
return gui;
}
}
@@ -0,0 +1,369 @@
package fr.luc.bettermcguis.builder;
import fr.luc.bettermcguis.api.GuiClickAction;
import fr.luc.bettermcguis.api.GuiItem;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.LeatherArmorMeta;
import org.bukkit.inventory.meta.PotionMeta;
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.potion.PotionType;
import java.util.*;
import java.util.stream.Collectors;
/**
* Constructeur moderne et fluide d'{@link ItemStack} supportant Kyori Adventure, MiniMessage,
* têtes personnalisées, armures teintées, flags et PersistentDataContainer.
*/
public class ItemBuilder {
private final ItemStack itemStack;
private final ItemMeta meta;
/**
* Initialise le builder avec un matériau de base.
*
* @param material Le matériau Bukkit.
*/
public ItemBuilder(Material material) {
this(new ItemStack(Objects.requireNonNull(material, "material cannot be null")));
}
/**
* Initialise le builder à partir d'un {@link ItemStack} existant.
*
* @param itemStack L'item à cloner et modifier.
*/
public ItemBuilder(ItemStack itemStack) {
Objects.requireNonNull(itemStack, "itemStack cannot be null");
this.itemStack = itemStack.clone();
this.meta = this.itemStack.getItemMeta();
}
/**
* Fabrique statique pour un matériau donné.
*/
public static ItemBuilder of(Material material) {
return new ItemBuilder(material);
}
/**
* Fabrique statique pour un {@link ItemStack}.
*/
public static ItemBuilder of(ItemStack itemStack) {
return new ItemBuilder(itemStack);
}
/**
* Fabrique statique pour une tête de joueur (PLAYER_HEAD).
*/
public static ItemBuilder skull() {
return new ItemBuilder(Material.PLAYER_HEAD);
}
/**
* Fabrique statique pour un item décoratif de vitrage ou panneau.
*/
public static ItemBuilder filler(Material material) {
return of(material).name(" ");
}
/**
* Définit la quantité d'items dans la pile (1 à 64).
*
* @param amount Le montant.
* @return Ce builder pour chaînage.
*/
public ItemBuilder amount(int amount) {
this.itemStack.setAmount(Math.max(1, Math.min(64, amount)));
return this;
}
/**
* Définit le nom d'affichage au format MiniMessage.
*
* @param miniMessage Le texte formaté en MiniMessage.
* @return Ce builder.
*/
public ItemBuilder name(String miniMessage) {
if (meta != null && miniMessage != null) {
Component component = MiniMessage.miniMessage().deserialize(miniMessage);
meta.displayName(component);
}
return this;
}
/**
* Définit le nom d'affichage avec un {@link Component} Adventure natif.
*
* @param component Le composant texte.
* @return Ce builder.
*/
public ItemBuilder name(Component component) {
if (meta != null && component != null) {
meta.displayName(component);
}
return this;
}
/**
* Définit le lore complet à partir de lignes MiniMessage.
*
* @param lines Les lignes de lore au format MiniMessage.
* @return Ce builder.
*/
public ItemBuilder lore(String... lines) {
return lore(Arrays.asList(lines));
}
/**
* Définit le lore complet à partir d'une liste de chaînes MiniMessage.
*
* @param lines La liste des lignes.
* @return Ce builder.
*/
public ItemBuilder lore(List<String> lines) {
if (meta != null && lines != null) {
List<Component> components = lines.stream()
.map(line -> MiniMessage.miniMessage().deserialize(line))
.collect(Collectors.toList());
meta.lore(components);
}
return this;
}
/**
* Définit le lore à partir d'une liste de composants Adventure.
*
* @param components La liste des composants.
* @return Ce builder.
*/
public ItemBuilder loreComponents(List<Component> components) {
if (meta != null && components != null) {
meta.lore(components);
}
return this;
}
/**
* Ajoute une ou plusieurs lignes au lore existant.
*
* @param lines Les lignes à ajouter.
* @return Ce builder.
*/
public ItemBuilder addLore(String... lines) {
if (meta != null && lines != null) {
List<Component> current = meta.lore();
if (current == null) {
current = new ArrayList<>();
} else {
current = new ArrayList<>(current);
}
for (String line : lines) {
current.add(MiniMessage.miniMessage().deserialize(line));
}
meta.lore(current);
}
return this;
}
/**
* Ajoute un enchantement.
*
* @param enchantment L'enchantement.
* @param level Le niveau.
* @return Ce builder.
*/
public ItemBuilder enchant(Enchantment enchantment, int level) {
if (meta != null && enchantment != null) {
meta.addEnchant(enchantment, level, true);
}
return this;
}
/**
* Donne un effet brillant (lueur d'enchantement) sans afficher de texte d'enchantement.
*
* @param glowing true pour activer la brillance.
* @return Ce builder.
*/
public ItemBuilder glowing(boolean glowing) {
if (glowing) {
enchant(Enchantment.LUCK, 1);
flags(ItemFlag.HIDE_ENCHANTS);
}
return this;
}
/**
* Ajoute des drapeaux d'item (ItemFlags).
*
* @param flags Les drapeaux.
* @return Ce builder.
*/
public ItemBuilder flags(ItemFlag... flags) {
if (meta != null && flags != null) {
meta.addItemFlags(flags);
}
return this;
}
/**
* Masque tous les attributs, enchantements et effets de potion de l'item.
*
* @return Ce builder.
*/
public ItemBuilder hideAll() {
if (meta != null) {
meta.addItemFlags(ItemFlag.values());
}
return this;
}
/**
* Définit le CustomModelData de l'item.
*
* @param customModelData L'identifiant numérique de modèle 3D.
* @return Ce builder.
*/
public ItemBuilder customModelData(int customModelData) {
if (meta != null) {
meta.setCustomModelData(customModelData);
}
return this;
}
/**
* Marque l'item comme incassable.
*
* @param unbreakable true pour incassable.
* @return Ce builder.
*/
public ItemBuilder unbreakable(boolean unbreakable) {
if (meta != null) {
meta.setUnbreakable(unbreakable);
}
return this;
}
/**
* Configure le propriétaire d'une tête de joueur par son pseudo.
*
* @param playerName Le nom du joueur.
* @return Ce builder.
*/
public ItemBuilder skullOwner(String playerName) {
if (meta instanceof SkullMeta skullMeta && playerName != null) {
skullMeta.setOwner(playerName);
}
return this;
}
/**
* Configure le propriétaire d'une tête par UUID.
*
* @param uuid L'UUID du joueur.
* @return Ce builder.
*/
public ItemBuilder skullOwner(UUID uuid) {
if (meta instanceof SkullMeta skullMeta && uuid != null) {
skullMeta.setOwningPlayer(Bukkit.getOfflinePlayer(uuid));
}
return this;
}
/**
* Définit la couleur d'une pièce d'armure en cuir.
*
* @param color La couleur Bukkit.
* @return Ce builder.
*/
public ItemBuilder color(Color color) {
if (meta instanceof LeatherArmorMeta leatherMeta && color != null) {
leatherMeta.setColor(color);
}
return this;
}
/**
* Définit la couleur RGB d'une armure en cuir.
*
* @param red 0-255
* @param green 0-255
* @param blue 0-255
* @return Ce builder.
*/
public ItemBuilder color(int red, int green, int blue) {
return color(Color.fromRGB(red, green, blue));
}
/**
* Stocke une donnée texte dans le PersistentDataContainer.
*
* @param key La clé NamespacedKey.
* @param value La valeur texte.
* @return Ce builder.
*/
public ItemBuilder pdcString(NamespacedKey key, String value) {
if (meta != null && key != null && value != null) {
meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, value);
}
return this;
}
/**
* Stocke un entier dans le PersistentDataContainer.
*
* @param key La clé NamespacedKey.
* @param value La valeur entière.
* @return Ce builder.
*/
public ItemBuilder pdcInt(NamespacedKey key, int value) {
if (meta != null && key != null) {
meta.getPersistentDataContainer().set(key, PersistentDataType.INTEGER, value);
}
return this;
}
/**
* Construit et retourne l'{@link ItemStack} configuré.
*
* @return Le nouvel ItemStack.
*/
public ItemStack build() {
ItemStack item = itemStack.clone();
if (meta != null) {
item.setItemMeta(meta);
}
return item;
}
/**
* Convertit directement ce builder en {@link GuiItem} statique.
*
* @return L'instance de {@link GuiItem}.
*/
public GuiItem asGuiItem() {
return GuiItem.of(build());
}
/**
* Convertit directement ce builder en {@link GuiItem} avec une action de clic.
*
* @param clickAction L'action de clic à exécuter.
* @return L'instance de {@link GuiItem}.
*/
public GuiItem asGuiItem(GuiClickAction clickAction) {
return GuiItem.of(build(), clickAction);
}
}
@@ -0,0 +1,199 @@
package fr.luc.bettermcguis.builder;
import fr.luc.bettermcguis.api.GuiItem;
import fr.luc.bettermcguis.api.GuiType;
import fr.luc.bettermcguis.api.slot.SlotRange;
import fr.luc.bettermcguis.event.GuiPageChangeEvent;
import fr.luc.bettermcguis.pattern.GuiMask;
import fr.luc.bettermcguis.type.PaginatedGui;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Consumer;
/**
* Constructeur fluide pour concevoir des inventaires paginés ({@link PaginatedGui}).
*/
public class PaginatedGuiBuilder extends GuiBuilder {
private final List<GuiItem> pageItems = new ArrayList<>();
private final List<Integer> itemSlots = new ArrayList<>();
private Integer previousPageSlot;
private Integer nextPageSlot;
private Integer pageIndicatorSlot;
private GuiItem customPreviousButton;
private GuiItem customNextButton;
private BiFunction<Integer, Integer, GuiItem> pageIndicatorSupplier;
private final List<Consumer<GuiPageChangeEvent>> pageChangeHooks = new ArrayList<>();
public PaginatedGuiBuilder() {}
@Override
public PaginatedGuiBuilder title(String miniMessage) {
super.title(miniMessage);
return this;
}
@Override
public PaginatedGuiBuilder type(GuiType type) {
super.type(type);
return this;
}
@Override
public PaginatedGuiBuilder rows(int rows) {
super.rows(rows);
return this;
}
@Override
public PaginatedGuiBuilder item(int slot, GuiItem item) {
super.item(slot, item);
return this;
}
@Override
public PaginatedGuiBuilder fillBorder(GuiItem item) {
super.fillBorder(item);
return this;
}
@Override
public PaginatedGuiBuilder fillBorder(org.bukkit.Material material) {
super.fillBorder(material);
return this;
}
/**
* Ajoute un item paginé.
*/
public PaginatedGuiBuilder addPageItem(GuiItem item) {
this.pageItems.add(item);
return this;
}
/**
* Ajoute une collection complète d'items paginés.
*/
public PaginatedGuiBuilder addPageItems(Collection<GuiItem> items) {
if (items != null) {
this.pageItems.addAll(items);
}
return this;
}
/**
* Définit les slots réservés pour les items paginés.
*/
public PaginatedGuiBuilder itemSlots(List<Integer> slots) {
this.itemSlots.clear();
if (slots != null) {
this.itemSlots.addAll(slots);
}
return this;
}
/**
* Définit les slots d'items à partir d'une plage {@link SlotRange}.
*/
public PaginatedGuiBuilder itemSlots(SlotRange range) {
if (range != null) {
itemSlots(range.getSlots());
}
return this;
}
/**
* Définit les slots d'items à partir d'un masque {@link GuiMask}.
*/
public PaginatedGuiBuilder itemSlots(GuiMask mask) {
if (mask != null) {
itemSlots(mask.resolveSlots(type.getRows(), type.getColumns()));
}
return this;
}
/**
* Configure le bouton de page précédente.
*/
public PaginatedGuiBuilder previousButton(int slot, GuiItem item) {
this.previousPageSlot = slot;
this.customPreviousButton = item;
return this;
}
/**
* Configure le bouton de page suivante.
*/
public PaginatedGuiBuilder nextButton(int slot, GuiItem item) {
this.nextPageSlot = slot;
this.customNextButton = item;
return this;
}
/**
* Configure l'indicateur de page.
*/
public PaginatedGuiBuilder pageIndicator(int slot, BiFunction<Integer, Integer, GuiItem> supplier) {
this.pageIndicatorSlot = slot;
this.pageIndicatorSupplier = supplier;
return this;
}
/**
* Hook déclenché lors du changement de page.
*/
public PaginatedGuiBuilder onPageChange(Consumer<GuiPageChangeEvent> hook) {
if (hook != null) {
this.pageChangeHooks.add(hook);
}
return this;
}
@Override
public PaginatedGui build() {
PaginatedGui gui = new PaginatedGui(title, type);
for (var entry : items.entrySet()) {
gui.setItem(entry.getKey(), entry.getValue());
}
for (var pattern : patterns) {
gui.applyPattern(pattern);
}
for (int s : editableSlots) {
gui.setEditable(s, true);
}
for (var entry : properties.entrySet()) {
gui.setProperty(entry.getKey(), entry.getValue());
}
for (var hook : openHooks) gui.onOpen(hook);
for (var hook : closeHooks) gui.onClose(hook);
for (var hook : clickHooks) gui.onClick(hook);
for (var hook : outsideClickHooks) gui.onOutsideClick(hook);
for (var hook : bottomClickHooks) gui.onBottomClick(hook);
if (!itemSlots.isEmpty()) {
gui.setItemSlots(itemSlots);
}
if (previousPageSlot != null) {
gui.setPreviousPageButton(previousPageSlot, customPreviousButton);
}
if (nextPageSlot != null) {
gui.setNextPageButton(nextPageSlot, customNextButton);
}
if (pageIndicatorSlot != null) {
gui.setPageIndicator(pageIndicatorSlot, pageIndicatorSupplier);
}
gui.addPageItems(pageItems);
for (var hook : pageChangeHooks) {
gui.onPageChange(hook);
}
return gui;
}
}
@@ -0,0 +1,75 @@
package fr.luc.bettermcguis.builder;
import fr.luc.bettermcguis.api.GuiItem;
import fr.luc.bettermcguis.pattern.GuiPattern;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Constructeur fluide pour concevoir des motifs de placement ASCII ({@link GuiPattern}).
*/
public class PatternBuilder {
private final List<String> lines = new ArrayList<>();
private final Map<Character, GuiItem> bindings = new HashMap<>();
public PatternBuilder() {}
public PatternBuilder(String... lines) {
lines(lines);
}
/**
* Définit les lignes du motif ASCII.
*/
public PatternBuilder lines(String... lines) {
if (lines != null) {
this.lines.addAll(Arrays.asList(lines));
}
return this;
}
/**
* Associe un caractère à un {@link GuiItem}.
*/
public PatternBuilder bind(char character, GuiItem item) {
this.bindings.put(character, item);
return this;
}
/**
* Associe un caractère à un {@link ItemStack}.
*/
public PatternBuilder bind(char character, ItemStack itemStack) {
return bind(character, GuiItem.of(itemStack));
}
/**
* Associe un caractère à un matériau Bukkit simple.
*/
public PatternBuilder bind(char character, Material material) {
return bind(character, GuiItem.of(material));
}
/**
* Associe un caractère à un item de remplissage (vitrage avec nom vide).
*/
public PatternBuilder bindFiller(char character, Material material) {
return bind(character, GuiItem.filler(material));
}
/**
* Construit l'instance finale de {@link GuiPattern}.
*/
public GuiPattern build() {
GuiPattern pattern = new GuiPattern(lines);
pattern.bindAll(bindings);
return pattern;
}
}
@@ -0,0 +1,95 @@
package fr.luc.bettermcguis.builder;
import fr.luc.bettermcguis.api.GuiItem;
import fr.luc.bettermcguis.api.GuiType;
import fr.luc.bettermcguis.type.TabbedGui;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
/**
* Constructeur fluide pour concevoir des inventaires à onglets ({@link TabbedGui}).
*/
public class TabbedGuiBuilder extends GuiBuilder {
private final Map<String, Integer> tabButtons = new LinkedHashMap<>();
private final Map<String, GuiItem> tabButtonItems = new LinkedHashMap<>();
private final Map<String, Consumer<TabbedGui>> tabInitializers = new LinkedHashMap<>();
public TabbedGuiBuilder() {}
@Override
public TabbedGuiBuilder title(String miniMessage) {
super.title(miniMessage);
return this;
}
@Override
public TabbedGuiBuilder type(GuiType type) {
super.type(type);
return this;
}
@Override
public TabbedGuiBuilder rows(int rows) {
super.rows(rows);
return this;
}
/**
* Enregistre un onglet avec son bouton et sa configuration d'items.
*
* @param tabId L'identifiant de l'onglet.
* @param buttonSlot Le slot du bouton d'onglet.
* @param buttonItem L'item visuel du bouton.
* @param tabConfig La configuration des items de cet onglet.
* @return Ce builder pour chaînage.
*/
public TabbedGuiBuilder tab(String tabId, int buttonSlot, GuiItem buttonItem, Consumer<TabbedGui> tabConfig) {
this.tabButtons.put(tabId, buttonSlot);
this.tabButtonItems.put(tabId, buttonItem);
if (tabConfig != null) {
this.tabInitializers.put(tabId, tabConfig);
}
return this;
}
@Override
public TabbedGui build() {
TabbedGui gui = new TabbedGui(title, type);
for (var entry : items.entrySet()) {
gui.setItem(entry.getKey(), entry.getValue());
}
for (var pattern : patterns) {
gui.applyPattern(pattern);
}
for (int s : editableSlots) {
gui.setEditable(s, true);
}
for (var entry : properties.entrySet()) {
gui.setProperty(entry.getKey(), entry.getValue());
}
for (var hook : openHooks) gui.onOpen(hook);
for (var hook : closeHooks) gui.onClose(hook);
for (var hook : clickHooks) gui.onClick(hook);
for (var hook : outsideClickHooks) gui.onOutsideClick(hook);
for (var hook : bottomClickHooks) gui.onBottomClick(hook);
for (var entry : tabButtons.entrySet()) {
String tabId = entry.getKey();
int slot = entry.getValue();
GuiItem item = tabButtonItems.get(tabId);
gui.addTab(tabId, slot, item);
Consumer<TabbedGui> init = tabInitializers.get(tabId);
if (init != null) {
init.accept(gui);
}
}
return gui;
}
}
@@ -0,0 +1,53 @@
package fr.luc.bettermcguis.demo;
import fr.luc.bettermcguis.BetterMcGuis;
import fr.luc.bettermcguis.demo.guis.DemoMainMenuGui;
import fr.luc.bettermcguis.demo.listeners.DemoGuiListener;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
/**
* Plugin de démonstration montrant l'initialisation de betterMcGuis,
* l'enregistrement d'écouteurs et l'ouverture de menus via commande.
*/
public class DemoGuiPlugin extends JavaPlugin implements CommandExecutor {
private BetterMcGuis guiManager;
@Override
public void onEnable() {
// Initialisation de betterMcGuis
this.guiManager = BetterMcGuis.create(this);
// Enregistrement des listeners d'audit
this.guiManager.registerListeners(new DemoGuiListener());
// Enregistrement d'une commande /menu pour ouvrir le GUI
if (getCommand("menu") != null) {
getCommand("menu").setExecutor(this);
}
getLogger().info("betterMcGuis DemoPlugin active avec succes !");
}
@Override
public void onDisable() {
if (guiManager != null) {
guiManager.unregisterAll();
}
getLogger().info("betterMcGuis DemoPlugin desactive.");
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (sender instanceof Player player) {
DemoMainMenuGui.create().open(player);
return true;
}
sender.sendMessage("Seul un joueur peut ouvrir ce menu.");
return true;
}
}
@@ -0,0 +1,65 @@
package fr.luc.bettermcguis.demo.guis;
import fr.luc.bettermcguis.BetterMcGuis;
import fr.luc.bettermcguis.api.Gui;
import fr.luc.bettermcguis.builder.ItemBuilder;
import org.bukkit.Material;
import org.bukkit.Sound;
/**
* Menu principal de démonstration illustrant la création d'un GUI interactif moderne.
*/
public class DemoMainMenuGui {
public static Gui create() {
return BetterMcGuis.builder()
.title("<gradient:#ff5e62:#ff9966><bold>Menu Principal Démo</bold></gradient>")
.rows(3)
.fillBorder(Material.BLACK_STAINED_GLASS_PANE)
// Bouton 1 : Boutique Paginée
.item(11, ItemBuilder.of(Material.EMERALD)
.name("<green><bold>Boutique du Serveur</bold></green>")
.lore(
"<gray>Parcourez notre catalogue d'items.</gray>",
"",
"<yellow>▶ Cliquez pour ouvrir la boutique</yellow>"
)
.glowing(true)
.asGuiItem(ctx -> {
ctx.playSound(Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.2f);
DemoPaginatedShopGui.create().open(ctx.getPlayer());
})
)
// Bouton 2 : Poubelle / Recyclage
.item(13, ItemBuilder.of(Material.CAULDRON)
.name("<red><bold>Poubelle / Débarras</bold></red>")
.lore(
"<gray>Déposez vos items indésirables ici.</gray>",
"",
"<yellow>▶ Cliquez pour ouvrir la poubelle</yellow>"
)
.asGuiItem(ctx -> {
ctx.playSound(Sound.BLOCK_CHEST_OPEN, 1.0f, 1.0f);
DemoStorageTrashGui.create().open(ctx.getPlayer());
})
)
// Bouton 3 : Profil & Statistiques
.item(15, ItemBuilder.skull()
.name("<aqua><bold>Votre Profil</bold></aqua>")
.lore(
"<gray>Consultez vos statistiques personnelles.</gray>",
"",
"<yellow>▶ Cliquez pour actualiser</yellow>"
)
.asGuiItem(ctx -> {
ctx.playSound(Sound.UI_BUTTON_CLICK, 1.0f, 1.0f);
ctx.replySuccess("Votre profil est à jour !");
})
)
.build();
}
}
@@ -0,0 +1,58 @@
package fr.luc.bettermcguis.demo.guis;
import fr.luc.bettermcguis.BetterMcGuis;
import fr.luc.bettermcguis.api.Gui;
import fr.luc.bettermcguis.builder.ItemBuilder;
import org.bukkit.Material;
import org.bukkit.Sound;
/**
* Menu de boutique paginé de démonstration avec items interactifs et boutons de navigation.
*/
public class DemoPaginatedShopGui {
public static Gui create() {
fr.luc.bettermcguis.builder.PaginatedGuiBuilder builder = BetterMcGuis.paginated()
.title("<gradient:#00c6ff:#0072ff><bold>Boutique Paginée</bold></gradient>")
.rows(5)
.fillBorder(Material.GRAY_STAINED_GLASS_PANE);
// Bouton retour au menu principal en bas à gauche
builder.item(36, ItemBuilder.of(Material.BARRIER)
.name("<red>Retour au Menu Principal</red>")
.asGuiItem(ctx -> {
ctx.playSound(Sound.UI_BUTTON_CLICK, 1.0f, 0.8f);
DemoMainMenuGui.create().open(ctx.getPlayer());
})
);
// Génère 50 articles dans le catalogue paginé
Material[] sampleMaterials = new Material[]{
Material.DIAMOND, Material.NETHERITE_INGOT, Material.GOLDEN_APPLE, Material.ENCHANTED_GOLDEN_APPLE,
Material.ELYTRA, Material.TOTEM_OF_UNDYING, Material.EXPERIENCE_BOTTLE, Material.BEACON,
Material.SHULKER_BOX, Material.NETHER_STAR, Material.ENDER_PEARL, Material.BLAZE_ROD,
Material.TRIDENT, Material.NETHERITE_SWORD, Material.BOW, Material.CROSSBOW
};
for (int i = 1; i <= 40; i++) {
Material mat = sampleMaterials[(i - 1) % sampleMaterials.length];
int price = i * 100;
int itemId = i;
builder.addPageItem(ItemBuilder.of(mat)
.name("<gold><bold>Article #" + itemId + "</bold></gold>")
.lore(
"<gray>Prix : <green>" + price + " $</green></gray>",
"",
"<yellow>▶ Clic-gauche pour acheter</yellow>"
)
.asGuiItem(ctx -> {
ctx.playSound(Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.5f);
ctx.replySuccess("Vous avez acheté <gold>Article #" + itemId + "</gold> pour <green>" + price + " $</green> !");
})
);
}
return builder.build();
}
}
@@ -0,0 +1,46 @@
package fr.luc.bettermcguis.demo.guis;
import fr.luc.bettermcguis.api.Gui;
import fr.luc.bettermcguis.api.GuiType;
import fr.luc.bettermcguis.api.slot.SlotRange;
import fr.luc.bettermcguis.builder.ItemBuilder;
import fr.luc.bettermcguis.type.StorageGui;
import org.bukkit.Material;
import org.bukkit.Sound;
/**
* Menu de poubelle / zone de dépôt de démonstration (StorageGui) permettant de détruire des items.
*/
public class DemoStorageTrashGui {
public static Gui create() {
StorageGui gui = new StorageGui("<gradient:#eb3349:#f45c43><bold>Poubelle Publique</bold></gradient>", GuiType.CHEST_4_ROWS);
// Remplit les bordures avec du vitrage rouge
gui.fillBorder(ItemBuilder.filler(Material.RED_STAINED_GLASS_PANE).asGuiItem());
// Définit l'intérieur comme zone de stockage éditable
gui.setStorageSlots(SlotRange.interior(4, 9));
gui.returnItemsOnClose(false); // Détruit définitivement les items à la fermeture
// Bouton de vidage immédiat en bas au centre
gui.setItem(31, ItemBuilder.of(Material.LAVA_BUCKET)
.name("<red><bold>Vider la Poubelle Immédiatement</bold></red>")
.lore(
"<gray>Détruit tous les objets placés dans la zone centrale.</gray>",
"",
"<yellow>▶ Cliquez pour incinérer les items</yellow>"
)
.asGuiItem(ctx -> {
ctx.playSound(Sound.BLOCK_LAVA_EXTINGUISH, 1.0f, 1.0f);
var topInv = ctx.getPlayer().getOpenInventory().getTopInventory();
for (int slot : SlotRange.interior(4, 9).getSlots()) {
topInv.setItem(slot, null);
}
ctx.replySuccess("La poubelle a été vidée et les items ont été incinérés !");
})
);
return gui;
}
}
@@ -0,0 +1,36 @@
package fr.luc.bettermcguis.demo.listeners;
import fr.luc.bettermcguis.event.GuiClickEvent;
import fr.luc.bettermcguis.event.GuiCloseEvent;
import fr.luc.bettermcguis.event.GuiOpenEvent;
import fr.luc.bettermcguis.event.GuiPageChangeEvent;
import fr.luc.bettermcguis.event.annotation.GuiEventHandler;
/**
* Écouteur global de démonstration illustrant la capture d'événements de GUI avec {@link GuiEventHandler}.
*/
public class DemoGuiListener {
@GuiEventHandler(priority = 10)
public void onOpen(GuiOpenEvent event) {
System.out.println("[GUI Log] Ouverture du menu '" + event.getGui().getTitle() + "' pour " + event.getPlayer().getName());
}
@GuiEventHandler
public void onClose(GuiCloseEvent event) {
System.out.println("[GUI Log] Fermeture du menu '" + event.getGui().getTitle() + "' par " + event.getPlayer().getName());
}
@GuiEventHandler
public void onClick(GuiClickEvent event) {
var ctx = event.getContext();
if (ctx.getGuiItem() != null) {
System.out.println("[GUI Log] Clic sur slot #" + ctx.getSlot() + " (" + ctx.getClickType() + ") par " + ctx.getPlayer().getName());
}
}
@GuiEventHandler
public void onPageChange(GuiPageChangeEvent event) {
System.out.println("[GUI Log] Changement de page : " + event.getOldPage() + " -> " + event.getNewPage());
}
}
@@ -0,0 +1,32 @@
package fr.luc.bettermcguis.event;
import fr.luc.bettermcguis.api.Gui;
import org.bukkit.entity.Player;
/**
* Classe de base pour les événements de GUI pouvant être annulés.
*/
public abstract class CancellableGuiEvent extends GuiEvent {
private boolean cancelled = false;
public CancellableGuiEvent(Gui gui, Player player) {
super(gui, player);
}
/**
* @return true si l'événement a été annulé, sinon false.
*/
public boolean isCancelled() {
return cancelled;
}
/**
* Définit si l'événement doit être annulé.
*
* @param cancelled true pour interrompre l'action.
*/
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
}
@@ -0,0 +1,27 @@
package fr.luc.bettermcguis.event;
import fr.luc.bettermcguis.api.GuiClickContext;
/**
* Déclenché lors d'une interaction / clic d'un joueur dans un inventaire GUI.
*/
public class GuiClickEvent extends CancellableGuiEvent {
private final GuiClickContext context;
public GuiClickEvent(GuiClickContext context) {
super(context.getGui(), context.getPlayer());
this.context = context;
this.setCancelled(context.isCancelled());
}
public GuiClickContext getContext() {
return context;
}
@Override
public void setCancelled(boolean cancelled) {
super.setCancelled(cancelled);
context.setCancelled(cancelled);
}
}
@@ -0,0 +1,22 @@
package fr.luc.bettermcguis.event;
import fr.luc.bettermcguis.api.Gui;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryCloseEvent;
/**
* Déclenché lors de la fermeture d'un GUI par un joueur.
*/
public class GuiCloseEvent extends GuiEvent {
private final InventoryCloseEvent rawEvent;
public GuiCloseEvent(Gui gui, Player player, InventoryCloseEvent rawEvent) {
super(gui, player);
this.rawEvent = rawEvent;
}
public InventoryCloseEvent getRawEvent() {
return rawEvent;
}
}
@@ -0,0 +1,34 @@
package fr.luc.bettermcguis.event;
import fr.luc.bettermcguis.api.Gui;
import org.bukkit.entity.Player;
import java.util.Objects;
/**
* Classe abstraite de base pour tous les événements du cycle de vie des GUIs.
*/
public abstract class GuiEvent {
protected final Gui gui;
protected final Player player;
public GuiEvent(Gui gui, Player player) {
this.gui = Objects.requireNonNull(gui, "gui cannot be null");
this.player = player;
}
/**
* @return L'instance de {@link Gui} concernée par l'événement.
*/
public Gui getGui() {
return gui;
}
/**
* @return Le joueur concerné, ou {@code null} pour un événement global.
*/
public Player getPlayer() {
return player;
}
}
@@ -0,0 +1,17 @@
package fr.luc.bettermcguis.event;
/**
* Interface fonctionnelle pour écouter un événement spécifique de GUI.
*
* @param <T> Le type d'événement écouté.
*/
@FunctionalInterface
public interface GuiEventListener<T extends GuiEvent> {
/**
* Traite l'événement transmis.
*
* @param event L'instance de l'événement.
*/
void onEvent(T event);
}
@@ -0,0 +1,112 @@
package fr.luc.bettermcguis.event;
import fr.luc.bettermcguis.event.annotation.GuiEventHandler;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Bus d'événements central gérant l'enregistrement et la distribution des événements de GUI.
*/
public class GuiEventManager {
private record RegisteredListener(
Class<? extends GuiEvent> eventType,
int priority,
String titleFilter,
GuiEventListener<?> listener
) implements Comparable<RegisteredListener> {
@Override
public int compareTo(RegisteredListener o) {
return Integer.compare(o.priority, this.priority); // Ordre décroissant
}
}
private final Map<Class<? extends GuiEvent>, List<RegisteredListener>> listeners = new ConcurrentHashMap<>();
/**
* Enregistre un écouteur programmatique avec une priorité par défaut de 0.
*/
public <T extends GuiEvent> void registerListener(Class<T> eventType, GuiEventListener<T> listener) {
registerListener(eventType, 0, "", listener);
}
/**
* Enregistre un écouteur programmatique avec priorité et filtre de titre.
*/
public <T extends GuiEvent> void registerListener(Class<T> eventType, int priority, String titleFilter, GuiEventListener<T> listener) {
Objects.requireNonNull(eventType, "eventType cannot be null");
Objects.requireNonNull(listener, "listener cannot be null");
RegisteredListener reg = new RegisteredListener(eventType, priority, titleFilter != null ? titleFilter : "", listener);
listeners.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>()).add(reg);
listeners.get(eventType).sort(RegisteredListener::compareTo);
}
/**
* Scanne et enregistre les méthodes annotées {@link GuiEventHandler} d'un objet écouteur.
*
* @param listenerInstance L'instance de classe contenant les méthodes d'écoute.
*/
@SuppressWarnings("unchecked")
public void registerListeners(Object listenerInstance) {
if (listenerInstance == null) return;
for (Method method : listenerInstance.getClass().getDeclaredMethods()) {
if (method.isAnnotationPresent(GuiEventHandler.class)) {
Class<?>[] params = method.getParameterTypes();
if (params.length == 1 && GuiEvent.class.isAssignableFrom(params[0])) {
Class<? extends GuiEvent> eventType = (Class<? extends GuiEvent>) params[0];
GuiEventHandler annotation = method.getAnnotation(GuiEventHandler.class);
method.setAccessible(true);
GuiEventListener<GuiEvent> listener = event -> {
try {
method.invoke(listenerInstance, event);
} catch (Exception e) {
e.printStackTrace();
}
};
registerListener(eventType, annotation.priority(), annotation.guiTitle(), (GuiEventListener) listener);
}
}
}
}
/**
* Distribue un événement à tous les écouteurs enregistrés correspondants.
*
* @param event L'événement à diffuser.
*/
@SuppressWarnings("unchecked")
public void dispatch(GuiEvent event) {
if (event == null) return;
Class<?> clazz = event.getClass();
for (Map.Entry<Class<? extends GuiEvent>, List<RegisteredListener>> entry : listeners.entrySet()) {
if (entry.getKey().isAssignableFrom(clazz)) {
for (RegisteredListener reg : entry.getValue()) {
if (!reg.titleFilter().isEmpty() && !event.getGui().getTitle().equalsIgnoreCase(reg.titleFilter())) {
continue;
}
try {
((GuiEventListener<GuiEvent>) reg.listener()).onEvent(event);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
/**
* Supprime tous les écouteurs enregistrés.
*/
public void clear() {
listeners.clear();
}
}
@@ -0,0 +1,22 @@
package fr.luc.bettermcguis.event;
import fr.luc.bettermcguis.api.Gui;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryOpenEvent;
/**
* Déclenché avant l'ouverture d'un GUI pour un joueur. Peut être annulé.
*/
public class GuiOpenEvent extends CancellableGuiEvent {
private final InventoryOpenEvent rawEvent;
public GuiOpenEvent(Gui gui, Player player, InventoryOpenEvent rawEvent) {
super(gui, player);
this.rawEvent = rawEvent;
}
public InventoryOpenEvent getRawEvent() {
return rawEvent;
}
}
@@ -0,0 +1,27 @@
package fr.luc.bettermcguis.event;
import fr.luc.bettermcguis.api.Gui;
import org.bukkit.entity.Player;
/**
* Déclenché lors d'un changement de page dans un menu paginé (PaginatedGui).
*/
public class GuiPageChangeEvent extends GuiEvent {
private final int oldPage;
private final int newPage;
public GuiPageChangeEvent(Gui gui, Player player, int oldPage, int newPage) {
super(gui, player);
this.oldPage = oldPage;
this.newPage = newPage;
}
public int getOldPage() {
return oldPage;
}
public int getNewPage() {
return newPage;
}
}
@@ -0,0 +1,14 @@
package fr.luc.bettermcguis.event;
import fr.luc.bettermcguis.api.Gui;
import org.bukkit.entity.Player;
/**
* Déclenché lors du rafraîchissement d'un GUI pour un joueur.
*/
public class GuiRefreshEvent extends GuiEvent {
public GuiRefreshEvent(Gui gui, Player player) {
super(gui, player);
}
}
@@ -0,0 +1,24 @@
package fr.luc.bettermcguis.event.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation permettant de marquer une méthode comme écouteur d'événements du cycle de vie des GUIs.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GuiEventHandler {
/**
* Priorité d'exécution de l'écouteur (les valeurs les plus élevées sont exécutées en premier).
*/
int priority() default 0;
/**
* Filtre optionnel sur le titre du GUI (si renseigné, seuls les GUIs dont le titre correspond déclencheront la méthode).
*/
String guiTitle() default "";
}
@@ -0,0 +1,59 @@
package fr.luc.bettermcguis.holder;
import fr.luc.bettermcguis.api.Gui;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import java.util.Objects;
import java.util.UUID;
/**
* Implémentation personnalisée d'{@link InventoryHolder} permettant d'associer
* un inventaire Bukkit/Paper natif à une instance de {@link Gui} de la bibliothèque betterMcGuis.
*/
public class BetterGuiHolder implements InventoryHolder {
private final Gui gui;
private final UUID viewerUuid;
private Inventory inventory;
/**
* Crée un nouveau holder reliant un GUI à son visualisateur.
*
* @param gui Le GUI associé.
* @param viewer Le joueur visualisant l'inventaire.
*/
public BetterGuiHolder(Gui gui, Player viewer) {
this.gui = Objects.requireNonNull(gui, "gui cannot be null");
this.viewerUuid = viewer != null ? viewer.getUniqueId() : null;
}
/**
* @return L'instance de {@link Gui} sous-jacente.
*/
public Gui getGui() {
return gui;
}
/**
* @return L'UUID du joueur visualisant le GUI.
*/
public UUID getViewerUuid() {
return viewerUuid;
}
/**
* Définit l'inventaire Bukkit créé pour ce holder.
*
* @param inventory L'inventaire Bukkit.
*/
public void setInventory(Inventory inventory) {
this.inventory = inventory;
}
@Override
public Inventory getInventory() {
return inventory;
}
}
@@ -0,0 +1,162 @@
package fr.luc.bettermcguis.listener;
import fr.luc.bettermcguis.BetterMcGuis;
import fr.luc.bettermcguis.api.*;
import fr.luc.bettermcguis.api.slot.SlotPos;
import fr.luc.bettermcguis.event.GuiClickEvent;
import fr.luc.bettermcguis.event.GuiCloseEvent;
import fr.luc.bettermcguis.event.GuiOpenEvent;
import fr.luc.bettermcguis.holder.BetterGuiHolder;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.*;
import org.bukkit.inventory.Inventory;
/**
* Écouteur Bukkit central interceptant de manière sécurisée toutes les interactions d'inventaires
* pour les acheminer vers les instances de {@link Gui} et le bus d'événements de betterMcGuis.
*/
public class BukkitGuiEventListener implements Listener {
private final BetterMcGuis manager;
public BukkitGuiEventListener(BetterMcGuis manager) {
this.manager = manager;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void onInventoryClick(InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player player)) {
return;
}
Inventory topInventory = event.getView().getTopInventory();
if (!(topInventory.getHolder() instanceof BetterGuiHolder holder)) {
return;
}
Gui gui = holder.getGui();
int rawSlot = event.getRawSlot();
// 1. Clic en dehors de la fenêtre d'inventaire
if (rawSlot == -999 || event.getSlotType() == InventoryType.SlotType.OUTSIDE) {
GuiClickContext context = new GuiClickContext(
player, gui, null, -999, null,
event.getClick(), event.getAction(),
null, event.getCursor(), event
);
gui.handleOutsideClick(context);
manager.getEventManager().dispatch(new GuiClickEvent(context));
return;
}
// 2. Clic dans l'inventaire supérieur (Le GUI lui-même)
if (rawSlot < topInventory.getSize() && rawSlot >= 0) {
boolean editable = gui.isEditable(rawSlot);
GuiItem item = gui.getItem(rawSlot);
SlotPos pos = SlotPos.fromSlot(rawSlot, gui.getColumns());
GuiClickContext context = new GuiClickContext(
player, gui, item, rawSlot, pos,
event.getClick(), event.getAction(),
event.getCurrentItem(), event.getCursor(), event
);
// Si le slot n'est pas explicitement éditable, on bloque l'interaction native
if (!editable) {
event.setCancelled(true);
context.setCancelled(true);
} else {
context.setCancelled(false);
}
if (item != null) {
item.handleClick(context);
}
gui.handleClick(context);
manager.getEventManager().dispatch(new GuiClickEvent(context));
return;
}
// 3. Clic dans l'inventaire inférieur (Inventaire du joueur)
if (rawSlot >= topInventory.getSize()) {
// Sécurité : Empêche le Shift-Clic d'injecter des items dans des slots non éditables du GUI
if (event.isShiftClick()) {
boolean hasEditableSlots = !gui.getEditableSlots().isEmpty();
if (!hasEditableSlots) {
event.setCancelled(true);
}
}
// Sécurité : Échange par touche numérique (Hotbar Number Key Swap)
if (event.getClick() == ClickType.NUMBER_KEY) {
// Bloque si le slot visé n'est pas éditable
if (rawSlot < topInventory.getSize() && !gui.isEditable(rawSlot)) {
event.setCancelled(true);
}
}
GuiClickContext context = new GuiClickContext(
player, gui, null, event.getSlot(), null,
event.getClick(), event.getAction(),
event.getCurrentItem(), event.getCursor(), event
);
gui.handleBottomClick(context);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void onInventoryDrag(InventoryDragEvent event) {
if (!(event.getWhoClicked() instanceof Player player)) {
return;
}
Inventory topInventory = event.getView().getTopInventory();
if (!(topInventory.getHolder() instanceof BetterGuiHolder holder)) {
return;
}
Gui gui = holder.getGui();
// Vérifie si l'un des slots touchés par le glisser-déposer appartient au GUI supérieur
for (int rawSlot : event.getRawSlots()) {
if (rawSlot < topInventory.getSize()) {
if (!gui.isEditable(rawSlot)) {
event.setCancelled(true);
return;
}
}
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onInventoryOpen(InventoryOpenEvent event) {
if (!(event.getPlayer() instanceof Player player)) {
return;
}
if (event.getInventory().getHolder() instanceof BetterGuiHolder holder) {
Gui gui = holder.getGui();
GuiOpenContext context = new GuiOpenContext(player, gui, event);
gui.handleOpen(context);
manager.getEventManager().dispatch(new GuiOpenEvent(gui, player, event));
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onInventoryClose(InventoryCloseEvent event) {
if (!(event.getPlayer() instanceof Player player)) {
return;
}
if (event.getInventory().getHolder() instanceof BetterGuiHolder holder) {
Gui gui = holder.getGui();
GuiCloseContext context = new GuiCloseContext(player, gui, event);
gui.handleClose(context);
manager.getEventManager().dispatch(new GuiCloseEvent(gui, player, event));
}
}
}
@@ -0,0 +1,55 @@
package fr.luc.bettermcguis.pattern;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Représente un masque de sélection de slots basé sur des lignes de caractères binaires (0 et 1 ou caractères au choix).
*/
public class GuiMask {
private final List<String> lines = new ArrayList<>();
private final char activeChar;
/**
* Crée un masque avec '1' comme caractère actif par défaut.
*/
public GuiMask(String... lines) {
this('1', lines);
}
/**
* Crée un masque en précisant le caractère actif.
*
* @param activeChar Le caractère représentant un slot sélectionné.
* @param lines Les lignes du masque.
*/
public GuiMask(char activeChar, String... lines) {
this.activeChar = activeChar;
if (lines != null) {
this.lines.addAll(Arrays.asList(lines));
}
}
/**
* Résout la liste des slots sélectionnés par ce masque pour des dimensions données.
*
* @param rows Le nombre de lignes.
* @param cols Le nombre de colonnes (ex: 9).
* @return La liste des slots actifs (0-indexés).
*/
public List<Integer> resolveSlots(int rows, int cols) {
List<Integer> slots = new ArrayList<>();
for (int r = 0; r < Math.min(lines.size(), rows); r++) {
String line = lines.get(r);
for (int c = 0; c < Math.min(line.length(), cols); c++) {
if (line.charAt(c) == activeChar) {
slots.add(r * cols + c);
}
}
}
return Collections.unmodifiableList(slots);
}
}
@@ -0,0 +1,108 @@
package fr.luc.bettermcguis.pattern;
import fr.luc.bettermcguis.api.Gui;
import fr.luc.bettermcguis.api.GuiItem;
import java.util.*;
/**
* Permet de définir la disposition visuelle d'un GUI sous forme d'une matrice textuelle ASCII (pattern).
* Chaque caractère de la matrice est associé à un {@link GuiItem}.
*/
public class GuiPattern {
private final List<String> rows = new ArrayList<>();
private final Map<Character, GuiItem> itemBindings = new HashMap<>();
/**
* Crée un pattern à partir des lignes fournies.
*
* @param lines Les lignes de texte représentant la grille de l'inventaire.
*/
public GuiPattern(String... lines) {
if (lines != null) {
this.rows.addAll(Arrays.asList(lines));
}
}
/**
* Crée un pattern à partir d'une liste de lignes.
*/
public GuiPattern(List<String> lines) {
if (lines != null) {
this.rows.addAll(lines);
}
}
/**
* Fabrique statique pour créer un pattern.
*
* @param lines Les lignes du motif.
* @return L'instance de {@link GuiPattern}.
*/
public static GuiPattern of(String... lines) {
return new GuiPattern(lines);
}
/**
* Associe un caractère à un {@link GuiItem}.
*
* @param character Le caractère de la grille (ex: '#', 'X', 'A').
* @param item L'item correspondant.
* @return Cette instance de pattern pour chaînage.
*/
public GuiPattern bind(char character, GuiItem item) {
this.itemBindings.put(character, item);
return this;
}
/**
* Associe plusieurs bindings à la fois.
*
* @param bindings La table de mapping caractère -> GuiItem.
* @return Cette instance.
*/
public GuiPattern bindAll(Map<Character, GuiItem> bindings) {
if (bindings != null) {
this.itemBindings.putAll(bindings);
}
return this;
}
/**
* Applique ce motif sur l'inventaire GUI cible.
*
* @param gui Le GUI sur lequel placer les items selon la matrice.
*/
public void apply(Gui gui) {
Objects.requireNonNull(gui, "gui cannot be null");
int cols = gui.getColumns();
int maxRows = gui.getRows();
for (int r = 0; r < Math.min(rows.size(), maxRows); r++) {
String line = rows.get(r);
for (int c = 0; c < Math.min(line.length(), cols); c++) {
char ch = line.charAt(c);
if (itemBindings.containsKey(ch)) {
GuiItem item = itemBindings.get(ch);
int slot = r * cols + c;
gui.setItem(slot, item);
}
}
}
}
/**
* @return La liste des lignes du motif.
*/
public List<String> getRows() {
return Collections.unmodifiableList(rows);
}
/**
* @return La table des liaisons de caractères vers items.
*/
public Map<Character, GuiItem> getItemBindings() {
return Collections.unmodifiableMap(itemBindings);
}
}
@@ -0,0 +1,122 @@
package fr.luc.bettermcguis.type;
import fr.luc.bettermcguis.animation.Frame;
import fr.luc.bettermcguis.api.GuiItem;
import fr.luc.bettermcguis.api.GuiType;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitTask;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Implémentation d'un GUI animé cadencé par une tâche Bukkit (frames multiples, titres mouvants, carrousels).
*/
public class AnimatedGui extends SimpleGui {
private final List<Frame> frames = new ArrayList<>();
private int currentFrameIndex = 0;
private BukkitTask animationTask;
private boolean loop = true;
public AnimatedGui(String title, GuiType type) {
super(title, type);
}
public AnimatedGui(String title, int rows) {
super(title, rows);
}
/**
* Ajoute une frame à la séquence d'animation.
*
* @param frame La frame.
* @return Ce GUI pour chaînage.
*/
public AnimatedGui addFrame(Frame frame) {
if (frame != null) {
this.frames.add(frame);
}
return this;
}
/**
* Définit si l'animation doit tourner en boucle continue.
*/
public AnimatedGui loop(boolean loop) {
this.loop = loop;
return this;
}
/**
* Démarre la lecture de l'animation.
*
* @param plugin Le plugin gérant la tâche Bukkit.
* @param interval L'intervalle de temps entre chaque frame.
*/
public void startAnimation(Plugin plugin, Duration interval) {
Objects.requireNonNull(plugin, "plugin cannot be null");
stopAnimation();
if (frames.isEmpty()) return;
long ticks = Math.max(1, (interval != null ? interval.toMillis() : 500) / 50);
this.animationTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
if (getViewers().isEmpty()) {
// Pas de spectateur -> pause temporaire ou stop
return;
}
advanceFrame();
}, 0L, ticks);
}
/**
* Avance d'une frame et met à jour les slots pour tous les spectateurs.
*/
public void advanceFrame() {
if (frames.isEmpty()) return;
Frame frame = frames.get(currentFrameIndex);
for (Map.Entry<Integer, GuiItem> entry : frame.getItems().entrySet()) {
super.setItem(entry.getKey(), entry.getValue());
}
if (frame.getTitle() != null) {
super.setTitle(frame.getTitle());
}
currentFrameIndex++;
if (currentFrameIndex >= frames.size()) {
if (loop) {
currentFrameIndex = 0;
} else {
stopAnimation();
}
}
}
/**
* Arrête l'animation en cours.
*/
public void stopAnimation() {
if (animationTask != null) {
animationTask.cancel();
animationTask = null;
}
}
@Override
public void close(Player player) {
super.close(player);
if (getViewers().isEmpty()) {
stopAnimation();
}
}
}
@@ -0,0 +1,348 @@
package fr.luc.bettermcguis.type;
import fr.luc.bettermcguis.api.GuiClickAction;
import fr.luc.bettermcguis.api.GuiItem;
import fr.luc.bettermcguis.api.GuiType;
import fr.luc.bettermcguis.api.slot.SlotRange;
import fr.luc.bettermcguis.builder.ItemBuilder;
import fr.luc.bettermcguis.event.GuiPageChangeEvent;
import fr.luc.bettermcguis.pattern.GuiMask;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiFunction;
import java.util.function.Consumer;
/**
* Implémentation d'un inventaire paginé (Paginated GUI) gérant automatiquement la navigation par pages,
* le calcul dynamique du nombre de pages, les boutons suivant/précédent et l'affichage d'un indicateur de page.
*/
public class PaginatedGui extends SimpleGui {
private final List<GuiItem> pageItems = new ArrayList<>();
private final List<Integer> itemSlots = new ArrayList<>();
private int currentPage = 1; // 1-indexed pour plus de lisibilité
private Integer previousPageSlot;
private Integer nextPageSlot;
private Integer pageIndicatorSlot;
private GuiItem customPreviousButton;
private GuiItem customNextButton;
private BiFunction<Integer, Integer, GuiItem> pageIndicatorSupplier;
private final List<Consumer<GuiPageChangeEvent>> pageChangeHooks = new CopyOnWriteArrayList<>();
/**
* Initialise un GUI paginé avec un titre et un type d'inventaire.
*/
public PaginatedGui(String title, GuiType type) {
super(title, type);
initDefaultItemSlots();
}
/**
* Initialise un GUI paginé avec un nombre de lignes de coffre.
*/
public PaginatedGui(String title, int rows) {
super(title, rows);
initDefaultItemSlots();
}
private void initDefaultItemSlots() {
// Par défaut, si 3 lignes ou plus, on utilise l'intérieur
if (getRows() >= 3) {
this.itemSlots.addAll(SlotRange.interior(getRows(), getColumns()).getSlots());
this.previousPageSlot = (getRows() - 1) * getColumns() + 3; // ex: slot 48
this.pageIndicatorSlot = (getRows() - 1) * getColumns() + 4; // ex: slot 49
this.nextPageSlot = (getRows() - 1) * getColumns() + 5; // ex: slot 50
} else {
for (int i = 0; i < getSize() - 2; i++) {
this.itemSlots.add(i);
}
this.previousPageSlot = getSize() - 2;
this.nextPageSlot = getSize() - 1;
}
}
/**
* Ajoute un item paginé à la collection.
*
* @param item L'item à ajouter.
*/
public void addPageItem(GuiItem item) {
if (item != null) {
this.pageItems.add(item);
}
}
/**
* Ajoute une collection complète d'items paginés.
*
* @param items Les items.
*/
public void addPageItems(Collection<GuiItem> items) {
if (items != null) {
this.pageItems.addAll(items);
}
}
/**
* Remplace l'intégralité de la collection d'items paginés.
*
* @param items La nouvelle liste d'items.
*/
public void setPageItems(List<GuiItem> items) {
this.pageItems.clear();
if (items != null) {
this.pageItems.addAll(items);
}
}
/**
* Supprime tous les items de la collection paginée.
*/
public void clearPageItems() {
this.pageItems.clear();
this.currentPage = 1;
}
/**
* Définit explicitement la liste des slots réservés pour les items paginés.
*
* @param slots Les numéros de slots.
*/
public void setItemSlots(List<Integer> slots) {
this.itemSlots.clear();
if (slots != null) {
this.itemSlots.addAll(slots);
}
}
/**
* Définit les slots d'items à partir d'une plage {@link SlotRange}.
*/
public void setItemSlots(SlotRange range) {
if (range != null) {
setItemSlots(range.getSlots());
}
}
/**
* Définit les slots d'items à partir d'un masque binaire {@link GuiMask}.
*/
public void setItemSlots(GuiMask mask) {
if (mask != null) {
setItemSlots(mask.resolveSlots(getRows(), getColumns()));
}
}
/**
* Configure le bouton de page précédente.
*
* @param slot Le numéro de slot.
* @param item L'item du bouton (ou null pour le bouton par défaut).
*/
public void setPreviousPageButton(int slot, GuiItem item) {
this.previousPageSlot = slot;
this.customPreviousButton = item;
}
/**
* Configure le bouton de page suivante.
*
* @param slot Le numéro de slot.
* @param item L'item du bouton (ou null pour le bouton par défaut).
*/
public void setNextPageButton(int slot, GuiItem item) {
this.nextPageSlot = slot;
this.customNextButton = item;
}
/**
* Configure l'indicateur de page (ex: "Page 2/5").
*
* @param slot Le numéro de slot.
* @param indicatorSupplier Fonction fournissant l'item selon (pageActuelle, pageMax).
*/
public void setPageIndicator(int slot, BiFunction<Integer, Integer, GuiItem> indicatorSupplier) {
this.pageIndicatorSlot = slot;
this.pageIndicatorSupplier = indicatorSupplier;
}
/**
* @return Le nombre d'items pouvant être affichés par page.
*/
public int getPageSize() {
return Math.max(1, itemSlots.size());
}
/**
* @return Le nombre total de pages nécessaires pour afficher tous les items (au minimum 1).
*/
public int getTotalPages() {
if (pageItems.isEmpty()) {
return 1;
}
return (int) Math.ceil((double) pageItems.size() / getPageSize());
}
/**
* @return Le numéro de la page actuelle (1-indexé).
*/
public int getCurrentPage() {
return currentPage;
}
/**
* Définit la page actuelle en effectuant les vérifications de bornes et déclenchant l'événement.
*
* @param page Le numéro de page (1 à totalPages).
* @return true si la page a changé, sinon false.
*/
public boolean setCurrentPage(int page) {
int max = getTotalPages();
int target = Math.max(1, Math.min(max, page));
if (target == this.currentPage) {
return false;
}
int oldPage = this.currentPage;
this.currentPage = target;
GuiPageChangeEvent event = new GuiPageChangeEvent(this, null, oldPage, target);
for (Consumer<GuiPageChangeEvent> hook : pageChangeHooks) {
try {
hook.accept(event);
} catch (Exception e) {
e.printStackTrace();
}
}
refreshAll();
return true;
}
/**
* @return true si une page suivante existe après la page actuelle.
*/
public boolean hasNextPage() {
return currentPage < getTotalPages();
}
/**
* @return true si une page précédente existe avant la page actuelle.
*/
public boolean hasPreviousPage() {
return currentPage > 1;
}
/**
* Passe à la page suivante si possible.
*
* @return true si la navigation a réussi.
*/
public boolean nextPage() {
if (hasNextPage()) {
return setCurrentPage(currentPage + 1);
}
return false;
}
/**
* Revient à la page précédente si possible.
*
* @return true si la navigation a réussi.
*/
public boolean previousPage() {
if (hasPreviousPage()) {
return setCurrentPage(currentPage - 1);
}
return false;
}
/**
* Attache un écouteur appelé lors d'un changement de page.
*
* @param hook L'action à exécuter.
* @return Ce GUI pour chaînage.
*/
public PaginatedGui onPageChange(Consumer<GuiPageChangeEvent> hook) {
if (hook != null) {
this.pageChangeHooks.add(hook);
}
return this;
}
@Override
public Inventory createInventory(Player player) {
populatePageElements();
return super.createInventory(player);
}
@Override
public void refresh(Player player) {
populatePageElements();
super.refresh(player);
}
private void populatePageElements() {
// 1. Nettoie les slots d'items paginés
for (int slot : itemSlots) {
super.removeItem(slot);
}
// 2. Remplit les items pour la page actuelle
int pageSize = getPageSize();
int startIndex = (currentPage - 1) * pageSize;
int endIndex = Math.min(startIndex + pageSize, pageItems.size());
for (int i = 0; i < (endIndex - startIndex); i++) {
int slot = itemSlots.get(i);
GuiItem item = pageItems.get(startIndex + i);
super.setItem(slot, item);
}
// 3. Bouton Page Précédente
if (previousPageSlot != null) {
if (hasPreviousPage()) {
GuiItem prevBtn = (customPreviousButton != null) ? customPreviousButton :
ItemBuilder.of(Material.ARROW)
.name("<yellow>◀ Page Précédente</yellow>")
.lore("<gray>Aller à la page " + (currentPage - 1) + "</gray>")
.asGuiItem(ctx -> previousPage());
super.setItem(previousPageSlot, prevBtn);
} else {
super.removeItem(previousPageSlot);
}
}
// 4. Bouton Page Suivante
if (nextPageSlot != null) {
if (hasNextPage()) {
GuiItem nextBtn = (customNextButton != null) ? customNextButton :
ItemBuilder.of(Material.ARROW)
.name("<yellow>Page Suivante ▶</yellow>")
.lore("<gray>Aller à la page " + (currentPage + 1) + "</gray>")
.asGuiItem(ctx -> nextPage());
super.setItem(nextPageSlot, nextBtn);
} else {
super.removeItem(nextPageSlot);
}
}
// 5. Indicateur de Page
if (pageIndicatorSlot != null) {
int total = getTotalPages();
GuiItem indicator = (pageIndicatorSupplier != null) ? pageIndicatorSupplier.apply(currentPage, total) :
ItemBuilder.of(Material.PAPER)
.name("<gold><bold>Page " + currentPage + " / " + total + "</bold></gold>")
.lore("<gray>Total d'éléments : <yellow>" + pageItems.size() + "</yellow></gray>")
.asGuiItem();
super.setItem(pageIndicatorSlot, indicator);
}
}
}
@@ -0,0 +1,382 @@
package fr.luc.bettermcguis.type;
import fr.luc.bettermcguis.api.*;
import fr.luc.bettermcguis.api.slot.SlotRange;
import fr.luc.bettermcguis.holder.BetterGuiHolder;
import fr.luc.bettermcguis.pattern.GuiPattern;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Consumer;
/**
* Implémentation standard et autonome d'un menu d'inventaire interactif (GUI).
*/
public class SimpleGui implements Gui {
protected String title;
protected Component titleComponent;
protected final GuiType type;
protected final Map<Integer, GuiItem> items = new ConcurrentHashMap<>();
protected final Set<Integer> editableSlots = new CopyOnWriteArraySet<>();
protected final Map<String, Object> properties = new ConcurrentHashMap<>();
protected final Set<UUID> viewers = new CopyOnWriteArraySet<>();
// Hooks du cycle de vie
protected final List<Consumer<GuiOpenContext>> openHooks = new CopyOnWriteArrayList<>();
protected final List<Consumer<GuiCloseContext>> closeHooks = new CopyOnWriteArrayList<>();
protected final List<Consumer<GuiClickContext>> clickHooks = new CopyOnWriteArrayList<>();
protected final List<Consumer<GuiClickContext>> outsideClickHooks = new CopyOnWriteArrayList<>();
protected final List<Consumer<GuiClickContext>> bottomClickHooks = new CopyOnWriteArrayList<>();
/**
* Initialise un GUI avec un titre et un type d'inventaire.
*
* @param title Le titre au format MiniMessage.
* @param type Le type de l'inventaire.
*/
public SimpleGui(String title, GuiType type) {
this.type = Objects.requireNonNull(type, "GuiType cannot be null");
setTitle(title != null ? title : "Menu");
}
/**
* Initialise un GUI avec un nombre de lignes de coffre (1 à 6).
*
* @param title Le titre au format MiniMessage.
* @param rows Le nombre de lignes (1 à 6).
*/
public SimpleGui(String title, int rows) {
this(title, GuiType.chest(rows));
}
@Override
public String getTitle() {
return title;
}
@Override
public Component getTitleComponent() {
return titleComponent;
}
@Override
public void setTitle(String miniMessage) {
this.title = miniMessage != null ? miniMessage : "";
this.titleComponent = MiniMessage.miniMessage().deserialize(this.title);
}
@Override
public GuiType getType() {
return type;
}
@Override
public void setItem(int slot, GuiItem item) {
if (slot < 0 || slot >= getSize()) {
return;
}
if (item == null) {
items.remove(slot);
} else {
items.put(slot, item);
}
// Met à jour visuellement les inventaires ouverts pour les visualisateurs
for (Player viewer : getViewers()) {
if (viewer.getOpenInventory().getTopInventory().getHolder() instanceof BetterGuiHolder holder
&& holder.getGui() == this) {
Inventory top = viewer.getOpenInventory().getTopInventory();
ItemStack stack = (item != null && item.isVisibleFor(viewer)) ? item.getItemStack() : null;
top.setItem(slot, stack);
}
}
}
@Override
public GuiItem getItem(int slot) {
return items.get(slot);
}
@Override
public void removeItem(int slot) {
setItem(slot, null);
}
@Override
public void clear() {
items.clear();
for (Player viewer : getViewers()) {
refresh(viewer);
}
}
@Override
public void fill(GuiItem item) {
for (int i = 0; i < getSize(); i++) {
setItem(i, item);
}
}
@Override
public void fillBorder(GuiItem item) {
fillRange(SlotRange.border(getRows(), getColumns()), item);
}
@Override
public void fillRange(SlotRange range, GuiItem item) {
if (range != null) {
for (int slot : range.getSlots()) {
setItem(slot, item);
}
}
}
@Override
public void applyPattern(GuiPattern pattern) {
if (pattern != null) {
pattern.apply(this);
}
}
@Override
public void open(Player player) {
Objects.requireNonNull(player, "player cannot be null");
Inventory inventory = createInventory(player);
GuiOpenContext openContext = new GuiOpenContext(player, this, null);
handleOpen(openContext);
if (!openContext.isCancelled()) {
player.openInventory(inventory);
viewers.add(player.getUniqueId());
}
}
@Override
public void close(Player player) {
if (player != null) {
viewers.remove(player.getUniqueId());
player.closeInventory();
}
}
@Override
public void refresh(Player player) {
if (player == null || !player.isOnline()) return;
if (player.getOpenInventory().getTopInventory().getHolder() instanceof BetterGuiHolder holder
&& holder.getGui() == this) {
Inventory inv = player.getOpenInventory().getTopInventory();
for (int i = 0; i < getSize(); i++) {
if (editableSlots.contains(i)) {
continue;
}
GuiItem item = items.get(i);
if (item != null && item.isVisibleFor(player)) {
inv.setItem(i, item.getItemStack());
} else {
inv.setItem(i, null);
}
}
player.updateInventory();
}
}
@Override
public void refreshAll() {
for (Player viewer : getViewers()) {
refresh(viewer);
}
}
@Override
public Set<Player> getViewers() {
Set<Player> playerSet = new HashSet<>();
for (UUID uuid : viewers) {
Player p = Bukkit.getPlayer(uuid);
if (p != null && p.isOnline()) {
playerSet.add(p);
} else {
viewers.remove(uuid);
}
}
return Collections.unmodifiableSet(playerSet);
}
@Override
public Inventory createInventory(Player player) {
BetterGuiHolder holder = new BetterGuiHolder(this, player);
Inventory inventory;
if (type.getBukkitType() == org.bukkit.event.inventory.InventoryType.CHEST) {
inventory = Bukkit.createInventory(holder, getSize(), titleComponent);
} else {
inventory = Bukkit.createInventory(holder, type.getBukkitType(), titleComponent);
}
holder.setInventory(inventory);
// Place les items
for (Map.Entry<Integer, GuiItem> entry : items.entrySet()) {
int slot = entry.getKey();
GuiItem item = entry.getValue();
if (slot >= 0 && slot < getSize() && item != null) {
if (player == null || item.isVisibleFor(player)) {
inventory.setItem(slot, item.getItemStack());
}
}
}
return inventory;
}
@Override
public void setEditable(int slot, boolean editable) {
if (editable) {
editableSlots.add(slot);
} else {
editableSlots.remove(slot);
}
}
@Override
public boolean isEditable(int slot) {
return editableSlots.contains(slot);
}
@Override
public Set<Integer> getEditableSlots() {
return Collections.unmodifiableSet(editableSlots);
}
@Override
public void setProperty(String key, Object value) {
if (value == null) {
properties.remove(key);
} else {
properties.put(key, value);
}
}
@SuppressWarnings("unchecked")
@Override
public <T> T getProperty(String key, Class<T> type) {
Object val = properties.get(key);
if (val != null && type.isInstance(val)) {
return (T) val;
}
return null;
}
@SuppressWarnings("unchecked")
@Override
public <T> T getProperty(String key, T defaultValue) {
Object val = properties.get(key);
if (val != null && defaultValue != null && defaultValue.getClass().isInstance(val)) {
return (T) val;
}
return defaultValue;
}
@Override
public Map<String, Object> getProperties() {
return Collections.unmodifiableMap(properties);
}
@Override
public Gui onOpen(Consumer<GuiOpenContext> hook) {
this.openHooks.add(Objects.requireNonNull(hook, "hook cannot be null"));
return this;
}
@Override
public Gui onClose(Consumer<GuiCloseContext> hook) {
this.closeHooks.add(Objects.requireNonNull(hook, "hook cannot be null"));
return this;
}
@Override
public Gui onClick(Consumer<GuiClickContext> hook) {
this.clickHooks.add(Objects.requireNonNull(hook, "hook cannot be null"));
return this;
}
@Override
public Gui onOutsideClick(Consumer<GuiClickContext> hook) {
this.outsideClickHooks.add(Objects.requireNonNull(hook, "hook cannot be null"));
return this;
}
@Override
public Gui onBottomClick(Consumer<GuiClickContext> hook) {
this.bottomClickHooks.add(Objects.requireNonNull(hook, "hook cannot be null"));
return this;
}
@Override
public void handleOpen(GuiOpenContext context) {
for (Consumer<GuiOpenContext> hook : openHooks) {
try {
hook.accept(context);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void handleClose(GuiCloseContext context) {
if (context.getPlayer() != null) {
viewers.remove(context.getPlayer().getUniqueId());
}
for (Consumer<GuiCloseContext> hook : closeHooks) {
try {
hook.accept(context);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void handleClick(GuiClickContext context) {
for (Consumer<GuiClickContext> hook : clickHooks) {
try {
hook.accept(context);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void handleOutsideClick(GuiClickContext context) {
for (Consumer<GuiClickContext> hook : outsideClickHooks) {
try {
hook.accept(context);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void handleBottomClick(GuiClickContext context) {
for (Consumer<GuiClickContext> hook : bottomClickHooks) {
try {
hook.accept(context);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
@@ -0,0 +1,106 @@
package fr.luc.bettermcguis.type;
import fr.luc.bettermcguis.api.GuiType;
import fr.luc.bettermcguis.api.slot.SlotRange;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import java.util.*;
import java.util.function.BiConsumer;
/**
* Implémentation d'un GUI de stockage / zone de dépôt (Storage GUI).
* Permet aux joueurs de déposer, retirer ou modifier librement des items dans des slots spécifiés (ex: poubelle, coffre d'échange, enclume).
*/
public class StorageGui extends SimpleGui {
private final Set<Integer> storageSlots = new HashSet<>();
private BiConsumer<Player, ItemStack> itemDepositHook;
private BiConsumer<Player, ItemStack> itemWithdrawHook;
private boolean returnItemsOnClose = true;
public StorageGui(String title, GuiType type) {
super(title, type);
}
public StorageGui(String title, int rows) {
super(title, rows);
}
/**
* Définit les slots utilisables comme zone de stockage modifiable.
*
* @param slots Les slots éditables.
* @return Ce GUI pour chaînage.
*/
public StorageGui setStorageSlots(Collection<Integer> slots) {
if (slots != null) {
for (int s : slots) {
this.storageSlots.add(s);
super.setEditable(s, true);
}
}
return this;
}
/**
* Définit les slots de stockage via une plage {@link SlotRange}.
*/
public StorageGui setStorageSlots(SlotRange range) {
if (range != null) {
setStorageSlots(range.getSlots());
}
return this;
}
/**
* Définit si les items laissés dans la zone de stockage doivent être restitués au joueur lors de la fermeture.
*
* @param returnItems true pour restituer dans l'inventaire du joueur (ou au sol si plein).
*/
public StorageGui returnItemsOnClose(boolean returnItems) {
this.returnItemsOnClose = returnItems;
return this;
}
/**
* Récupère tous les items actuellement présents dans les slots de stockage.
*
* @param inventory L'inventaire Bukkit ouvert.
* @return La liste des {@link ItemStack} présents.
*/
public List<ItemStack> getStoredItems(Inventory inventory) {
List<ItemStack> list = new ArrayList<>();
if (inventory != null) {
for (int slot : storageSlots) {
ItemStack item = inventory.getItem(slot);
if (item != null && !item.getType().isAir()) {
list.add(item);
}
}
}
return list;
}
@Override
public void handleClose(fr.luc.bettermcguis.api.GuiCloseContext context) {
Player player = context.getPlayer();
Inventory inv = context.getRawEvent() != null ? context.getRawEvent().getInventory() : null;
if (returnItemsOnClose && player != null && inv != null) {
for (int slot : storageSlots) {
ItemStack item = inv.getItem(slot);
if (item != null && !item.getType().isAir()) {
HashMap<Integer, ItemStack> leftover = player.getInventory().addItem(item);
for (ItemStack drop : leftover.values()) {
player.getWorld().dropItemNaturally(player.getLocation(), drop);
}
inv.setItem(slot, null);
}
}
}
super.handleClose(context);
}
}
@@ -0,0 +1,144 @@
package fr.luc.bettermcguis.type;
import fr.luc.bettermcguis.api.GuiItem;
import fr.luc.bettermcguis.api.GuiType;
import fr.luc.bettermcguis.pattern.GuiPattern;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import java.util.*;
/**
* Implémentation d'un menu à onglets (Tabbed GUI) permettant de basculer instantanément
* entre plusieurs vues / catégories sans avoir à fermer ou rouvrir l'inventaire du joueur.
*/
public class TabbedGui extends SimpleGui {
private final Map<String, Map<Integer, GuiItem>> tabItems = new LinkedHashMap<>();
private final Map<String, Integer> tabButtonSlots = new LinkedHashMap<>();
private final Map<String, GuiItem> tabButtonItems = new LinkedHashMap<>();
private String activeTabId;
/**
* Initialise un GUI à onglets.
*/
public TabbedGui(String title, GuiType type) {
super(title, type);
}
/**
* Initialise un GUI à onglets avec un nombre de lignes de coffre.
*/
public TabbedGui(String title, int rows) {
super(title, rows);
}
/**
* Enregistre un nouvel onglet avec son bouton d'accès.
*
* @param tabId L'identifiant unique de l'onglet (ex: "shop", "quetes", "profil").
* @param buttonSlot Le slot sur lequel sera placé le bouton de navigation.
* @param buttonItem L'item du bouton représentant l'onglet.
* @return Cette instance de {@link TabbedGui} pour chaînage.
*/
public TabbedGui addTab(String tabId, int buttonSlot, GuiItem buttonItem) {
Objects.requireNonNull(tabId, "tabId cannot be null");
this.tabItems.putIfAbsent(tabId, new HashMap<>());
if (buttonSlot >= 0) {
this.tabButtonSlots.put(tabId, buttonSlot);
this.tabButtonItems.put(tabId, buttonItem);
// Attache l'action de sélection d'onglet
GuiItem itemWithAction = (buttonItem != null) ? buttonItem : GuiItem.empty();
super.setItem(buttonSlot, itemWithAction.onClick(ctx -> selectTab(tabId)));
}
if (this.activeTabId == null) {
this.activeTabId = tabId;
}
return this;
}
/**
* Définit un item pour un onglet spécifique.
*
* @param tabId L'identifiant de l'onglet.
* @param slot Le slot cible.
* @param item L'item à placer.
*/
public void setTabItem(String tabId, int slot, GuiItem item) {
Map<Integer, GuiItem> map = tabItems.computeIfAbsent(tabId, k -> new HashMap<>());
if (item == null) {
map.remove(slot);
} else {
map.put(slot, item);
}
if (tabId.equalsIgnoreCase(activeTabId)) {
super.setItem(slot, item);
}
}
/**
* Applique un motif ASCII pour un onglet spécifique.
*
* @param tabId L'identifiant de l'onglet.
* @param pattern Le motif.
*/
public void applyTabPattern(String tabId, GuiPattern pattern) {
if (pattern != null) {
int cols = getColumns();
int maxRows = getRows();
List<String> rows = pattern.getRows();
Map<Character, GuiItem> bindings = pattern.getItemBindings();
for (int r = 0; r < Math.min(rows.size(), maxRows); r++) {
String line = rows.get(r);
for (int c = 0; c < Math.min(line.length(), cols); c++) {
char ch = line.charAt(c);
if (bindings.containsKey(ch)) {
setTabItem(tabId, r * cols + c, bindings.get(ch));
}
}
}
}
}
/**
* Sélectionne et affiche l'onglet demandé pour tous les visualisateurs.
*
* @param tabId L'identifiant de l'onglet à afficher.
* @return true si l'onglet existe et a été sélectionné.
*/
public boolean selectTab(String tabId) {
if (!tabItems.containsKey(tabId) || tabId.equalsIgnoreCase(activeTabId)) {
return false;
}
this.activeTabId = tabId;
applyActiveTabItems();
refreshAll();
return true;
}
/**
* @return L'identifiant de l'onglet actuellement affiché.
*/
public String getActiveTabId() {
return activeTabId;
}
private void applyActiveTabItems() {
if (activeTabId == null) return;
Map<Integer, GuiItem> currentItems = tabItems.get(activeTabId);
if (currentItems != null) {
for (Map.Entry<Integer, GuiItem> entry : currentItems.entrySet()) {
super.setItem(entry.getKey(), entry.getValue());
}
}
}
@Override
public Inventory createInventory(Player player) {
applyActiveTabItems();
return super.createInventory(player);
}
}
@@ -0,0 +1,104 @@
package fr.luc.bettermcguis;
import fr.luc.bettermcguis.api.GuiItem;
import fr.luc.bettermcguis.api.GuiType;
import fr.luc.bettermcguis.api.slot.SlotPos;
import fr.luc.bettermcguis.api.slot.SlotRange;
import fr.luc.bettermcguis.pattern.GuiMask;
import fr.luc.bettermcguis.pattern.GuiPattern;
import fr.luc.bettermcguis.type.SimpleGui;
import org.bukkit.Material;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class GuiLayoutAndPatternTest {
@Test
@DisplayName("SlotPos doit convertir correctement (row, col) en slot absolu et inversement")
void testSlotPosConversions() {
SlotPos pos = SlotPos.of(1, 4); // Ligne 1, Col 4 dans un coffre de 9 cols -> slot 13
assertEquals(13, pos.toSlot(9));
SlotPos fromSlot = SlotPos.fromSlot(13, 9);
assertEquals(1, fromSlot.getRow());
assertEquals(4, fromSlot.getColumn());
}
@Test
@DisplayName("SlotRange.border doit calculer les bordures exactes d'un coffre 3 lignes")
void testSlotRangeBorder() {
SlotRange border = SlotRange.border(3, 9);
List<Integer> slots = border.getSlots();
// Total slots = 27. Bordures = 9 (ligne 0) + 9 (ligne 2) + 2 (col 0 et col 8 de ligne 1) = 20
assertEquals(20, slots.size());
assertTrue(slots.contains(0));
assertTrue(slots.contains(8));
assertTrue(slots.contains(9));
assertTrue(slots.contains(17));
assertTrue(slots.contains(18));
assertTrue(slots.contains(26));
assertFalse(slots.contains(13)); // Le centre (1, 4) n'est pas une bordure
}
@Test
@DisplayName("SlotRange.interior doit calculer l'intérieur d'un coffre 3 lignes")
void testSlotRangeInterior() {
SlotRange interior = SlotRange.interior(3, 9);
List<Integer> slots = interior.getSlots();
// 7 slots intérieurs (10 à 16)
assertEquals(7, slots.size());
assertTrue(slots.contains(10));
assertTrue(slots.contains(13));
assertTrue(slots.contains(16));
assertFalse(slots.contains(9));
assertFalse(slots.contains(17));
}
@Test
@DisplayName("GuiPattern doit appliquer les items selon les caractères de la matrice")
void testGuiPatternApplication() {
SimpleGui gui = new SimpleGui("Pattern Test", GuiType.CHEST_3_ROWS);
GuiItem glass = GuiItem.of(Material.BLACK_STAINED_GLASS_PANE);
GuiItem diamond = GuiItem.of(Material.DIAMOND);
GuiPattern pattern = GuiPattern.of(
"#########",
"# D #",
"#########"
)
.bind('#', glass)
.bind('D', diamond);
gui.applyPattern(pattern);
assertEquals(glass, gui.getItem(0));
assertEquals(glass, gui.getItem(8));
assertEquals(diamond, gui.getItem(13)); // Ligne 1, Col 4
assertNull(gui.getItem(10)); // Emplacement vide ' '
}
@Test
@DisplayName("GuiMask doit résoudre les slots actifs marqués par '1'")
void testGuiMaskResolution() {
GuiMask mask = new GuiMask(
"100000001",
"000010000",
"100000001"
);
List<Integer> activeSlots = mask.resolveSlots(3, 9);
assertEquals(5, activeSlots.size());
assertTrue(activeSlots.contains(0));
assertTrue(activeSlots.contains(8));
assertTrue(activeSlots.contains(13));
assertTrue(activeSlots.contains(18));
assertTrue(activeSlots.contains(26));
}
}
@@ -0,0 +1,79 @@
package fr.luc.bettermcguis;
import fr.luc.bettermcguis.api.GuiClickContext;
import fr.luc.bettermcguis.api.GuiItem;
import fr.luc.bettermcguis.api.GuiType;
import fr.luc.bettermcguis.event.GuiClickEvent;
import fr.luc.bettermcguis.event.GuiEventManager;
import fr.luc.bettermcguis.event.annotation.GuiEventHandler;
import fr.luc.bettermcguis.type.SimpleGui;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryAction;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.*;
public class GuiLifecycleAndEventTest {
@Test
@DisplayName("GuiItem doit exécuter son callback de clic et annuler l'action par défaut")
void testGuiItemClickHandling() {
AtomicBoolean itemActionRan = new AtomicBoolean(false);
GuiItem item = GuiItem.of(Material.COMPASS)
.onClick(ctx -> itemActionRan.set(true));
Player player = Mockito.mock(Player.class);
SimpleGui gui = new SimpleGui("Test", GuiType.CHEST_1_ROW);
GuiClickContext ctx = new GuiClickContext(
player, gui, item, 4, null,
ClickType.LEFT, InventoryAction.PICKUP_ALL,
null, null, null
);
item.handleClick(ctx);
assertTrue(itemActionRan.get());
assertTrue(ctx.isCancelled()); // Annulé par défaut
}
@Test
@DisplayName("GuiEventManager doit trier les écouteurs par priorité et distribuer les événements")
void testEventManagerPriorityAndDispatch() {
GuiEventManager eventManager = new GuiEventManager();
List<String> executionOrder = new ArrayList<>();
Object listener = new Object() {
@GuiEventHandler(priority = 5)
public void onLowPriorityClick(GuiClickEvent event) {
executionOrder.add("low");
}
@GuiEventHandler(priority = 100)
public void onHighPriorityClick(GuiClickEvent event) {
executionOrder.add("high");
}
};
eventManager.registerListeners(listener);
Player player = Mockito.mock(Player.class);
SimpleGui gui = new SimpleGui("Priority Test", GuiType.CHEST_1_ROW);
GuiClickContext ctx = new GuiClickContext(player, gui, null, 0, null, ClickType.LEFT, InventoryAction.NOTHING, null, null, null);
eventManager.dispatch(new GuiClickEvent(ctx));
assertEquals(2, executionOrder.size());
assertEquals("high", executionOrder.get(0));
assertEquals("low", executionOrder.get(1));
}
}
@@ -0,0 +1,48 @@
package fr.luc.bettermcguis;
import fr.luc.bettermcguis.api.GuiItem;
import fr.luc.bettermcguis.builder.ItemBuilder;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class ItemBuilderTest {
@Test
@DisplayName("ItemBuilder doit créer un ItemStack valide avec quantité et matériau")
void testBasicItemCreation() {
ItemStack item = ItemBuilder.of(Material.DIAMOND)
.amount(16)
.build();
assertNotNull(item);
assertEquals(Material.DIAMOND, item.getType());
assertEquals(16, item.getAmount());
}
@Test
@DisplayName("ItemBuilder doit se convertir en GuiItem interactif")
void testAsGuiItem() {
boolean[] clicked = new boolean[]{false};
GuiItem guiItem = ItemBuilder.of(Material.EMERALD)
.name("<green>Test</green>")
.asGuiItem(ctx -> clicked[0] = true);
assertNotNull(guiItem);
assertEquals(Material.EMERALD, guiItem.getItemStack().getType());
assertNotNull(guiItem.getClickAction());
}
@Test
@DisplayName("ItemBuilder.filler doit créer un item avec un nom vide")
void testFillerItem() {
ItemBuilder builder = ItemBuilder.filler(Material.BLACK_STAINED_GLASS_PANE);
assertNotNull(builder);
ItemStack stack = builder.build();
assertEquals(Material.BLACK_STAINED_GLASS_PANE, stack.getType());
}
}
@@ -0,0 +1,69 @@
package fr.luc.bettermcguis;
import fr.luc.bettermcguis.api.GuiItem;
import fr.luc.bettermcguis.api.GuiType;
import fr.luc.bettermcguis.api.slot.SlotRange;
import fr.luc.bettermcguis.type.PaginatedGui;
import org.bukkit.Material;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
public class PaginatedGuiTest {
@Test
@DisplayName("PaginatedGui doit calculer correctement le nombre total de pages")
void testPaginationMath() {
PaginatedGui gui = new PaginatedGui("Paginated Test", GuiType.CHEST_3_ROWS);
gui.setItemSlots(SlotRange.of(10, 16)); // 7 slots par page
assertEquals(7, gui.getPageSize());
assertEquals(1, gui.getTotalPages()); // 0 items -> 1 page vide
for (int i = 0; i < 20; i++) {
gui.addPageItem(GuiItem.of(Material.GOLD_INGOT));
}
// 20 items / 7 par page -> 3 pages (7 + 7 + 6)
assertEquals(3, gui.getTotalPages());
assertEquals(1, gui.getCurrentPage());
assertTrue(gui.hasNextPage());
assertFalse(gui.hasPreviousPage());
}
@Test
@DisplayName("PaginatedGui doit naviguer entre les pages et déclencher l'événement")
void testPageNavigationAndEvents() {
PaginatedGui gui = new PaginatedGui("Paginated Test", GuiType.CHEST_3_ROWS);
gui.setItemSlots(SlotRange.of(10, 16));
for (int i = 0; i < 20; i++) {
gui.addPageItem(GuiItem.of(Material.EMERALD));
}
AtomicInteger eventFired = new AtomicInteger(0);
gui.onPageChange(evt -> {
eventFired.incrementAndGet();
assertEquals(1, evt.getOldPage());
assertEquals(2, evt.getNewPage());
});
assertTrue(gui.nextPage());
assertEquals(2, gui.getCurrentPage());
assertEquals(1, eventFired.get());
assertTrue(gui.hasPreviousPage());
assertTrue(gui.hasNextPage());
assertTrue(gui.nextPage());
assertEquals(3, gui.getCurrentPage());
assertFalse(gui.hasNextPage()); // Fin du catalogue
// Impossible d'avancer plus loin
assertFalse(gui.nextPage());
assertEquals(3, gui.getCurrentPage());
}
}