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:
+17
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -1,3 +1,359 @@
|
||||
# betterMcGuis
|
||||
# 🖼️ betterMcGuis
|
||||
|
||||
Bibliotheque Java moderne, fluide et evenementielle pour la creation dynamique de GUIs et menus interactifs Minecraft Paper/Spigot.
|
||||
[](https://www.oracle.com/java/)
|
||||
[](https://papermc.io/)
|
||||
[](https://docs.advntr.dev/)
|
||||
[](#)
|
||||
|
||||
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).
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -0,0 +1,2 @@
|
||||
org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8
|
||||
systemProp.file.encoding=UTF-8
|
||||
Vendored
BIN
Binary file not shown.
+9
@@ -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
|
||||
@@ -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
@@ -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%
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user