This commit is contained in:
@@ -25,7 +25,7 @@ slightly faster round. Last player standing wins.
|
||||
All admin/config commands live under `/blockparty` (alias `/bp`) and work from console
|
||||
where it makes sense (`create`, `delete`, `enable`, `disable`, `info`, `list`, `reload`).
|
||||
Commands that require a physical location (`setlobby`, `setspawn`, `setspectator`,
|
||||
`pos1`, `pos2`) require a player sender.
|
||||
`setbillboard`, `pos1`, `pos2`) require a player sender.
|
||||
|
||||
### Admin
|
||||
| Command | Description |
|
||||
@@ -38,6 +38,8 @@ Commands that require a physical location (`setlobby`, `setspawn`, `setspectator
|
||||
| `/blockparty setlobby <arena>` | Set the waiting-room location |
|
||||
| `/blockparty setspawn <arena>` | Set the in-game spawn location |
|
||||
| `/blockparty setspectator <arena>` | Set the spectator/eliminated location |
|
||||
| `/blockparty setbillboard <arena>` | (Optional) place a display block here that mirrors the current target color |
|
||||
| `/blockparty delbillboard <arena>` | Remove the billboard, if one is set |
|
||||
| `/blockparty pos1 <arena>` | Set floor region corner 1 |
|
||||
| `/blockparty pos2 <arena>` | Set floor region corner 2 |
|
||||
| `/blockparty setfloor <arena> <mat1,mat2,...>` | Set the allowed floor materials |
|
||||
@@ -83,9 +85,12 @@ Commands that require a physical location (`setlobby`, `setspawn`, `setspectator
|
||||
still required once so BlockParty captures/serializes the exact layout it should restore
|
||||
each round (BlockParty does not scan the world every round — it restores from the saved
|
||||
layout for performance and determinism).
|
||||
8. `/bp info lobby1` to confirm there are no validation errors.
|
||||
9. `/bp enable lobby1`.
|
||||
10. Players run `/bp join lobby1`. Once `min-players` is reached the countdown starts
|
||||
8. Optionally, stand somewhere visible near/above the floor and run `/bp setbillboard lobby1`
|
||||
to place a big display block that mirrors the round's target color — purely cosmetic and
|
||||
entirely optional (see "Billboard" below).
|
||||
9. `/bp info lobby1` to confirm there are no validation errors.
|
||||
10. `/bp enable lobby1`.
|
||||
11. Players run `/bp join lobby1`. Once `min-players` is reached the countdown starts
|
||||
automatically; joining an arena mid-countdown is allowed by default
|
||||
(`allow-join-while-waiting: true`).
|
||||
|
||||
@@ -99,6 +104,23 @@ Commands that require a physical location (`setlobby`, `setspawn`, `setspectator
|
||||
(`generated-layout`) so it can be restored exactly, every round, without re-scanning or
|
||||
re-randomizing the world.
|
||||
|
||||
### Billboard
|
||||
|
||||
An arena billboard is a `BlockDisplay` entity that visually shows the currently selected
|
||||
target color/block each round, in addition to the title/actionbar/hotbar-item cues. It is
|
||||
entirely optional:
|
||||
|
||||
- Per-arena: only spawned if `/bp setbillboard <arena>` has been run (stored as `billboard`
|
||||
in the arena's yml). `/bp delbillboard <arena>` removes it.
|
||||
- Per-arena toggle: `billboard-enabled` in the arena's yml (default `true`) lets you keep the
|
||||
location set but temporarily turn it off without deleting it.
|
||||
- Global toggle: `ui.billboard` in `config.yml` (default `true`) disables billboards across
|
||||
every arena at once, e.g. for performance-constrained servers.
|
||||
|
||||
The entity is non-persistent (not saved to the world, so it never survives a server restart on
|
||||
its own — BlockParty re-creates it on demand) and is despawned automatically when the arena is
|
||||
disabled, deleted, or the plugin shuts down, so it never leaks entities.
|
||||
|
||||
## Player usage
|
||||
|
||||
- `/bp join <arena>` to enter the waiting lobby. Your inventory, armor, offhand, health,
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import us.tss3.blockparty.BlockPartyPlugin;
|
||||
import us.tss3.blockparty.billboard.BillboardManager;
|
||||
import us.tss3.blockparty.config.ArenaConfig;
|
||||
import us.tss3.blockparty.floor.FloorManager;
|
||||
import us.tss3.blockparty.logic.ArenaPhase;
|
||||
@@ -35,6 +36,7 @@ public class Arena {
|
||||
private final BlockPartyPlugin plugin;
|
||||
private final ArenaConfig config;
|
||||
private final FloorManager floorManager;
|
||||
private final BillboardManager billboardManager;
|
||||
private final ArenaStateMachine stateMachine = new ArenaStateMachine(ArenaPhase.DISABLED);
|
||||
private final Random random = new Random();
|
||||
private final TargetColorSelector selector = new TargetColorSelector(random::nextInt);
|
||||
@@ -56,6 +58,7 @@ public class Arena {
|
||||
this.plugin = plugin;
|
||||
this.config = config;
|
||||
this.floorManager = new FloorManager(plugin, config);
|
||||
this.billboardManager = new BillboardManager(plugin, config);
|
||||
if (config.isEnabled()) {
|
||||
stateMachine.transition(ArenaPhase.WAITING);
|
||||
}
|
||||
@@ -109,6 +112,7 @@ public class Arena {
|
||||
|
||||
public void disable() {
|
||||
cancelAllTasks();
|
||||
billboardManager.despawn();
|
||||
// Force reset players out regardless of state
|
||||
List<UUID> all = new ArrayList<>();
|
||||
all.addAll(players);
|
||||
@@ -318,6 +322,7 @@ public class Arena {
|
||||
p.getInventory().setItem(8, display.clone());
|
||||
plugin.getSoundUtil().play(p, plugin.getConfigManager().getSound("target-select"), 1f, 1f);
|
||||
}
|
||||
billboardManager.show(currentTarget);
|
||||
}
|
||||
|
||||
private void setupBossBar() {
|
||||
@@ -493,6 +498,7 @@ public class Arena {
|
||||
floorManager.cancelActiveTask();
|
||||
cancelAllTasks();
|
||||
round = 0;
|
||||
billboardManager.clear();
|
||||
stateMachine.transition(ArenaPhase.WAITING);
|
||||
}
|
||||
|
||||
@@ -516,6 +522,7 @@ public class Arena {
|
||||
/** Called on plugin disable / world unload to safely stop everything and restore players. */
|
||||
public void shutdown() {
|
||||
cancelAllTasks();
|
||||
billboardManager.despawn();
|
||||
List<UUID> all = allParticipants();
|
||||
for (UUID uuid : all) {
|
||||
Player p = plugin.getServer().getPlayer(uuid);
|
||||
@@ -529,6 +536,10 @@ public class Arena {
|
||||
spectators.clear();
|
||||
}
|
||||
|
||||
public BillboardManager getBillboardManager() {
|
||||
return billboardManager;
|
||||
}
|
||||
|
||||
private String formatMaterial(Material material) {
|
||||
String name = material.name().replace("_CONCRETE", "").replace("_", " ").toLowerCase();
|
||||
return name.substring(0, 1).toUpperCase() + name.substring(1);
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package us.tss3.blockparty.billboard;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.BlockDisplay;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.util.Transformation;
|
||||
import org.joml.AxisAngle4f;
|
||||
import org.joml.Vector3f;
|
||||
import us.tss3.blockparty.BlockPartyPlugin;
|
||||
import us.tss3.blockparty.config.ArenaConfig;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Owns an optional {@link BlockDisplay} entity that visually mirrors an arena's currently
|
||||
* selected target color. Purely cosmetic: fully optional (global toggle + per-arena
|
||||
* toggle/location), and every method safely no-ops when disabled, unset, or the arena's
|
||||
* world isn't loaded.
|
||||
*/
|
||||
public class BillboardManager {
|
||||
|
||||
private static final AxisAngle4f NO_ROTATION = new AxisAngle4f(0f, 0f, 0f, 1f);
|
||||
|
||||
private final BlockPartyPlugin plugin;
|
||||
private final ArenaConfig config;
|
||||
private UUID entityId;
|
||||
|
||||
public BillboardManager(BlockPartyPlugin plugin, ArenaConfig config) {
|
||||
this.plugin = plugin;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
private boolean isActive() {
|
||||
return plugin.getConfigManager().isBillboardEnabled() && config.hasBillboard()
|
||||
&& config.getBillboard().getWorld() != null;
|
||||
}
|
||||
|
||||
/** Shows (spawning the display entity if needed) the given material as the current target. */
|
||||
public void show(Material material) {
|
||||
if (!isActive()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
BlockDisplay display = resolve();
|
||||
if (display == null) {
|
||||
display = spawn();
|
||||
}
|
||||
if (display != null) {
|
||||
display.setBlock(material.createBlockData());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to update billboard for arena '" + config.getName() + "'", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/** Blanks the display (e.g. between matches) without destroying the entity. */
|
||||
public void clear() {
|
||||
BlockDisplay display = resolve();
|
||||
if (display != null) {
|
||||
display.setBlock(Material.AIR.createBlockData());
|
||||
}
|
||||
}
|
||||
|
||||
/** Fully removes the display entity. Safe to call even if nothing was ever spawned. */
|
||||
public void despawn() {
|
||||
BlockDisplay display = resolve();
|
||||
if (display != null) {
|
||||
display.remove();
|
||||
}
|
||||
entityId = null;
|
||||
}
|
||||
|
||||
/** Forces the entity to be re-created at its (possibly updated) configured location next time it's shown. */
|
||||
public void invalidate() {
|
||||
despawn();
|
||||
}
|
||||
|
||||
private BlockDisplay resolve() {
|
||||
if (entityId == null) {
|
||||
return null;
|
||||
}
|
||||
Entity entity = plugin.getServer().getEntity(entityId);
|
||||
if (entity instanceof BlockDisplay display && !display.isDead()) {
|
||||
return display;
|
||||
}
|
||||
entityId = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
private BlockDisplay spawn() {
|
||||
Location loc = config.getBillboard();
|
||||
if (loc == null || loc.getWorld() == null) {
|
||||
return null;
|
||||
}
|
||||
BlockDisplay display = loc.getWorld().spawn(loc, BlockDisplay.class, bd -> {
|
||||
bd.setPersistent(false);
|
||||
bd.setGlowing(true);
|
||||
bd.setTransformation(new Transformation(
|
||||
new Vector3f(-0.75f, 0f, -0.75f),
|
||||
NO_ROTATION,
|
||||
new Vector3f(1.5f, 1.5f, 1.5f),
|
||||
NO_ROTATION));
|
||||
});
|
||||
entityId = display.getUniqueId();
|
||||
return display;
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,8 @@ public class BlockPartyCommand implements CommandExecutor, TabCompleter {
|
||||
case "setlobby" -> setLobby(sender, args);
|
||||
case "setspawn" -> setSpawn(sender, args);
|
||||
case "setspectator" -> setSpectator(sender, args);
|
||||
case "setbillboard" -> setBillboard(sender, args);
|
||||
case "delbillboard" -> delBillboard(sender, args);
|
||||
case "pos1" -> pos1(sender, args);
|
||||
case "pos2" -> pos2(sender, args);
|
||||
case "setfloor" -> setFloor(sender, args);
|
||||
@@ -75,6 +77,8 @@ public class BlockPartyCommand implements CommandExecutor, TabCompleter {
|
||||
"/blockparty setlobby <arena>",
|
||||
"/blockparty setspawn <arena>",
|
||||
"/blockparty setspectator <arena>",
|
||||
"/blockparty setbillboard <arena>",
|
||||
"/blockparty delbillboard <arena>",
|
||||
"/blockparty pos1 <arena>",
|
||||
"/blockparty pos2 <arena>",
|
||||
"/blockparty setfloor <arena>",
|
||||
@@ -191,6 +195,27 @@ public class BlockPartyCommand implements CommandExecutor, TabCompleter {
|
||||
});
|
||||
}
|
||||
|
||||
private void setBillboard(CommandSender sender, String[] args) {
|
||||
if (!requireAdmin(sender, "blockparty.admin.setup") || !requirePlayer(sender)) return;
|
||||
withArena(sender, args, arena -> {
|
||||
arena.getConfig().setBillboard(((Player) sender).getLocation());
|
||||
arena.getConfig().setBillboardEnabled(true);
|
||||
arena.getBillboardManager().invalidate();
|
||||
plugin.getArenaManager().save(arena);
|
||||
sender.sendMessage(plugin.getMessages().get("admin.billboard-set", Map.of("arena", arena.getConfig().getName())));
|
||||
});
|
||||
}
|
||||
|
||||
private void delBillboard(CommandSender sender, String[] args) {
|
||||
if (!requireAdmin(sender, "blockparty.admin.setup")) return;
|
||||
withArena(sender, args, arena -> {
|
||||
arena.getBillboardManager().despawn();
|
||||
arena.getConfig().setBillboard(null);
|
||||
plugin.getArenaManager().save(arena);
|
||||
sender.sendMessage(plugin.getMessages().get("admin.billboard-removed", Map.of("arena", arena.getConfig().getName())));
|
||||
});
|
||||
}
|
||||
|
||||
private void pos1(CommandSender sender, String[] args) {
|
||||
if (!requireAdmin(sender, "blockparty.admin.setup") || !requirePlayer(sender)) return;
|
||||
withArena(sender, args, arena -> {
|
||||
@@ -277,6 +302,7 @@ public class BlockPartyCommand implements CommandExecutor, TabCompleter {
|
||||
sender.sendMessage("§7Lobby set: §f" + (cfg.getLobby() != null));
|
||||
sender.sendMessage("§7Spawn set: §f" + (cfg.getSpawn() != null));
|
||||
sender.sendMessage("§7Spectator set: §f" + (cfg.getSpectator() != null));
|
||||
sender.sendMessage("§7Billboard set: §f" + (cfg.getBillboard() != null) + (cfg.getBillboard() != null ? " (enabled: " + cfg.isBillboardEnabled() + ")" : ""));
|
||||
sender.sendMessage("§7Floor region set: §f" + cfg.hasFloorRegion());
|
||||
sender.sendMessage("§7Floor generated: §f" + cfg.hasGeneratedLayout());
|
||||
sender.sendMessage("§7Floor materials: §f" + cfg.getFloorMaterials().size());
|
||||
@@ -394,7 +420,7 @@ public class BlockPartyCommand implements CommandExecutor, TabCompleter {
|
||||
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||
if (args.length == 1) {
|
||||
return List.of("help", "join", "leave", "arenas", "stats", "create", "delete", "enable", "disable",
|
||||
"setlobby", "setspawn", "setspectator", "pos1", "pos2", "setfloor", "generate", "info", "list", "reload")
|
||||
"setlobby", "setspawn", "setspectator", "setbillboard", "delbillboard", "pos1", "pos2", "setfloor", "generate", "info", "list", "reload")
|
||||
.stream().filter(s -> s.startsWith(args[0].toLowerCase())).collect(Collectors.toList());
|
||||
}
|
||||
if (args.length == 2) {
|
||||
|
||||
@@ -15,6 +15,8 @@ public class ArenaConfig {
|
||||
private Location lobby;
|
||||
private Location spawn;
|
||||
private Location spectator;
|
||||
private Location billboard;
|
||||
private boolean billboardEnabled = true;
|
||||
private int[] pos1; // x,y,z block coords
|
||||
private int[] pos2;
|
||||
|
||||
@@ -84,6 +86,26 @@ public class ArenaConfig {
|
||||
this.spectator = spectator;
|
||||
}
|
||||
|
||||
public Location getBillboard() {
|
||||
return billboard;
|
||||
}
|
||||
|
||||
public void setBillboard(Location billboard) {
|
||||
this.billboard = billboard;
|
||||
}
|
||||
|
||||
public boolean isBillboardEnabled() {
|
||||
return billboardEnabled;
|
||||
}
|
||||
|
||||
public void setBillboardEnabled(boolean billboardEnabled) {
|
||||
this.billboardEnabled = billboardEnabled;
|
||||
}
|
||||
|
||||
public boolean hasBillboard() {
|
||||
return billboard != null && billboardEnabled;
|
||||
}
|
||||
|
||||
public int[] getPos1() {
|
||||
return pos1;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ public class ArenaConfigLoader {
|
||||
cfg.setLobby(LocationUtil.fromSection(yml.getConfigurationSection("lobby")));
|
||||
cfg.setSpawn(LocationUtil.fromSection(yml.getConfigurationSection("spawn")));
|
||||
cfg.setSpectator(LocationUtil.fromSection(yml.getConfigurationSection("spectator")));
|
||||
cfg.setBillboard(LocationUtil.fromSection(yml.getConfigurationSection("billboard")));
|
||||
cfg.setBillboardEnabled(yml.getBoolean("billboard-enabled", true));
|
||||
if (yml.contains("pos1")) {
|
||||
cfg.setPos1(new int[]{yml.getInt("pos1.x"), yml.getInt("pos1.y"), yml.getInt("pos1.z")});
|
||||
}
|
||||
@@ -106,6 +108,10 @@ public class ArenaConfigLoader {
|
||||
if (cfg.getSpectator() != null) {
|
||||
LocationUtil.toSection(yml.createSection("spectator"), cfg.getSpectator());
|
||||
}
|
||||
if (cfg.getBillboard() != null) {
|
||||
LocationUtil.toSection(yml.createSection("billboard"), cfg.getBillboard());
|
||||
}
|
||||
yml.set("billboard-enabled", cfg.isBillboardEnabled());
|
||||
if (cfg.getPos1() != null) {
|
||||
yml.set("pos1.x", cfg.getPos1()[0]);
|
||||
yml.set("pos1.y", cfg.getPos1()[1]);
|
||||
|
||||
@@ -33,6 +33,10 @@ public class ConfigManager {
|
||||
return config.getBoolean("ui.bossbar", true);
|
||||
}
|
||||
|
||||
public boolean isBillboardEnabled() {
|
||||
return config.getBoolean("ui.billboard", true);
|
||||
}
|
||||
|
||||
public boolean isTitlesEnabled() {
|
||||
return config.getBoolean("ui.titles", true);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,11 @@ ui:
|
||||
titles: true
|
||||
actionbar: true
|
||||
scoreboard-update-interval-ticks: 10
|
||||
# Global switch for the optional billboard: a BlockDisplay entity placed via
|
||||
# /blockparty setbillboard <arena> that visually mirrors the currently selected
|
||||
# target color each round. Also requires the per-arena billboard to be set and
|
||||
# enabled (billboard-enabled in the arena's yml, default true).
|
||||
billboard: true
|
||||
|
||||
sounds:
|
||||
join: ENTITY_EXPERIENCE_ORB_PICKUP
|
||||
|
||||
@@ -20,6 +20,8 @@ admin:
|
||||
lobby-set: "<green>Lobby set for '%arena%'.</green>"
|
||||
spawn-set: "<green>Spawn set for '%arena%'.</green>"
|
||||
spectator-set: "<green>Spectator location set for '%arena%'.</green>"
|
||||
billboard-set: "<green>Billboard set for '%arena%'. It will display the target color each round.</green>"
|
||||
billboard-removed: "<green>Billboard removed for '%arena%'.</green>"
|
||||
pos1-set: "<green>Position 1 set for '%arena%'.</green>"
|
||||
pos2-set: "<green>Position 2 set for '%arena%'.</green>"
|
||||
floor-set: "<green>Floor palette for '%arena%' set to %count% material(s).</green>"
|
||||
|
||||
Reference in New Issue
Block a user