diff --git a/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/Minigame.java b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/Minigame.java new file mode 100644 index 0000000..f99a8ba --- /dev/null +++ b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/Minigame.java @@ -0,0 +1,20 @@ +package de.kentoj.scrow.bukkit.minigame; + +import java.util.concurrent.CompletableFuture; + +public interface Minigame { + + /** + * Required participant count + */ + int getMinParticipants(); + + /** + * Maximally allowed participant count + */ + int getMaxParticipants(); + + PlayerManager getPlayerManager(); + + CompletableFuture start(); +} diff --git a/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/PlayerManager.java b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/PlayerManager.java new file mode 100644 index 0000000..a52e9b0 --- /dev/null +++ b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/PlayerManager.java @@ -0,0 +1,58 @@ +package de.kentoj.scrow.bukkit.minigame; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Stream; + + +public class PlayerManager { + + private final Set participants = new HashSet<>(); + private final Set spectators = new HashSet<>(); + + public final Stream getParticipants() { + return participants.stream().map(Bukkit::getPlayer); + } + + public final Stream getSpectators() { + return spectators.stream().map(Bukkit::getPlayer); + } + + public int getParticipantCount() { + return participants.size(); + } + + public void addParticipant(Player player) { + participants.add(player.getUniqueId()); + } + + /** + * @return if the player was a participant + */ + public boolean removeParticipant(Player player) { + return participants.remove(player.getUniqueId()); + } + + public boolean isParticipant(Player player) { + return participants.contains(player.getUniqueId()); + } + + public void addSpectator(Player player) { + spectators.add(player.getUniqueId()); + } + + /** + * @return if the player was in a spectator + */ + public boolean removeSpectator(Player player) { + return spectators.remove(player.getUniqueId()); + } + + public Stream getAllPlayers() { + return Stream.concat(getParticipants(), getSpectators()); + } +} diff --git a/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/lobby/LobbyPhase.java b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/lobby/LobbyPhase.java new file mode 100644 index 0000000..29f49ca --- /dev/null +++ b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/lobby/LobbyPhase.java @@ -0,0 +1,64 @@ +package de.kentoj.scrow.bukkit.minigame.lobby; + +import de.kentoj.scrow.bukkit.minigame.Minigame; +import de.kentoj.scrow.bukkit.minigame.phase.AbstractPhase; +import de.kentoj.scrow.bukkit.minigame.phase.LinearPhaseFlow; +import de.kentoj.scrow.bukkit.minigame.phase.PhaseFlow; +import de.kentoj.scrow.bukkit.minigame.phase.event.EventCanceller; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockExplodeEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.entity.EntityTargetLivingEntityEvent; +import org.bukkit.event.entity.FoodLevelChangeEvent; +import org.bukkit.event.hanging.HangingBreakByEntityEvent; +import org.bukkit.event.player.*; + +public class LobbyPhase extends AbstractPhase { + + private final Minigame game; + private final PhaseFlow phaseFlow; + private final LinearPhaseFlow subPhaseFlow; + + public LobbyPhase(Minigame game, PhaseFlow phaseFlow) { + super(game, phaseFlow); + this.game = game; + this.phaseFlow = phaseFlow; + subPhaseFlow = new LinearPhaseFlow("hunt.lobby"); + subPhaseFlow.addPhase(new WaitForMinimumPlayersPhase(game, subPhaseFlow)); + subPhaseFlow.addPhase(new WaitForMaximumPlayersPhase(game, subPhaseFlow)); + + addEventHandler(PlayerJoinEvent.class, this::onJoin); + addEventHandler(PlayerQuitEvent.class, this::onQuit); + + addEventHandler(HangingBreakByEntityEvent.class, new EventCanceller<>()); + addEventHandler(PlayerInteractAtEntityEvent.class, new EventCanceller<>()); + addEventHandler(PlayerInteractEntityEvent.class, new EventCanceller<>()); + addEventHandler(PlayerDropItemEvent.class, new EventCanceller<>()); + addEventHandler(BlockExplodeEvent.class, new EventCanceller<>()); + addEventHandler(EntityExplodeEvent.class, new EventCanceller<>()); + addEventHandler(EntityDamageEvent.class, new EventCanceller<>()); + addEventHandler(FoodLevelChangeEvent.class, new EventCanceller<>()); + addEventHandler(BlockBreakEvent.class, new EventCanceller<>()); + addEventHandler(BlockPlaceEvent.class, new EventCanceller<>()); + addEventHandler(EntityTargetLivingEntityEvent.class, new EventCanceller<>()); + } + + @Override + public void onStart() { + subPhaseFlow.start().thenRun(phaseFlow::advancePhase); + } + + private void onJoin(PlayerJoinEvent ev) { + game.getPlayerManager().addParticipant(ev.getPlayer()); + game.getPlayerManager().getAllPlayers().forEach(p -> + p.sendMessage("§8[§a+§8] §7" + ev.getPlayer().getName())); + } + + private void onQuit(PlayerQuitEvent ev) { + if (game.getPlayerManager().removeParticipant(ev.getPlayer())) + game.getPlayerManager().getAllPlayers().forEach(p -> + p.sendMessage("§8[§c-§8] §7" + ev.getPlayer().getName())); + } +} diff --git a/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/lobby/WaitForMaximumPlayersPhase.java b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/lobby/WaitForMaximumPlayersPhase.java new file mode 100644 index 0000000..f0a2108 --- /dev/null +++ b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/lobby/WaitForMaximumPlayersPhase.java @@ -0,0 +1,47 @@ +package de.kentoj.scrow.bukkit.minigame.lobby; + +import de.kentoj.scrow.bukkit.minigame.Minigame; +import de.kentoj.scrow.bukkit.minigame.phase.AbstractPhase; +import de.kentoj.scrow.bukkit.minigame.phase.PhaseFlow; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.title.Title; +import org.bukkit.event.player.PlayerQuitEvent; + +import java.time.Duration; + +public class WaitForMaximumPlayersPhase extends AbstractPhase { + + private final Minigame game; + private final PhaseFlow phaseFlow; + private int left; + + WaitForMaximumPlayersPhase(Minigame game, PhaseFlow phaseFlow) { + super(game, phaseFlow); + this.game = game; + this.phaseFlow = phaseFlow; + addEventHandler(PlayerQuitEvent.class, this::onQuit); + } + + private void onQuit(PlayerQuitEvent ev) { + var isEnough = game.getPlayerManager().getParticipantCount() >= game.getMinParticipants(); + if (!isEnough) phaseFlow.rewindPhase(); + } + + @Override + public void onStart() { + this.left = 5; + } + + @Override + protected void onTick(int tick) { + if (tick % 20 == 0) { + game.getPlayerManager().getAllPlayers().forEach(player -> player.showTitle(Title.title( + Component.text("Game starting in " + left + " seconds"), + Component.text("have fun"), + Title.Times.times(Duration.ofMillis(50), Duration.ZERO, Duration.ofMillis(50)) + ))); + left--; + if (left == 0) phaseFlow.advancePhase(); + } + } +} diff --git a/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/lobby/WaitForMinimumPlayersPhase.java b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/lobby/WaitForMinimumPlayersPhase.java new file mode 100644 index 0000000..41571f6 --- /dev/null +++ b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/lobby/WaitForMinimumPlayersPhase.java @@ -0,0 +1,24 @@ +package de.kentoj.scrow.bukkit.minigame.lobby; + +import de.kentoj.scrow.bukkit.minigame.Minigame; +import de.kentoj.scrow.bukkit.minigame.phase.AbstractPhase; +import de.kentoj.scrow.bukkit.minigame.phase.PhaseFlow; +import org.bukkit.event.player.PlayerJoinEvent; + +public class WaitForMinimumPlayersPhase extends AbstractPhase { + + private final Minigame game; + private final PhaseFlow phaseFlow; + + WaitForMinimumPlayersPhase(Minigame game, PhaseFlow phaseFlow) { + super(game, phaseFlow); + this.game = game; + this.phaseFlow = phaseFlow; + this.addEventHandler(PlayerJoinEvent.class, this::onJoin); + } + + private void onJoin(PlayerJoinEvent ev) { + boolean isEnoughParticipants = game.getPlayerManager().getParticipantCount() >= game.getMinParticipants(); + if (isEnoughParticipants) phaseFlow.advancePhase(); + } +} diff --git a/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/AbstractPhase.java b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/AbstractPhase.java new file mode 100644 index 0000000..90a695f --- /dev/null +++ b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/AbstractPhase.java @@ -0,0 +1,99 @@ +package de.kentoj.scrow.bukkit.minigame.phase; + +import com.google.errorprone.annotations.ForOverride; +import de.kentoj.scrow.bukkit.ScrowAPI; +import de.kentoj.scrow.bukkit.minigame.Minigame; +import de.kentoj.scrow.bukkit.minigame.phase.event.EventHandler; +import de.kentoj.scrowlib.convention.ScrowMessageStyle; +import lombok.extern.slf4j.Slf4j; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.event.Event; +import org.bukkit.event.EventPriority; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +@Slf4j +public abstract class AbstractPhase implements Phase { + + private static final ScrowMessageStyle style = new ScrowMessageStyle("Game", NamedTextColor.RED); + + private final List> handlers = new ArrayList<>(); + private CompletableFuture future; + + private final T game; + private final PhaseFlow phaseFlow; + + protected AbstractPhase(T game, PhaseFlow phaseFlow) { + this.game = game; + this.phaseFlow = phaseFlow; + } + + @ForOverride + protected void onStart() { + } + + @ForOverride + protected void onEnd() { + } + + @ForOverride + protected void onTick(int tick) { + } + + @Override + public final void start() { + if (isActive()) return; + future = new CompletableFuture<>(); + future.thenRun(() -> handlers.forEach(PhaseEventHandler::disable)); + future.thenRun(() -> future = null); + future.thenRun(this::onEnd); + + handlers.forEach(PhaseEventHandler::enable); + try { + onStart(); + } catch (Throwable throwable) { + log.error("Error while starting phase {}. Rewinding to previous phase...", getClass().getCanonicalName(), throwable); + phaseFlow.rewindPhase(); + game.getPlayerManager().getAllPlayers().forEach(player -> + player.sendMessage(style.err("Rewinding phase due to an error.")) + ); + } + runTicker(); + } + + @Override + public final void end() { + if (!isActive()) return; + future.complete(null); + } + + public final void addEventHandler(Class eventClass, EventPriority priority, EventHandler handler) { + handlers.add(new PhaseEventHandler<>(eventClass, priority, handler::handle)); + } + + public final void addEventHandler(Class eventClass, EventHandler handler) { + handlers.add(new PhaseEventHandler<>(eventClass, EventPriority.NORMAL, handler::handle)); + } + + private boolean isActive() { + return future != null; + } + + private void runTicker() { + new BukkitRunnable() { + int tick = 0; + + @Override + public void run() { + if (!isActive()) { + this.cancel(); + return; + } + onTick(tick++); + } + }.runTaskTimer(ScrowAPI.plugin, 1, 1); + } +} diff --git a/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/LinearPhaseFlow.java b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/LinearPhaseFlow.java new file mode 100644 index 0000000..6093bc0 --- /dev/null +++ b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/LinearPhaseFlow.java @@ -0,0 +1,73 @@ +package de.kentoj.scrow.bukkit.minigame.phase; + +import com.google.common.base.Preconditions; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.bukkit.Bukkit; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +@RequiredArgsConstructor +public class LinearPhaseFlow implements PhaseFlow { + + @Getter + private final String name; + + private final List phases = new ArrayList<>(); + private CompletableFuture onEnd = null; + private int curInd = -1; + + public void addPhase(Phase phase) { + this.phases.add(phase); + } + + @Override + public void advancePhase() { + Phase cur; + + cur = getCurrent(); + if (cur != null) cur.end(); + + curInd++; + cur = getCurrent(); + if (cur == null) { + curInd--; + onEnd.complete(null); + return; + } + cur.start(); + } + + @Override + public void rewindPhase() { + Phase cur; + + cur = getCurrent(); + if (cur != null) cur.end(); + + curInd--; + cur = getCurrent(); + if (cur == null) { + curInd++; + throw new IllegalStateException("no previous phase"); + } + cur.start(); + } + + private @Nullable Phase getCurrent() { + if (curInd < 0 || curInd >= phases.size()) return null; + return phases.get(curInd); + } + + @Override + public CompletableFuture start() { + Preconditions.checkState(onEnd == null, "attempting to start game that is already active"); + onEnd = new CompletableFuture<>(); + onEnd.thenRun(() -> onEnd = null); + advancePhase(); + return onEnd; + } +} diff --git a/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/Phase.java b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/Phase.java new file mode 100644 index 0000000..295d509 --- /dev/null +++ b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/Phase.java @@ -0,0 +1,16 @@ +package de.kentoj.scrow.bukkit.minigame.phase; + +public interface Phase { + + /** + * will do nothing if the phase is already active. + * Implementations shall advance the phase using {@link PhaseFlow#advancePhase()}. + */ + void start(); + + /** + * will do nothing if the phase is not active. + * THIS DOES NOT ADVANCE TO THE NEXT PHASE. USE {@link PhaseFlow#advancePhase()} INSTEAD. + **/ + void end(); +} diff --git a/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/PhaseEventHandler.java b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/PhaseEventHandler.java new file mode 100644 index 0000000..69daff6 --- /dev/null +++ b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/PhaseEventHandler.java @@ -0,0 +1,28 @@ +package de.kentoj.scrow.bukkit.minigame.phase; + +import de.kentoj.scrow.bukkit.ScrowAPI; +import lombok.RequiredArgsConstructor; +import org.bukkit.Bukkit; +import org.bukkit.event.Event; +import org.bukkit.event.EventPriority; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; + +import java.util.function.Consumer; + +@RequiredArgsConstructor +class PhaseEventHandler implements Listener { + private final Class eventClass; + private final EventPriority priority; + private final Consumer handler; + + @SuppressWarnings("unchecked") + public void enable() { + Bukkit.getPluginManager().registerEvent(eventClass, this, priority, + (__, ev) -> handler.accept((E) ev), ScrowAPI.plugin); + } + + public void disable() { + HandlerList.unregisterAll(this); + } +} diff --git a/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/PhaseFlow.java b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/PhaseFlow.java new file mode 100644 index 0000000..534ee61 --- /dev/null +++ b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/PhaseFlow.java @@ -0,0 +1,23 @@ +package de.kentoj.scrow.bukkit.minigame.phase; + +import java.util.concurrent.CompletableFuture; + +public interface PhaseFlow { + + String getName(); + + /** + * end the current phase, go to the next phase and activate it + */ + void advancePhase(); + + /** + * end the current phase, go to the previous phase and activate it + */ + void rewindPhase(); + + /** + * CompletableFuture is completed when the game finishes + */ + CompletableFuture start(); +} diff --git a/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/event/EventCanceller.java b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/event/EventCanceller.java new file mode 100644 index 0000000..6962388 --- /dev/null +++ b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/event/EventCanceller.java @@ -0,0 +1,10 @@ +package de.kentoj.scrow.bukkit.minigame.phase.event; + +import org.bukkit.event.Cancellable; + +public class EventCanceller implements EventHandler { + @Override + public void handle(Cancellable ev) { + ev.setCancelled(true); + } +} diff --git a/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/event/EventHandler.java b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/event/EventHandler.java new file mode 100644 index 0000000..0f88c2e --- /dev/null +++ b/core-bukkit-api/src/main/java/de/kentoj/scrow/bukkit/minigame/phase/event/EventHandler.java @@ -0,0 +1,7 @@ +package de.kentoj.scrow.bukkit.minigame.phase.event; + +@FunctionalInterface +public interface EventHandler { + + void handle(T ev); +}