Shuffle floor colors each round, keep eliminated players out of spectator mode, add optional match music
Build / build (push) Successful in 1m17s
Build / build (push) Successful in 1m17s
This commit is contained in:
@@ -114,6 +114,13 @@ work fine from console.
|
||||
configured palette) across the whole region and serializes it into the arena's yml file
|
||||
(`generated-layout`) so it can be restored exactly, every round, without re-scanning or
|
||||
re-randomizing the world.
|
||||
- By default the *positions* from that saved layout are reused every round, but which color
|
||||
sits at which position is reshuffled (`floor.shuffle-each-round: true` in `config.yml`) at
|
||||
the start of the match and before every round's restore — so the board isn't a static,
|
||||
memorizable pattern for the whole game. The shuffle only permutes materials in memory; the
|
||||
saved `generated-layout` itself is never modified, so a restart or another `/bp generate`
|
||||
still starts from the original. Set `shuffle-each-round: false` to always restore the exact
|
||||
saved layout unchanged instead.
|
||||
|
||||
### Billboards
|
||||
|
||||
@@ -149,7 +156,10 @@ nothing leaks.
|
||||
target block is placed in your hotbar (slot 9) as a visual reminder. A boss bar (if
|
||||
enabled) shows the remaining time as a shrinking progress bar.
|
||||
- Get eliminated by not standing on the target color when the floor clears, or by falling
|
||||
off entirely. Eliminated players become spectators at the configured spectator location.
|
||||
off entirely. Eliminated players are teleported to the configured spectator location and
|
||||
marked as spectators for gameplay purposes (no damage, can't interfere), but stay in
|
||||
adventure mode rather than true spectator mode — no noclip/flight, just watching from where
|
||||
they landed.
|
||||
- Last player standing wins; rewards, stats and celebration effects (fireworks, titles,
|
||||
sounds) fire automatically.
|
||||
- **Solo play**: if a match starts with only one player (e.g. `min-players: 1` for testing),
|
||||
@@ -166,14 +176,34 @@ nothing leaks.
|
||||
future/optional batched floor operations — round-end removal and each round's floor reset
|
||||
are always instantaneous, all blocks at once, by design),
|
||||
`defaults.*` (used only when `/bp create` seeds a new arena), `default-floor-materials`
|
||||
(the full 16-color concrete palette by default), `rewards.winner.commands`, and
|
||||
`integrations.placeholderapi`.
|
||||
(the full 16-color concrete palette by default), `floor.shuffle-each-round`, `music.*`
|
||||
(see "Music" below), `rewards.winner.commands`, and `integrations.placeholderapi`.
|
||||
- **`messages.yml`** — every user-facing string, in [MiniMessage](https://docs.advntr.dev/minimessage/format.html)
|
||||
format (Adventure, bundled with Paper). Placeholders like `%player%`, `%arena%`, `%round%`,
|
||||
`%time%`, `%color%` are substituted per-message.
|
||||
- **`arenas/<name>.yml`** — one file per arena; a broken file only disables that one arena
|
||||
(the rest still load) and the error is logged.
|
||||
|
||||
### Music
|
||||
|
||||
Optional background music for the duration of a match, off by default:
|
||||
|
||||
```yaml
|
||||
music:
|
||||
enabled: true
|
||||
track: MUSIC_DISC_PIGSTEP
|
||||
volume: 1.0
|
||||
```
|
||||
|
||||
`track` is any Bukkit `Sound` enum name that's actually a track (one of the `MUSIC_DISC_*`
|
||||
sounds, or a `MUSIC_*` ambient track) — invalid names are logged and skipped, never crash the
|
||||
plugin. It plays on the client's dedicated **Music** volume slider (via `SoundCategory.MUSIC`),
|
||||
separately from sound effects, starting when a match begins. It's paused the instant the floor
|
||||
wipes each round and resumed once the floor has fully reset for the next round. Note this is a
|
||||
stop/replay-from-the-start rather than a true mid-track pause/resume — vanilla Minecraft's sound
|
||||
API has no seek/resume-from-position, so each "resume" restarts the track from the beginning.
|
||||
It's stopped entirely at match end, arena disable, or plugin shutdown.
|
||||
|
||||
### Reload behavior
|
||||
|
||||
`/blockparty reload` re-reads `config.yml`, `messages.yml`, and every arena file. Arenas
|
||||
|
||||
@@ -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, () ->
|
||||
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;
|
||||
|
||||
@@ -21,10 +21,27 @@ sounds:
|
||||
round-complete: BLOCK_NOTE_BLOCK_BELL
|
||||
victory: UI_TOAST_CHALLENGE_COMPLETE
|
||||
|
||||
music:
|
||||
# Optional background music for the duration of a match, played on the client's MUSIC
|
||||
# volume slider (separate from sound effects). Off by default since it's a matter of taste.
|
||||
enabled: false
|
||||
# Any Bukkit Sound enum name that's actually a track, e.g. one of the MUSIC_DISC_* sounds
|
||||
# or a MUSIC_* ambient track. Invalid names are logged and skipped, never crash the plugin.
|
||||
track: MUSIC_DISC_PIGSTEP
|
||||
volume: 1.0
|
||||
|
||||
performance:
|
||||
# blocks processed per server tick when generating/removing/restoring the floor
|
||||
floor-blocks-per-tick: 200
|
||||
|
||||
floor:
|
||||
# If true (default), the floor's material-to-position layout is randomly reshuffled
|
||||
# (same set of positions and same count of each material, just rearranged) at the start
|
||||
# of the match and before every round restore, so the board isn't a static, memorizable
|
||||
# pattern across the whole game. Purely in-memory - the arena's saved generated-layout
|
||||
# (from /bp generate) is never modified by shuffling.
|
||||
shuffle-each-round: true
|
||||
|
||||
# example/default timings used when creating a brand-new arena via /blockparty create
|
||||
defaults:
|
||||
countdown-duration: 15
|
||||
|
||||
Reference in New Issue
Block a user