GUIParent & GUI-based Animations

This commit is contained in:
NichtStudioCode 2021-01-28 00:31:21 +01:00
parent 3eb57e9fae
commit e4980188e2
10 changed files with 272 additions and 135 deletions

@ -1,7 +1,7 @@
package de.studiocode.invgui.animation;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import de.studiocode.invgui.gui.GUI;
import de.studiocode.invgui.window.Window;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@ -10,20 +10,19 @@ import java.util.function.BiConsumer;
public interface Animation {
/**
* Sets the {@link Player} that will see this animation.
* Useful for playing sounds in a showHandler. ({@link #addShowHandler(BiConsumer)})
* Sets the {@link GUI} this {@link Animation} will take place in.
*
* @param player The {@link Player} that will se this {@link Animation}.
* @param gui The {@link GUI} this {@link Animation} will take place in
*/
void setPlayer(@NotNull Player player);
void setGUI(GUI gui);
/**
* Sets the bounds of the {@link Inventory} this {@link Animation} will take place in.
* Sets the {@link Window}s that will see this animation.
* Useful for playing sounds in a showHandler. ({@link #addShowHandler(BiConsumer)})
*
* @param width The width of the {@link Inventory}
* @param height The height {@link Inventory}
* @param windows The {@link Window}s that will see this animation
*/
void setBounds(int width, int height);
void setWindows(@NotNull List<Window> windows);
/**
* Sets the slots that should be shown.

@ -2,41 +2,58 @@ package de.studiocode.invgui.animation.impl;
import de.studiocode.invgui.InvGui;
import de.studiocode.invgui.animation.Animation;
import de.studiocode.invgui.gui.GUI;
import de.studiocode.invgui.util.SlotUtils;
import de.studiocode.invgui.window.Window;
import org.bukkit.Bukkit;
import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
public abstract class BaseAnimation implements Animation {
private final int tickDelay;
private final List<Runnable> finishHandlers = new ArrayList<>();
private final int tickDelay;
private GUI gui;
private int width;
private int height;
private int size;
private Player player;
private List<Window> windows;
private CopyOnWriteArrayList<Integer> slots;
private BiConsumer<Integer, Integer> show;
private BukkitTask task;
private int frame;
private int noViewerTicks;
public BaseAnimation(int tickDelay) {
this.tickDelay = tickDelay;
}
protected abstract void handleFrame(int frame);
@Override
public void setGUI(GUI gui) {
this.gui = gui;
this.width = gui.getWidth();
this.height = gui.getHeight();
}
@Override
public void setBounds(int width, int height) {
this.width = width;
this.height = height;
this.size = width * height;
public void setWindows(@NotNull List<Window> windows) {
this.windows = windows;
}
@Override
public void setSlots(List<Integer> slots) {
this.slots = new CopyOnWriteArrayList<>(slots);
}
@Override
@ -45,22 +62,32 @@ public abstract class BaseAnimation implements Animation {
else this.show = show;
}
protected void show(int... slots) {
for (int i : slots) show.accept(frame, i);
}
@Override
public void addFinishHandler(@NotNull Runnable finish) {
finishHandlers.add(finish);
}
public CopyOnWriteArrayList<Integer> getSlots() {
return slots;
@Override
public void start() {
task = Bukkit.getScheduler().runTaskTimer(InvGui.getInstance().getPlugin(), () -> {
// if there are now viewers for more than 3 ticks, the animation can be cancelled
if (getViewers().isEmpty()) {
noViewerTicks++;
if (noViewerTicks > 3) {
gui.cancelAnimation();
return;
}
} else noViewerTicks = 0;
// handle the next frame
handleFrame(frame);
frame++;
}, 0, tickDelay);
}
@Override
public void setSlots(List<Integer> slots) {
this.slots = new CopyOnWriteArrayList<>(slots);
public void cancel() {
task.cancel();
}
protected void finish() {
@ -68,11 +95,14 @@ public abstract class BaseAnimation implements Animation {
finishHandlers.forEach(Runnable::run);
}
public void start() {
task = Bukkit.getScheduler().runTaskTimer(InvGui.getInstance().getPlugin(), () -> {
handleFrame(frame);
frame++;
}, 0, tickDelay);
protected abstract void handleFrame(int frame);
public CopyOnWriteArrayList<Integer> getSlots() {
return slots;
}
protected void show(int... slots) {
for (int i : slots) show.accept(frame, i);
}
protected int convToIndex(int x, int y) {
@ -82,10 +112,6 @@ public abstract class BaseAnimation implements Animation {
return SlotUtils.convertToIndex(x, y, width);
}
public void cancel() {
task.cancel();
}
protected int getWidth() {
return width;
}
@ -94,16 +120,14 @@ public abstract class BaseAnimation implements Animation {
return height;
}
protected int getSize() {
return size;
}
protected Player getPlayer() {
return player;
}
public void setPlayer(@NotNull Player player) {
this.player = player;
public List<Player> getViewers() {
return windows.stream()
.map(window -> {
List<HumanEntity> viewers = window.getInventory().getViewers();
return (Player) (viewers.isEmpty() ? null : viewers.get(0));
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}

@ -7,8 +7,8 @@ public abstract class SoundAnimation extends BaseAnimation {
public SoundAnimation(int tickDelay, boolean sound) {
super(tickDelay);
if (sound) addShowHandler((frame, index) -> getPlayer().playSound(getPlayer().getLocation(),
Sound.ENTITY_ITEM_PICKUP, 1, 1));
if (sound) addShowHandler((frame, index) -> getViewers().forEach(player ->
player.playSound(player.getLocation(), Sound.ENTITY_ITEM_PICKUP, 1, 1)));
}
}

@ -1,5 +1,6 @@
package de.studiocode.invgui.gui;
import de.studiocode.invgui.animation.Animation;
import de.studiocode.invgui.gui.SlotElement.ItemStackHolder;
import de.studiocode.invgui.gui.impl.*;
import de.studiocode.invgui.item.Item;
@ -11,6 +12,10 @@ import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Set;
import java.util.function.Predicate;
/**
* A GUI is a container for width * height {@link SlotElement}s.<br>
@ -59,7 +64,7 @@ public interface GUI {
* @param y The y coordinate
* @param slotElement The {@link SlotElement} to be placed there.
*/
void setSlotElement(int x, int y, @NotNull SlotElement slotElement);
void setSlotElement(int x, int y, SlotElement slotElement);
/**
* Sets the {@link SlotElement} on these coordinates.
@ -68,7 +73,7 @@ public interface GUI {
* @param index The slot index
* @param slotElement The {@link SlotElement} to be placed there.
*/
void setSlotElement(int index, @NotNull SlotElement slotElement);
void setSlotElement(int index, SlotElement slotElement);
/**
* Gets the {@link SlotElement} on these coordinates.
@ -204,6 +209,40 @@ public interface GUI {
*/
void handleItemShift(InventoryClickEvent event);
/**
* Adds a {@link GUIParent} to the set of {@link GUIParent}s.
*
* @param parent The {@link GUIParent} to add
*/
void addParent(@NotNull GUIParent parent);
/**
* Removes a {@link GUIParent} from the set of {@link GUIParent}s
*
* @param parent The {@link GUIParent} to remove
*/
void removeParent(@NotNull GUIParent parent);
/**
* Gets all {@link GUIParent}s.
*
* @return The {@link GUIParent}s of this {@link GUI}
*/
Set<GUIParent> getParents();
/**
* Plays an {@link Animation}.
*
* @param animation The {@link Animation} to play.
* @param filter The filter that selects which {@link ItemStackHolder}s should be animated.
*/
void playAnimation(@NotNull Animation animation, @Nullable Predicate<ItemStackHolder> filter);
/**
* Cancels the running {@link Animation} if there is one.
*/
void cancelAnimation();
// ---- fill methods ----
/**

@ -0,0 +1,13 @@
package de.studiocode.invgui.gui;
public interface GUIParent {
/**
* Called by the child {@link GUI} to report an update of a {@link SlotElement}.
*
* @param child The child {@link GUI} whose {@link SlotElement} has changed
* @param slotIndex The slot index of the changed {@link SlotElement} in the child {@link GUI}
*/
void handleSlotElementUpdate(GUI child, int slotIndex);
}

