Shuffle floor colors each round, keep eliminated players out of spectator mode, add optional match music
Build / build (push) Successful in 1m17s

This commit is contained in:
Michael Burgess
2026-08-07 11:24:43 -04:00
parent 796f8bd1de
commit a29f359009
6 changed files with 160 additions and 9 deletions
@@ -117,6 +117,7 @@ public class Arena {
public void disable() {
cancelAllTasks();
billboardManager.despawn();
pauseMusic();
// Force reset players out regardless of state
List<UUID> all = new ArrayList<>();
all.addAll(players);
@@ -257,6 +258,9 @@ public class Arena {
if (!floorManager.hasLayout()) {
floorManager.generate(random);
}
if (plugin.getConfigManager().isShuffleFloorEachRound()) {
floorManager.shuffle(random);
}
floorManager.restoreFull(plugin.getConfigManager().getFloorBatchBlocksPerTick(), () -> {
Location safeSpawn = safeSpawnLocation();
for (UUID uuid : players) {
@@ -269,6 +273,7 @@ public class Arena {
p.teleport(safeSpawn);
}
}
startMusic();
startNextRound();
});
}
@@ -359,6 +364,7 @@ public class Arena {
private void processRoundEnd() {
playToParticipants("floor-disappear");
pauseMusic();
floorManager.removeNonTarget(currentTarget, plugin.getConfigManager().getFloorBatchBlocksPerTick(), () -> {
BukkitTask delay = plugin.getServer().getScheduler().runTaskLater(plugin, this::evaluateEliminations,
config.getFloorRemoveDelaySeconds() * 20L);
@@ -379,6 +385,36 @@ public class Arena {
}
}
/** Starts the configured background music (if enabled) for every current player/spectator,
* from the beginning of the track — Bukkit has no true "resume from position" API, so
* pause/resume below is implemented as stop/replay-from-start rather than a real seek. */
private void startMusic() {
if (!plugin.getConfigManager().isMusicEnabled()) {
return;
}
String track = plugin.getConfigManager().getMusicTrack();
float volume = plugin.getConfigManager().getMusicVolume();
for (UUID uuid : allParticipants()) {
Player p = plugin.getServer().getPlayer(uuid);
if (p != null) {
plugin.getSoundUtil().playMusic(p, track, volume);
}
}
}
private void pauseMusic() {
if (!plugin.getConfigManager().isMusicEnabled()) {
return;
}
String track = plugin.getConfigManager().getMusicTrack();
for (UUID uuid : allParticipants()) {
Player p = plugin.getServer().getPlayer(uuid);
if (p != null) {
plugin.getSoundUtil().stopMusic(p, track);
}
}
}
private void evaluateEliminations() {
List<EliminationLogic.PlayerFloorState<UUID, Material>> states = new ArrayList<>();
for (UUID uuid : players) {
@@ -418,11 +454,16 @@ public class Arena {
return;
}
BukkitTask restoreDelay = plugin.getServer().getScheduler().runTaskLater(plugin, () ->
floorManager.restoreFull(plugin.getConfigManager().getFloorBatchBlocksPerTick(), () -> {
playToParticipants("round-complete");
startNextRound();
}),
BukkitTask restoreDelay = plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
if (plugin.getConfigManager().isShuffleFloorEachRound()) {
floorManager.shuffle(random);
}
floorManager.restoreFull(plugin.getConfigManager().getFloorBatchBlocksPerTick(), () -> {
playToParticipants("round-complete");
startMusic();
startNextRound();
});
},
config.getFloorRestoreDelaySeconds() * 20L);
tasks.add(restoreDelay);
}
@@ -437,7 +478,11 @@ public class Arena {
session.setEliminated(true);
}
if (p != null) {
p.setGameMode(GameMode.SPECTATOR);
// Stay in adventure mode rather than true spectator mode (no noclip/flight) -
// being physically placed at the spectator location plus the gameplay
// restriction listener's damage-cancel for the SPECTATOR role is enough for
// them to safely watch without interacting.
p.setGameMode(GameMode.ADVENTURE);
if (config.getSpectator() != null) {
p.teleport(config.getSpectator());
}
@@ -453,6 +498,7 @@ public class Arena {
private void endMatch(UUID winner) {
stateMachine.transition(ArenaPhase.ENDING);
cancelAllTasks();
pauseMusic();
if (winner != null) {
Player winnerPlayer = plugin.getServer().getPlayer(winner);
String winnerName = winnerPlayer != null ? winnerPlayer.getName() : "Unknown";
@@ -551,6 +597,7 @@ public class Arena {
public void shutdown() {
cancelAllTasks();
billboardManager.despawn();
pauseMusic();
List<UUID> all = allParticipants();
for (UUID uuid : all) {
Player p = plugin.getServer().getPlayer(uuid);
@@ -93,6 +93,22 @@ public class ConfigManager {
return config.getInt("performance.floor-blocks-per-tick", 200);
}
public boolean isShuffleFloorEachRound() {
return config.getBoolean("floor.shuffle-each-round", true);
}
public boolean isMusicEnabled() {
return config.getBoolean("music.enabled", false);
}
public String getMusicTrack() {
return config.getString("music.track", "MUSIC_DISC_PIGSTEP");
}
public float getMusicVolume() {
return (float) config.getDouble("music.volume", 1.0);
}
public boolean isPlaceholderApiEnabled() {
return config.getBoolean("integrations.placeholderapi", true);
}
@@ -9,6 +9,7 @@ import us.tss3.blockparty.config.ArenaConfig;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
@@ -89,6 +90,26 @@ public class FloorManager {
return new ArrayList<>(layout.values());
}
/**
* Randomly permutes which material sits at which position, without changing the set of
* positions or how many blocks of each material exist (so target-color counts/fairness
* from {@link #generate} are preserved) — used so the board looks different each round
* instead of always restoring to the exact same static pattern. Purely in-memory: the
* arena's persisted {@code generated-layout} is left untouched, so a restart or
* `/bp generate` still starts from the original saved layout.
*/
public void shuffle(Random random) {
if (layout.size() < 2) {
return;
}
List<Material> materials = new ArrayList<>(layout.values());
Collections.shuffle(materials, random);
List<String> keys = new ArrayList<>(layout.keySet());
for (int i = 0; i < keys.size(); i++) {
layout.put(keys.get(i), materials.get(i));
}
}
/**
* Places every block from the stored layout, all within a single tick, so the floor
* visibly resets instantly at the start of each round instead of filling in gradually.
@@ -2,6 +2,7 @@ package us.tss3.blockparty.util;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.SoundCategory;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
@@ -36,6 +37,25 @@ public final class SoundUtil {
location.getWorld().playSound(location, sound, volume, pitch);
}
/** Plays a configured track to a player on the MUSIC category (respects their music
* volume slider independently of SFX), from the start. */
public void playMusic(Player player, String soundName, float volume) {
Sound sound = resolve(soundName);
if (sound == null) {
return;
}
player.playSound(player.getLocation(), sound, SoundCategory.MUSIC, volume, 1f);
}
/** Stops a configured track for a player on the MUSIC category, if currently playing. */
public void stopMusic(Player player, String soundName) {
Sound sound = resolve(soundName);
if (sound == null) {
return;
}
player.stopSound(sound, SoundCategory.MUSIC);
}
private Sound resolve(String name) {
if (name == null || name.isBlank()) {
return null;