Fix solo-play win condition, floor-spawn clipping, and make round-end block removal instantaneous
Build / build (push) Successful in 1m17s
Build / build (push) Successful in 1m17s
This commit is contained in:
@@ -134,12 +134,18 @@ disabled, deleted, or the plugin shuts down, so it never leaks entities.
|
|||||||
off entirely. Eliminated players become spectators at the configured spectator location.
|
off entirely. Eliminated players become spectators at the configured spectator location.
|
||||||
- Last player standing wins; rewards, stats and celebration effects (fireworks, titles,
|
- Last player standing wins; rewards, stats and celebration effects (fireworks, titles,
|
||||||
sounds) fire automatically.
|
sounds) fire automatically.
|
||||||
|
- **Solo play**: if a match starts with only one player (e.g. `min-players: 1` for testing),
|
||||||
|
"one player remaining" is the expected steady state, not a win — the match keeps running
|
||||||
|
rounds, each shorter than the last, until that player is actually eliminated or leaves.
|
||||||
|
With two or more players, the match ends the moment only one remains.
|
||||||
|
|
||||||
## Configuration overview
|
## Configuration overview
|
||||||
|
|
||||||
- **`config.yml`** — global settings: `ui.*` toggles for scoreboard/bossbar/titles/actionbar,
|
- **`config.yml`** — global settings: `ui.*` toggles for scoreboard/bossbar/titles/actionbar,
|
||||||
`sounds.*` (Bukkit `Sound` enum names; invalid names are logged and skipped, never crash
|
`sounds.*` (Bukkit `Sound` enum names; invalid names are logged and skipped, never crash
|
||||||
the plugin), `performance.floor-blocks-per-tick` (batch size for floor mutations),
|
the plugin), `performance.floor-blocks-per-tick` (batch size for floor *generation* and
|
||||||
|
*restoration*, spread across ticks — the round-end removal of non-target blocks is always
|
||||||
|
instantaneous, all at once, by design),
|
||||||
`defaults.*` (used only when `/bp create` seeds a new arena), `default-floor-materials`
|
`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
|
(the full 16-color concrete palette by default), `rewards.winner.commands`, and
|
||||||
`integrations.placeholderapi`.
|
`integrations.placeholderapi`.
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ public class Arena {
|
|||||||
|
|
||||||
private int countdownRemaining;
|
private int countdownRemaining;
|
||||||
private int round = 0;
|
private int round = 0;
|
||||||
|
/** True when the match started with exactly one player (solo/testing play): in that case
|
||||||
|
* the match doesn't end just because "one player remains" — it keeps running rounds until
|
||||||
|
* that player is actually eliminated or leaves. */
|
||||||
|
private boolean soloMode;
|
||||||
private Material currentTarget;
|
private Material currentTarget;
|
||||||
private Material previousTarget;
|
private Material previousTarget;
|
||||||
private int currentRoundTime;
|
private int currentRoundTime;
|
||||||
@@ -248,19 +252,21 @@ public class Arena {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
round = 0;
|
round = 0;
|
||||||
|
soloMode = players.size() == 1;
|
||||||
previousTarget = null;
|
previousTarget = null;
|
||||||
if (!floorManager.hasLayout()) {
|
if (!floorManager.hasLayout()) {
|
||||||
floorManager.generate(random);
|
floorManager.generate(random);
|
||||||
}
|
}
|
||||||
floorManager.restoreFull(plugin.getConfigManager().getFloorBatchBlocksPerTick(), () -> {
|
floorManager.restoreFull(plugin.getConfigManager().getFloorBatchBlocksPerTick(), () -> {
|
||||||
|
Location safeSpawn = safeSpawnLocation();
|
||||||
for (UUID uuid : players) {
|
for (UUID uuid : players) {
|
||||||
Player p = plugin.getServer().getPlayer(uuid);
|
Player p = plugin.getServer().getPlayer(uuid);
|
||||||
if (p == null) {
|
if (p == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
p.setGameMode(GameMode.ADVENTURE);
|
p.setGameMode(GameMode.ADVENTURE);
|
||||||
if (config.getSpawn() != null) {
|
if (safeSpawn != null) {
|
||||||
p.teleport(config.getSpawn());
|
p.teleport(safeSpawn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
startNextRound();
|
startNextRound();
|
||||||
@@ -272,10 +278,12 @@ public class Arena {
|
|||||||
endMatch(null);
|
endMatch(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Only treat "down to one player" as a win once at least one round has actually been
|
// In solo mode the match only ends when the lone player is actually eliminated (see
|
||||||
// played (round > 0). At round 0 (the very first round after the countdown), a single
|
// evaluateEliminations) or leaves (players.isEmpty() above) — "one player remaining"
|
||||||
// player is expected whenever min-players is configured as 1 for solo play/testing.
|
// is the expected steady state, not a win condition, so round count keeps climbing.
|
||||||
if (round > 0 && EliminationLogic.isMatchOver(players.size())) {
|
// In normal multiplayer, only treat "down to one player" as a win once at least one
|
||||||
|
// round has actually been played (round > 0); at round 0 that's just the initial join.
|
||||||
|
if (!soloMode && round > 0 && EliminationLogic.isMatchOver(players.size())) {
|
||||||
endMatch(players.get(0));
|
endMatch(players.get(0));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -379,8 +387,12 @@ public class Arena {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (EliminationLogic.isMatchOver(players.size())) {
|
// In solo mode, "1 player left" is the normal steady state — only end when that
|
||||||
UUID winner = players.isEmpty() ? null : players.get(0);
|
// player is actually eliminated (players empty). In multiplayer, end as soon as at
|
||||||
|
// most one player remains.
|
||||||
|
boolean over = soloMode ? players.isEmpty() : EliminationLogic.isMatchOver(players.size());
|
||||||
|
if (over) {
|
||||||
|
UUID winner = (soloMode || players.isEmpty()) ? null : players.get(0);
|
||||||
if (bossBar != null) {
|
if (bossBar != null) {
|
||||||
for (UUID uuid : spectators) {
|
for (UUID uuid : spectators) {
|
||||||
Player p = plugin.getServer().getPlayer(uuid);
|
Player p = plugin.getServer().getPlayer(uuid);
|
||||||
@@ -536,6 +548,23 @@ public class Arena {
|
|||||||
spectators.clear();
|
spectators.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The configured spawn, lifted above the floor's top block layer if it would otherwise
|
||||||
|
* place the player inside/underneath the freshly (re)generated floor. */
|
||||||
|
private Location safeSpawnLocation() {
|
||||||
|
Location spawn = config.getSpawn();
|
||||||
|
if (spawn == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (config.hasFloorRegion()) {
|
||||||
|
int floorTopY = Math.max(config.getPos1()[1], config.getPos2()[1]);
|
||||||
|
if (spawn.getBlockY() <= floorTopY) {
|
||||||
|
spawn = spawn.clone();
|
||||||
|
spawn.setY(floorTopY + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return spawn;
|
||||||
|
}
|
||||||
|
|
||||||
public BillboardManager getBillboardManager() {
|
public BillboardManager getBillboardManager() {
|
||||||
return billboardManager;
|
return billboardManager;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,15 @@ public class FloorManager {
|
|||||||
activeTask = runnable.runTaskTimer(plugin, 0L, 1L);
|
activeTask = runnable.runTaskTimer(plugin, 0L, 1L);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Removes every block that is not the target material, batched across ticks. */
|
/**
|
||||||
|
* Removes every block that is not the target material, all within a single tick so the
|
||||||
|
* non-target floor visibly disappears all at once rather than block-by-block. Unlike
|
||||||
|
* {@link #restoreFull} / {@link #generate}, this is intentionally not spread across ticks:
|
||||||
|
* the "reveal" moment is meant to be instantaneous. Arena floors are expected to stay in
|
||||||
|
* the tens-to-low-thousands of blocks; for a given target color roughly (palette-size - 1)
|
||||||
|
* / palette-size of the floor is removed, which stays comfortably within a single-tick
|
||||||
|
* synchronous block-mutation budget.
|
||||||
|
*/
|
||||||
public void removeNonTarget(Material target, int blocksPerTick, Runnable onComplete) {
|
public void removeNonTarget(Material target, int blocksPerTick, Runnable onComplete) {
|
||||||
World world = config.getWorld();
|
World world = config.getWorld();
|
||||||
if (world == null) {
|
if (world == null) {
|
||||||
@@ -131,31 +139,14 @@ public class FloorManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cancelActiveTask();
|
cancelActiveTask();
|
||||||
Deque<String> queue = new ArrayDeque<>();
|
|
||||||
for (Map.Entry<String, Material> entry : layout.entrySet()) {
|
for (Map.Entry<String, Material> entry : layout.entrySet()) {
|
||||||
if (entry.getValue() != target) {
|
if (entry.getValue() != target) {
|
||||||
queue.add(entry.getKey());
|
setBlock(world, entry.getKey(), Material.AIR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
org.bukkit.scheduler.BukkitRunnable runnable = new org.bukkit.scheduler.BukkitRunnable() {
|
if (onComplete != null) {
|
||||||
@Override
|
onComplete.run();
|
||||||
public void run() {
|
}
|
||||||
int processed = 0;
|
|
||||||
while (processed < blocksPerTick && !queue.isEmpty()) {
|
|
||||||
String key = queue.poll();
|
|
||||||
setBlock(world, key, Material.AIR);
|
|
||||||
processed++;
|
|
||||||
}
|
|
||||||
if (queue.isEmpty()) {
|
|
||||||
cancel();
|
|
||||||
activeTask = null;
|
|
||||||
if (onComplete != null) {
|
|
||||||
onComplete.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
activeTask = runnable.runTaskTimer(plugin, 0L, 1L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clears the entire floor region to air, batched. */
|
/** Clears the entire floor region to air, batched. */
|
||||||
|
|||||||
Reference in New Issue
Block a user