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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user