Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,529 @@
|
||||
package us.tss3.blockparty.arena;
|
||||
|
||||
import net.kyori.adventure.bossbar.BossBar;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.title.Title;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import us.tss3.blockparty.BlockPartyPlugin;
|
||||
import us.tss3.blockparty.config.ArenaConfig;
|
||||
import us.tss3.blockparty.floor.FloorManager;
|
||||
import us.tss3.blockparty.logic.ArenaPhase;
|
||||
import us.tss3.blockparty.logic.ArenaStateMachine;
|
||||
import us.tss3.blockparty.logic.CountdownLogic;
|
||||
import us.tss3.blockparty.logic.EliminationLogic;
|
||||
import us.tss3.blockparty.logic.RoundTimeCalculator;
|
||||
import us.tss3.blockparty.logic.TargetColorSelector;
|
||||
import us.tss3.blockparty.model.PlayerState;
|
||||
import us.tss3.blockparty.session.PlayerSession;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
/** A single running arena: owns its state machine, tasks and round lifecycle. */
|
||||
public class Arena {
|
||||
|
||||
private final BlockPartyPlugin plugin;
|
||||
private final ArenaConfig config;
|
||||
private final FloorManager floorManager;
|
||||
private final ArenaStateMachine stateMachine = new ArenaStateMachine(ArenaPhase.DISABLED);
|
||||
private final Random random = new Random();
|
||||
private final TargetColorSelector selector = new TargetColorSelector(random::nextInt);
|
||||
|
||||
private final List<UUID> players = new ArrayList<>();
|
||||
private final List<UUID> spectators = new ArrayList<>();
|
||||
|
||||
private final List<BukkitTask> tasks = new ArrayList<>();
|
||||
private BossBar bossBar;
|
||||
|
||||
private int countdownRemaining;
|
||||
private int round = 0;
|
||||
private Material currentTarget;
|
||||
private Material previousTarget;
|
||||
private int currentRoundTime;
|
||||
private int roundTimeRemaining;
|
||||
|
||||
public Arena(BlockPartyPlugin plugin, ArenaConfig config) {
|
||||
this.plugin = plugin;
|
||||
this.config = config;
|
||||
this.floorManager = new FloorManager(plugin, config);
|
||||
if (config.isEnabled()) {
|
||||
stateMachine.transition(ArenaPhase.WAITING);
|
||||
}
|
||||
}
|
||||
|
||||
public ArenaConfig getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
public FloorManager getFloorManager() {
|
||||
return floorManager;
|
||||
}
|
||||
|
||||
public ArenaPhase getPhase() {
|
||||
return stateMachine.getState();
|
||||
}
|
||||
|
||||
public List<UUID> getPlayers() {
|
||||
return players;
|
||||
}
|
||||
|
||||
public List<UUID> getSpectators() {
|
||||
return spectators;
|
||||
}
|
||||
|
||||
public int getRound() {
|
||||
return round;
|
||||
}
|
||||
|
||||
public Material getCurrentTarget() {
|
||||
return currentTarget;
|
||||
}
|
||||
|
||||
public int getRoundTimeRemaining() {
|
||||
return roundTimeRemaining;
|
||||
}
|
||||
|
||||
public int getCurrentRoundTime() {
|
||||
return currentRoundTime;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return getPhase() != ArenaPhase.DISABLED;
|
||||
}
|
||||
|
||||
public void enable() {
|
||||
if (stateMachine.transition(ArenaPhase.WAITING)) {
|
||||
config.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void disable() {
|
||||
cancelAllTasks();
|
||||
// Force reset players out regardless of state
|
||||
List<UUID> all = new ArrayList<>();
|
||||
all.addAll(players);
|
||||
all.addAll(spectators);
|
||||
for (UUID uuid : all) {
|
||||
removePlayerForce(uuid);
|
||||
}
|
||||
stateMachine.transition(ArenaPhase.DISABLED);
|
||||
config.setEnabled(false);
|
||||
}
|
||||
|
||||
// ---------- Join / Leave ----------
|
||||
|
||||
public boolean canJoin() {
|
||||
ArenaPhase phase = getPhase();
|
||||
if (phase != ArenaPhase.WAITING && !(phase == ArenaPhase.STARTING && config.isAllowJoinWhileStarting())) {
|
||||
return false;
|
||||
}
|
||||
return players.size() < config.getMaxPlayers();
|
||||
}
|
||||
|
||||
public void addPlayer(Player player) {
|
||||
PlayerState state = PlayerState.capture(player);
|
||||
PlayerSession session = new PlayerSession(player.getUniqueId(), config.getName(), state);
|
||||
plugin.getSessionManager().add(session);
|
||||
players.add(player.getUniqueId());
|
||||
preparePlayerForLobby(player);
|
||||
plugin.getScoreboardManager().attach(player, this);
|
||||
|
||||
maybeStartCountdown();
|
||||
}
|
||||
|
||||
private void preparePlayerForLobby(Player player) {
|
||||
player.getInventory().clear();
|
||||
player.setGameMode(GameMode.ADVENTURE);
|
||||
player.setHealth(20.0);
|
||||
player.setFoodLevel(20);
|
||||
player.setFireTicks(0);
|
||||
if (config.getLobby() != null) {
|
||||
player.teleport(config.getLobby());
|
||||
}
|
||||
}
|
||||
|
||||
public void removePlayer(UUID uuid, boolean restoreState) {
|
||||
players.remove(uuid);
|
||||
spectators.remove(uuid);
|
||||
PlayerSession session = plugin.getSessionManager().get(uuid);
|
||||
Player player = plugin.getServer().getPlayer(uuid);
|
||||
if (session != null) {
|
||||
if (restoreState && player != null) {
|
||||
session.getSavedState().restore(player);
|
||||
}
|
||||
plugin.getSessionManager().remove(uuid);
|
||||
}
|
||||
if (player != null) {
|
||||
plugin.getScoreboardManager().detach(player);
|
||||
}
|
||||
checkCountdownCancel();
|
||||
checkForWinner();
|
||||
}
|
||||
|
||||
private void removePlayerForce(UUID uuid) {
|
||||
removePlayer(uuid, true);
|
||||
}
|
||||
|
||||
private void checkCountdownCancel() {
|
||||
if (getPhase() == ArenaPhase.STARTING
|
||||
&& CountdownLogic.shouldCancel(players.size(), config.getMinPlayers(), true)) {
|
||||
cancelCountdown();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Countdown / Starting ----------
|
||||
|
||||
private void maybeStartCountdown() {
|
||||
if (getPhase() == ArenaPhase.WAITING
|
||||
&& CountdownLogic.shouldStart(players.size(), config.getMinPlayers(), false)) {
|
||||
startCountdown();
|
||||
}
|
||||
}
|
||||
|
||||
private void startCountdown() {
|
||||
if (!stateMachine.transition(ArenaPhase.STARTING)) {
|
||||
return;
|
||||
}
|
||||
countdownRemaining = config.getCountdownDuration();
|
||||
BukkitTask task = plugin.getServer().getScheduler().runTaskTimer(plugin, () -> {
|
||||
if (CountdownLogic.isFinished(countdownRemaining)) {
|
||||
return;
|
||||
}
|
||||
broadcastCountdownTick();
|
||||
countdownRemaining = CountdownLogic.tick(countdownRemaining);
|
||||
if (CountdownLogic.isFinished(countdownRemaining)) {
|
||||
beginMatch();
|
||||
}
|
||||
}, 0L, 20L);
|
||||
tasks.add(task);
|
||||
}
|
||||
|
||||
private void cancelCountdown() {
|
||||
cancelAllTasks();
|
||||
stateMachine.transition(ArenaPhase.WAITING);
|
||||
for (UUID uuid : players) {
|
||||
Player p = plugin.getServer().getPlayer(uuid);
|
||||
if (p != null) {
|
||||
p.sendMessage(plugin.getMessages().get("countdown.cancelled"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void broadcastCountdownTick() {
|
||||
Map<String, String> ph = Map.of("time", String.valueOf(countdownRemaining), "arena", config.getName());
|
||||
for (UUID uuid : players) {
|
||||
Player p = plugin.getServer().getPlayer(uuid);
|
||||
if (p == null) {
|
||||
continue;
|
||||
}
|
||||
if (plugin.getConfigManager().isActionbarEnabled()) {
|
||||
p.sendActionBar(plugin.getMessages().get("countdown.actionbar", ph));
|
||||
}
|
||||
if (plugin.getConfigManager().isTitlesEnabled() && (countdownRemaining <= 5 || countdownRemaining == config.getCountdownDuration())) {
|
||||
p.showTitle(Title.title(plugin.getMessages().get("countdown.title", ph), Component.empty()));
|
||||
}
|
||||
plugin.getSoundUtil().play(p, plugin.getConfigManager().getSound("countdown"), 1f, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Match flow ----------
|
||||
|
||||
private void beginMatch() {
|
||||
cancelAllTasks();
|
||||
if (!stateMachine.transition(ArenaPhase.RUNNING)) {
|
||||
return;
|
||||
}
|
||||
round = 0;
|
||||
previousTarget = null;
|
||||
if (!floorManager.hasLayout()) {
|
||||
floorManager.generate(random);
|
||||
}
|
||||
floorManager.restoreFull(plugin.getConfigManager().getFloorBatchBlocksPerTick(), () -> {
|
||||
for (UUID uuid : players) {
|
||||
Player p = plugin.getServer().getPlayer(uuid);
|
||||
if (p == null) {
|
||||
continue;
|
||||
}
|
||||
p.setGameMode(GameMode.ADVENTURE);
|
||||
if (config.getSpawn() != null) {
|
||||
p.teleport(config.getSpawn());
|
||||
}
|
||||
}
|
||||
startNextRound();
|
||||
});
|
||||
}
|
||||
|
||||
private void startNextRound() {
|
||||
if (EliminationLogic.isMatchOver(players.size())) {
|
||||
endMatch(players.isEmpty() ? null : (players.isEmpty() ? null : players.get(0)));
|
||||
return;
|
||||
}
|
||||
round++;
|
||||
List<Material> available = floorManager.materialsInLayout().stream().distinct().toList();
|
||||
if (available.isEmpty()) {
|
||||
available = config.getFloorMaterials();
|
||||
}
|
||||
currentTarget = selector.selectExcluding(available, previousTarget);
|
||||
previousTarget = currentTarget;
|
||||
currentRoundTime = RoundTimeCalculator.timeForRound(
|
||||
config.getStartingRoundTime(), config.getMinimumRoundTime(), config.getRoundTimeReduction(), round);
|
||||
roundTimeRemaining = currentRoundTime;
|
||||
|
||||
announceTarget();
|
||||
setupBossBar();
|
||||
|
||||
BukkitTask task = plugin.getServer().getScheduler().runTaskTimer(plugin, () -> {
|
||||
roundTimeRemaining--;
|
||||
updateBossBar();
|
||||
if (roundTimeRemaining <= 0) {
|
||||
cancelAllTasks();
|
||||
processRoundEnd();
|
||||
}
|
||||
}, 20L, 20L);
|
||||
tasks.add(task);
|
||||
}
|
||||
|
||||
private void announceTarget() {
|
||||
Map<String, String> ph = Map.of("color", formatMaterial(currentTarget), "round", String.valueOf(round),
|
||||
"time", String.valueOf(currentRoundTime));
|
||||
ItemStack display = new ItemStack(currentTarget);
|
||||
for (UUID uuid : players) {
|
||||
Player p = plugin.getServer().getPlayer(uuid);
|
||||
if (p == null) {
|
||||
continue;
|
||||
}
|
||||
if (plugin.getConfigManager().isTitlesEnabled()) {
|
||||
p.showTitle(Title.title(plugin.getMessages().get("round.title", ph), plugin.getMessages().get("round.subtitle", ph)));
|
||||
}
|
||||
if (plugin.getConfigManager().isActionbarEnabled()) {
|
||||
p.sendActionBar(plugin.getMessages().get("round.actionbar", ph));
|
||||
}
|
||||
p.getInventory().setItem(8, display.clone());
|
||||
plugin.getSoundUtil().play(p, plugin.getConfigManager().getSound("target-select"), 1f, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupBossBar() {
|
||||
if (!plugin.getConfigManager().isBossbarEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (bossBar == null) {
|
||||
bossBar = BossBar.bossBar(Component.text("BlockParty"), 1f, BossBar.Color.YELLOW, BossBar.Overlay.PROGRESS);
|
||||
}
|
||||
bossBar.progress(1f);
|
||||
for (UUID uuid : players) {
|
||||
Player p = plugin.getServer().getPlayer(uuid);
|
||||
if (p != null) {
|
||||
p.showBossBar(bossBar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBossBar() {
|
||||
if (bossBar == null || currentRoundTime <= 0) {
|
||||
return;
|
||||
}
|
||||
float progress = Math.max(0f, Math.min(1f, (float) roundTimeRemaining / (float) currentRoundTime));
|
||||
bossBar.progress(progress);
|
||||
}
|
||||
|
||||
private void processRoundEnd() {
|
||||
plugin.getSoundUtil().play(config.getSpawn(), plugin.getConfigManager().getSound("floor-disappear"), 1f, 1f);
|
||||
floorManager.removeNonTarget(currentTarget, plugin.getConfigManager().getFloorBatchBlocksPerTick(), () -> {
|
||||
BukkitTask delay = plugin.getServer().getScheduler().runTaskLater(plugin, this::evaluateEliminations,
|
||||
config.getFloorRemoveDelaySeconds() * 20L);
|
||||
tasks.add(delay);
|
||||
});
|
||||
}
|
||||
|
||||
private void evaluateEliminations() {
|
||||
List<EliminationLogic.PlayerFloorState<UUID, Material>> states = new ArrayList<>();
|
||||
for (UUID uuid : players) {
|
||||
Player p = plugin.getServer().getPlayer(uuid);
|
||||
Material standing = null;
|
||||
if (p != null && p.getWorld() != null) {
|
||||
standing = floorManager.materialAtLocation(p.getWorld(), p.getLocation().getX(), p.getLocation().getY(), p.getLocation().getZ());
|
||||
}
|
||||
states.add(new EliminationLogic.PlayerFloorState<>(uuid, standing));
|
||||
}
|
||||
List<UUID> eliminated = EliminationLogic.determineEliminated(states, currentTarget);
|
||||
for (UUID uuid : eliminated) {
|
||||
eliminatePlayer(uuid);
|
||||
}
|
||||
for (UUID uuid : players) {
|
||||
PlayerSession session = plugin.getSessionManager().get(uuid);
|
||||
if (session != null && !eliminated.contains(uuid)) {
|
||||
session.incrementRoundsSurvived();
|
||||
}
|
||||
}
|
||||
|
||||
if (EliminationLogic.isMatchOver(players.size())) {
|
||||
UUID winner = players.isEmpty() ? null : players.get(0);
|
||||
if (bossBar != null) {
|
||||
for (UUID uuid : spectators) {
|
||||
Player p = plugin.getServer().getPlayer(uuid);
|
||||
if (p != null) {
|
||||
p.hideBossBar(bossBar);
|
||||
}
|
||||
}
|
||||
}
|
||||
endMatch(winner);
|
||||
return;
|
||||
}
|
||||
|
||||
BukkitTask restoreDelay = plugin.getServer().getScheduler().runTaskLater(plugin, () ->
|
||||
floorManager.restoreFull(plugin.getConfigManager().getFloorBatchBlocksPerTick(), this::startNextRound),
|
||||
config.getFloorRestoreDelaySeconds() * 20L);
|
||||
tasks.add(restoreDelay);
|
||||
}
|
||||
|
||||
private void eliminatePlayer(UUID uuid) {
|
||||
players.remove(uuid);
|
||||
spectators.add(uuid);
|
||||
Player p = plugin.getServer().getPlayer(uuid);
|
||||
PlayerSession session = plugin.getSessionManager().get(uuid);
|
||||
if (session != null) {
|
||||
session.setRole(PlayerSession.Role.SPECTATOR);
|
||||
session.setEliminated(true);
|
||||
}
|
||||
if (p != null) {
|
||||
p.setGameMode(GameMode.SPECTATOR);
|
||||
if (config.getSpectator() != null) {
|
||||
p.teleport(config.getSpectator());
|
||||
}
|
||||
p.sendMessage(plugin.getMessages().get("game.eliminated", Map.of("arena", config.getName(), "round", String.valueOf(round))));
|
||||
plugin.getSoundUtil().play(p, plugin.getConfigManager().getSound("eliminate"), 1f, 1f);
|
||||
if (bossBar != null) {
|
||||
p.hideBossBar(bossBar);
|
||||
}
|
||||
}
|
||||
plugin.getStatsManager().recordElimination(uuid, round);
|
||||
}
|
||||
|
||||
private void endMatch(UUID winner) {
|
||||
stateMachine.transition(ArenaPhase.ENDING);
|
||||
cancelAllTasks();
|
||||
if (winner != null) {
|
||||
Player winnerPlayer = plugin.getServer().getPlayer(winner);
|
||||
String winnerName = winnerPlayer != null ? winnerPlayer.getName() : "Unknown";
|
||||
for (UUID uuid : allParticipants()) {
|
||||
Player p = plugin.getServer().getPlayer(uuid);
|
||||
if (p == null) {
|
||||
continue;
|
||||
}
|
||||
p.showTitle(Title.title(plugin.getMessages().get("game.win-title", Map.of("player", winnerName)),
|
||||
Component.empty()));
|
||||
plugin.getSoundUtil().play(p, plugin.getConfigManager().getSound("victory"), 1f, 1f);
|
||||
}
|
||||
if (winnerPlayer != null) {
|
||||
spawnCelebration(winnerPlayer);
|
||||
}
|
||||
plugin.getStatsManager().recordWin(winner);
|
||||
plugin.getRewardManager().dispatchWinnerRewards(winnerName, config.getName(), round);
|
||||
} else {
|
||||
for (UUID uuid : allParticipants()) {
|
||||
Player p = plugin.getServer().getPlayer(uuid);
|
||||
if (p != null) {
|
||||
p.sendMessage(plugin.getMessages().get("game.no-winner"));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (UUID uuid : players) {
|
||||
plugin.getStatsManager().recordGamePlayed(uuid);
|
||||
}
|
||||
for (UUID uuid : spectators) {
|
||||
plugin.getStatsManager().recordGamePlayed(uuid);
|
||||
}
|
||||
|
||||
BukkitTask endTask = plugin.getServer().getScheduler().runTaskLater(plugin, this::resetToWaiting,
|
||||
config.getWinEndingDelaySeconds() * 20L);
|
||||
tasks.add(endTask);
|
||||
}
|
||||
|
||||
private void spawnCelebration(Player winner) {
|
||||
try {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
org.bukkit.entity.Firework fw = winner.getWorld().spawn(winner.getLocation(), org.bukkit.entity.Firework.class);
|
||||
org.bukkit.inventory.meta.FireworkMeta meta = fw.getFireworkMeta();
|
||||
meta.addEffect(org.bukkit.FireworkEffect.builder()
|
||||
.withColor(org.bukkit.Color.YELLOW, org.bukkit.Color.LIME)
|
||||
.with(org.bukkit.FireworkEffect.Type.BALL_LARGE)
|
||||
.trail(true)
|
||||
.build());
|
||||
meta.setPower(1);
|
||||
fw.setFireworkMeta(meta);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private List<UUID> allParticipants() {
|
||||
List<UUID> all = new ArrayList<>(players);
|
||||
all.addAll(spectators);
|
||||
return all;
|
||||
}
|
||||
|
||||
private void resetToWaiting() {
|
||||
List<UUID> all = allParticipants();
|
||||
for (UUID uuid : all) {
|
||||
removePlayer(uuid, true);
|
||||
}
|
||||
players.clear();
|
||||
spectators.clear();
|
||||
if (bossBar != null) {
|
||||
bossBar.removeViewer(plugin.getServer().getConsoleSender());
|
||||
}
|
||||
floorManager.cancelActiveTask();
|
||||
cancelAllTasks();
|
||||
round = 0;
|
||||
stateMachine.transition(ArenaPhase.WAITING);
|
||||
}
|
||||
|
||||
private void checkForWinner() {
|
||||
if (getPhase() == ArenaPhase.RUNNING && EliminationLogic.isMatchOver(players.size())) {
|
||||
cancelAllTasks();
|
||||
evaluateEliminations();
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelAllTasks() {
|
||||
for (BukkitTask task : tasks) {
|
||||
if (task != null) {
|
||||
task.cancel();
|
||||
}
|
||||
}
|
||||
tasks.clear();
|
||||
floorManager.cancelActiveTask();
|
||||
}
|
||||
|
||||
/** Called on plugin disable / world unload to safely stop everything and restore players. */
|
||||
public void shutdown() {
|
||||
cancelAllTasks();
|
||||
List<UUID> all = allParticipants();
|
||||
for (UUID uuid : all) {
|
||||
Player p = plugin.getServer().getPlayer(uuid);
|
||||
PlayerSession session = plugin.getSessionManager().get(uuid);
|
||||
if (session != null && p != null) {
|
||||
session.getSavedState().restore(p);
|
||||
}
|
||||
plugin.getSessionManager().remove(uuid);
|
||||
}
|
||||
players.clear();
|
||||
spectators.clear();
|
||||
}
|
||||
|
||||
private String formatMaterial(Material material) {
|
||||
String name = material.name().replace("_CONCRETE", "").replace("_", " ").toLowerCase();
|
||||
return name.substring(0, 1).toUpperCase() + name.substring(1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user