From 806d1789a1ade4d6da5408d6e35a52a6dddb6f70 Mon Sep 17 00:00:00 2001 From: GeminiAntigravityCLI Date: Sun, 23 Aug 2026 18:10:49 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20initialisation=20compl=C3=A8te=20de=20l?= =?UTF-8?q?a=20biblioth=C3=A8que=20betterMcGuis=20(Fluent=20DSL,=20ItemBui?= =?UTF-8?q?lder,=20PaginatedGui,=20TabbedGui,=20AnimatedGui,=20Patterns=20?= =?UTF-8?q?ASCII,=20Moteur=20d'=C3=A9v=C3=A9nements,=20S=C3=A9curit=C3=A9?= =?UTF-8?q?=20anti-glitch=20et=20documentation)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 17 + GEMINI.md | 23 ++ LICENSE | 21 + README.md | 360 ++++++++++++++++- build.gradle.kts | 77 ++++ docs/01_GETTING_STARTED.md | 89 ++++ docs/02_GUI_BUILDER_DSL.md | 94 +++++ docs/03_ITEM_BUILDER_AND_ITEMS.md | 111 +++++ docs/04_PATTERNS_AND_MASKS.md | 82 ++++ docs/05_PAGINATED_AND_ADVANCED_GUIS.md | 89 ++++ docs/06_LIFECYCLE_AND_EVENTS.md | 91 +++++ docs/07_STORAGE_AND_EDITABLE_SLOTS.md | 59 +++ gradle.properties | 2 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48462 bytes gradle/wrapper/gradle-wrapper.properties | 9 + gradlew | 248 ++++++++++++ gradlew.bat | 82 ++++ settings.gradle.kts | 1 + .../fr/luc/bettermcguis/BetterMcGuis.java | 167 ++++++++ .../fr/luc/bettermcguis/animation/Frame.java | 67 +++ .../java/fr/luc/bettermcguis/api/Gui.java | 277 +++++++++++++ .../luc/bettermcguis/api/GuiClickAction.java | 15 + .../luc/bettermcguis/api/GuiClickContext.java | 229 +++++++++++ .../luc/bettermcguis/api/GuiCloseContext.java | 34 ++ .../java/fr/luc/bettermcguis/api/GuiItem.java | 228 +++++++++++ .../luc/bettermcguis/api/GuiOpenContext.java | 46 +++ .../java/fr/luc/bettermcguis/api/GuiType.java | 81 ++++ .../fr/luc/bettermcguis/api/slot/SlotPos.java | 92 +++++ .../luc/bettermcguis/api/slot/SlotRange.java | 97 +++++ .../builder/AnimatedGuiBuilder.java | 96 +++++ .../luc/bettermcguis/builder/GuiBuilder.java | 238 +++++++++++ .../luc/bettermcguis/builder/ItemBuilder.java | 369 +++++++++++++++++ .../builder/PaginatedGuiBuilder.java | 199 +++++++++ .../bettermcguis/builder/PatternBuilder.java | 75 ++++ .../builder/TabbedGuiBuilder.java | 95 +++++ .../luc/bettermcguis/demo/DemoGuiPlugin.java | 53 +++ .../demo/guis/DemoMainMenuGui.java | 65 +++ .../demo/guis/DemoPaginatedShopGui.java | 58 +++ .../demo/guis/DemoStorageTrashGui.java | 46 +++ .../demo/listeners/DemoGuiListener.java | 36 ++ .../event/CancellableGuiEvent.java | 32 ++ .../luc/bettermcguis/event/GuiClickEvent.java | 27 ++ .../luc/bettermcguis/event/GuiCloseEvent.java | 22 + .../fr/luc/bettermcguis/event/GuiEvent.java | 34 ++ .../bettermcguis/event/GuiEventListener.java | 17 + .../bettermcguis/event/GuiEventManager.java | 112 +++++ .../luc/bettermcguis/event/GuiOpenEvent.java | 22 + .../event/GuiPageChangeEvent.java | 27 ++ .../bettermcguis/event/GuiRefreshEvent.java | 14 + .../event/annotation/GuiEventHandler.java | 24 ++ .../bettermcguis/holder/BetterGuiHolder.java | 59 +++ .../listener/BukkitGuiEventListener.java | 162 ++++++++ .../fr/luc/bettermcguis/pattern/GuiMask.java | 55 +++ .../luc/bettermcguis/pattern/GuiPattern.java | 108 +++++ .../fr/luc/bettermcguis/type/AnimatedGui.java | 122 ++++++ .../luc/bettermcguis/type/PaginatedGui.java | 348 ++++++++++++++++ .../fr/luc/bettermcguis/type/SimpleGui.java | 382 ++++++++++++++++++ .../fr/luc/bettermcguis/type/StorageGui.java | 106 +++++ .../fr/luc/bettermcguis/type/TabbedGui.java | 144 +++++++ .../bettermcguis/GuiLayoutAndPatternTest.java | 104 +++++ .../GuiLifecycleAndEventTest.java | 79 ++++ .../fr/luc/bettermcguis/ItemBuilderTest.java | 48 +++ .../fr/luc/bettermcguis/PaginatedGuiTest.java | 69 ++++ 63 files changed, 6233 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 GEMINI.md create mode 100644 LICENSE create mode 100644 build.gradle.kts create mode 100644 docs/01_GETTING_STARTED.md create mode 100644 docs/02_GUI_BUILDER_DSL.md create mode 100644 docs/03_ITEM_BUILDER_AND_ITEMS.md create mode 100644 docs/04_PATTERNS_AND_MASKS.md create mode 100644 docs/05_PAGINATED_AND_ADVANCED_GUIS.md create mode 100644 docs/06_LIFECYCLE_AND_EVENTS.md create mode 100644 docs/07_STORAGE_AND_EDITABLE_SLOTS.md create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts create mode 100644 src/main/java/fr/luc/bettermcguis/BetterMcGuis.java create mode 100644 src/main/java/fr/luc/bettermcguis/animation/Frame.java create mode 100644 src/main/java/fr/luc/bettermcguis/api/Gui.java create mode 100644 src/main/java/fr/luc/bettermcguis/api/GuiClickAction.java create mode 100644 src/main/java/fr/luc/bettermcguis/api/GuiClickContext.java create mode 100644 src/main/java/fr/luc/bettermcguis/api/GuiCloseContext.java create mode 100644 src/main/java/fr/luc/bettermcguis/api/GuiItem.java create mode 100644 src/main/java/fr/luc/bettermcguis/api/GuiOpenContext.java create mode 100644 src/main/java/fr/luc/bettermcguis/api/GuiType.java create mode 100644 src/main/java/fr/luc/bettermcguis/api/slot/SlotPos.java create mode 100644 src/main/java/fr/luc/bettermcguis/api/slot/SlotRange.java create mode 100644 src/main/java/fr/luc/bettermcguis/builder/AnimatedGuiBuilder.java create mode 100644 src/main/java/fr/luc/bettermcguis/builder/GuiBuilder.java create mode 100644 src/main/java/fr/luc/bettermcguis/builder/ItemBuilder.java create mode 100644 src/main/java/fr/luc/bettermcguis/builder/PaginatedGuiBuilder.java create mode 100644 src/main/java/fr/luc/bettermcguis/builder/PatternBuilder.java create mode 100644 src/main/java/fr/luc/bettermcguis/builder/TabbedGuiBuilder.java create mode 100644 src/main/java/fr/luc/bettermcguis/demo/DemoGuiPlugin.java create mode 100644 src/main/java/fr/luc/bettermcguis/demo/guis/DemoMainMenuGui.java create mode 100644 src/main/java/fr/luc/bettermcguis/demo/guis/DemoPaginatedShopGui.java create mode 100644 src/main/java/fr/luc/bettermcguis/demo/guis/DemoStorageTrashGui.java create mode 100644 src/main/java/fr/luc/bettermcguis/demo/listeners/DemoGuiListener.java create mode 100644 src/main/java/fr/luc/bettermcguis/event/CancellableGuiEvent.java create mode 100644 src/main/java/fr/luc/bettermcguis/event/GuiClickEvent.java create mode 100644 src/main/java/fr/luc/bettermcguis/event/GuiCloseEvent.java create mode 100644 src/main/java/fr/luc/bettermcguis/event/GuiEvent.java create mode 100644 src/main/java/fr/luc/bettermcguis/event/GuiEventListener.java create mode 100644 src/main/java/fr/luc/bettermcguis/event/GuiEventManager.java create mode 100644 src/main/java/fr/luc/bettermcguis/event/GuiOpenEvent.java create mode 100644 src/main/java/fr/luc/bettermcguis/event/GuiPageChangeEvent.java create mode 100644 src/main/java/fr/luc/bettermcguis/event/GuiRefreshEvent.java create mode 100644 src/main/java/fr/luc/bettermcguis/event/annotation/GuiEventHandler.java create mode 100644 src/main/java/fr/luc/bettermcguis/holder/BetterGuiHolder.java create mode 100644 src/main/java/fr/luc/bettermcguis/listener/BukkitGuiEventListener.java create mode 100644 src/main/java/fr/luc/bettermcguis/pattern/GuiMask.java create mode 100644 src/main/java/fr/luc/bettermcguis/pattern/GuiPattern.java create mode 100644 src/main/java/fr/luc/bettermcguis/type/AnimatedGui.java create mode 100644 src/main/java/fr/luc/bettermcguis/type/PaginatedGui.java create mode 100644 src/main/java/fr/luc/bettermcguis/type/SimpleGui.java create mode 100644 src/main/java/fr/luc/bettermcguis/type/StorageGui.java create mode 100644 src/main/java/fr/luc/bettermcguis/type/TabbedGui.java create mode 100644 src/test/java/fr/luc/bettermcguis/GuiLayoutAndPatternTest.java create mode 100644 src/test/java/fr/luc/bettermcguis/GuiLifecycleAndEventTest.java create mode 100644 src/test/java/fr/luc/bettermcguis/ItemBuilderTest.java create mode 100644 src/test/java/fr/luc/bettermcguis/PaginatedGuiTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed35b23 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..98087d6 --- /dev/null +++ b/GEMINI.md @@ -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. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..16b160c --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md index efc1051..35f109d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,359 @@ -# betterMcGuis +# đŸ–Œïž betterMcGuis -Bibliotheque Java moderne, fluide et evenementielle pour la creation dynamique de GUIs et menus interactifs Minecraft Paper/Spigot. \ No newline at end of file +[![Java](https://img.shields.io/badge/Java-17%2B%20%2F%2021-orange.svg)](https://www.oracle.com/java/) +[![Paper](https://img.shields.io/badge/Paper-1.20.4%2B-blue.svg)](https://papermc.io/) +[![Kyori Adventure](https://img.shields.io/badge/Adventure-MiniMessage-purple.svg)](https://docs.advntr.dev/) +[![License](https://img.shields.io/badge/License-MIT-green.svg)](#) + +Une bibliothĂšque Java moderne, fluide et Ă©vĂ©nementielle permettant de crĂ©er, animer et gĂ©rer des interfaces graphiques (GUIs / Menus interactifs / Inventaires) sur serveurs Minecraft Paper et Spigot (1.20.4+). + +--- + +## 📑 Sommaire +- [📚 Guides DĂ©taillĂ©s (Dossier `/docs`)](#-guides-dĂ©taillĂ©s-dossier-docs) +- [🌟 FonctionnalitĂ©s](#-fonctionnalitĂ©s) +- [đŸ—ïž Architecture & Flux d'Interactions](#-architecture--flux-dinteractions) +- [📩 Installation & Configuration](#-installation--configuration) +- [🚀 Guide de DĂ©marrage Rapide](#-guide-de-dĂ©marrage-rapide) +- [💡 Exemples de Menus](#-exemples-de-menus) + - [1. Menu Principal Standard (`GuiBuilder`)](#1-menu-principal-standard-guibuilder) + - [2. Menu PaginĂ© Dynamique (`PaginatedGui`)](#2-menu-paginĂ©-dynamique-paginatedgui) + - [3. Motifs ASCII & Grilles de Disposition (`GuiPattern`)](#3-motifs-ascii--grilles-de-disposition-guipattern) + - [4. Menus Ă  Onglets sans RĂ©ouverture (`TabbedGui`)](#4-menus-Ă -onglets-sans-rĂ©ouverture-tabbedgui) + - [5. Zones de DĂ©pĂŽt / Poubelle (`StorageGui`)](#5-zones-de-dĂ©pĂŽt--poubelle-storagegui) +- [💎 Construction d'Items (`ItemBuilder`)](#-construction-ditems-itembuilder) +- [🎯 SystĂšme d'ÉvĂ©nements & Écouteurs AnnotĂ©s](#-systĂšme-dĂ©vĂ©nements--Ă©couteurs-annotĂ©s) +- [📂 Organisation des Packages](#-organisation-des-packages) + +--- + +## 📚 Guides DĂ©taillĂ©s (Dossier `/docs`) + +Pour une exploration approfondie de chaque composant, consultez la documentation modulaire : +* 📘 [**01. Guide de DĂ©marrage Rapide**](docs/01_GETTING_STARTED.md) : Installation Gradle/Maven, initialisation dans `JavaPlugin` et cycle de vie. +* đŸ—ïž [**02. Fluent GUI Builder DSL**](docs/02_GUI_BUILDER_DSL.md) : MĂ©thodes de placement de slots, dimensions, bordures, remplissages et propriĂ©tĂ©s dynamiques. +* 💎 [**03. ItemBuilder & GuiItem**](docs/03_ITEM_BUILDER_AND_ITEMS.md) : Formatage MiniMessage, tĂȘtes personnalisĂ©es, armures teintĂ©es, sons, cooldowns et `GuiClickContext`. +* 🎹 [**04. Motifs & Masques ASCII**](docs/04_PATTERNS_AND_MASKS.md) : Conception visuelle de grilles par matrices textuelles (`GuiPattern`, `GuiMask`). +* 📚 [**05. Menus PaginĂ©s, Onglets & Animations**](docs/05_PAGINATED_AND_ADVANCED_GUIS.md) : Pagination automatique, onglets commutables sans rĂ©ouverture et frames animĂ©es. +* 🎯 [**06. Cycle de Vie & ÉvĂ©nements**](docs/06_LIFECYCLE_AND_EVENTS.md) : Hooks locaux (`.onClick()`, `.onClose()`) et Ă©couteurs globaux annotĂ©s `@GuiEventHandler`. +* 📩 [**07. Zones de Stockage & Slots Éditables**](docs/07_STORAGE_AND_EDITABLE_SLOTS.md) : Menus de dĂ©pĂŽts, restitution d'items et sĂ©curitĂ©s anti-glitch. + +--- + +## 🌟 FonctionnalitĂ©s + +- **DSL Fluide & DĂ©claratif** : Construction rapide et lisible (`BetterMcGuis.builder("Titre", 3)`). +- **Formatage Moderne MiniMessage & Adventure** : Prise en charge native des gradients HEX, balises de style et composants texte. +- **Grilles & Motifs ASCII (`GuiPattern`)** : Dessinez vos interfaces directement en texte sous forme de matrice avec associations de caractĂšres. +- **Pagination Intelligente (`PaginatedGui`)** : Calcul automatique des pages, boutons de navigation dynamiques et indicateurs de progression. +- **Onglets Commutables (`TabbedGui`)** : Basculez entre plusieurs vues de menu instantanĂ©ment sans fermer la fenĂȘtre du joueur. +- **Animations CadencĂ©es (`AnimatedGui`)** : Lecture de sĂ©quences de frames temporelles (titres animĂ©s, coffres mystĂšres, carrousels). +- **Protection Anti-Duplication & SĂ©curitĂ©** : Blocage natif et sĂ©curisĂ© des vols d'items, glisser-dĂ©poser illĂ©gaux, shift-clics et raccourcis de hotbar. +- **Moteur d'ÉvĂ©nements Riches** : Bus d'Ă©vĂ©nements global avec prioritĂ©s et annotations `@GuiEventHandler`. + +--- + +## đŸ—ïž Architecture & Flux d'Interactions + +```mermaid +flowchart TD + Player["Joueur en Jeu"] -->|Effectue un Clic / Drag / Fermeture| BukkitEvt["BukkitGuiEventListener (SĂ©curitĂ© & Interception)"] + + BukkitEvt --> HolderCheck{"Inventaire liĂ© Ă  BetterGuiHolder ?"} + HolderCheck -- Non --> Ignore["Ignore l'interaction"] + + HolderCheck -- Oui --> SlotTypeCheck{"Localisation du Clic"} + + SlotTypeCheck -- ExtĂ©rieur (-999) --> OutsideHook["gui.handleOutsideClick() & GuiClickEvent"] + SlotTypeCheck -- Inventaire Joueur --> BottomCheck{"Shift-Clic ou Hotbar Swap vers slot protĂ©gĂ© ?"} + BottomCheck -- Oui --> BlockShift["event.setCancelled(true) & Blocage"] + BottomCheck -- Non --> BottomHook["gui.handleBottomClick()"] + + SlotTypeCheck -- Inventaire SupĂ©rieur (GUI) --> EditCheck{"Le Slot est-il Editable ?"} + EditCheck -- Non (DĂ©faut) --> CancelDef["event.setCancelled(true) (EmpĂȘche le vol)"] + EditCheck -- Oui --> AllowEdit["event.setCancelled(false) (Autorise le dĂ©pĂŽt)"] + + CancelDef --> CoolCheck{"Cooldown Actif sur l'Item ?"} + AllowEdit --> CoolCheck + + CoolCheck -- Oui --> CoolBlock["Interruption silencieuse"] + CoolCheck -- Non --> SoundTrigger["DĂ©clenchement du Son configurĂ©"] + + SoundTrigger --> ItemAction["ExĂ©cution de l'Action MĂ©tier (item.handleClick)"] + ItemAction --> GuiHook["ExĂ©cution du Hook Local (gui.handleClick)"] + GuiHook --> EventBus["Diffusion sur le Bus Global (GuiClickEvent)"] +``` + +--- + +## 📩 Installation & Configuration + +### Gradle (Kotlin DSL) + +```kotlin +repositories { + mavenCentral() + maven("https://repo.papermc.io/repository/maven-public/") +} + +dependencies { + implementation("fr.luc:betterMcGuis:1.0.0-SNAPSHOT") +} +``` + +--- + +## 🚀 Guide de DĂ©marrage Rapide + +### 1. Initialisation dans votre `JavaPlugin` + +```java +package fr.luc.monplugin; + +import fr.luc.bettermcguis.BetterMcGuis; +import org.bukkit.plugin.java.JavaPlugin; + +public class MonPlugin extends JavaPlugin { + + private BetterMcGuis guiManager; + + @Override + public void onEnable() { + // Initialisation de betterMcGuis (enregistre automatiquement les Ă©couteurs Bukkit) + this.guiManager = BetterMcGuis.create(this); + + // Enregistrement d'Ă©couteurs annotĂ©s globaux (optionnel) + this.guiManager.registerListeners(new MonEcouteurDeGui()); + } + + @Override + public void onDisable() { + // Nettoyage complet + if (guiManager != null) { + guiManager.unregisterAll(); + } + } +} +``` + +--- + +## 💡 Exemples de Menus + +### 1. Menu Principal Standard (`GuiBuilder`) + +```java +BetterMcGuis.builder() + .title("Menu Principal") + .rows(3) + .fillBorder(Material.BLACK_STAINED_GLASS_PANE) + + // Bouton central interactif + .item(13, BetterMcGuis.item(Material.NETHER_STAR) + .name("RĂ©compense Quotidienne") + .lore( + "RĂ©cupĂ©rez votre bonus de connexion !", + "", + "▶ Cliquez pour rĂ©clamer" + ) + .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("Boutique du Serveur") + .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("Article #" + id + "") + .lore("Prix : 100 $") + .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("QuĂȘte Facile").asGuiItem()) + .bind('2', BetterMcGuis.item(Material.WRITABLE_BOOK).name("QuĂȘte Moyenne").asGuiItem()) + .bind('3', BetterMcGuis.item(Material.ENCHANTED_BOOK).name("QuĂȘte Difficile").asGuiItem()) + .bind('C', BetterMcGuis.item(Material.BARRIER).name("Fermer").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("Statistiques").asGuiItem(), tab -> { + tab.setTabItem("stats", 22, BetterMcGuis.item(Material.EXPERIENCE_BOTTLE).name("Niveau 42").asGuiItem()); + }) + // Onglet 2 : RĂ©compenses + .tab("rewards", 15, BetterMcGuis.item(Material.CHEST).name("RĂ©compenses").asGuiItem(), tab -> { + tab.setTabItem("rewards", 22, BetterMcGuis.item(Material.GOLD_INGOT).name("Bonus DĂ©bloquĂ©").asGuiItem()); + }) + .build() + .open(player); +``` + +--- + +### 5. Zones de DĂ©pĂŽt / Poubelle (`StorageGui`) + +```java +StorageGui trash = new StorageGui("Poubelle Publique", 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("Casque de Diamant Divin") + .lore( + "ForgĂ© par les anciens dieux.", + "", + "Protection : IV" + ) + .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). \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..000064d --- /dev/null +++ b/build.gradle.kts @@ -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 { + options.encoding = "UTF-8" + options.release.set(17) +} + +tasks.withType { + 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("mavenJava") { + from(components["java"]) + groupId = "fr.luc" + artifactId = "betterMcGuis" + version = "1.0.0-SNAPSHOT" + } + } +} diff --git a/docs/01_GETTING_STARTED.md b/docs/01_GETTING_STARTED.md new file mode 100644 index 0000000..0e123e3 --- /dev/null +++ b/docs/01_GETTING_STARTED.md @@ -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("Menu de Bienvenue") + .rows(3) + .fillBorder(Material.BLACK_STAINED_GLASS_PANE) + .item(13, BetterMcGuis.item(Material.DIAMOND) + .name("RĂ©clamer le Cadeau") + .lore("Cliquez pour recevoir 1 Diamant gratuit !") + .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). diff --git a/docs/02_GUI_BUILDER_DSL.md b/docs/02_GUI_BUILDER_DSL.md new file mode 100644 index 0000000..81ac511 --- /dev/null +++ b/docs/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("Distributeur") + .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("Nouveau Titre")` : 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). diff --git a/docs/03_ITEM_BUILDER_AND_ITEMS.md b/docs/03_ITEM_BUILDER_AND_ITEMS.md new file mode 100644 index 0000000..c7ce91d --- /dev/null +++ b/docs/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("Lame des Enfers") + .lore( + "Une Ă©pĂ©e forgĂ©e dans les profondeurs du Nether.", + "", + "DĂ©gĂąts : +15", + "Effet : Enflamme les cibles" + ) + .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 : +* `Texte` : DĂ©gradĂ©s de couleurs HEX. +* ``, ``, ``, `` : Mises en forme. +* `Texte Arc-en-ciel` : Animation multicolore. +* ``, ``, ``, ``, ``, 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("Profil de Luc") + .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("Confirmer l'achat") + .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("Message")`| 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). diff --git a/docs/04_PATTERNS_AND_MASKS.md b/docs/04_PATTERNS_AND_MASKS.md new file mode 100644 index 0000000..52b1f7c --- /dev/null +++ b/docs/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("Boutique").asGuiItem(this::ouvrirShop)) + .bind('K', ItemBuilder.of(Material.CHEST).name("Kits").asGuiItem(this::ouvrirKits)) + .bind('Q', ItemBuilder.of(Material.BOOK).name("QuĂȘtes").asGuiItem(this::ouvrirQuetes)) + .bind('C', ItemBuilder.of(Material.BARRIER).name("Fermer").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("TrĂ©sor").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). diff --git a/docs/05_PAGINATED_AND_ADVANCED_GUIS.md b/docs/05_PAGINATED_AND_ADVANCED_GUIS.md new file mode 100644 index 0000000..7ef4c66 --- /dev/null +++ b/docs/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("◀ Page PrĂ©cĂ©dente").asGuiItem()) + .nextButton(42, ItemBuilder.of(Material.ARROW).name("Page Suivante ▶").asGuiItem()) + // Indicateur de page personnalisĂ©e + .pageIndicator(40, (page, total) -> ItemBuilder.of(Material.PAPER) + .name("Page " + page + " / " + total + "") + .asGuiItem() + ); + +// Ajout de 100 items au catalogue +for (int i = 1; i <= 100; i++) { + shop.addPageItem(ItemBuilder.of(Material.GOLD_INGOT) + .name("Lingot #" + i + "") + .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("Guerrier").asGuiItem(), tabGui -> { + tabGui.setTabItem("warrior", 20, ItemBuilder.of(Material.DIAMOND_SWORD).name("ÉpĂ©e Lourde").asGuiItem()); + tabGui.setTabItem("warrior", 24, ItemBuilder.of(Material.SHIELD).name("Bouclier").asGuiItem()); + }) + + // Onglet 2 : Archer + .tab("archer", 15, ItemBuilder.of(Material.BOW).name("Archer").asGuiItem(), tabGui -> { + tabGui.setTabItem("archer", 22, ItemBuilder.of(Material.CROSSBOW).name("ArbalĂšte PrĂ©cise").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("Ouverture de Coffre MystĂšre...") + .rows(3) + // Frame 1 + .frame(f -> f.item(13, ItemBuilder.of(Material.CHEST).name("Chargement.").asGuiItem())) + // Frame 2 + .frame(f -> f.item(13, ItemBuilder.of(Material.ENDER_CHEST).name("Chargement..").asGuiItem())) + // Frame 3 : RĂ©vĂ©lation du lot ! + .frame(f -> f.item(13, ItemBuilder.of(Material.NETHER_STAR).name("Bravo ! TrĂ©sor Obtenu !").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). diff --git a/docs/06_LIFECYCLE_AND_EVENTS.md b/docs/06_LIFECYCLE_AND_EVENTS.md new file mode 100644 index 0000000..51a82ae --- /dev/null +++ b/docs/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 = "Boutique PaginĂ©e") + 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). diff --git a/docs/07_STORAGE_AND_EDITABLE_SLOTS.md b/docs/07_STORAGE_AND_EDITABLE_SLOTS.md new file mode 100644 index 0000000..5f2789f --- /dev/null +++ b/docs/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("Poubelle", 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. diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..93f0692 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 +systemProp.file.encoding=UTF-8 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..b1b8ef56b44f16b14dc800fa8103a6d89abb526f GIT binary patch literal 48462 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 R2np~f1o-$>Q}y+){|A@R9n$~+ literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..f94fe18 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..249efbb --- /dev/null +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..a51ec4f --- /dev/null +++ b/gradlew.bat @@ -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% diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..ad27875 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "betterMcGuis" diff --git a/src/main/java/fr/luc/bettermcguis/BetterMcGuis.java b/src/main/java/fr/luc/bettermcguis/BetterMcGuis.java new file mode 100644 index 0000000..c8e30b7 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/BetterMcGuis.java @@ -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(); + } +} diff --git a/src/main/java/fr/luc/bettermcguis/animation/Frame.java b/src/main/java/fr/luc/bettermcguis/animation/Frame.java new file mode 100644 index 0000000..6a17425 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/animation/Frame.java @@ -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 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 getItems() { + return Collections.unmodifiableMap(items); + } + + public String getTitle() { + return title; + } +} diff --git a/src/main/java/fr/luc/bettermcguis/api/Gui.java b/src/main/java/fr/luc/bettermcguis/api/Gui.java new file mode 100644 index 0000000..fb0b4b3 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/api/Gui.java @@ -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 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 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 Le type gĂ©nĂ©rique. + * @return La valeur, ou {@code null}. + */ + T getProperty(String key, Class type); + + /** + * RĂ©cupĂšre une propriĂ©tĂ© ou retourne une valeur par dĂ©faut. + */ + T getProperty(String key, T defaultValue); + + /** + * @return La table complĂšte des propriĂ©tĂ©s du GUI. + */ + Map getProperties(); + + /** + * Attache un Ă©couteur dĂ©clenchĂ© lors de l'ouverture du menu. + */ + Gui onOpen(Consumer hook); + + /** + * Attache un Ă©couteur dĂ©clenchĂ© lors de la fermeture du menu. + */ + Gui onClose(Consumer hook); + + /** + * Attache un Ă©couteur dĂ©clenchĂ© lors de n'importe quel clic dans l'inventaire supĂ©rieur. + */ + Gui onClick(Consumer hook); + + /** + * Attache un Ă©couteur dĂ©clenchĂ© lors d'un clic en dehors de l'inventaire (extĂ©rieur). + */ + Gui onOutsideClick(Consumer hook); + + /** + * Attache un Ă©couteur dĂ©clenchĂ© lors d'un clic dans l'inventaire du joueur (inventaire infĂ©rieur). + */ + Gui onBottomClick(Consumer 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); +} diff --git a/src/main/java/fr/luc/bettermcguis/api/GuiClickAction.java b/src/main/java/fr/luc/bettermcguis/api/GuiClickAction.java new file mode 100644 index 0000000..1b36af1 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/api/GuiClickAction.java @@ -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); +} diff --git a/src/main/java/fr/luc/bettermcguis/api/GuiClickContext.java b/src/main/java/fr/luc/bettermcguis/api/GuiClickContext.java new file mode 100644 index 0000000..a24ea75 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/api/GuiClickContext.java @@ -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("✔ " + (miniMessage != null ? miniMessage : "")); + } + + /** + * Envoie un message d'erreur prĂ©fixĂ© en rouge. + */ + public void replyError(String miniMessage) { + reply("✖ " + (miniMessage != null ? miniMessage : "")); + } + + /** + * Envoie un message d'information prĂ©fixĂ© en bleu clair. + */ + public void replyInfo(String miniMessage) { + reply("â„č " + (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); + } + } +} diff --git a/src/main/java/fr/luc/bettermcguis/api/GuiCloseContext.java b/src/main/java/fr/luc/bettermcguis/api/GuiCloseContext.java new file mode 100644 index 0000000..23641dd --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/api/GuiCloseContext.java @@ -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; + } +} diff --git a/src/main/java/fr/luc/bettermcguis/api/GuiItem.java b/src/main/java/fr/luc/bettermcguis/api/GuiItem.java new file mode 100644 index 0000000..fbdb450 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/api/GuiItem.java @@ -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 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 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 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; + } +} diff --git a/src/main/java/fr/luc/bettermcguis/api/GuiOpenContext.java b/src/main/java/fr/luc/bettermcguis/api/GuiOpenContext.java new file mode 100644 index 0000000..eda5fcf --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/api/GuiOpenContext.java @@ -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); + } + } +} diff --git a/src/main/java/fr/luc/bettermcguis/api/GuiType.java b/src/main/java/fr/luc/bettermcguis/api/GuiType.java new file mode 100644 index 0000000..79b6270 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/api/GuiType.java @@ -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)"); + }; + } +} diff --git a/src/main/java/fr/luc/bettermcguis/api/slot/SlotPos.java b/src/main/java/fr/luc/bettermcguis/api/slot/SlotPos.java new file mode 100644 index 0000000..d3c807b --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/api/slot/SlotPos.java @@ -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 + ")"; + } +} diff --git a/src/main/java/fr/luc/bettermcguis/api/slot/SlotRange.java b/src/main/java/fr/luc/bettermcguis/api/slot/SlotRange.java new file mode 100644 index 0000000..d1e52c3 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/api/slot/SlotRange.java @@ -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 slots; + + /** + * CrĂ©e une plage avec une liste de slots. + * + * @param slots Les numĂ©ros de slots. + */ + public SlotRange(List 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 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 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 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 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 getSlots() { + return Collections.unmodifiableList(slots); + } +} diff --git a/src/main/java/fr/luc/bettermcguis/builder/AnimatedGuiBuilder.java b/src/main/java/fr/luc/bettermcguis/builder/AnimatedGuiBuilder.java new file mode 100644 index 0000000..fa98b28 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/builder/AnimatedGuiBuilder.java @@ -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 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 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; + } +} diff --git a/src/main/java/fr/luc/bettermcguis/builder/GuiBuilder.java b/src/main/java/fr/luc/bettermcguis/builder/GuiBuilder.java new file mode 100644 index 0000000..7ff6225 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/builder/GuiBuilder.java @@ -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 items = new HashMap<>(); + protected final Set editableSlots = new HashSet<>(); + protected final Map properties = new HashMap<>(); + protected final List patterns = new ArrayList<>(); + + protected final List> openHooks = new ArrayList<>(); + protected final List> closeHooks = new ArrayList<>(); + protected final List> clickHooks = new ArrayList<>(); + protected final List> outsideClickHooks = new ArrayList<>(); + protected final List> 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 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 hook) { + this.openHooks.add(hook); + return this; + } + + /** + * Hook exĂ©cutĂ© Ă  la fermeture du GUI. + */ + public GuiBuilder onClose(Consumer hook) { + this.closeHooks.add(hook); + return this; + } + + /** + * Hook exĂ©cutĂ© lors d'un clic dans le GUI. + */ + public GuiBuilder onClick(Consumer hook) { + this.clickHooks.add(hook); + return this; + } + + /** + * Hook exĂ©cutĂ© lors d'un clic extĂ©rieur. + */ + public GuiBuilder onOutsideClick(Consumer hook) { + this.outsideClickHooks.add(hook); + return this; + } + + /** + * Hook exĂ©cutĂ© lors d'un clic dans l'inventaire du joueur. + */ + public GuiBuilder onBottomClick(Consumer 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 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 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; + } +} diff --git a/src/main/java/fr/luc/bettermcguis/builder/ItemBuilder.java b/src/main/java/fr/luc/bettermcguis/builder/ItemBuilder.java new file mode 100644 index 0000000..92e94ca --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/builder/ItemBuilder.java @@ -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 lines) { + if (meta != null && lines != null) { + List 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 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 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); + } +} diff --git a/src/main/java/fr/luc/bettermcguis/builder/PaginatedGuiBuilder.java b/src/main/java/fr/luc/bettermcguis/builder/PaginatedGuiBuilder.java new file mode 100644 index 0000000..c4450ed --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/builder/PaginatedGuiBuilder.java @@ -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 pageItems = new ArrayList<>(); + private final List itemSlots = new ArrayList<>(); + private Integer previousPageSlot; + private Integer nextPageSlot; + private Integer pageIndicatorSlot; + private GuiItem customPreviousButton; + private GuiItem customNextButton; + private BiFunction pageIndicatorSupplier; + private final List> 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 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 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 supplier) { + this.pageIndicatorSlot = slot; + this.pageIndicatorSupplier = supplier; + return this; + } + + /** + * Hook dĂ©clenchĂ© lors du changement de page. + */ + public PaginatedGuiBuilder onPageChange(Consumer 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; + } +} diff --git a/src/main/java/fr/luc/bettermcguis/builder/PatternBuilder.java b/src/main/java/fr/luc/bettermcguis/builder/PatternBuilder.java new file mode 100644 index 0000000..2bf93ab --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/builder/PatternBuilder.java @@ -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 lines = new ArrayList<>(); + private final Map 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; + } +} diff --git a/src/main/java/fr/luc/bettermcguis/builder/TabbedGuiBuilder.java b/src/main/java/fr/luc/bettermcguis/builder/TabbedGuiBuilder.java new file mode 100644 index 0000000..b9ef97b --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/builder/TabbedGuiBuilder.java @@ -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 tabButtons = new LinkedHashMap<>(); + private final Map tabButtonItems = new LinkedHashMap<>(); + private final Map> 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 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 init = tabInitializers.get(tabId); + if (init != null) { + init.accept(gui); + } + } + + return gui; + } +} diff --git a/src/main/java/fr/luc/bettermcguis/demo/DemoGuiPlugin.java b/src/main/java/fr/luc/bettermcguis/demo/DemoGuiPlugin.java new file mode 100644 index 0000000..df584bc --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/demo/DemoGuiPlugin.java @@ -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; + } +} diff --git a/src/main/java/fr/luc/bettermcguis/demo/guis/DemoMainMenuGui.java b/src/main/java/fr/luc/bettermcguis/demo/guis/DemoMainMenuGui.java new file mode 100644 index 0000000..359e0b4 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/demo/guis/DemoMainMenuGui.java @@ -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("Menu Principal DĂ©mo") + .rows(3) + .fillBorder(Material.BLACK_STAINED_GLASS_PANE) + + // Bouton 1 : Boutique PaginĂ©e + .item(11, ItemBuilder.of(Material.EMERALD) + .name("Boutique du Serveur") + .lore( + "Parcourez notre catalogue d'items.", + "", + "▶ Cliquez pour ouvrir la boutique" + ) + .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("Poubelle / DĂ©barras") + .lore( + "DĂ©posez vos items indĂ©sirables ici.", + "", + "▶ Cliquez pour ouvrir la poubelle" + ) + .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("Votre Profil") + .lore( + "Consultez vos statistiques personnelles.", + "", + "▶ Cliquez pour actualiser" + ) + .asGuiItem(ctx -> { + ctx.playSound(Sound.UI_BUTTON_CLICK, 1.0f, 1.0f); + ctx.replySuccess("Votre profil est Ă  jour !"); + }) + ) + + .build(); + } +} diff --git a/src/main/java/fr/luc/bettermcguis/demo/guis/DemoPaginatedShopGui.java b/src/main/java/fr/luc/bettermcguis/demo/guis/DemoPaginatedShopGui.java new file mode 100644 index 0000000..9807a64 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/demo/guis/DemoPaginatedShopGui.java @@ -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("Boutique PaginĂ©e") + .rows(5) + .fillBorder(Material.GRAY_STAINED_GLASS_PANE); + + // Bouton retour au menu principal en bas Ă  gauche + builder.item(36, ItemBuilder.of(Material.BARRIER) + .name("Retour au Menu Principal") + .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("Article #" + itemId + "") + .lore( + "Prix : " + price + " $", + "", + "▶ Clic-gauche pour acheter" + ) + .asGuiItem(ctx -> { + ctx.playSound(Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.5f); + ctx.replySuccess("Vous avez achetĂ© Article #" + itemId + " pour " + price + " $ !"); + }) + ); + } + + return builder.build(); + } +} diff --git a/src/main/java/fr/luc/bettermcguis/demo/guis/DemoStorageTrashGui.java b/src/main/java/fr/luc/bettermcguis/demo/guis/DemoStorageTrashGui.java new file mode 100644 index 0000000..5901ee1 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/demo/guis/DemoStorageTrashGui.java @@ -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("Poubelle Publique", 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("Vider la Poubelle ImmĂ©diatement") + .lore( + "DĂ©truit tous les objets placĂ©s dans la zone centrale.", + "", + "▶ Cliquez pour incinĂ©rer les items" + ) + .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; + } +} diff --git a/src/main/java/fr/luc/bettermcguis/demo/listeners/DemoGuiListener.java b/src/main/java/fr/luc/bettermcguis/demo/listeners/DemoGuiListener.java new file mode 100644 index 0000000..bdc27ab --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/demo/listeners/DemoGuiListener.java @@ -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()); + } +} diff --git a/src/main/java/fr/luc/bettermcguis/event/CancellableGuiEvent.java b/src/main/java/fr/luc/bettermcguis/event/CancellableGuiEvent.java new file mode 100644 index 0000000..725ace1 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/event/CancellableGuiEvent.java @@ -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; + } +} diff --git a/src/main/java/fr/luc/bettermcguis/event/GuiClickEvent.java b/src/main/java/fr/luc/bettermcguis/event/GuiClickEvent.java new file mode 100644 index 0000000..31d68b6 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/event/GuiClickEvent.java @@ -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); + } +} diff --git a/src/main/java/fr/luc/bettermcguis/event/GuiCloseEvent.java b/src/main/java/fr/luc/bettermcguis/event/GuiCloseEvent.java new file mode 100644 index 0000000..618e51e --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/event/GuiCloseEvent.java @@ -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; + } +} diff --git a/src/main/java/fr/luc/bettermcguis/event/GuiEvent.java b/src/main/java/fr/luc/bettermcguis/event/GuiEvent.java new file mode 100644 index 0000000..caf457a --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/event/GuiEvent.java @@ -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; + } +} diff --git a/src/main/java/fr/luc/bettermcguis/event/GuiEventListener.java b/src/main/java/fr/luc/bettermcguis/event/GuiEventListener.java new file mode 100644 index 0000000..263d2b3 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/event/GuiEventListener.java @@ -0,0 +1,17 @@ +package fr.luc.bettermcguis.event; + +/** + * Interface fonctionnelle pour Ă©couter un Ă©vĂ©nement spĂ©cifique de GUI. + * + * @param Le type d'Ă©vĂ©nement Ă©coutĂ©. + */ +@FunctionalInterface +public interface GuiEventListener { + + /** + * Traite l'Ă©vĂ©nement transmis. + * + * @param event L'instance de l'Ă©vĂ©nement. + */ + void onEvent(T event); +} diff --git a/src/main/java/fr/luc/bettermcguis/event/GuiEventManager.java b/src/main/java/fr/luc/bettermcguis/event/GuiEventManager.java new file mode 100644 index 0000000..2be9f5e --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/event/GuiEventManager.java @@ -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 eventType, + int priority, + String titleFilter, + GuiEventListener listener + ) implements Comparable { + @Override + public int compareTo(RegisteredListener o) { + return Integer.compare(o.priority, this.priority); // Ordre dĂ©croissant + } + } + + private final Map, List> listeners = new ConcurrentHashMap<>(); + + /** + * Enregistre un Ă©couteur programmatique avec une prioritĂ© par dĂ©faut de 0. + */ + public void registerListener(Class eventType, GuiEventListener listener) { + registerListener(eventType, 0, "", listener); + } + + /** + * Enregistre un Ă©couteur programmatique avec prioritĂ© et filtre de titre. + */ + public void registerListener(Class eventType, int priority, String titleFilter, GuiEventListener 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 eventType = (Class) params[0]; + GuiEventHandler annotation = method.getAnnotation(GuiEventHandler.class); + + method.setAccessible(true); + GuiEventListener 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, List> 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) reg.listener()).onEvent(event); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + } + + /** + * Supprime tous les Ă©couteurs enregistrĂ©s. + */ + public void clear() { + listeners.clear(); + } +} diff --git a/src/main/java/fr/luc/bettermcguis/event/GuiOpenEvent.java b/src/main/java/fr/luc/bettermcguis/event/GuiOpenEvent.java new file mode 100644 index 0000000..ea16a03 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/event/GuiOpenEvent.java @@ -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; + } +} diff --git a/src/main/java/fr/luc/bettermcguis/event/GuiPageChangeEvent.java b/src/main/java/fr/luc/bettermcguis/event/GuiPageChangeEvent.java new file mode 100644 index 0000000..de0da44 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/event/GuiPageChangeEvent.java @@ -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; + } +} diff --git a/src/main/java/fr/luc/bettermcguis/event/GuiRefreshEvent.java b/src/main/java/fr/luc/bettermcguis/event/GuiRefreshEvent.java new file mode 100644 index 0000000..4c14bb1 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/event/GuiRefreshEvent.java @@ -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); + } +} diff --git a/src/main/java/fr/luc/bettermcguis/event/annotation/GuiEventHandler.java b/src/main/java/fr/luc/bettermcguis/event/annotation/GuiEventHandler.java new file mode 100644 index 0000000..bbaa851 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/event/annotation/GuiEventHandler.java @@ -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 ""; +} diff --git a/src/main/java/fr/luc/bettermcguis/holder/BetterGuiHolder.java b/src/main/java/fr/luc/bettermcguis/holder/BetterGuiHolder.java new file mode 100644 index 0000000..6fade19 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/holder/BetterGuiHolder.java @@ -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; + } +} diff --git a/src/main/java/fr/luc/bettermcguis/listener/BukkitGuiEventListener.java b/src/main/java/fr/luc/bettermcguis/listener/BukkitGuiEventListener.java new file mode 100644 index 0000000..b151b71 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/listener/BukkitGuiEventListener.java @@ -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)); + } + } +} diff --git a/src/main/java/fr/luc/bettermcguis/pattern/GuiMask.java b/src/main/java/fr/luc/bettermcguis/pattern/GuiMask.java new file mode 100644 index 0000000..14a12c3 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/pattern/GuiMask.java @@ -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 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 resolveSlots(int rows, int cols) { + List 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); + } +} diff --git a/src/main/java/fr/luc/bettermcguis/pattern/GuiPattern.java b/src/main/java/fr/luc/bettermcguis/pattern/GuiPattern.java new file mode 100644 index 0000000..83c1198 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/pattern/GuiPattern.java @@ -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 rows = new ArrayList<>(); + private final Map 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 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 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 getRows() { + return Collections.unmodifiableList(rows); + } + + /** + * @return La table des liaisons de caractĂšres vers items. + */ + public Map getItemBindings() { + return Collections.unmodifiableMap(itemBindings); + } +} diff --git a/src/main/java/fr/luc/bettermcguis/type/AnimatedGui.java b/src/main/java/fr/luc/bettermcguis/type/AnimatedGui.java new file mode 100644 index 0000000..b2be017 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/type/AnimatedGui.java @@ -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 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 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(); + } + } +} diff --git a/src/main/java/fr/luc/bettermcguis/type/PaginatedGui.java b/src/main/java/fr/luc/bettermcguis/type/PaginatedGui.java new file mode 100644 index 0000000..75555c6 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/type/PaginatedGui.java @@ -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 pageItems = new ArrayList<>(); + private final List 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 pageIndicatorSupplier; + + private final List> 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 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 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 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 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 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 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("◀ Page PrĂ©cĂ©dente") + .lore("Aller Ă  la page " + (currentPage - 1) + "") + .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("Page Suivante ▶") + .lore("Aller Ă  la page " + (currentPage + 1) + "") + .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("Page " + currentPage + " / " + total + "") + .lore("Total d'Ă©lĂ©ments : " + pageItems.size() + "") + .asGuiItem(); + super.setItem(pageIndicatorSlot, indicator); + } + } +} diff --git a/src/main/java/fr/luc/bettermcguis/type/SimpleGui.java b/src/main/java/fr/luc/bettermcguis/type/SimpleGui.java new file mode 100644 index 0000000..adc7aeb --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/type/SimpleGui.java @@ -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 items = new ConcurrentHashMap<>(); + protected final Set editableSlots = new CopyOnWriteArraySet<>(); + protected final Map properties = new ConcurrentHashMap<>(); + protected final Set viewers = new CopyOnWriteArraySet<>(); + + // Hooks du cycle de vie + protected final List> openHooks = new CopyOnWriteArrayList<>(); + protected final List> closeHooks = new CopyOnWriteArrayList<>(); + protected final List> clickHooks = new CopyOnWriteArrayList<>(); + protected final List> outsideClickHooks = new CopyOnWriteArrayList<>(); + protected final List> 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 getViewers() { + Set 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 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 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 getProperty(String key, Class type) { + Object val = properties.get(key); + if (val != null && type.isInstance(val)) { + return (T) val; + } + return null; + } + + @SuppressWarnings("unchecked") + @Override + public 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 getProperties() { + return Collections.unmodifiableMap(properties); + } + + @Override + public Gui onOpen(Consumer hook) { + this.openHooks.add(Objects.requireNonNull(hook, "hook cannot be null")); + return this; + } + + @Override + public Gui onClose(Consumer hook) { + this.closeHooks.add(Objects.requireNonNull(hook, "hook cannot be null")); + return this; + } + + @Override + public Gui onClick(Consumer hook) { + this.clickHooks.add(Objects.requireNonNull(hook, "hook cannot be null")); + return this; + } + + @Override + public Gui onOutsideClick(Consumer hook) { + this.outsideClickHooks.add(Objects.requireNonNull(hook, "hook cannot be null")); + return this; + } + + @Override + public Gui onBottomClick(Consumer hook) { + this.bottomClickHooks.add(Objects.requireNonNull(hook, "hook cannot be null")); + return this; + } + + @Override + public void handleOpen(GuiOpenContext context) { + for (Consumer 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 hook : closeHooks) { + try { + hook.accept(context); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Override + public void handleClick(GuiClickContext context) { + for (Consumer hook : clickHooks) { + try { + hook.accept(context); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Override + public void handleOutsideClick(GuiClickContext context) { + for (Consumer hook : outsideClickHooks) { + try { + hook.accept(context); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Override + public void handleBottomClick(GuiClickContext context) { + for (Consumer hook : bottomClickHooks) { + try { + hook.accept(context); + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} diff --git a/src/main/java/fr/luc/bettermcguis/type/StorageGui.java b/src/main/java/fr/luc/bettermcguis/type/StorageGui.java new file mode 100644 index 0000000..d09b868 --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/type/StorageGui.java @@ -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 storageSlots = new HashSet<>(); + private BiConsumer itemDepositHook; + private BiConsumer 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 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 getStoredItems(Inventory inventory) { + List 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 leftover = player.getInventory().addItem(item); + for (ItemStack drop : leftover.values()) { + player.getWorld().dropItemNaturally(player.getLocation(), drop); + } + inv.setItem(slot, null); + } + } + } + + super.handleClose(context); + } +} diff --git a/src/main/java/fr/luc/bettermcguis/type/TabbedGui.java b/src/main/java/fr/luc/bettermcguis/type/TabbedGui.java new file mode 100644 index 0000000..82c08ca --- /dev/null +++ b/src/main/java/fr/luc/bettermcguis/type/TabbedGui.java @@ -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> tabItems = new LinkedHashMap<>(); + private final Map tabButtonSlots = new LinkedHashMap<>(); + private final Map 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 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 rows = pattern.getRows(); + Map 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 currentItems = tabItems.get(activeTabId); + if (currentItems != null) { + for (Map.Entry entry : currentItems.entrySet()) { + super.setItem(entry.getKey(), entry.getValue()); + } + } + } + + @Override + public Inventory createInventory(Player player) { + applyActiveTabItems(); + return super.createInventory(player); + } +} diff --git a/src/test/java/fr/luc/bettermcguis/GuiLayoutAndPatternTest.java b/src/test/java/fr/luc/bettermcguis/GuiLayoutAndPatternTest.java new file mode 100644 index 0000000..5713ba1 --- /dev/null +++ b/src/test/java/fr/luc/bettermcguis/GuiLayoutAndPatternTest.java @@ -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 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 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 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)); + } +} diff --git a/src/test/java/fr/luc/bettermcguis/GuiLifecycleAndEventTest.java b/src/test/java/fr/luc/bettermcguis/GuiLifecycleAndEventTest.java new file mode 100644 index 0000000..3b84929 --- /dev/null +++ b/src/test/java/fr/luc/bettermcguis/GuiLifecycleAndEventTest.java @@ -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 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)); + } +} diff --git a/src/test/java/fr/luc/bettermcguis/ItemBuilderTest.java b/src/test/java/fr/luc/bettermcguis/ItemBuilderTest.java new file mode 100644 index 0000000..b150491 --- /dev/null +++ b/src/test/java/fr/luc/bettermcguis/ItemBuilderTest.java @@ -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("Test") + .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()); + } +} diff --git a/src/test/java/fr/luc/bettermcguis/PaginatedGuiTest.java b/src/test/java/fr/luc/bettermcguis/PaginatedGuiTest.java new file mode 100644 index 0000000..5ceefb7 --- /dev/null +++ b/src/test/java/fr/luc/bettermcguis/PaginatedGuiTest.java @@ -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()); + } +}