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

This commit is contained in:
2026-08-23 18:10:49 +02:00
parent b88eaad155
commit 806d1789a1
63 changed files with 6233 additions and 2 deletions
@@ -0,0 +1,167 @@
package fr.luc.bettermcguis;
import fr.luc.bettermcguis.builder.*;
import fr.luc.bettermcguis.event.GuiEventManager;
import fr.luc.bettermcguis.listener.BukkitGuiEventListener;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.event.HandlerList;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import java.util.Objects;
/**
* Point d'entrée principal et gestionnaire central de la bibliothèque **betterMcGuis**.
* Fournit l'accès aux constructeurs fluides de menus (simples, paginés, à onglets, animés),
* au constructeur d'items (ItemBuilder), aux motifs ASCII et au bus d'événements.
*/
public class BetterMcGuis {
private static BetterMcGuis instance;
private final Plugin plugin;
private final GuiEventManager eventManager;
private final BukkitGuiEventListener bukkitListener;
/**
* Initialise une instance de betterMcGuis pour un plugin donné.
*
* @param plugin Le plugin Bukkit/Paper propriétaire.
*/
public BetterMcGuis(Plugin plugin) {
this.plugin = plugin;
this.eventManager = new GuiEventManager();
this.bukkitListener = new BukkitGuiEventListener(this);
if (plugin != null) {
Bukkit.getPluginManager().registerEvents(bukkitListener, plugin);
}
if (instance == null) {
instance = this;
}
}
/**
* Initialise betterMcGuis pour un plugin Bukkit/Paper.
*
* @param plugin Le plugin propriétaire.
* @return L'instance configurée.
*/
public static BetterMcGuis create(Plugin plugin) {
return new BetterMcGuis(plugin);
}
/**
* @return L'instance globale ou par défaut.
*/
public static BetterMcGuis getInstance() {
return instance;
}
/**
* Crée un nouveau constructeur de menu standard ({@link GuiBuilder}).
*/
public static GuiBuilder builder() {
return new GuiBuilder();
}
/**
* Crée un nouveau constructeur de menu avec titre et nombre de lignes.
*/
public static GuiBuilder builder(String title, int rows) {
return new GuiBuilder().title(title).rows(rows);
}
/**
* Crée un nouveau constructeur d'inventaire paginé ({@link PaginatedGuiBuilder}).
*/
public static PaginatedGuiBuilder paginated() {
return new PaginatedGuiBuilder();
}
/**
* Crée un nouveau constructeur d'inventaire paginé avec titre et nombre de lignes.
*/
public static PaginatedGuiBuilder paginated(String title, int rows) {
return (PaginatedGuiBuilder) new PaginatedGuiBuilder().title(title).rows(rows);
}
/**
* Crée un nouveau constructeur de menu à onglets ({@link TabbedGuiBuilder}).
*/
public static TabbedGuiBuilder tabbed() {
return new TabbedGuiBuilder();
}
/**
* Crée un nouveau constructeur de menu à onglets avec titre et nombre de lignes.
*/
public static TabbedGuiBuilder tabbed(String title, int rows) {
return (TabbedGuiBuilder) new TabbedGuiBuilder().title(title).rows(rows);
}
/**
* Crée un nouveau constructeur de menu animé ({@link AnimatedGuiBuilder}).
*/
public static AnimatedGuiBuilder animated() {
return new AnimatedGuiBuilder();
}
/**
* Crée un nouveau constructeur d'ItemStack moderne ({@link ItemBuilder}).
*/
public static ItemBuilder item(Material material) {
return ItemBuilder.of(material);
}
/**
* Crée un constructeur d'item à partir d'un {@link ItemStack} existant.
*/
public static ItemBuilder item(ItemStack itemStack) {
return ItemBuilder.of(itemStack);
}
/**
* Crée un nouveau constructeur de motif / masque ASCII ({@link PatternBuilder}).
*/
public static PatternBuilder pattern(String... lines) {
return new PatternBuilder(lines);
}
/**
* Enregistre un ou plusieurs écouteurs d'événements de GUI annotés avec {@link fr.luc.bettermcguis.event.annotation.GuiEventHandler}.
*
* @param listeners Les objets écouteurs.
*/
public void registerListeners(Object... listeners) {
if (listeners != null) {
for (Object listener : listeners) {
eventManager.registerListeners(listener);
}
}
}
/**
* @return Le gestionnaire de bus d'événements central.
*/
public GuiEventManager getEventManager() {
return eventManager;
}
/**
* @return Le plugin Bukkit associé.
*/
public Plugin getPlugin() {
return plugin;
}
/**
* Désenregistre les écouteurs Bukkit et nettoie toutes les ressources lors de la désactivation du plugin.
*/
public void unregisterAll() {
HandlerList.unregisterAll(bukkitListener);
eventManager.clear();
}
}
@@ -0,0 +1,67 @@
package fr.luc.bettermcguis.animation;
import fr.luc.bettermcguis.api.GuiItem;
import fr.luc.bettermcguis.pattern.GuiPattern;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Représente une image / frame individuelle dans un GUI animé.
*/
public class Frame {
private final Map<Integer, GuiItem> items = new HashMap<>();
private String title;
public Frame() {}
public Frame(String title) {
this.title = title;
}
/**
* Définit le titre de cette frame.
*/
public Frame title(String title) {
this.title = title;
return this;
}
/**
* Définit un item dans cette frame.
*/
public Frame item(int slot, GuiItem item) {
this.items.put(slot, item);
return this;
}
/**
* Applique un motif à cette frame.
*/
public Frame pattern(GuiPattern pattern, int cols, int maxRows) {
if (pattern != null) {
var rows = pattern.getRows();
var bindings = pattern.getItemBindings();
for (int r = 0; r < Math.min(rows.size(), maxRows); r++) {
String line = rows.get(r);
for (int c = 0; c < Math.min(line.length(), cols); c++) {
char ch = line.charAt(c);
if (bindings.containsKey(ch)) {
items.put(r * cols + c, bindings.get(ch));
}
}
}
}
return this;
}
public Map<Integer, GuiItem> getItems() {
return Collections.unmodifiableMap(items);
}
public String getTitle() {
return title;
}
}
@@ -0,0 +1,277 @@
package fr.luc.bettermcguis.api;
import fr.luc.bettermcguis.api.slot.SlotPos;
import fr.luc.bettermcguis.api.slot.SlotRange;
import fr.luc.bettermcguis.pattern.GuiPattern;
import net.kyori.adventure.text.Component;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
/**
* Interface principale définissant le comportement d'un menu d'inventaire interactif (GUI).
*/
public interface Gui {
/**
* @return Le titre actuel du menu au format MiniMessage.
*/
String getTitle();
/**
* @return Le titre actuel sous forme de composant Adventure.
*/
Component getTitleComponent();
/**
* Modifie le titre du menu (met à jour le titre pour les joueurs connectés si supporté).
*
* @param miniMessage Le nouveau titre en MiniMessage.
*/
void setTitle(String miniMessage);
/**
* @return Le type et la géométrie de cet inventaire.
*/
GuiType getType();
/**
* @return Le nombre total de slots de l'inventaire.
*/
default int getSize() {
return getType().getSize();
}
/**
* @return Le nombre de lignes de l'inventaire.
*/
default int getRows() {
return getType().getRows();
}
/**
* @return Le nombre de colonnes de l'inventaire (ex: 9 pour un coffre).
*/
default int getColumns() {
return getType().getColumns();
}
/**
* Place un item dans un slot absolu (0-indexé).
*
* @param slot Le numéro de slot.
* @param item L'item à placer.
*/
void setItem(int slot, GuiItem item);
/**
* Place un item à des coordonnées (ligne, colonne) en 0-indexé.
*
* @param row La ligne (0 à rows - 1).
* @param col La colonne (0 à cols - 1).
* @param item L'item à placer.
*/
default void setItem(int row, int col, GuiItem item) {
setItem(row * getColumns() + col, item);
}
/**
* Place un item à une position {@link SlotPos}.
*
* @param pos La position.
* @param item L'item.
*/
default void setItem(SlotPos pos, GuiItem item) {
if (pos != null) {
setItem(pos.toSlot(getColumns()), item);
}
}
/**
* Récupère le {@link GuiItem} présent à un slot donné.
*
* @param slot Le numéro de slot absolu.
* @return Le GuiItem, ou {@code null} si le slot est vide.
*/
GuiItem getItem(int slot);
/**
* Supprime l'item présent au slot donné.
*
* @param slot Le numéro de slot.
*/
void removeItem(int slot);
/**
* Supprime tous les items de l'inventaire.
*/
void clear();
/**
* Remplit tous les slots vides ou existants avec un item donné.
*
* @param item L'item de remplissage.
*/
void fill(GuiItem item);
/**
* Remplit uniquement les bordures extérieures de l'inventaire.
*
* @param item L'item de bordure.
*/
void fillBorder(GuiItem item);
/**
* Remplit une plage de slots spécifique.
*
* @param range La plage de slots.
* @param item L'item.
*/
void fillRange(SlotRange range, GuiItem item);
/**
* Applique un motif / masque ASCII sur l'inventaire.
*
* @param pattern Le motif à appliquer.
*/
void applyPattern(GuiPattern pattern);
/**
* Ouvre l'inventaire pour un joueur.
*
* @param player Le joueur.
*/
void open(Player player);
/**
* Ferme l'inventaire pour un joueur.
*
* @param player Le joueur.
*/
void close(Player player);
/**
* Rafraîchit l'affichage de l'inventaire pour un joueur actuellement visualisateur.
*
* @param player Le joueur.
*/
void refresh(Player player);
/**
* Rafraîchit l'inventaire pour l'ensemble des joueurs visualisant actuellement ce GUI.
*/
void refreshAll();
/**
* @return L'ensemble des joueurs qui visualisent actuellement ce menu.
*/
Set<Player> getViewers();
/**
* Crée ou met à jour l'inventaire Bukkit sous-jacent pour un joueur.
*
* @param player Le joueur pour lequel l'inventaire est généré.
* @return L'instance {@link Inventory} Bukkit.
*/
Inventory createInventory(Player player);
/**
* Définit si un slot donné est modifiable par le joueur (permet de déposer ou retirer des items).
*
* @param slot Le numéro de slot.
* @param editable true pour autoriser les interactions de déplacement d'items.
*/
void setEditable(int slot, boolean editable);
/**
* @param slot Le numéro de slot.
* @return true si le joueur a le droit de poser ou retirer des items dans ce slot.
*/
boolean isEditable(int slot);
/**
* @return L'ensemble des slots éditables par les joueurs.
*/
Set<Integer> getEditableSlots();
/**
* Stocke une propriété personnalisée dans le GUI.
*
* @param key La clé identifiant la propriété.
* @param value La valeur.
*/
void setProperty(String key, Object value);
/**
* Récupère une propriété typée.
*
* @param key La clé.
* @param type Le type attendu.
* @param <T> Le type générique.
* @return La valeur, ou {@code null}.
*/
<T> T getProperty(String key, Class<T> type);
/**
* Récupère une propriété ou retourne une valeur par défaut.
*/
<T> T getProperty(String key, T defaultValue);
/**
* @return La table complète des propriétés du GUI.
*/
Map<String, Object> getProperties();
/**
* Attache un écouteur déclenché lors de l'ouverture du menu.
*/
Gui onOpen(Consumer<GuiOpenContext> hook);
/**
* Attache un écouteur déclenché lors de la fermeture du menu.
*/
Gui onClose(Consumer<GuiCloseContext> hook);
/**
* Attache un écouteur déclenché lors de n'importe quel clic dans l'inventaire supérieur.
*/
Gui onClick(Consumer<GuiClickContext> hook);
/**
* Attache un écouteur déclenché lors d'un clic en dehors de l'inventaire (extérieur).
*/
Gui onOutsideClick(Consumer<GuiClickContext> hook);
/**
* Attache un écouteur déclenché lors d'un clic dans l'inventaire du joueur (inventaire inférieur).
*/
Gui onBottomClick(Consumer<GuiClickContext> hook);
/**
* Exécute le hook d'ouverture.
*/
void handleOpen(GuiOpenContext context);
/**
* Exécute le hook de fermeture.
*/
void handleClose(GuiCloseContext context);
/**
* Exécute le hook de clic global.
*/
void handleClick(GuiClickContext context);
/**
* Exécute le hook de clic extérieur.
*/
void handleOutsideClick(GuiClickContext context);
/**
* Exécute le hook de clic dans l'inventaire inférieur.
*/
void handleBottomClick(GuiClickContext context);
}
@@ -0,0 +1,15 @@
package fr.luc.bettermcguis.api;
/**
* Interface fonctionnelle déclenchée lors d'un clic sur un item d'un GUI.
*/
@FunctionalInterface
public interface GuiClickAction {
/**
* Exécute l'action associée au clic.
*
* @param context Le contexte complet du clic.
*/
void execute(GuiClickContext context);
}
@@ -0,0 +1,229 @@
package fr.luc.bettermcguis.api;
import fr.luc.bettermcguis.api.slot.SlotPos;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.ItemStack;
import java.util.Objects;
/**
* Contexte transmis lors d'une interaction / clic sur un slot ou item d'un GUI.
*/
public class GuiClickContext {
private final Player player;
private final Gui gui;
private final GuiItem guiItem;
private final int slot;
private final SlotPos slotPos;
private final ClickType clickType;
private final InventoryAction inventoryAction;
private final ItemStack currentItem;
private final ItemStack cursorItem;
private final InventoryClickEvent rawEvent;
private boolean cancelled = true; // Par défaut, les clics sont annulés pour éviter le vol d'items
/**
* Crée un nouveau contexte de clic.
*/
public GuiClickContext(Player player, Gui gui, GuiItem guiItem, int slot, SlotPos slotPos,
ClickType clickType, InventoryAction inventoryAction,
ItemStack currentItem, ItemStack cursorItem, InventoryClickEvent rawEvent) {
this.player = Objects.requireNonNull(player, "player cannot be null");
this.gui = Objects.requireNonNull(gui, "gui cannot be null");
this.guiItem = guiItem;
this.slot = slot;
this.slotPos = slotPos;
this.clickType = clickType;
this.inventoryAction = inventoryAction;
this.currentItem = currentItem;
this.cursorItem = cursorItem;
this.rawEvent = rawEvent;
}
/**
* @return Le joueur ayant effectué le clic.
*/
public Player getPlayer() {
return player;
}
/**
* @return Le GUI dans lequel le clic a eu lieu.
*/
public Gui getGui() {
return gui;
}
/**
* @return L'item {@link GuiItem} cliqué, ou {@code null} si slot vide.
*/
public GuiItem getGuiItem() {
return guiItem;
}
/**
* @return Le numéro de slot absolu (0-indexé).
*/
public int getSlot() {
return slot;
}
/**
* @return La position (ligne, colonne) du slot cliqué.
*/
public SlotPos getSlotPos() {
return slotPos;
}
/**
* @return Le type de clic (ex: LEFT, RIGHT, SHIFT_LEFT, MIDDLE, NUMBER_KEY).
*/
public ClickType getClickType() {
return clickType;
}
/**
* @return L'action d'inventaire Bukkit (ex: PICKUP_ALL, PLACE_ALL).
*/
public InventoryAction getInventoryAction() {
return inventoryAction;
}
/**
* @return L'ItemStack actuellement présent dans le slot cliqué.
*/
public ItemStack getCurrentItem() {
return currentItem;
}
/**
* @return L'ItemStack actuellement tenu sur le curseur de la souris.
*/
public ItemStack getCursorItem() {
return cursorItem;
}
/**
* @return L'événement Bukkit natif {@link InventoryClickEvent}, ou {@code null} lors de tests unitaires.
*/
public InventoryClickEvent getRawEvent() {
return rawEvent;
}
/**
* @return true si le clic est annulé (empêche la prise ou le déplacement d'items).
*/
public boolean isCancelled() {
return cancelled;
}
/**
* Définit si le clic doit être annulé.
*
* @param cancelled true pour bloquer l'action Bukkit native, false pour autoriser la modification du slot.
*/
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
if (rawEvent != null) {
rawEvent.setCancelled(cancelled);
}
}
/**
* @return true s'il s'agit d'un clic gauche (simple ou avec Shift).
*/
public boolean isLeftClick() {
return clickType == ClickType.LEFT || clickType == ClickType.SHIFT_LEFT;
}
/**
* @return true s'il s'agit d'un clic droit (simple ou avec Shift).
*/
public boolean isRightClick() {
return clickType == ClickType.RIGHT || clickType == ClickType.SHIFT_RIGHT;
}
/**
* @return true s'il s'agit d'un shift-clic.
*/
public boolean isShiftClick() {
return clickType == ClickType.SHIFT_LEFT || clickType == ClickType.SHIFT_RIGHT;
}
/**
* Ferme l'inventaire actuel pour le joueur.
*/
public void close() {
player.closeInventory();
}
/**
* Rafraîchit l'affichage du GUI pour le joueur.
*/
public void refresh() {
gui.refresh(player);
}
/**
* Joue un son au joueur.
*
* @param sound Le son Bukkit.
* @param volume Le volume.
* @param pitch La hauteur de ton.
*/
public void playSound(Sound sound, float volume, float pitch) {
if (player != null && sound != null) {
player.playSound(player.getLocation(), sound, volume, pitch);
}
}
/**
* Envoie un message formaté avec MiniMessage au joueur.
*
* @param miniMessage Le texte au format MiniMessage.
*/
public void reply(String miniMessage) {
if (player != null && miniMessage != null) {
player.sendMessage(MiniMessage.miniMessage().deserialize(miniMessage));
}
}
/**
* Envoie un message de succès préfixé en vert.
*/
public void replySuccess(String miniMessage) {
reply("<green>✔ </green>" + (miniMessage != null ? miniMessage : ""));
}
/**
* Envoie un message d'erreur préfixé en rouge.
*/
public void replyError(String miniMessage) {
reply("<red>✖ </red>" + (miniMessage != null ? miniMessage : ""));
}
/**
* Envoie un message d'information préfixé en bleu clair.
*/
public void replyInfo(String miniMessage) {
reply("<aqua> </aqua>" + (miniMessage != null ? miniMessage : ""));
}
/**
* Envoie un composant Kyori Adventure au joueur.
*
* @param component Le composant texte.
*/
public void reply(Component component) {
if (player != null && component != null) {
player.sendMessage(component);
}
}
}
@@ -0,0 +1,34 @@
package fr.luc.bettermcguis.api;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryCloseEvent;
import java.util.Objects;
/**
* Contexte transmis lors de la fermeture d'un GUI par un joueur.
*/
public class GuiCloseContext {
private final Player player;
private final Gui gui;
private final InventoryCloseEvent rawEvent;
public GuiCloseContext(Player player, Gui gui, InventoryCloseEvent rawEvent) {
this.player = Objects.requireNonNull(player, "player cannot be null");
this.gui = Objects.requireNonNull(gui, "gui cannot be null");
this.rawEvent = rawEvent;
}
public Player getPlayer() {
return player;
}
public Gui getGui() {
return gui;
}
public InventoryCloseEvent getRawEvent() {
return rawEvent;
}
}
@@ -0,0 +1,228 @@
package fr.luc.bettermcguis.api;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
/**
* Représente un item interactif ou décoratif placé dans un slot de GUI.
* Encapsule un {@link ItemStack}, des écouteurs de clics, des sons, des conditions de visibilité et des cooldowns.
*/
public class GuiItem {
private final ItemStack itemStack;
private GuiClickAction clickAction;
private Predicate<Player> visibilityCondition;
private boolean closeOnClick = false;
private boolean cancelClick = true;
private Sound clickSound;
private float soundVolume = 1.0f;
private float soundPitch = 1.0f;
private Duration clickCooldown = Duration.ZERO;
private final Map<UUID, Instant> cooldowns = new ConcurrentHashMap<>();
/**
* Crée un GuiItem à partir d'un {@link ItemStack}.
*
* @param itemStack L'item Minecraft sous-jacent.
*/
public GuiItem(ItemStack itemStack) {
this.itemStack = itemStack != null ? itemStack.clone() : new ItemStack(Material.AIR);
}
/**
* Crée un GuiItem avec une action de clic associée.
*
* @param itemStack L'item sous-jacent.
* @param clickAction L'action exécutée lors du clic.
*/
public GuiItem(ItemStack itemStack, GuiClickAction clickAction) {
this(itemStack);
this.clickAction = clickAction;
}
/**
* Fabrique statique à partir d'un {@link ItemStack}.
*/
public static GuiItem of(ItemStack itemStack) {
return new GuiItem(itemStack);
}
/**
* Fabrique statique avec action de clic.
*/
public static GuiItem of(ItemStack itemStack, GuiClickAction clickAction) {
return new GuiItem(itemStack, clickAction);
}
/**
* Fabrique statique à partir d'un matériau simple.
*/
public static GuiItem of(Material material) {
return new GuiItem(new ItemStack(material));
}
/**
* Fabrique statique pour un item décoratif de remplissage.
*/
public static GuiItem filler(Material material) {
ItemStack item = new ItemStack(material);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName(" ");
item.setItemMeta(meta);
}
return new GuiItem(item);
}
/**
* Fabrique statique pour un item vide (AIR).
*/
public static GuiItem empty() {
return new GuiItem(new ItemStack(Material.AIR));
}
/**
* Définit l'action de clic pour cet item.
*
* @param clickAction L'action de clic.
* @return Cette instance de {@link GuiItem} pour chaînage.
*/
public GuiItem onClick(GuiClickAction clickAction) {
this.clickAction = clickAction;
return this;
}
/**
* Conditionne la visibilité de cet item selon le joueur visualisant l'inventaire.
*
* @param condition Le prédicat testant le joueur.
* @return Cette instance.
*/
public GuiItem visibleIf(Predicate<Player> condition) {
this.visibilityCondition = condition;
return this;
}
/**
* Ferme l'inventaire automatiquement dès que le joueur clique sur cet item.
*
* @return Cette instance.
*/
public GuiItem closeOnClick() {
this.closeOnClick = true;
return this;
}
/**
* Joue un son au joueur lors du clic.
*
* @param sound Le son Bukkit.
* @param volume Le volume.
* @param pitch La tonalité.
* @return Cette instance.
*/
public GuiItem sound(Sound sound, float volume, float pitch) {
this.clickSound = sound;
this.soundVolume = volume;
this.soundPitch = pitch;
return this;
}
/**
* Joue un son avec volume et pitch par défaut (1.0f).
*/
public GuiItem sound(Sound sound) {
return sound(sound, 1.0f, 1.0f);
}
/**
* Définit un cooldown entre chaque clic pour un joueur.
*
* @param cooldown La durée de cooldown.
* @return Cette instance.
*/
public GuiItem cooldown(Duration cooldown) {
this.clickCooldown = cooldown != null ? cooldown : Duration.ZERO;
return this;
}
/**
* Indique si l'événement Bukkit de clic doit être annulé (empêche de prendre l'item).
*
* @param cancel true pour annuler (comportement par défaut).
* @return Cette instance.
*/
public GuiItem cancelClick(boolean cancel) {
this.cancelClick = cancel;
return this;
}
/**
* Gère le déclenchement de l'action de clic pour un contexte donné.
*
* @param context Le contexte de clic.
*/
public void handleClick(GuiClickContext context) {
if (cancelClick) {
context.setCancelled(true);
}
Player player = context.getPlayer();
if (player != null && !clickCooldown.isZero()) {
Instant now = Instant.now();
Instant expire = cooldowns.get(player.getUniqueId());
if (expire != null && now.isBefore(expire)) {
// Cooldown actif
return;
}
cooldowns.put(player.getUniqueId(), now.plus(clickCooldown));
}
if (clickSound != null && player != null) {
player.playSound(player.getLocation(), clickSound, soundVolume, soundPitch);
}
if (clickAction != null) {
clickAction.execute(context);
}
if (closeOnClick && player != null) {
context.close();
}
}
/**
* @return L'{@link ItemStack} sous-jacent.
*/
public ItemStack getItemStack() {
return itemStack;
}
/**
* @return true si l'item est visible pour le joueur donné.
*/
public boolean isVisibleFor(Player player) {
if (visibilityCondition == null) {
return true;
}
return visibilityCondition.test(player);
}
/**
* @return L'action de clic attachée.
*/
public GuiClickAction getClickAction() {
return clickAction;
}
}
@@ -0,0 +1,46 @@
package fr.luc.bettermcguis.api;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryOpenEvent;
import java.util.Objects;
/**
* Contexte transmis lors de l'ouverture d'un GUI.
*/
public class GuiOpenContext {
private final Player player;
private final Gui gui;
private final InventoryOpenEvent rawEvent;
private boolean cancelled = false;
public GuiOpenContext(Player player, Gui gui, InventoryOpenEvent rawEvent) {
this.player = Objects.requireNonNull(player, "player cannot be null");
this.gui = Objects.requireNonNull(gui, "gui cannot be null");
this.rawEvent = rawEvent;
}
public Player getPlayer() {
return player;
}
public Gui getGui() {
return gui;
}
public InventoryOpenEvent getRawEvent() {
return rawEvent;
}
public boolean isCancelled() {
return cancelled;
}
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
if (rawEvent != null) {
rawEvent.setCancelled(cancelled);
}
}
}
@@ -0,0 +1,81 @@
package fr.luc.bettermcguis.api;
import org.bukkit.event.inventory.InventoryType;
import java.util.Objects;
/**
* Définit le type et la taille d'un inventaire GUI (Coffre 1 à 6 lignes, Hopper, Distributeur, etc.).
*/
public enum GuiType {
CHEST_1_ROW(9, InventoryType.CHEST, 1),
CHEST_2_ROWS(18, InventoryType.CHEST, 2),
CHEST_3_ROWS(27, InventoryType.CHEST, 3),
CHEST_4_ROWS(36, InventoryType.CHEST, 4),
CHEST_5_ROWS(45, InventoryType.CHEST, 5),
CHEST_6_ROWS(54, InventoryType.CHEST, 6),
HOPPER(5, InventoryType.HOPPER, 1),
DISPENSER(9, InventoryType.DISPENSER, 3),
DROPPER(9, InventoryType.DROPPER, 3),
ANVIL(3, InventoryType.ANVIL, 1),
WORKBENCH(10, InventoryType.WORKBENCH, 3),
BREWING(5, InventoryType.BREWING, 1);
private final int size;
private final InventoryType bukkitType;
private final int rows;
GuiType(int size, InventoryType bukkitType, int rows) {
this.size = size;
this.bukkitType = bukkitType;
this.rows = rows;
}
/**
* @return Le nombre total de slots de l'inventaire.
*/
public int getSize() {
return size;
}
/**
* @return Le type d'inventaire Bukkit correspondant.
*/
public InventoryType getBukkitType() {
return bukkitType;
}
/**
* @return Le nombre de lignes de l'inventaire.
*/
public int getRows() {
return rows;
}
/**
* @return Le nombre de colonnes de l'inventaire (9 pour un coffre, 5 pour hopper, 3 pour dispenser).
*/
public int getColumns() {
return size / rows;
}
/**
* Retourne le type de coffre correspondant au nombre de lignes demandé (1 à 6).
*
* @param rows Le nombre de lignes (1 à 6).
* @return Le type {@link GuiType}.
* @throws IllegalArgumentException si rows n'est pas compris entre 1 et 6.
*/
public static GuiType chest(int rows) {
return switch (rows) {
case 1 -> CHEST_1_ROW;
case 2 -> CHEST_2_ROWS;
case 3 -> CHEST_3_ROWS;
case 4 -> CHEST_4_ROWS;
case 5 -> CHEST_5_ROWS;
case 6 -> CHEST_6_ROWS;
default -> throw new IllegalArgumentException("Nombre de lignes de coffre invalide : " + rows + " (attendu: 1-6)");
};
}
}
@@ -0,0 +1,92 @@
package fr.luc.bettermcguis.api.slot;
import java.util.Objects;
/**
* Représente la position d'un slot dans un inventaire GUI sous forme de coordonnées (ligne, colonne) en index 0.
*/
public class SlotPos {
private final int row;
private final int column;
/**
* Crée une position de slot à partir d'une ligne et d'une colonne (0-indexés).
*
* @param row La ligne (0 à rows - 1).
* @param column La colonne (0 à cols - 1).
*/
public SlotPos(int row, int column) {
if (row < 0 || column < 0) {
throw new IllegalArgumentException("Les coordonnées de slot ne peuvent pas être négatives : (" + row + ", " + column + ")");
}
this.row = row;
this.column = column;
}
/**
* Crée une instance de {@link SlotPos}.
*
* @param row La ligne (0-indexée).
* @param column La colonne (0-indexée).
* @return La position correspondante.
*/
public static SlotPos of(int row, int column) {
return new SlotPos(row, column);
}
/**
* Calcule la position (ligne, colonne) à partir d'un slot brut absolu et du nombre de colonnes par ligne.
*
* @param slot L'index brut du slot (0-indexé).
* @param columns Le nombre de colonnes (ex: 9 pour un coffre).
* @return La position {@link SlotPos}.
*/
public static SlotPos fromSlot(int slot, int columns) {
if (slot < 0 || columns <= 0) {
throw new IllegalArgumentException("Slot ou nombre de colonnes invalide : slot=" + slot + ", cols=" + columns);
}
return new SlotPos(slot / columns, slot % columns);
}
/**
* @return L'index de ligne (0-indexé).
*/
public int getRow() {
return row;
}
/**
* @return L'index de colonne (0-indexé).
*/
public int getColumn() {
return column;
}
/**
* Convertit cette position (ligne, colonne) en index de slot absolu (0-indexé).
*
* @param columns Le nombre de colonnes de l'inventaire (ex: 9 pour un coffre).
* @return Le numéro de slot absolu.
*/
public int toSlot(int columns) {
return (row * columns) + column;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SlotPos slotPos)) return false;
return row == slotPos.row && column == slotPos.column;
}
@Override
public int hashCode() {
return Objects.hash(row, column);
}
@Override
public String toString() {
return "SlotPos(row=" + row + ", col=" + column + ")";
}
}
@@ -0,0 +1,97 @@
package fr.luc.bettermcguis.api.slot;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.IntStream;
/**
* Représente un ensemble ou une plage de slots dans un inventaire GUI.
*/
public class SlotRange {
private final List<Integer> slots;
/**
* Crée une plage avec une liste de slots.
*
* @param slots Les numéros de slots.
*/
public SlotRange(List<Integer> slots) {
this.slots = new ArrayList<>(slots);
}
/**
* Crée une plage continue de slots entre {@code from} et {@code to} inclus.
*
* @param from Slot de début (inclus).
* @param to Slot de fin (inclus).
* @return La plage correspondante.
*/
public static SlotRange of(int from, int to) {
List<Integer> list = new ArrayList<>();
int start = Math.min(from, to);
int end = Math.max(from, to);
for (int i = start; i <= end; i++) {
list.add(i);
}
return new SlotRange(list);
}
/**
* Crée une plage à partir d'une liste explicite de slots.
*
* @param slots Les slots.
* @return La plage correspondante.
*/
public static SlotRange ofSlots(int... slots) {
List<Integer> list = new ArrayList<>();
for (int s : slots) {
list.add(s);
}
return new SlotRange(list);
}
/**
* Crée une plage correspondant aux bordures extérieures d'un inventaire.
*
* @param rows Le nombre de lignes.
* @param cols Le nombre de colonnes (ex: 9).
* @return La plage des slots de bordure.
*/
public static SlotRange border(int rows, int cols) {
List<Integer> list = new ArrayList<>();
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (r == 0 || r == rows - 1 || c == 0 || c == cols - 1) {
list.add(r * cols + c);
}
}
}
return new SlotRange(list);
}
/**
* Crée une plage correspondant à l'intérieur (sans les bordures) d'un inventaire.
*
* @param rows Le nombre de lignes.
* @param cols Le nombre de colonnes.
* @return La plage des slots intérieurs.
*/
public static SlotRange interior(int rows, int cols) {
List<Integer> list = new ArrayList<>();
for (int r = 1; r < rows - 1; r++) {
for (int c = 1; c < cols - 1; c++) {
list.add(r * cols + c);
}
}
return new SlotRange(list);
}
/**
* @return La liste non modifiable des slots de cette plage.
*/
public List<Integer> getSlots() {
return Collections.unmodifiableList(slots);
}
}
@@ -0,0 +1,96 @@
package fr.luc.bettermcguis.builder;
import fr.luc.bettermcguis.animation.Frame;
import fr.luc.bettermcguis.api.GuiType;
import fr.luc.bettermcguis.type.AnimatedGui;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* Constructeur fluide pour concevoir des inventaires animés ({@link AnimatedGui}).
*/
public class AnimatedGuiBuilder extends GuiBuilder {
private final List<Frame> frames = new ArrayList<>();
private boolean loop = true;
public AnimatedGuiBuilder() {}
@Override
public AnimatedGuiBuilder title(String miniMessage) {
super.title(miniMessage);
return this;
}
@Override
public AnimatedGuiBuilder type(GuiType type) {
super.type(type);
return this;
}
@Override
public AnimatedGuiBuilder rows(int rows) {
super.rows(rows);
return this;
}
/**
* Ajoute une frame à l'animation.
*/
public AnimatedGuiBuilder frame(Frame frame) {
this.frames.add(frame);
return this;
}
/**
* Ajoute une frame configurée via une fonction lambda.
*/
public AnimatedGuiBuilder frame(Consumer<Frame> frameConfig) {
Frame f = new Frame();
if (frameConfig != null) {
frameConfig.accept(f);
}
return frame(f);
}
/**
* Active ou désactive la lecture en boucle.
*/
public AnimatedGuiBuilder loop(boolean loop) {
this.loop = loop;
return this;
}
@Override
public AnimatedGui build() {
AnimatedGui gui = new AnimatedGui(title, type);
for (var entry : items.entrySet()) {
gui.setItem(entry.getKey(), entry.getValue());
}
for (var pattern : patterns) {
gui.applyPattern(pattern);
}
for (int s : editableSlots) {
gui.setEditable(s, true);
}
for (var entry : properties.entrySet()) {
gui.setProperty(entry.getKey(), entry.getValue());
}
for (var hook : openHooks) gui.onOpen(hook);
for (var hook : closeHooks) gui.onClose(hook);
for (var hook : clickHooks) gui.onClick(hook);
for (var hook : outsideClickHooks) gui.onOutsideClick(hook);
for (var hook : bottomClickHooks) gui.onBottomClick(hook);
for (Frame frame : frames) {
gui.addFrame(frame);
}
gui.loop(loop);
return gui;
}
}
@@ -0,0 +1,238 @@
package fr.luc.bettermcguis.builder;
import fr.luc.bettermcguis.api.*;
import fr.luc.bettermcguis.api.slot.SlotPos;
import fr.luc.bettermcguis.api.slot.SlotRange;
import fr.luc.bettermcguis.pattern.GuiPattern;
import fr.luc.bettermcguis.type.SimpleGui;
import net.kyori.adventure.text.Component;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import java.util.*;
import java.util.function.Consumer;
/**
* Constructeur fluide pour concevoir des menus d'inventaires interactifs standards ({@link SimpleGui}).
*/
public class GuiBuilder {
protected String title = "Menu";
protected GuiType type = GuiType.CHEST_3_ROWS;
protected final Map<Integer, GuiItem> items = new HashMap<>();
protected final Set<Integer> editableSlots = new HashSet<>();
protected final Map<String, Object> properties = new HashMap<>();
protected final List<GuiPattern> patterns = new ArrayList<>();
protected final List<Consumer<GuiOpenContext>> openHooks = new ArrayList<>();
protected final List<Consumer<GuiCloseContext>> closeHooks = new ArrayList<>();
protected final List<Consumer<GuiClickContext>> clickHooks = new ArrayList<>();
protected final List<Consumer<GuiClickContext>> outsideClickHooks = new ArrayList<>();
protected final List<Consumer<GuiClickContext>> bottomClickHooks = new ArrayList<>();
public GuiBuilder() {}
/**
* Définit le titre du GUI au format MiniMessage.
*/
public GuiBuilder title(String miniMessage) {
this.title = miniMessage;
return this;
}
/**
* Définit le type et les dimensions du GUI.
*/
public GuiBuilder type(GuiType type) {
this.type = Objects.requireNonNull(type, "type cannot be null");
return this;
}
/**
* Définit un inventaire de type coffre avec le nombre de lignes spécifié (1 à 6).
*/
public GuiBuilder rows(int rows) {
this.type = GuiType.chest(rows);
return this;
}
/**
* Place un {@link GuiItem} dans un slot absolu (0-indexé).
*/
public GuiBuilder item(int slot, GuiItem item) {
this.items.put(slot, item);
return this;
}
/**
* Place un {@link ItemStack} dans un slot avec une action optionnelle.
*/
public GuiBuilder item(int slot, ItemStack item, GuiClickAction action) {
return item(slot, GuiItem.of(item, action));
}
/**
* Place un item à des coordonnées (ligne, colonne) en 0-indexé.
*/
public GuiBuilder item(int row, int col, GuiItem item) {
return item(row * type.getColumns() + col, item);
}
/**
* Place un item à une position {@link SlotPos}.
*/
public GuiBuilder item(SlotPos pos, GuiItem item) {
if (pos != null) {
item(pos.toSlot(type.getColumns()), item);
}
return this;
}
/**
* Remplit les bordures extérieures avec un item.
*/
public GuiBuilder fillBorder(GuiItem item) {
for (int slot : SlotRange.border(type.getRows(), type.getColumns()).getSlots()) {
this.items.put(slot, item);
}
return this;
}
/**
* Remplit les bordures extérieures avec un matériau de vitrage.
*/
public GuiBuilder fillBorder(Material material) {
return fillBorder(GuiItem.filler(material));
}
/**
* Remplit une plage de slots.
*/
public GuiBuilder fillRange(SlotRange range, GuiItem item) {
if (range != null) {
for (int slot : range.getSlots()) {
this.items.put(slot, item);
}
}
return this;
}
/**
* Applique un motif / masque ASCII {@link GuiPattern}.
*/
public GuiBuilder pattern(GuiPattern pattern) {
if (pattern != null) {
this.patterns.add(pattern);
}
return this;
}
/**
* Construit et applique un motif via une fonction de configuration de {@link PatternBuilder}.
*/
public GuiBuilder pattern(Consumer<PatternBuilder> patternConfig) {
if (patternConfig != null) {
PatternBuilder builder = new PatternBuilder();
patternConfig.accept(builder);
pattern(builder.build());
}
return this;
}
/**
* Marque un ou plusieurs slots comme éditables par les joueurs.
*/
public GuiBuilder editable(int... slots) {
for (int s : slots) {
this.editableSlots.add(s);
}
return this;
}
/**
* Marque une plage de slots comme éditables.
*/
public GuiBuilder editable(SlotRange range) {
if (range != null) {
this.editableSlots.addAll(range.getSlots());
}
return this;
}
/**
* Définit une propriété personnalisée.
*/
public GuiBuilder property(String key, Object value) {
this.properties.put(key, value);
return this;
}
/**
* Hook exécuté à l'ouverture du GUI.
*/
public GuiBuilder onOpen(Consumer<GuiOpenContext> hook) {
this.openHooks.add(hook);
return this;
}
/**
* Hook exécuté à la fermeture du GUI.
*/
public GuiBuilder onClose(Consumer<GuiCloseContext> hook) {
this.closeHooks.add(hook);
return this;
}
/**
* Hook exécuté lors d'un clic dans le GUI.
*/
public GuiBuilder onClick(Consumer<GuiClickContext> hook) {
this.clickHooks.add(hook);
return this;
}
/**
* Hook exécuté lors d'un clic extérieur.
*/
public GuiBuilder onOutsideClick(Consumer<GuiClickContext> hook) {
this.outsideClickHooks.add(hook);
return this;
}
/**
* Hook exécuté lors d'un clic dans l'inventaire du joueur.
*/
public GuiBuilder onBottomClick(Consumer<GuiClickContext> hook) {
this.bottomClickHooks.add(hook);
return this;
}
/**
* Construit l'instance finale de {@link SimpleGui}.
*
* @return L'instance configurée de {@link SimpleGui}.
*/
public SimpleGui build() {
SimpleGui gui = new SimpleGui(title, type);
for (Map.Entry<Integer, GuiItem> entry : items.entrySet()) {
gui.setItem(entry.getKey(), entry.getValue());
}
for (GuiPattern pattern : patterns) {
gui.applyPattern(pattern);
}
for (int s : editableSlots) {
gui.setEditable(s, true);
}
for (Map.Entry<String, Object> entry : properties.entrySet()) {
gui.setProperty(entry.getKey(), entry.getValue());
}
for (var hook : openHooks) gui.onOpen(hook);
for (var hook : closeHooks) gui.onClose(hook);
for (var hook : clickHooks) gui.onClick(hook);
for (var hook : outsideClickHooks) gui.onOutsideClick(hook);
for (var hook : bottomClickHooks) gui.onBottomClick(hook);
return gui;
}
}
@@ -0,0 +1,369 @@
package fr.luc.bettermcguis.builder;
import fr.luc.bettermcguis.api.GuiClickAction;
import fr.luc.bettermcguis.api.GuiItem;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.LeatherArmorMeta;
import org.bukkit.inventory.meta.PotionMeta;
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.potion.PotionType;
import java.util.*;
import java.util.stream.Collectors;
/**
* Constructeur moderne et fluide d'{@link ItemStack} supportant Kyori Adventure, MiniMessage,
* têtes personnalisées, armures teintées, flags et PersistentDataContainer.
*/
public class ItemBuilder {
private final ItemStack itemStack;
private final ItemMeta meta;
/**
* Initialise le builder avec un matériau de base.
*
* @param material Le matériau Bukkit.
*/
public ItemBuilder(Material material) {
this(new ItemStack(Objects.requireNonNull(material, "material cannot be null")));
}
/**
* Initialise le builder à partir d'un {@link ItemStack} existant.
*
* @param itemStack L'item à cloner et modifier.
*/
public ItemBuilder(ItemStack itemStack) {
Objects.requireNonNull(itemStack, "itemStack cannot be null");
this.itemStack = itemStack.clone();
this.meta = this.itemStack.getItemMeta();
}
/**
* Fabrique statique pour un matériau donné.
*/
public static ItemBuilder of(Material material) {
return new ItemBuilder(material);
}
/**
* Fabrique statique pour un {@link ItemStack}.
*/
public static ItemBuilder of(ItemStack itemStack) {
return new ItemBuilder(itemStack);
}
/**
* Fabrique statique pour une tête de joueur (PLAYER_HEAD).
*/
public static ItemBuilder skull() {
return new ItemBuilder(Material.PLAYER_HEAD);
}
/**
* Fabrique statique pour un item décoratif de vitrage ou panneau.
*/
public static ItemBuilder filler(Material material) {
return of(material).name(" ");
}
/**
* Définit la quantité d'items dans la pile (1 à 64).
*
* @param amount Le montant.
* @return Ce builder pour chaînage.
*/
public ItemBuilder amount(int amount) {
this.itemStack.setAmount(Math.max(1, Math.min(64, amount)));
return this;
}
/**
* Définit le nom d'affichage au format MiniMessage.
*
* @param miniMessage Le texte formaté en MiniMessage.
* @return Ce builder.
*/
public ItemBuilder name(String miniMessage) {
if (meta != null && miniMessage != null) {
Component component = MiniMessage.miniMessage().deserialize(miniMessage);
meta.displayName(component);
}
return this;
}
/**
* Définit le nom d'affichage avec un {@link Component} Adventure natif.
*
* @param component Le composant texte.
* @return Ce builder.
*/
public ItemBuilder name(Component component) {
if (meta != null && component != null) {
meta.displayName(component);
}
return this;
}
/**
* Définit le lore complet à partir de lignes MiniMessage.
*
* @param lines Les lignes de lore au format MiniMessage.
* @return Ce builder.
*/
public ItemBuilder lore(String... lines) {
return lore(Arrays.asList(lines));
}
/**
* Définit le lore complet à partir d'une liste de chaînes MiniMessage.
*
* @param lines La liste des lignes.
* @return Ce builder.
*/
public ItemBuilder lore(List<String> lines) {
if (meta != null && lines != null) {
List<Component> components = lines.stream()
.map(line -> MiniMessage.miniMessage().deserialize(line))
.collect(Collectors.toList());
meta.lore(components);
}
return this;
}
/**
* Définit le lore à partir d'une liste de composants Adventure.
*
* @param components La liste des composants.
* @return Ce builder.
*/
public ItemBuilder loreComponents(List<Component> components) {
if (meta != null && components != null) {
meta.lore(components);
}
return this;
}
/**
* Ajoute une ou plusieurs lignes au lore existant.
*
* @param lines Les lignes à ajouter.
* @return Ce builder.
*/
public ItemBuilder addLore(String... lines) {
if (meta != null && lines != null) {
List<Component> current = meta.lore();
if (current == null) {
current = new ArrayList<>();
} else {
current = new ArrayList<>(current);
}
for (String line : lines) {
current.add(MiniMessage.miniMessage().deserialize(line));
}
meta.lore(current);
}
return this;
}
/**
* Ajoute un enchantement.
*
* @param enchantment L'enchantement.
* @param level Le niveau.
* @return Ce builder.
*/
public ItemBuilder enchant(Enchantment enchantment, int level) {
if (meta != null && enchantment != null) {
meta.addEnchant(enchantment, level, true);
}
return this;
}
/**
* Donne un effet brillant (lueur d'enchantement) sans afficher de texte d'enchantement.
*
* @param glowing true pour activer la brillance.
* @return Ce builder.
*/
public ItemBuilder glowing(boolean glowing) {
if (glowing) {
enchant(Enchantment.LUCK, 1);
flags(ItemFlag.HIDE_ENCHANTS);
}
return this;
}
/**
* Ajoute des drapeaux d'item (ItemFlags).
*
* @param flags Les drapeaux.
* @return Ce builder.
*/
public ItemBuilder flags(ItemFlag... flags) {
if (meta != null && flags != null) {
meta.addItemFlags(flags);
}
return this;
}
/**
* Masque tous les attributs, enchantements et effets de potion de l'item.
*
* @return Ce builder.
*/
public ItemBuilder hideAll() {
if (meta != null) {
meta.addItemFlags(ItemFlag.values());
}
return this;
}
/**
* Définit le CustomModelData de l'item.
*
* @param customModelData L'identifiant numérique de modèle 3D.
* @return Ce builder.
*/
public ItemBuilder customModelData(int customModelData) {
if (meta != null) {
meta.setCustomModelData(customModelData);
}
return this;
}
/**
* Marque l'item comme incassable.
*
* @param unbreakable true pour incassable.
* @return Ce builder.
*/
public ItemBuilder unbreakable(boolean unbreakable) {
if (meta != null) {
meta.setUnbreakable(unbreakable);
}
return this;
}
/**
* Configure le propriétaire d'une tête de joueur par son pseudo.
*
* @param playerName Le nom du joueur.
* @return Ce builder.
*/
public ItemBuilder skullOwner(String playerName) {
if (meta instanceof SkullMeta skullMeta && playerName != null) {
skullMeta.setOwner(playerName);
}
return this;
}
/**
* Configure le propriétaire d'une tête par UUID.
*
* @param uuid L'UUID du joueur.
* @return Ce builder.
*/
public ItemBuilder skullOwner(UUID uuid) {
if (meta instanceof SkullMeta skullMeta && uuid != null) {
skullMeta.setOwningPlayer(Bukkit.getOfflinePlayer(uuid));
}
return this;
}
/**
* Définit la couleur d'une pièce d'armure en cuir.
*
* @param color La couleur Bukkit.
* @return Ce builder.
*/
public ItemBuilder color(Color color) {
if (meta instanceof LeatherArmorMeta leatherMeta && color != null) {
leatherMeta.setColor(color);
}
return this;
}
/**
* Définit la couleur RGB d'une armure en cuir.
*
* @param red 0-255
* @param green 0-255
* @param blue 0-255
* @return Ce builder.
*/
public ItemBuilder color(int red, int green, int blue) {
return color(Color.fromRGB(red, green, blue));
}
/**
* Stocke une donnée texte dans le PersistentDataContainer.
*
* @param key La clé NamespacedKey.
* @param value La valeur texte.
* @return Ce builder.
*/
public ItemBuilder pdcString(NamespacedKey key, String value) {
if (meta != null && key != null && value != null) {
meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, value);
}
return this;
}
/**
* Stocke un entier dans le PersistentDataContainer.
*
* @param key La clé NamespacedKey.
* @param value La valeur entière.
* @return Ce builder.
*/
public ItemBuilder pdcInt(NamespacedKey key, int value) {
if (meta != null && key != null) {
meta.getPersistentDataContainer().set(key, PersistentDataType.INTEGER, value);
}
return this;
}
/**
* Construit et retourne l'{@link ItemStack} configuré.
*
* @return Le nouvel ItemStack.
*/
public ItemStack build() {
ItemStack item = itemStack.clone();
if (meta != null) {
item.setItemMeta(meta);
}
return item;
}
/**
* Convertit directement ce builder en {@link GuiItem} statique.
*
* @return L'instance de {@link GuiItem}.
*/
public GuiItem asGuiItem() {
return GuiItem.of(build());
}
/**
* Convertit directement ce builder en {@link GuiItem} avec une action de clic.
*
* @param clickAction L'action de clic à exécuter.
* @return L'instance de {@link GuiItem}.
*/
public GuiItem asGuiItem(GuiClickAction clickAction) {
return GuiItem.of(build(), clickAction);
}
}
@@ -0,0 +1,199 @@
package fr.luc.bettermcguis.builder;
import fr.luc.bettermcguis.api.GuiItem;
import fr.luc.bettermcguis.api.GuiType;
import fr.luc.bettermcguis.api.slot.SlotRange;
import fr.luc.bettermcguis.event.GuiPageChangeEvent;
import fr.luc.bettermcguis.pattern.GuiMask;
import fr.luc.bettermcguis.type.PaginatedGui;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Consumer;
/**
* Constructeur fluide pour concevoir des inventaires paginés ({@link PaginatedGui}).
*/
public class PaginatedGuiBuilder extends GuiBuilder {
private final List<GuiItem> pageItems = new ArrayList<>();
private final List<Integer> itemSlots = new ArrayList<>();
private Integer previousPageSlot;
private Integer nextPageSlot;
private Integer pageIndicatorSlot;
private GuiItem customPreviousButton;
private GuiItem customNextButton;
private BiFunction<Integer, Integer, GuiItem> pageIndicatorSupplier;
private final List<Consumer<GuiPageChangeEvent>> pageChangeHooks = new ArrayList<>();
public PaginatedGuiBuilder() {}
@Override
public PaginatedGuiBuilder title(String miniMessage) {
super.title(miniMessage);
return this;
}
@Override
public PaginatedGuiBuilder type(GuiType type) {
super.type(type);
return this;
}
@Override
public PaginatedGuiBuilder rows(int rows) {
super.rows(rows);
return this;
}
@Override
public PaginatedGuiBuilder item(int slot, GuiItem item) {
super.item(slot, item);
return this;
}
@Override
public PaginatedGuiBuilder fillBorder(GuiItem item) {
super.fillBorder(item);
return this;
}
@Override
public PaginatedGuiBuilder fillBorder(org.bukkit.Material material) {
super.fillBorder(material);
return this;
}
/**
* Ajoute un item paginé.
*/
public PaginatedGuiBuilder addPageItem(GuiItem item) {
this.pageItems.add(item);
return this;
}
/**
* Ajoute une collection complète d'items paginés.
*/
public PaginatedGuiBuilder addPageItems(Collection<GuiItem> items) {
if (items != null) {
this.pageItems.addAll(items);
}
return this;
}
/**
* Définit les slots réservés pour les items paginés.
*/
public PaginatedGuiBuilder itemSlots(List<Integer> slots) {
this.itemSlots.clear();
if (slots != null) {
this.itemSlots.addAll(slots);
}
return this;
}
/**
* Définit les slots d'items à partir d'une plage {@link SlotRange}.
*/
public PaginatedGuiBuilder itemSlots(SlotRange range) {
if (range != null) {
itemSlots(range.getSlots());
}
return this;
}
/**
* Définit les slots d'items à partir d'un masque {@link GuiMask}.
*/
public PaginatedGuiBuilder itemSlots(GuiMask mask) {
if (mask != null) {
itemSlots(mask.resolveSlots(type.getRows(), type.getColumns()));
}
return this;
}
/**
* Configure le bouton de page précédente.
*/
public PaginatedGuiBuilder previousButton(int slot, GuiItem item) {
this.previousPageSlot = slot;
this.customPreviousButton = item;
return this;
}
/**
* Configure le bouton de page suivante.
*/
public PaginatedGuiBuilder nextButton(int slot, GuiItem item) {
this.nextPageSlot = slot;
this.customNextButton = item;
return this;
}
/**
* Configure l'indicateur de page.
*/
public PaginatedGuiBuilder pageIndicator(int slot, BiFunction<Integer, Integer, GuiItem> supplier) {
this.pageIndicatorSlot = slot;
this.pageIndicatorSupplier = supplier;
return this;
}
/**
* Hook déclenché lors du changement de page.
*/
public PaginatedGuiBuilder onPageChange(Consumer<GuiPageChangeEvent> hook) {
if (hook != null) {
this.pageChangeHooks.add(hook);
}
return this;
}
@Override
public PaginatedGui build() {
PaginatedGui gui = new PaginatedGui(title, type);
for (var entry : items.entrySet()) {
gui.setItem(entry.getKey(), entry.getValue());
}
for (var pattern : patterns) {
gui.applyPattern(pattern);
}
for (int s : editableSlots) {
gui.setEditable(s, true);
}
for (var entry : properties.entrySet()) {
gui.setProperty(entry.getKey(), entry.getValue());
}
for (var hook : openHooks) gui.onOpen(hook);
for (var hook : closeHooks) gui.onClose(hook);
for (var hook : clickHooks) gui.onClick(hook);
for (var hook : outsideClickHooks) gui.onOutsideClick(hook);
for (var hook : bottomClickHooks) gui.onBottomClick(hook);
if (!itemSlots.isEmpty()) {
gui.setItemSlots(itemSlots);
}
if (previousPageSlot != null) {
gui.setPreviousPageButton(previousPageSlot, customPreviousButton);
}
if (nextPageSlot != null) {
gui.setNextPageButton(nextPageSlot, customNextButton);
}
if (pageIndicatorSlot != null) {
gui.setPageIndicator(pageIndicatorSlot, pageIndicatorSupplier);
}
gui.addPageItems(pageItems);
for (var hook : pageChangeHooks) {
gui.onPageChange(hook);
}
return gui;
}
}
@@ -0,0 +1,75 @@
package fr.luc.bettermcguis.builder;
import fr.luc.bettermcguis.api.GuiItem;
import fr.luc.bettermcguis.pattern.GuiPattern;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Constructeur fluide pour concevoir des motifs de placement ASCII ({@link GuiPattern}).
*/
public class PatternBuilder {
private final List<String> lines = new ArrayList<>();
private final Map<Character, GuiItem> bindings = new HashMap<>();
public PatternBuilder() {}
public PatternBuilder(String... lines) {
lines(lines);
}
/**
* Définit les lignes du motif ASCII.
*/
public PatternBuilder lines(String... lines) {
if (lines != null) {
this.lines.addAll(Arrays.asList(lines));
}
return this;
}
/**
* Associe un caractère à un {@link GuiItem}.
*/
public PatternBuilder bind(char character, GuiItem item) {
this.bindings.put(character, item);
return this;
}
/**
* Associe un caractère à un {@link ItemStack}.
*/
public PatternBuilder bind(char character, ItemStack itemStack) {
return bind(character, GuiItem.of(itemStack));
}
/**
* Associe un caractère à un matériau Bukkit simple.
*/
public PatternBuilder bind(char character, Material material) {
return bind(character, GuiItem.of(material));
}
/**
* Associe un caractère à un item de remplissage (vitrage avec nom vide).
*/
public PatternBuilder bindFiller(char character, Material material) {
return bind(character, GuiItem.filler(material));
}
/**
* Construit l'instance finale de {@link GuiPattern}.
*/
public GuiPattern build() {
GuiPattern pattern = new GuiPattern(lines);
pattern.bindAll(bindings);
return pattern;
}
}
@@ -0,0 +1,95 @@
package fr.luc.bettermcguis.builder;
import fr.luc.bettermcguis.api.GuiItem;
import fr.luc.bettermcguis.api.GuiType;
import fr.luc.bettermcguis.type.TabbedGui;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
/**
* Constructeur fluide pour concevoir des inventaires à onglets ({@link TabbedGui}).
*/
public class TabbedGuiBuilder extends GuiBuilder {
private final Map<String, Integer> tabButtons = new LinkedHashMap<>();
private final Map<String, GuiItem> tabButtonItems = new LinkedHashMap<>();
private final Map<String, Consumer<TabbedGui>> tabInitializers = new LinkedHashMap<>();
public TabbedGuiBuilder() {}
@Override
public TabbedGuiBuilder title(String miniMessage) {
super.title(miniMessage);
return this;
}
@Override
public TabbedGuiBuilder type(GuiType type) {
super.type(type);
return this;
}
@Override
public TabbedGuiBuilder rows(int rows) {
super.rows(rows);
return this;
}
/**
* Enregistre un onglet avec son bouton et sa configuration d'items.
*
* @param tabId L'identifiant de l'onglet.
* @param buttonSlot Le slot du bouton d'onglet.
* @param buttonItem L'item visuel du bouton.
* @param tabConfig La configuration des items de cet onglet.
* @return Ce builder pour chaînage.
*/
public TabbedGuiBuilder tab(String tabId, int buttonSlot, GuiItem buttonItem, Consumer<TabbedGui> tabConfig) {
this.tabButtons.put(tabId, buttonSlot);
this.tabButtonItems.put(tabId, buttonItem);
if (tabConfig != null) {
this.tabInitializers.put(tabId, tabConfig);
}
return this;
}
@Override
public TabbedGui build() {
TabbedGui gui = new TabbedGui(title, type);
for (var entry : items.entrySet()) {
gui.setItem(entry.getKey(), entry.getValue());
}
for (var pattern : patterns) {
gui.applyPattern(pattern);
}
for (int s : editableSlots) {
gui.setEditable(s, true);
}
for (var entry : properties.entrySet()) {
gui.setProperty(entry.getKey(), entry.getValue());
}
for (var hook : openHooks) gui.onOpen(hook);
for (var hook : closeHooks) gui.onClose(hook);
for (var hook : clickHooks) gui.onClick(hook);
for (var hook : outsideClickHooks) gui.onOutsideClick(hook);
for (var hook : bottomClickHooks) gui.onBottomClick(hook);
for (var entry : tabButtons.entrySet()) {
String tabId = entry.getKey();
int slot = entry.getValue();
GuiItem item = tabButtonItems.get(tabId);
gui.addTab(tabId, slot, item);
Consumer<TabbedGui> init = tabInitializers.get(tabId);
if (init != null) {
init.accept(gui);
}
}
return gui;
}
}
@@ -0,0 +1,53 @@
package fr.luc.bettermcguis.demo;
import fr.luc.bettermcguis.BetterMcGuis;
import fr.luc.bettermcguis.demo.guis.DemoMainMenuGui;
import fr.luc.bettermcguis.demo.listeners.DemoGuiListener;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
/**
* Plugin de démonstration montrant l'initialisation de betterMcGuis,
* l'enregistrement d'écouteurs et l'ouverture de menus via commande.
*/
public class DemoGuiPlugin extends JavaPlugin implements CommandExecutor {
private BetterMcGuis guiManager;
@Override
public void onEnable() {
// Initialisation de betterMcGuis
this.guiManager = BetterMcGuis.create(this);
// Enregistrement des listeners d'audit
this.guiManager.registerListeners(new DemoGuiListener());
// Enregistrement d'une commande /menu pour ouvrir le GUI
if (getCommand("menu") != null) {
getCommand("menu").setExecutor(this);
}
getLogger().info("betterMcGuis DemoPlugin active avec succes !");
}
@Override
public void onDisable() {
if (guiManager != null) {
guiManager.unregisterAll();
}
getLogger().info("betterMcGuis DemoPlugin desactive.");
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (sender instanceof Player player) {
DemoMainMenuGui.create().open(player);
return true;
}
sender.sendMessage("Seul un joueur peut ouvrir ce menu.");
return true;
}
}
@@ -0,0 +1,65 @@
package fr.luc.bettermcguis.demo.guis;
import fr.luc.bettermcguis.BetterMcGuis;
import fr.luc.bettermcguis.api.Gui;
import fr.luc.bettermcguis.builder.ItemBuilder;
import org.bukkit.Material;
import org.bukkit.Sound;
/**
* Menu principal de démonstration illustrant la création d'un GUI interactif moderne.
*/
public class DemoMainMenuGui {
public static Gui create() {
return BetterMcGuis.builder()
.title("<gradient:#ff5e62:#ff9966><bold>Menu Principal Démo</bold></gradient>")
.rows(3)
.fillBorder(Material.BLACK_STAINED_GLASS_PANE)
// Bouton 1 : Boutique Paginée
.item(11, ItemBuilder.of(Material.EMERALD)
.name("<green><bold>Boutique du Serveur</bold></green>")
.lore(
"<gray>Parcourez notre catalogue d'items.</gray>",
"",
"<yellow>▶ Cliquez pour ouvrir la boutique</yellow>"
)
.glowing(true)
.asGuiItem(ctx -> {
ctx.playSound(Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.2f);
DemoPaginatedShopGui.create().open(ctx.getPlayer());
})
)
// Bouton 2 : Poubelle / Recyclage
.item(13, ItemBuilder.of(Material.CAULDRON)
.name("<red><bold>Poubelle / Débarras</bold></red>")
.lore(
"<gray>Déposez vos items indésirables ici.</gray>",
"",
"<yellow>▶ Cliquez pour ouvrir la poubelle</yellow>"
)
.asGuiItem(ctx -> {
ctx.playSound(Sound.BLOCK_CHEST_OPEN, 1.0f, 1.0f);
DemoStorageTrashGui.create().open(ctx.getPlayer());
})
)
// Bouton 3 : Profil & Statistiques
.item(15, ItemBuilder.skull()
.name("<aqua><bold>Votre Profil</bold></aqua>")
.lore(
"<gray>Consultez vos statistiques personnelles.</gray>",
"",
"<yellow>▶ Cliquez pour actualiser</yellow>"
)
.asGuiItem(ctx -> {
ctx.playSound(Sound.UI_BUTTON_CLICK, 1.0f, 1.0f);
ctx.replySuccess("Votre profil est à jour !");
})
)
.build();
}
}
@@ -0,0 +1,58 @@
package fr.luc.bettermcguis.demo.guis;
import fr.luc.bettermcguis.BetterMcGuis;
import fr.luc.bettermcguis.api.Gui;
import fr.luc.bettermcguis.builder.ItemBuilder;
import org.bukkit.Material;
import org.bukkit.Sound;
/**
* Menu de boutique paginé de démonstration avec items interactifs et boutons de navigation.
*/
public class DemoPaginatedShopGui {
public static Gui create() {
fr.luc.bettermcguis.builder.PaginatedGuiBuilder builder = BetterMcGuis.paginated()
.title("<gradient:#00c6ff:#0072ff><bold>Boutique Paginée</bold></gradient>")
.rows(5)
.fillBorder(Material.GRAY_STAINED_GLASS_PANE);
// Bouton retour au menu principal en bas à gauche
builder.item(36, ItemBuilder.of(Material.BARRIER)
.name("<red>Retour au Menu Principal</red>")
.asGuiItem(ctx -> {
ctx.playSound(Sound.UI_BUTTON_CLICK, 1.0f, 0.8f);
DemoMainMenuGui.create().open(ctx.getPlayer());
})
);
// Génère 50 articles dans le catalogue paginé
Material[] sampleMaterials = new Material[]{
Material.DIAMOND, Material.NETHERITE_INGOT, Material.GOLDEN_APPLE, Material.ENCHANTED_GOLDEN_APPLE,
Material.ELYTRA, Material.TOTEM_OF_UNDYING, Material.EXPERIENCE_BOTTLE, Material.BEACON,
Material.SHULKER_BOX, Material.NETHER_STAR, Material.ENDER_PEARL, Material.BLAZE_ROD,
Material.TRIDENT, Material.NETHERITE_SWORD, Material.BOW, Material.CROSSBOW
};
for (int i = 1; i <= 40; i++) {
Material mat = sampleMaterials[(i - 1) % sampleMaterials.length];
int price = i * 100;
int itemId = i;
builder.addPageItem(ItemBuilder.of(mat)
.name("<gold><bold>Article #" + itemId + "</bold></gold>")
.lore(
"<gray>Prix : <green>" + price + " $</green></gray>",
"",
"<yellow>▶ Clic-gauche pour acheter</yellow>"
)
.asGuiItem(ctx -> {
ctx.playSound(Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.5f);
ctx.replySuccess("Vous avez acheté <gold>Article #" + itemId + "</gold> pour <green>" + price + " $</green> !");
})
);
}
return builder.build();
}
}
@@ -0,0 +1,46 @@
package fr.luc.bettermcguis.demo.guis;
import fr.luc.bettermcguis.api.Gui;
import fr.luc.bettermcguis.api.GuiType;
import fr.luc.bettermcguis.api.slot.SlotRange;
import fr.luc.bettermcguis.builder.ItemBuilder;
import fr.luc.bettermcguis.type.StorageGui;
import org.bukkit.Material;
import org.bukkit.Sound;
/**
* Menu de poubelle / zone de dépôt de démonstration (StorageGui) permettant de détruire des items.
*/
public class DemoStorageTrashGui {
public static Gui create() {
StorageGui gui = new StorageGui("<gradient:#eb3349:#f45c43><bold>Poubelle Publique</bold></gradient>", GuiType.CHEST_4_ROWS);
// Remplit les bordures avec du vitrage rouge
gui.fillBorder(ItemBuilder.filler(Material.RED_STAINED_GLASS_PANE).asGuiItem());
// Définit l'intérieur comme zone de stockage éditable
gui.setStorageSlots(SlotRange.interior(4, 9));
gui.returnItemsOnClose(false); // Détruit définitivement les items à la fermeture
// Bouton de vidage immédiat en bas au centre
gui.setItem(31, ItemBuilder.of(Material.LAVA_BUCKET)
.name("<red><bold>Vider la Poubelle Immédiatement</bold></red>")
.lore(
"<gray>Détruit tous les objets placés dans la zone centrale.</gray>",
"",
"<yellow>▶ Cliquez pour incinérer les items</yellow>"
)
.asGuiItem(ctx -> {
ctx.playSound(Sound.BLOCK_LAVA_EXTINGUISH, 1.0f, 1.0f);
var topInv = ctx.getPlayer().getOpenInventory().getTopInventory();
for (int slot : SlotRange.interior(4, 9).getSlots()) {
topInv.setItem(slot, null);
}
ctx.replySuccess("La poubelle a été vidée et les items ont été incinérés !");
})
);
return gui;
}
}
@@ -0,0 +1,36 @@
package fr.luc.bettermcguis.demo.listeners;
import fr.luc.bettermcguis.event.GuiClickEvent;
import fr.luc.bettermcguis.event.GuiCloseEvent;
import fr.luc.bettermcguis.event.GuiOpenEvent;
import fr.luc.bettermcguis.event.GuiPageChangeEvent;
import fr.luc.bettermcguis.event.annotation.GuiEventHandler;
/**
* Écouteur global de démonstration illustrant la capture d'événements de GUI avec {@link GuiEventHandler}.
*/
public class DemoGuiListener {
@GuiEventHandler(priority = 10)
public void onOpen(GuiOpenEvent event) {
System.out.println("[GUI Log] Ouverture du menu '" + event.getGui().getTitle() + "' pour " + event.getPlayer().getName());
}
@GuiEventHandler
public void onClose(GuiCloseEvent event) {
System.out.println("[GUI Log] Fermeture du menu '" + event.getGui().getTitle() + "' par " + event.getPlayer().getName());
}
@GuiEventHandler
public void onClick(GuiClickEvent event) {
var ctx = event.getContext();
if (ctx.getGuiItem() != null) {
System.out.println("[GUI Log] Clic sur slot #" + ctx.getSlot() + " (" + ctx.getClickType() + ") par " + ctx.getPlayer().getName());
}
}
@GuiEventHandler
public void onPageChange(GuiPageChangeEvent event) {
System.out.println("[GUI Log] Changement de page : " + event.getOldPage() + " -> " + event.getNewPage());
}
}
@@ -0,0 +1,32 @@
package fr.luc.bettermcguis.event;
import fr.luc.bettermcguis.api.Gui;
import org.bukkit.entity.Player;
/**
* Classe de base pour les événements de GUI pouvant être annulés.
*/
public abstract class CancellableGuiEvent extends GuiEvent {
private boolean cancelled = false;
public CancellableGuiEvent(Gui gui, Player player) {
super(gui, player);
}
/**
* @return true si l'événement a été annulé, sinon false.
*/
public boolean isCancelled() {
return cancelled;
}
/**
* Définit si l'événement doit être annulé.
*
* @param cancelled true pour interrompre l'action.
*/
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
}
@@ -0,0 +1,27 @@
package fr.luc.bettermcguis.event;
import fr.luc.bettermcguis.api.GuiClickContext;
/**
* Déclenché lors d'une interaction / clic d'un joueur dans un inventaire GUI.
*/
public class GuiClickEvent extends CancellableGuiEvent {
private final GuiClickContext context;
public GuiClickEvent(GuiClickContext context) {
super(context.getGui(), context.getPlayer());
this.context = context;
this.setCancelled(context.isCancelled());
}
public GuiClickContext getContext() {
return context;
}
@Override
public void setCancelled(boolean cancelled) {
super.setCancelled(cancelled);
context.setCancelled(cancelled);
}
}
@@ -0,0 +1,22 @@
package fr.luc.bettermcguis.event;
import fr.luc.bettermcguis.api.Gui;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryCloseEvent;
/**
* Déclenché lors de la fermeture d'un GUI par un joueur.
*/
public class GuiCloseEvent extends GuiEvent {
private final InventoryCloseEvent rawEvent;
public GuiCloseEvent(Gui gui, Player player, InventoryCloseEvent rawEvent) {
super(gui, player);
this.rawEvent = rawEvent;
}
public InventoryCloseEvent getRawEvent() {
return rawEvent;
}
}
@@ -0,0 +1,34 @@
package fr.luc.bettermcguis.event;
import fr.luc.bettermcguis.api.Gui;
import org.bukkit.entity.Player;
import java.util.Objects;
/**
* Classe abstraite de base pour tous les événements du cycle de vie des GUIs.
*/
public abstract class GuiEvent {
protected final Gui gui;
protected final Player player;
public GuiEvent(Gui gui, Player player) {
this.gui = Objects.requireNonNull(gui, "gui cannot be null");
this.player = player;
}
/**
* @return L'instance de {@link Gui} concernée par l'événement.
*/
public Gui getGui() {
return gui;
}
/**
* @return Le joueur concerné, ou {@code null} pour un événement global.
*/
public Player getPlayer() {
return player;
}
}
@@ -0,0 +1,17 @@
package fr.luc.bettermcguis.event;
/**
* Interface fonctionnelle pour écouter un événement spécifique de GUI.
*
* @param <T> Le type d'événement écouté.
*/
@FunctionalInterface
public interface GuiEventListener<T extends GuiEvent> {
/**
* Traite l'événement transmis.
*
* @param event L'instance de l'événement.
*/
void onEvent(T event);
}
@@ -0,0 +1,112 @@
package fr.luc.bettermcguis.event;
import fr.luc.bettermcguis.event.annotation.GuiEventHandler;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Bus d'événements central gérant l'enregistrement et la distribution des événements de GUI.
*/
public class GuiEventManager {
private record RegisteredListener(
Class<? extends GuiEvent> eventType,
int priority,
String titleFilter,
GuiEventListener<?> listener
) implements Comparable<RegisteredListener> {
@Override
public int compareTo(RegisteredListener o) {
return Integer.compare(o.priority, this.priority); // Ordre décroissant
}
}
private final Map<Class<? extends GuiEvent>, List<RegisteredListener>> listeners = new ConcurrentHashMap<>();
/**
* Enregistre un écouteur programmatique avec une priorité par défaut de 0.
*/
public <T extends GuiEvent> void registerListener(Class<T> eventType, GuiEventListener<T> listener) {
registerListener(eventType, 0, "", listener);
}
/**
* Enregistre un écouteur programmatique avec priorité et filtre de titre.
*/
public <T extends GuiEvent> void registerListener(Class<T> eventType, int priority, String titleFilter, GuiEventListener<T> listener) {
Objects.requireNonNull(eventType, "eventType cannot be null");
Objects.requireNonNull(listener, "listener cannot be null");
RegisteredListener reg = new RegisteredListener(eventType, priority, titleFilter != null ? titleFilter : "", listener);
listeners.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>()).add(reg);
listeners.get(eventType).sort(RegisteredListener::compareTo);
}
/**
* Scanne et enregistre les méthodes annotées {@link GuiEventHandler} d'un objet écouteur.
*
* @param listenerInstance L'instance de classe contenant les méthodes d'écoute.
*/
@SuppressWarnings("unchecked")
public void registerListeners(Object listenerInstance) {
if (listenerInstance == null) return;
for (Method method : listenerInstance.getClass().getDeclaredMethods()) {
if (method.isAnnotationPresent(GuiEventHandler.class)) {
Class<?>[] params = method.getParameterTypes();
if (params.length == 1 && GuiEvent.class.isAssignableFrom(params[0])) {
Class<? extends GuiEvent> eventType = (Class<? extends GuiEvent>) params[0];
GuiEventHandler annotation = method.getAnnotation(GuiEventHandler.class);
method.setAccessible(true);
GuiEventListener<GuiEvent> listener = event -> {
try {
method.invoke(listenerInstance, event);
} catch (Exception e) {
e.printStackTrace();
}
};
registerListener(eventType, annotation.priority(), annotation.guiTitle(), (GuiEventListener) listener);
}
}
}
}
/**
* Distribue un événement à tous les écouteurs enregistrés correspondants.
*
* @param event L'événement à diffuser.
*/
@SuppressWarnings("unchecked")
public void dispatch(GuiEvent event) {
if (event == null) return;
Class<?> clazz = event.getClass();
for (Map.Entry<Class<? extends GuiEvent>, List<RegisteredListener>> entry : listeners.entrySet()) {
if (entry.getKey().isAssignableFrom(clazz)) {
for (RegisteredListener reg : entry.getValue()) {
if (!reg.titleFilter().isEmpty() && !event.getGui().getTitle().equalsIgnoreCase(reg.titleFilter())) {
continue;
}
try {
((GuiEventListener<GuiEvent>) reg.listener()).onEvent(event);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
/**
* Supprime tous les écouteurs enregistrés.
*/
public void clear() {
listeners.clear();
}
}
@@ -0,0 +1,22 @@
package fr.luc.bettermcguis.event;
import fr.luc.bettermcguis.api.Gui;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryOpenEvent;
/**
* Déclenché avant l'ouverture d'un GUI pour un joueur. Peut être annulé.
*/
public class GuiOpenEvent extends CancellableGuiEvent {
private final InventoryOpenEvent rawEvent;
public GuiOpenEvent(Gui gui, Player player, InventoryOpenEvent rawEvent) {
super(gui, player);
this.rawEvent = rawEvent;
}
public InventoryOpenEvent getRawEvent() {
return rawEvent;
}
}
@@ -0,0 +1,27 @@
package fr.luc.bettermcguis.event;
import fr.luc.bettermcguis.api.Gui;
import org.bukkit.entity.Player;
/**
* Déclenché lors d'un changement de page dans un menu paginé (PaginatedGui).
*/
public class GuiPageChangeEvent extends GuiEvent {
private final int oldPage;
private final int newPage;
public GuiPageChangeEvent(Gui gui, Player player, int oldPage, int newPage) {
super(gui, player);
this.oldPage = oldPage;
this.newPage = newPage;
}
public int getOldPage() {
return oldPage;
}
public int getNewPage() {
return newPage;
}
}
@@ -0,0 +1,14 @@
package fr.luc.bettermcguis.event;
import fr.luc.bettermcguis.api.Gui;
import org.bukkit.entity.Player;
/**
* Déclenché lors du rafraîchissement d'un GUI pour un joueur.
*/
public class GuiRefreshEvent extends GuiEvent {
public GuiRefreshEvent(Gui gui, Player player) {
super(gui, player);
}
}
@@ -0,0 +1,24 @@
package fr.luc.bettermcguis.event.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation permettant de marquer une méthode comme écouteur d'événements du cycle de vie des GUIs.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GuiEventHandler {
/**
* Priorité d'exécution de l'écouteur (les valeurs les plus élevées sont exécutées en premier).
*/
int priority() default 0;
/**
* Filtre optionnel sur le titre du GUI (si renseigné, seuls les GUIs dont le titre correspond déclencheront la méthode).
*/
String guiTitle() default "";
}
@@ -0,0 +1,59 @@
package fr.luc.bettermcguis.holder;
import fr.luc.bettermcguis.api.Gui;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import java.util.Objects;
import java.util.UUID;
/**
* Implémentation personnalisée d'{@link InventoryHolder} permettant d'associer
* un inventaire Bukkit/Paper natif à une instance de {@link Gui} de la bibliothèque betterMcGuis.
*/
public class BetterGuiHolder implements InventoryHolder {
private final Gui gui;
private final UUID viewerUuid;
private Inventory inventory;
/**
* Crée un nouveau holder reliant un GUI à son visualisateur.
*
* @param gui Le GUI associé.
* @param viewer Le joueur visualisant l'inventaire.
*/
public BetterGuiHolder(Gui gui, Player viewer) {
this.gui = Objects.requireNonNull(gui, "gui cannot be null");
this.viewerUuid = viewer != null ? viewer.getUniqueId() : null;
}
/**
* @return L'instance de {@link Gui} sous-jacente.
*/
public Gui getGui() {
return gui;
}
/**
* @return L'UUID du joueur visualisant le GUI.
*/
public UUID getViewerUuid() {
return viewerUuid;
}
/**
* Définit l'inventaire Bukkit créé pour ce holder.
*
* @param inventory L'inventaire Bukkit.
*/
public void setInventory(Inventory inventory) {
this.inventory = inventory;
}
@Override
public Inventory getInventory() {
return inventory;
}
}
@@ -0,0 +1,162 @@
package fr.luc.bettermcguis.listener;
import fr.luc.bettermcguis.BetterMcGuis;
import fr.luc.bettermcguis.api.*;
import fr.luc.bettermcguis.api.slot.SlotPos;
import fr.luc.bettermcguis.event.GuiClickEvent;
import fr.luc.bettermcguis.event.GuiCloseEvent;
import fr.luc.bettermcguis.event.GuiOpenEvent;
import fr.luc.bettermcguis.holder.BetterGuiHolder;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.*;
import org.bukkit.inventory.Inventory;
/**
* Écouteur Bukkit central interceptant de manière sécurisée toutes les interactions d'inventaires
* pour les acheminer vers les instances de {@link Gui} et le bus d'événements de betterMcGuis.
*/
public class BukkitGuiEventListener implements Listener {
private final BetterMcGuis manager;
public BukkitGuiEventListener(BetterMcGuis manager) {
this.manager = manager;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void onInventoryClick(InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player player)) {
return;
}
Inventory topInventory = event.getView().getTopInventory();
if (!(topInventory.getHolder() instanceof BetterGuiHolder holder)) {
return;
}
Gui gui = holder.getGui();
int rawSlot = event.getRawSlot();
// 1. Clic en dehors de la fenêtre d'inventaire
if (rawSlot == -999 || event.getSlotType() == InventoryType.SlotType.OUTSIDE) {
GuiClickContext context = new GuiClickContext(
player, gui, null, -999, null,
event.getClick(), event.getAction(),
null, event.getCursor(), event
);
gui.handleOutsideClick(context);
manager.getEventManager().dispatch(new GuiClickEvent(context));
return;
}
// 2. Clic dans l'inventaire supérieur (Le GUI lui-même)
if (rawSlot < topInventory.getSize() && rawSlot >= 0) {
boolean editable = gui.isEditable(rawSlot);
GuiItem item = gui.getItem(rawSlot);
SlotPos pos = SlotPos.fromSlot(rawSlot, gui.getColumns());
GuiClickContext context = new GuiClickContext(
player, gui, item, rawSlot, pos,
event.getClick(), event.getAction(),
event.getCurrentItem(), event.getCursor(), event
);
// Si le slot n'est pas explicitement éditable, on bloque l'interaction native
if (!editable) {
event.setCancelled(true);
context.setCancelled(true);
} else {
context.setCancelled(false);
}
if (item != null) {
item.handleClick(context);
}
gui.handleClick(context);
manager.getEventManager().dispatch(new GuiClickEvent(context));
return;
}
// 3. Clic dans l'inventaire inférieur (Inventaire du joueur)
if (rawSlot >= topInventory.getSize()) {
// Sécurité : Empêche le Shift-Clic d'injecter des items dans des slots non éditables du GUI
if (event.isShiftClick()) {
boolean hasEditableSlots = !gui.getEditableSlots().isEmpty();
if (!hasEditableSlots) {
event.setCancelled(true);
}
}
// Sécurité : Échange par touche numérique (Hotbar Number Key Swap)
if (event.getClick() == ClickType.NUMBER_KEY) {
// Bloque si le slot visé n'est pas éditable
if (rawSlot < topInventory.getSize() && !gui.isEditable(rawSlot)) {
event.setCancelled(true);
}
}
GuiClickContext context = new GuiClickContext(
player, gui, null, event.getSlot(), null,
event.getClick(), event.getAction(),
event.getCurrentItem(), event.getCursor(), event
);
gui.handleBottomClick(context);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void onInventoryDrag(InventoryDragEvent event) {
if (!(event.getWhoClicked() instanceof Player player)) {
return;
}
Inventory topInventory = event.getView().getTopInventory();
if (!(topInventory.getHolder() instanceof BetterGuiHolder holder)) {
return;
}
Gui gui = holder.getGui();
// Vérifie si l'un des slots touchés par le glisser-déposer appartient au GUI supérieur
for (int rawSlot : event.getRawSlots()) {
if (rawSlot < topInventory.getSize()) {
if (!gui.isEditable(rawSlot)) {
event.setCancelled(true);
return;
}
}
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onInventoryOpen(InventoryOpenEvent event) {
if (!(event.getPlayer() instanceof Player player)) {
return;
}
if (event.getInventory().getHolder() instanceof BetterGuiHolder holder) {
Gui gui = holder.getGui();
GuiOpenContext context = new GuiOpenContext(player, gui, event);
gui.handleOpen(context);
manager.getEventManager().dispatch(new GuiOpenEvent(gui, player, event));
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onInventoryClose(InventoryCloseEvent event) {
if (!(event.getPlayer() instanceof Player player)) {
return;
}
if (event.getInventory().getHolder() instanceof BetterGuiHolder holder) {
Gui gui = holder.getGui();
GuiCloseContext context = new GuiCloseContext(player, gui, event);
gui.handleClose(context);
manager.getEventManager().dispatch(new GuiCloseEvent(gui, player, event));
}
}
}
@@ -0,0 +1,55 @@
package fr.luc.bettermcguis.pattern;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Représente un masque de sélection de slots basé sur des lignes de caractères binaires (0 et 1 ou caractères au choix).
*/
public class GuiMask {
private final List<String> lines = new ArrayList<>();
private final char activeChar;
/**
* Crée un masque avec '1' comme caractère actif par défaut.
*/
public GuiMask(String... lines) {
this('1', lines);
}
/**
* Crée un masque en précisant le caractère actif.
*
* @param activeChar Le caractère représentant un slot sélectionné.
* @param lines Les lignes du masque.
*/
public GuiMask(char activeChar, String... lines) {
this.activeChar = activeChar;
if (lines != null) {
this.lines.addAll(Arrays.asList(lines));
}
}
/**
* Résout la liste des slots sélectionnés par ce masque pour des dimensions données.
*
* @param rows Le nombre de lignes.
* @param cols Le nombre de colonnes (ex: 9).
* @return La liste des slots actifs (0-indexés).
*/
public List<Integer> resolveSlots(int rows, int cols) {
List<Integer> slots = new ArrayList<>();
for (int r = 0; r < Math.min(lines.size(), rows); r++) {
String line = lines.get(r);
for (int c = 0; c < Math.min(line.length(), cols); c++) {
if (line.charAt(c) == activeChar) {
slots.add(r * cols + c);
}
}
}
return Collections.unmodifiableList(slots);
}
}
@@ -0,0 +1,108 @@
package fr.luc.bettermcguis.pattern;
import fr.luc.bettermcguis.api.Gui;
import fr.luc.bettermcguis.api.GuiItem;
import java.util.*;
/**
* Permet de définir la disposition visuelle d'un GUI sous forme d'une matrice textuelle ASCII (pattern).
* Chaque caractère de la matrice est associé à un {@link GuiItem}.
*/
public class GuiPattern {
private final List<String> rows = new ArrayList<>();
private final Map<Character, GuiItem> itemBindings = new HashMap<>();
/**
* Crée un pattern à partir des lignes fournies.
*
* @param lines Les lignes de texte représentant la grille de l'inventaire.
*/
public GuiPattern(String... lines) {
if (lines != null) {
this.rows.addAll(Arrays.asList(lines));
}
}
/**
* Crée un pattern à partir d'une liste de lignes.
*/
public GuiPattern(List<String> lines) {
if (lines != null) {
this.rows.addAll(lines);
}
}
/**
* Fabrique statique pour créer un pattern.
*
* @param lines Les lignes du motif.
* @return L'instance de {@link GuiPattern}.
*/
public static GuiPattern of(String... lines) {
return new GuiPattern(lines);
}
/**
* Associe un caractère à un {@link GuiItem}.
*
* @param character Le caractère de la grille (ex: '#', 'X', 'A').
* @param item L'item correspondant.
* @return Cette instance de pattern pour chaînage.
*/
public GuiPattern bind(char character, GuiItem item) {
this.itemBindings.put(character, item);
return this;
}
/**
* Associe plusieurs bindings à la fois.
*
* @param bindings La table de mapping caractère -> GuiItem.
* @return Cette instance.
*/
public GuiPattern bindAll(Map<Character, GuiItem> bindings) {
if (bindings != null) {
this.itemBindings.putAll(bindings);
}
return this;
}
/**
* Applique ce motif sur l'inventaire GUI cible.
*
* @param gui Le GUI sur lequel placer les items selon la matrice.
*/
public void apply(Gui gui) {
Objects.requireNonNull(gui, "gui cannot be null");
int cols = gui.getColumns();
int maxRows = gui.getRows();
for (int r = 0; r < Math.min(rows.size(), maxRows); r++) {
String line = rows.get(r);
for (int c = 0; c < Math.min(line.length(), cols); c++) {
char ch = line.charAt(c);
if (itemBindings.containsKey(ch)) {
GuiItem item = itemBindings.get(ch);
int slot = r * cols + c;
gui.setItem(slot, item);
}
}
}
}
/**
* @return La liste des lignes du motif.
*/
public List<String> getRows() {
return Collections.unmodifiableList(rows);
}
/**
* @return La table des liaisons de caractères vers items.
*/
public Map<Character, GuiItem> getItemBindings() {
return Collections.unmodifiableMap(itemBindings);
}
}
@@ -0,0 +1,122 @@
package fr.luc.bettermcguis.type;
import fr.luc.bettermcguis.animation.Frame;
import fr.luc.bettermcguis.api.GuiItem;
import fr.luc.bettermcguis.api.GuiType;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitTask;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Implémentation d'un GUI animé cadencé par une tâche Bukkit (frames multiples, titres mouvants, carrousels).
*/
public class AnimatedGui extends SimpleGui {
private final List<Frame> frames = new ArrayList<>();
private int currentFrameIndex = 0;
private BukkitTask animationTask;
private boolean loop = true;
public AnimatedGui(String title, GuiType type) {
super(title, type);
}
public AnimatedGui(String title, int rows) {
super(title, rows);
}
/**
* Ajoute une frame à la séquence d'animation.
*
* @param frame La frame.
* @return Ce GUI pour chaînage.
*/
public AnimatedGui addFrame(Frame frame) {
if (frame != null) {
this.frames.add(frame);
}
return this;
}
/**
* Définit si l'animation doit tourner en boucle continue.
*/
public AnimatedGui loop(boolean loop) {
this.loop = loop;
return this;
}
/**
* Démarre la lecture de l'animation.
*
* @param plugin Le plugin gérant la tâche Bukkit.
* @param interval L'intervalle de temps entre chaque frame.
*/
public void startAnimation(Plugin plugin, Duration interval) {
Objects.requireNonNull(plugin, "plugin cannot be null");
stopAnimation();
if (frames.isEmpty()) return;
long ticks = Math.max(1, (interval != null ? interval.toMillis() : 500) / 50);
this.animationTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
if (getViewers().isEmpty()) {
// Pas de spectateur -> pause temporaire ou stop
return;
}
advanceFrame();
}, 0L, ticks);
}
/**
* Avance d'une frame et met à jour les slots pour tous les spectateurs.
*/
public void advanceFrame() {
if (frames.isEmpty()) return;
Frame frame = frames.get(currentFrameIndex);
for (Map.Entry<Integer, GuiItem> entry : frame.getItems().entrySet()) {
super.setItem(entry.getKey(), entry.getValue());
}
if (frame.getTitle() != null) {
super.setTitle(frame.getTitle());
}
currentFrameIndex++;
if (currentFrameIndex >= frames.size()) {
if (loop) {
currentFrameIndex = 0;
} else {
stopAnimation();
}
}
}
/**
* Arrête l'animation en cours.
*/
public void stopAnimation() {
if (animationTask != null) {
animationTask.cancel();
animationTask = null;
}
}
@Override
public void close(Player player) {
super.close(player);
if (getViewers().isEmpty()) {
stopAnimation();
}
}
}
@@ -0,0 +1,348 @@
package fr.luc.bettermcguis.type;
import fr.luc.bettermcguis.api.GuiClickAction;
import fr.luc.bettermcguis.api.GuiItem;
import fr.luc.bettermcguis.api.GuiType;
import fr.luc.bettermcguis.api.slot.SlotRange;
import fr.luc.bettermcguis.builder.ItemBuilder;
import fr.luc.bettermcguis.event.GuiPageChangeEvent;
import fr.luc.bettermcguis.pattern.GuiMask;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiFunction;
import java.util.function.Consumer;
/**
* Implémentation d'un inventaire paginé (Paginated GUI) gérant automatiquement la navigation par pages,
* le calcul dynamique du nombre de pages, les boutons suivant/précédent et l'affichage d'un indicateur de page.
*/
public class PaginatedGui extends SimpleGui {
private final List<GuiItem> pageItems = new ArrayList<>();
private final List<Integer> itemSlots = new ArrayList<>();
private int currentPage = 1; // 1-indexed pour plus de lisibilité
private Integer previousPageSlot;
private Integer nextPageSlot;
private Integer pageIndicatorSlot;
private GuiItem customPreviousButton;
private GuiItem customNextButton;
private BiFunction<Integer, Integer, GuiItem> pageIndicatorSupplier;
private final List<Consumer<GuiPageChangeEvent>> pageChangeHooks = new CopyOnWriteArrayList<>();
/**
* Initialise un GUI paginé avec un titre et un type d'inventaire.
*/
public PaginatedGui(String title, GuiType type) {
super(title, type);
initDefaultItemSlots();
}
/**
* Initialise un GUI paginé avec un nombre de lignes de coffre.
*/
public PaginatedGui(String title, int rows) {
super(title, rows);
initDefaultItemSlots();
}
private void initDefaultItemSlots() {
// Par défaut, si 3 lignes ou plus, on utilise l'intérieur
if (getRows() >= 3) {
this.itemSlots.addAll(SlotRange.interior(getRows(), getColumns()).getSlots());
this.previousPageSlot = (getRows() - 1) * getColumns() + 3; // ex: slot 48
this.pageIndicatorSlot = (getRows() - 1) * getColumns() + 4; // ex: slot 49
this.nextPageSlot = (getRows() - 1) * getColumns() + 5; // ex: slot 50
} else {
for (int i = 0; i < getSize() - 2; i++) {
this.itemSlots.add(i);
}
this.previousPageSlot = getSize() - 2;
this.nextPageSlot = getSize() - 1;
}
}
/**
* Ajoute un item paginé à la collection.
*
* @param item L'item à ajouter.
*/
public void addPageItem(GuiItem item) {
if (item != null) {
this.pageItems.add(item);
}
}
/**
* Ajoute une collection complète d'items paginés.
*
* @param items Les items.
*/
public void addPageItems(Collection<GuiItem> items) {
if (items != null) {
this.pageItems.addAll(items);
}
}
/**
* Remplace l'intégralité de la collection d'items paginés.
*
* @param items La nouvelle liste d'items.
*/
public void setPageItems(List<GuiItem> items) {
this.pageItems.clear();
if (items != null) {
this.pageItems.addAll(items);
}
}
/**
* Supprime tous les items de la collection paginée.
*/
public void clearPageItems() {
this.pageItems.clear();
this.currentPage = 1;
}
/**
* Définit explicitement la liste des slots réservés pour les items paginés.
*
* @param slots Les numéros de slots.
*/
public void setItemSlots(List<Integer> slots) {
this.itemSlots.clear();
if (slots != null) {
this.itemSlots.addAll(slots);
}
}
/**
* Définit les slots d'items à partir d'une plage {@link SlotRange}.
*/
public void setItemSlots(SlotRange range) {
if (range != null) {
setItemSlots(range.getSlots());
}
}
/**
* Définit les slots d'items à partir d'un masque binaire {@link GuiMask}.
*/
public void setItemSlots(GuiMask mask) {
if (mask != null) {
setItemSlots(mask.resolveSlots(getRows(), getColumns()));
}
}
/**
* Configure le bouton de page précédente.
*
* @param slot Le numéro de slot.
* @param item L'item du bouton (ou null pour le bouton par défaut).
*/
public void setPreviousPageButton(int slot, GuiItem item) {
this.previousPageSlot = slot;
this.customPreviousButton = item;
}
/**
* Configure le bouton de page suivante.
*
* @param slot Le numéro de slot.
* @param item L'item du bouton (ou null pour le bouton par défaut).
*/
public void setNextPageButton(int slot, GuiItem item) {
this.nextPageSlot = slot;
this.customNextButton = item;
}
/**
* Configure l'indicateur de page (ex: "Page 2/5").
*
* @param slot Le numéro de slot.
* @param indicatorSupplier Fonction fournissant l'item selon (pageActuelle, pageMax).
*/
public void setPageIndicator(int slot, BiFunction<Integer, Integer, GuiItem> indicatorSupplier) {
this.pageIndicatorSlot = slot;
this.pageIndicatorSupplier = indicatorSupplier;
}
/**
* @return Le nombre d'items pouvant être affichés par page.
*/
public int getPageSize() {
return Math.max(1, itemSlots.size());
}
/**
* @return Le nombre total de pages nécessaires pour afficher tous les items (au minimum 1).
*/
public int getTotalPages() {
if (pageItems.isEmpty()) {
return 1;
}
return (int) Math.ceil((double) pageItems.size() / getPageSize());
}
/**
* @return Le numéro de la page actuelle (1-indexé).
*/
public int getCurrentPage() {
return currentPage;
}
/**
* Définit la page actuelle en effectuant les vérifications de bornes et déclenchant l'événement.
*
* @param page Le numéro de page (1 à totalPages).
* @return true si la page a changé, sinon false.
*/
public boolean setCurrentPage(int page) {
int max = getTotalPages();
int target = Math.max(1, Math.min(max, page));
if (target == this.currentPage) {
return false;
}
int oldPage = this.currentPage;
this.currentPage = target;
GuiPageChangeEvent event = new GuiPageChangeEvent(this, null, oldPage, target);
for (Consumer<GuiPageChangeEvent> hook : pageChangeHooks) {
try {
hook.accept(event);
} catch (Exception e) {
e.printStackTrace();
}
}
refreshAll();
return true;
}
/**
* @return true si une page suivante existe après la page actuelle.
*/
public boolean hasNextPage() {
return currentPage < getTotalPages();
}
/**
* @return true si une page précédente existe avant la page actuelle.
*/
public boolean hasPreviousPage() {
return currentPage > 1;
}
/**
* Passe à la page suivante si possible.
*
* @return true si la navigation a réussi.
*/
public boolean nextPage() {
if (hasNextPage()) {
return setCurrentPage(currentPage + 1);
}
return false;
}
/**
* Revient à la page précédente si possible.
*
* @return true si la navigation a réussi.
*/
public boolean previousPage() {
if (hasPreviousPage()) {
return setCurrentPage(currentPage - 1);
}
return false;
}
/**
* Attache un écouteur appelé lors d'un changement de page.
*
* @param hook L'action à exécuter.
* @return Ce GUI pour chaînage.
*/
public PaginatedGui onPageChange(Consumer<GuiPageChangeEvent> hook) {
if (hook != null) {
this.pageChangeHooks.add(hook);
}
return this;
}
@Override
public Inventory createInventory(Player player) {
populatePageElements();
return super.createInventory(player);
}
@Override
public void refresh(Player player) {
populatePageElements();
super.refresh(player);
}
private void populatePageElements() {
// 1. Nettoie les slots d'items paginés
for (int slot : itemSlots) {
super.removeItem(slot);
}
// 2. Remplit les items pour la page actuelle
int pageSize = getPageSize();
int startIndex = (currentPage - 1) * pageSize;
int endIndex = Math.min(startIndex + pageSize, pageItems.size());
for (int i = 0; i < (endIndex - startIndex); i++) {
int slot = itemSlots.get(i);
GuiItem item = pageItems.get(startIndex + i);
super.setItem(slot, item);
}
// 3. Bouton Page Précédente
if (previousPageSlot != null) {
if (hasPreviousPage()) {
GuiItem prevBtn = (customPreviousButton != null) ? customPreviousButton :
ItemBuilder.of(Material.ARROW)
.name("<yellow>◀ Page Précédente</yellow>")
.lore("<gray>Aller à la page " + (currentPage - 1) + "</gray>")
.asGuiItem(ctx -> previousPage());
super.setItem(previousPageSlot, prevBtn);
} else {
super.removeItem(previousPageSlot);
}
}
// 4. Bouton Page Suivante
if (nextPageSlot != null) {
if (hasNextPage()) {
GuiItem nextBtn = (customNextButton != null) ? customNextButton :
ItemBuilder.of(Material.ARROW)
.name("<yellow>Page Suivante ▶</yellow>")
.lore("<gray>Aller à la page " + (currentPage + 1) + "</gray>")
.asGuiItem(ctx -> nextPage());
super.setItem(nextPageSlot, nextBtn);
} else {
super.removeItem(nextPageSlot);
}
}
// 5. Indicateur de Page
if (pageIndicatorSlot != null) {
int total = getTotalPages();
GuiItem indicator = (pageIndicatorSupplier != null) ? pageIndicatorSupplier.apply(currentPage, total) :
ItemBuilder.of(Material.PAPER)
.name("<gold><bold>Page " + currentPage + " / " + total + "</bold></gold>")
.lore("<gray>Total d'éléments : <yellow>" + pageItems.size() + "</yellow></gray>")
.asGuiItem();
super.setItem(pageIndicatorSlot, indicator);
}
}
}
@@ -0,0 +1,382 @@
package fr.luc.bettermcguis.type;
import fr.luc.bettermcguis.api.*;
import fr.luc.bettermcguis.api.slot.SlotRange;
import fr.luc.bettermcguis.holder.BetterGuiHolder;
import fr.luc.bettermcguis.pattern.GuiPattern;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Consumer;
/**
* Implémentation standard et autonome d'un menu d'inventaire interactif (GUI).
*/
public class SimpleGui implements Gui {
protected String title;
protected Component titleComponent;
protected final GuiType type;
protected final Map<Integer, GuiItem> items = new ConcurrentHashMap<>();
protected final Set<Integer> editableSlots = new CopyOnWriteArraySet<>();
protected final Map<String, Object> properties = new ConcurrentHashMap<>();
protected final Set<UUID> viewers = new CopyOnWriteArraySet<>();
// Hooks du cycle de vie
protected final List<Consumer<GuiOpenContext>> openHooks = new CopyOnWriteArrayList<>();
protected final List<Consumer<GuiCloseContext>> closeHooks = new CopyOnWriteArrayList<>();
protected final List<Consumer<GuiClickContext>> clickHooks = new CopyOnWriteArrayList<>();
protected final List<Consumer<GuiClickContext>> outsideClickHooks = new CopyOnWriteArrayList<>();
protected final List<Consumer<GuiClickContext>> bottomClickHooks = new CopyOnWriteArrayList<>();
/**
* Initialise un GUI avec un titre et un type d'inventaire.
*
* @param title Le titre au format MiniMessage.
* @param type Le type de l'inventaire.
*/
public SimpleGui(String title, GuiType type) {
this.type = Objects.requireNonNull(type, "GuiType cannot be null");
setTitle(title != null ? title : "Menu");
}
/**
* Initialise un GUI avec un nombre de lignes de coffre (1 à 6).
*
* @param title Le titre au format MiniMessage.
* @param rows Le nombre de lignes (1 à 6).
*/
public SimpleGui(String title, int rows) {
this(title, GuiType.chest(rows));
}
@Override
public String getTitle() {
return title;
}
@Override
public Component getTitleComponent() {
return titleComponent;
}
@Override
public void setTitle(String miniMessage) {
this.title = miniMessage != null ? miniMessage : "";
this.titleComponent = MiniMessage.miniMessage().deserialize(this.title);
}
@Override
public GuiType getType() {
return type;
}
@Override
public void setItem(int slot, GuiItem item) {
if (slot < 0 || slot >= getSize()) {
return;
}
if (item == null) {
items.remove(slot);
} else {
items.put(slot, item);
}
// Met à jour visuellement les inventaires ouverts pour les visualisateurs
for (Player viewer : getViewers()) {
if (viewer.getOpenInventory().getTopInventory().getHolder() instanceof BetterGuiHolder holder
&& holder.getGui() == this) {
Inventory top = viewer.getOpenInventory().getTopInventory();
ItemStack stack = (item != null && item.isVisibleFor(viewer)) ? item.getItemStack() : null;
top.setItem(slot, stack);
}
}
}
@Override
public GuiItem getItem(int slot) {
return items.get(slot);
}
@Override
public void removeItem(int slot) {
setItem(slot, null);
}
@Override
public void clear() {
items.clear();
for (Player viewer : getViewers()) {
refresh(viewer);
}
}
@Override
public void fill(GuiItem item) {
for (int i = 0; i < getSize(); i++) {
setItem(i, item);
}
}
@Override
public void fillBorder(GuiItem item) {
fillRange(SlotRange.border(getRows(), getColumns()), item);
}
@Override
public void fillRange(SlotRange range, GuiItem item) {
if (range != null) {
for (int slot : range.getSlots()) {
setItem(slot, item);
}
}
}
@Override
public void applyPattern(GuiPattern pattern) {
if (pattern != null) {
pattern.apply(this);
}
}
@Override
public void open(Player player) {
Objects.requireNonNull(player, "player cannot be null");
Inventory inventory = createInventory(player);
GuiOpenContext openContext = new GuiOpenContext(player, this, null);
handleOpen(openContext);
if (!openContext.isCancelled()) {
player.openInventory(inventory);
viewers.add(player.getUniqueId());
}
}
@Override
public void close(Player player) {
if (player != null) {
viewers.remove(player.getUniqueId());
player.closeInventory();
}
}
@Override
public void refresh(Player player) {
if (player == null || !player.isOnline()) return;
if (player.getOpenInventory().getTopInventory().getHolder() instanceof BetterGuiHolder holder
&& holder.getGui() == this) {
Inventory inv = player.getOpenInventory().getTopInventory();
for (int i = 0; i < getSize(); i++) {
if (editableSlots.contains(i)) {
continue;
}
GuiItem item = items.get(i);
if (item != null && item.isVisibleFor(player)) {
inv.setItem(i, item.getItemStack());
} else {
inv.setItem(i, null);
}
}
player.updateInventory();
}
}
@Override
public void refreshAll() {
for (Player viewer : getViewers()) {
refresh(viewer);
}
}
@Override
public Set<Player> getViewers() {
Set<Player> playerSet = new HashSet<>();
for (UUID uuid : viewers) {
Player p = Bukkit.getPlayer(uuid);
if (p != null && p.isOnline()) {
playerSet.add(p);
} else {
viewers.remove(uuid);
}
}
return Collections.unmodifiableSet(playerSet);
}
@Override
public Inventory createInventory(Player player) {
BetterGuiHolder holder = new BetterGuiHolder(this, player);
Inventory inventory;
if (type.getBukkitType() == org.bukkit.event.inventory.InventoryType.CHEST) {
inventory = Bukkit.createInventory(holder, getSize(), titleComponent);
} else {
inventory = Bukkit.createInventory(holder, type.getBukkitType(), titleComponent);
}
holder.setInventory(inventory);
// Place les items
for (Map.Entry<Integer, GuiItem> entry : items.entrySet()) {
int slot = entry.getKey();
GuiItem item = entry.getValue();
if (slot >= 0 && slot < getSize() && item != null) {
if (player == null || item.isVisibleFor(player)) {
inventory.setItem(slot, item.getItemStack());
}
}
}
return inventory;
}
@Override
public void setEditable(int slot, boolean editable) {
if (editable) {
editableSlots.add(slot);
} else {
editableSlots.remove(slot);
}
}
@Override
public boolean isEditable(int slot) {
return editableSlots.contains(slot);
}
@Override
public Set<Integer> getEditableSlots() {
return Collections.unmodifiableSet(editableSlots);
}
@Override
public void setProperty(String key, Object value) {
if (value == null) {
properties.remove(key);
} else {
properties.put(key, value);
}
}
@SuppressWarnings("unchecked")
@Override
public <T> T getProperty(String key, Class<T> type) {
Object val = properties.get(key);
if (val != null && type.isInstance(val)) {
return (T) val;
}
return null;
}
@SuppressWarnings("unchecked")
@Override
public <T> T getProperty(String key, T defaultValue) {
Object val = properties.get(key);
if (val != null && defaultValue != null && defaultValue.getClass().isInstance(val)) {
return (T) val;
}
return defaultValue;
}
@Override
public Map<String, Object> getProperties() {
return Collections.unmodifiableMap(properties);
}
@Override
public Gui onOpen(Consumer<GuiOpenContext> hook) {
this.openHooks.add(Objects.requireNonNull(hook, "hook cannot be null"));
return this;
}
@Override
public Gui onClose(Consumer<GuiCloseContext> hook) {
this.closeHooks.add(Objects.requireNonNull(hook, "hook cannot be null"));
return this;
}
@Override
public Gui onClick(Consumer<GuiClickContext> hook) {
this.clickHooks.add(Objects.requireNonNull(hook, "hook cannot be null"));
return this;
}
@Override
public Gui onOutsideClick(Consumer<GuiClickContext> hook) {
this.outsideClickHooks.add(Objects.requireNonNull(hook, "hook cannot be null"));
return this;
}
@Override
public Gui onBottomClick(Consumer<GuiClickContext> hook) {
this.bottomClickHooks.add(Objects.requireNonNull(hook, "hook cannot be null"));
return this;
}
@Override
public void handleOpen(GuiOpenContext context) {
for (Consumer<GuiOpenContext> hook : openHooks) {
try {
hook.accept(context);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void handleClose(GuiCloseContext context) {
if (context.getPlayer() != null) {
viewers.remove(context.getPlayer().getUniqueId());
}
for (Consumer<GuiCloseContext> hook : closeHooks) {
try {
hook.accept(context);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void handleClick(GuiClickContext context) {
for (Consumer<GuiClickContext> hook : clickHooks) {
try {
hook.accept(context);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void handleOutsideClick(GuiClickContext context) {
for (Consumer<GuiClickContext> hook : outsideClickHooks) {
try {
hook.accept(context);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void handleBottomClick(GuiClickContext context) {
for (Consumer<GuiClickContext> hook : bottomClickHooks) {
try {
hook.accept(context);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
@@ -0,0 +1,106 @@
package fr.luc.bettermcguis.type;
import fr.luc.bettermcguis.api.GuiType;
import fr.luc.bettermcguis.api.slot.SlotRange;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import java.util.*;
import java.util.function.BiConsumer;
/**
* Implémentation d'un GUI de stockage / zone de dépôt (Storage GUI).
* Permet aux joueurs de déposer, retirer ou modifier librement des items dans des slots spécifiés (ex: poubelle, coffre d'échange, enclume).
*/
public class StorageGui extends SimpleGui {
private final Set<Integer> storageSlots = new HashSet<>();
private BiConsumer<Player, ItemStack> itemDepositHook;
private BiConsumer<Player, ItemStack> itemWithdrawHook;
private boolean returnItemsOnClose = true;
public StorageGui(String title, GuiType type) {
super(title, type);
}
public StorageGui(String title, int rows) {
super(title, rows);
}
/**
* Définit les slots utilisables comme zone de stockage modifiable.
*
* @param slots Les slots éditables.
* @return Ce GUI pour chaînage.
*/
public StorageGui setStorageSlots(Collection<Integer> slots) {
if (slots != null) {
for (int s : slots) {
this.storageSlots.add(s);
super.setEditable(s, true);
}
}
return this;
}
/**
* Définit les slots de stockage via une plage {@link SlotRange}.
*/
public StorageGui setStorageSlots(SlotRange range) {
if (range != null) {
setStorageSlots(range.getSlots());
}
return this;
}
/**
* Définit si les items laissés dans la zone de stockage doivent être restitués au joueur lors de la fermeture.
*
* @param returnItems true pour restituer dans l'inventaire du joueur (ou au sol si plein).
*/
public StorageGui returnItemsOnClose(boolean returnItems) {
this.returnItemsOnClose = returnItems;
return this;
}
/**
* Récupère tous les items actuellement présents dans les slots de stockage.
*
* @param inventory L'inventaire Bukkit ouvert.
* @return La liste des {@link ItemStack} présents.
*/
public List<ItemStack> getStoredItems(Inventory inventory) {
List<ItemStack> list = new ArrayList<>();
if (inventory != null) {
for (int slot : storageSlots) {
ItemStack item = inventory.getItem(slot);
if (item != null && !item.getType().isAir()) {
list.add(item);
}
}
}
return list;
}
@Override
public void handleClose(fr.luc.bettermcguis.api.GuiCloseContext context) {
Player player = context.getPlayer();
Inventory inv = context.getRawEvent() != null ? context.getRawEvent().getInventory() : null;
if (returnItemsOnClose && player != null && inv != null) {
for (int slot : storageSlots) {
ItemStack item = inv.getItem(slot);
if (item != null && !item.getType().isAir()) {
HashMap<Integer, ItemStack> leftover = player.getInventory().addItem(item);
for (ItemStack drop : leftover.values()) {
player.getWorld().dropItemNaturally(player.getLocation(), drop);
}
inv.setItem(slot, null);
}
}
}
super.handleClose(context);
}
}
@@ -0,0 +1,144 @@
package fr.luc.bettermcguis.type;
import fr.luc.bettermcguis.api.GuiItem;
import fr.luc.bettermcguis.api.GuiType;
import fr.luc.bettermcguis.pattern.GuiPattern;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import java.util.*;
/**
* Implémentation d'un menu à onglets (Tabbed GUI) permettant de basculer instantanément
* entre plusieurs vues / catégories sans avoir à fermer ou rouvrir l'inventaire du joueur.
*/
public class TabbedGui extends SimpleGui {
private final Map<String, Map<Integer, GuiItem>> tabItems = new LinkedHashMap<>();
private final Map<String, Integer> tabButtonSlots = new LinkedHashMap<>();
private final Map<String, GuiItem> tabButtonItems = new LinkedHashMap<>();
private String activeTabId;
/**
* Initialise un GUI à onglets.
*/
public TabbedGui(String title, GuiType type) {
super(title, type);
}
/**
* Initialise un GUI à onglets avec un nombre de lignes de coffre.
*/
public TabbedGui(String title, int rows) {
super(title, rows);
}
/**
* Enregistre un nouvel onglet avec son bouton d'accès.
*
* @param tabId L'identifiant unique de l'onglet (ex: "shop", "quetes", "profil").
* @param buttonSlot Le slot sur lequel sera placé le bouton de navigation.
* @param buttonItem L'item du bouton représentant l'onglet.
* @return Cette instance de {@link TabbedGui} pour chaînage.
*/
public TabbedGui addTab(String tabId, int buttonSlot, GuiItem buttonItem) {
Objects.requireNonNull(tabId, "tabId cannot be null");
this.tabItems.putIfAbsent(tabId, new HashMap<>());
if (buttonSlot >= 0) {
this.tabButtonSlots.put(tabId, buttonSlot);
this.tabButtonItems.put(tabId, buttonItem);
// Attache l'action de sélection d'onglet
GuiItem itemWithAction = (buttonItem != null) ? buttonItem : GuiItem.empty();
super.setItem(buttonSlot, itemWithAction.onClick(ctx -> selectTab(tabId)));
}
if (this.activeTabId == null) {
this.activeTabId = tabId;
}
return this;
}
/**
* Définit un item pour un onglet spécifique.
*
* @param tabId L'identifiant de l'onglet.
* @param slot Le slot cible.
* @param item L'item à placer.
*/
public void setTabItem(String tabId, int slot, GuiItem item) {
Map<Integer, GuiItem> map = tabItems.computeIfAbsent(tabId, k -> new HashMap<>());
if (item == null) {
map.remove(slot);
} else {
map.put(slot, item);
}
if (tabId.equalsIgnoreCase(activeTabId)) {
super.setItem(slot, item);
}
}
/**
* Applique un motif ASCII pour un onglet spécifique.
*
* @param tabId L'identifiant de l'onglet.
* @param pattern Le motif.
*/
public void applyTabPattern(String tabId, GuiPattern pattern) {
if (pattern != null) {
int cols = getColumns();
int maxRows = getRows();
List<String> rows = pattern.getRows();
Map<Character, GuiItem> bindings = pattern.getItemBindings();
for (int r = 0; r < Math.min(rows.size(), maxRows); r++) {
String line = rows.get(r);
for (int c = 0; c < Math.min(line.length(), cols); c++) {
char ch = line.charAt(c);
if (bindings.containsKey(ch)) {
setTabItem(tabId, r * cols + c, bindings.get(ch));
}
}
}
}
}
/**
* Sélectionne et affiche l'onglet demandé pour tous les visualisateurs.
*
* @param tabId L'identifiant de l'onglet à afficher.
* @return true si l'onglet existe et a été sélectionné.
*/
public boolean selectTab(String tabId) {
if (!tabItems.containsKey(tabId) || tabId.equalsIgnoreCase(activeTabId)) {
return false;
}
this.activeTabId = tabId;
applyActiveTabItems();
refreshAll();
return true;
}
/**
* @return L'identifiant de l'onglet actuellement affiché.
*/
public String getActiveTabId() {
return activeTabId;
}
private void applyActiveTabItems() {
if (activeTabId == null) return;
Map<Integer, GuiItem> currentItems = tabItems.get(activeTabId);
if (currentItems != null) {
for (Map.Entry<Integer, GuiItem> entry : currentItems.entrySet()) {
super.setItem(entry.getKey(), entry.getValue());
}
}
}
@Override
public Inventory createInventory(Player player) {
applyActiveTabItems();
return super.createInventory(player);
}
}