@ -28,7 +28,7 @@ public abstract class BaseGUI extends IndexedGUI {
}
@Override
public void setSlotElement(int x, int y, @NotNull SlotElement slotElement) {
public void setSlotElement(int x, int y, SlotElement slotElement) {
setSlotElement(convToIndex(x, y), slotElement);
}
@ -81,7 +81,7 @@ public abstract class BaseGUI extends IndexedGUI {
public void fill(@NotNull SortedSet<Integer> slots, Item item, boolean replaceExisting) {
for (int slot : slots) {
if (!replaceExisting && slotElements[slot] != null) continue;
if (!replaceExisting && hasSlotElement(slot)) continue;
setItem(slot, item);
}
}
@ -89,14 +89,14 @@ public abstract class BaseGUI extends IndexedGUI {
@Override
public void fill(int start, int end, Item item, boolean replaceExisting) {
for (int i = start; i < end; i++) {
if (!replaceExisting && slotElements[i] != null) continue;
if (!replaceExisting && hasSlotElement(i)) continue;
setItem(i, item);
}
}
@Override
public void fill(Item item, boolean replaceExisting) {
fill(0, size, item, replaceExisting);
fill(0, getSize(), item, replaceExisting);
}
@Override

@ -1,6 +1,8 @@
package de.studiocode.invgui.gui.impl;
import de.studiocode.invgui.animation.Animation;
import de.studiocode.invgui.gui.GUI;
import de.studiocode.invgui.gui.GUIParent;
import de.studiocode.invgui.gui.SlotElement;
import de.studiocode.invgui.gui.SlotElement.ItemSlotElement;
import de.studiocode.invgui.gui.SlotElement.ItemStackHolder;
@ -10,19 +12,26 @@ import de.studiocode.invgui.item.Item;
import de.studiocode.invgui.util.ArrayUtils;
import de.studiocode.invgui.virtualinventory.VirtualInventory;
import de.studiocode.invgui.virtualinventory.event.ItemUpdateEvent;
import de.studiocode.invgui.window.Window;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Predicate;
abstract class IndexedGUI implements GUI {
abstract class IndexedGUI implements GUI, GUIParent {
protected final int size;
protected final SlotElement[] slotElements;
private final int size;
private final SlotElement[] slotElements;
private final Set<GUIParent> parents = new HashSet<>();
private SlotElement[] animationElements;
private Animation animation;
public IndexedGUI(int size) {
this.size = size;
@ -31,6 +40,12 @@ abstract class IndexedGUI implements GUI {
@Override
public void handleClick(int slotNumber, Player player, ClickType clickType, InventoryClickEvent event) {
if (animation != null) {
// cancel all clicks if an animation is running
event.setCancelled(true);
return;
}
SlotElement slotElement = slotElements[slotNumber];
if (slotElement instanceof LinkedSlotElement) {
LinkedSlotElement linkedElement = (LinkedSlotElement) slotElement;
@ -123,6 +138,8 @@ abstract class IndexedGUI implements GUI {
public void handleItemShift(InventoryClickEvent event) {
event.setCancelled(true);
if (animation != null) return; // cancel all clicks if an animation is running
Player player = (Player) event.getWhoClicked();
ItemStack clicked = event.getCurrentItem();
@ -157,8 +174,119 @@ abstract class IndexedGUI implements GUI {
}
@Override
public void setSlotElement(int index, @NotNull SlotElement slotElement) {
public void handleSlotElementUpdate(GUI child, int slotIndex) {
// find all SlotElements that link to this slotIndex in this child GUI and notify all parents
for (int index = 0; index < size; index++) {
SlotElement element = slotElements[index];
if (element instanceof LinkedSlotElement) {
LinkedSlotElement linkedSlotElement = (LinkedSlotElement) element;
if (linkedSlotElement.getGui() == child && linkedSlotElement.getSlotIndex() == slotIndex)
for (GUIParent parent : parents) parent.handleSlotElementUpdate(this, index);
}
}
}
@Override
public void addParent(@NotNull GUIParent parent) {
parents.add(parent);
}
@Override
public void removeParent(@NotNull GUIParent parent) {
parents.remove(parent);
}
@Override
public Set<GUIParent> getParents() {
return parents;
}
private List<Window> findAllWindows() {
List<Window> windows = new ArrayList<>();
List<GUIParent> parents = new ArrayList<>(this.parents);
while (!parents.isEmpty()) {
List<GUIParent> parents1 = new ArrayList<>(parents);
parents.clear();
for (GUIParent parent : parents1) {
if (parent instanceof GUI) parents.addAll(((GUI) parent).getParents());
else if (parent instanceof Window) windows.add((Window) parent);
}
}
return windows;
}
@Override
public void playAnimation(@NotNull Animation animation, @Nullable Predicate<ItemStackHolder> filter) {
if (animation != null) cancelAnimation();
this.animation = animation;
this.animationElements = slotElements.clone();
List<Integer> slots = new ArrayList<>();
for (int i = 0; i < size; i++) {
ItemStackHolder holder = getItemStackHolder(i);
if (holder != null && (filter == null || filter.test(holder))) {
slots.add(i);
setSlotElement(i, null);
}
}
animation.setSlots(slots);
animation.setGUI(this);
animation.setWindows(findAllWindows());
animation.addShowHandler((frame, index) -> setSlotElement(index, animationElements[index]));
animation.addFinishHandler(() -> {
this.animation = null;
this.animationElements = null;
});
animation.start();
}
@Override
public void cancelAnimation() {
if (this.animation != null) {
// cancel the scheduler task and set animation to null
animation.cancel();
animation = null;
// show all SlotElements again
for (int i = 0; i < size; i++) setSlotElement(i, animationElements[i]);
animationElements = null;
}
}
@Override
public void setSlotElement(int index, SlotElement slotElement) {
SlotElement oldElement = slotElements[index];
GUI oldLink = oldElement instanceof LinkedSlotElement ? ((LinkedSlotElement) oldElement).getGui() : null;
// set new SlotElement on index
slotElements[index] = slotElement;
GUI newLink = slotElement instanceof LinkedSlotElement ? ((LinkedSlotElement) slotElement).getGui() : null;
// notify parents that a SlotElement has been changed
parents.forEach(parent -> parent.handleSlotElementUpdate(this, index));
// if newLink is the same as oldLink, there isn't anything to be done
if (newLink == oldLink) return;
// if the slot previously linked to GUI
if (oldLink != null) {
// If no other slot still links to that GUI, remove this GUI from parents
if (Arrays.stream(slotElements)
.filter(element -> element instanceof LinkedSlotElement)
.map(element -> ((LinkedSlotElement) element).getGui())
.noneMatch(gui -> gui == oldLink)) oldLink.removeParent(this);
}
// if the slot now links to a GUI add this as parent
if (newLink != null) {
newLink.addParent(this);
}
}
@Override
@ -179,7 +307,7 @@ abstract class IndexedGUI implements GUI {
@Override
public void setItem(int index, Item item) {
remove(index);
if (item != null) slotElements[index] = new ItemSlotElement(item);
if (item != null) setSlotElement(index, new ItemSlotElement(item));
}
@Override
@ -211,7 +339,7 @@ abstract class IndexedGUI implements GUI {
@Override
public void remove(int index) {
slotElements[index] = null;
setSlotElement(index, null);
}
@Override

@ -1,8 +1,6 @@
package de.studiocode.invgui.window;
import de.studiocode.invgui.animation.Animation;
import de.studiocode.invgui.gui.GUI;
import de.studiocode.invgui.gui.SlotElement.ItemStackHolder;
import de.studiocode.invgui.item.Item;
import de.studiocode.invgui.item.itembuilder.ItemBuilder;
import de.studiocode.invgui.virtualinventory.VirtualInventory;
@ -15,15 +13,12 @@ import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
import java.util.function.Predicate;
/**
* A window is the way to show a player a GUI.
* Windows can only have one viewer at a time.
* Windows can only have one viewer.
*
* @see BaseWindow
* @see NormalInventoryWindow
@ -46,12 +41,6 @@ public interface Window {
*/
GUI getGui();
/**
* A method called every tick by the {@link WindowManager}
* to re-draw the {@link Item}s.
*/
void handleTick();
/**
* A method called by the {@link WindowManager} to notify the Window
* that one of its {@link Item}s has been clicked.
@ -126,14 +115,6 @@ public interface Window {
*/
void show();
/**
* Plays an animation.
*
* @param animation The animation to play.
* @param filter The filter that selects which Items should be animated.
*/
void playAnimation(@NotNull Animation animation, @Nullable Predicate<ItemStackHolder> filter);
/**
* Gets if the player is able to close the {@link Inventory}.
*

@ -27,8 +27,6 @@ public class WindowManager implements Listener {
private WindowManager() {
Bukkit.getPluginManager().registerEvents(this, InvGui.getInstance().getPlugin());
Bukkit.getScheduler().scheduleSyncRepeatingTask(InvGui.getInstance().getPlugin(),
() -> windows.forEach(Window::handleTick), 0, 1);
InvGui.getInstance().addDisableHandler(() -> windows.forEach(w -> w.close(true)));
}

@ -1,8 +1,8 @@
package de.studiocode.invgui.window.impl;
import de.studiocode.invgui.InvGui;
import de.studiocode.invgui.animation.Animation;
import de.studiocode.invgui.gui.GUI;
import de.studiocode.invgui.gui.GUIParent;
import de.studiocode.invgui.gui.SlotElement.ItemSlotElement;
import de.studiocode.invgui.gui.SlotElement.ItemStackHolder;
import de.studiocode.invgui.gui.SlotElement.VISlotElement;
@ -18,13 +18,10 @@ import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Predicate;
public abstract class BaseWindow implements Window {
public abstract class BaseWindow implements Window, GUIParent {
private final GUI gui;
private final int size;
@ -33,7 +30,6 @@ public abstract class BaseWindow implements Window {
private final boolean closeOnEvent;
private final ItemStackHolder[] itemsDisplayed;
private Animation animation;
private boolean closeable;
private boolean closed;
@ -46,6 +42,7 @@ public abstract class BaseWindow implements Window {
this.closeOnEvent = closeOnEvent;
this.itemsDisplayed = new ItemStackHolder[size];
gui.addParent(this);
initItems();
WindowManager.getInstance().addWindow(this);
}
@ -95,25 +92,18 @@ public abstract class BaseWindow implements Window {
}
@Override
public void handleTick() {
for (int i = 0; i < size; i++) {
ItemStackHolder holder = gui.getItemStackHolder(i);
if (itemsDisplayed[i] != holder) redrawItem(i, holder, true);
}
public void handleSlotElementUpdate(GUI child, int slotIndex) {
redrawItem(slotIndex, gui.getItemStackHolder(slotIndex), true);
}
@Override
public void handleClick(InventoryClickEvent event) {
if (animation == null) { // if not in animation, let the gui handle the click
gui.handleClick(event.getSlot(), (Player) event.getWhoClicked(), event.getClick(), event);
} else event.setCancelled(true);
}
@Override
public void handleItemShift(InventoryClickEvent event) {
if (animation == null) { // if not in animation, let the gui handle the item shift
gui.handleItemShift(event);
} else event.setCancelled(true);
}
@Override
@ -125,7 +115,6 @@ public abstract class BaseWindow implements Window {
@Override
public void handleClose(Player player) {
if (closeable) {
stopAnimation();
if (closeOnEvent) close(false);
} else {
if (player.equals(getViewer()))
@ -168,6 +157,8 @@ public abstract class BaseWindow implements Window {
.map(holder -> ((ItemSlotElement) holder).getItem())
.forEach(item -> item.removeWindow(this));
gui.removeParent(this);
if (closeForViewer) closeForViewer();
}
@ -187,42 +178,6 @@ public abstract class BaseWindow implements Window {
viewer.openInventory(inventory);
}
@Override
public void playAnimation(@NotNull Animation animation, @Nullable Predicate<ItemStackHolder> filter) {
if (getViewer() != null) {
this.animation = animation;
List<Integer> slots = new ArrayList<>();
for (int i = 0; i < size; i++) {
ItemStackHolder holder = itemsDisplayed[i];
if (holder != null && (filter == null || filter.test(holder))) {
slots.add(i);
inventory.setItem(i, null);
}
}
animation.setBounds(getGui().getWidth(), getGui().getHeight());
animation.setPlayer(getViewer());
animation.addShowHandler((frame, index) -> redrawItem(index, itemsDisplayed[index], false));
animation.addFinishHandler(() -> this.animation = null);
animation.setSlots(slots);
animation.start();
}
}
private void stopAnimation() {
if (this.animation != null) {
// cancel the scheduler task and set animation to null
animation.cancel();
animation = null;
// show all items again
for (int i = 0; i < gui.getSize(); i++)
redrawItem(i, itemsDisplayed[i], false);
}
}
@Override
public Player getViewer() {
return Bukkit.getPlayer(viewerUUID);