Make powerups claim-then-use, fix right-click, disable player collision

- Merge missing config.yml keys from the bundled defaults into an existing
  file on load, so upgrading over an old config no longer silently drops
  newer sections (this is what caused powerups to never spawn).
- Claiming a powerup now gives an inventory item instead of applying its
  effect instantly; right-click the item later to activate it.
- Switch the held item's material off placeable blocks (TORCH/END_ROD) to
  non-block items (SUGAR/FEATHER, configurable) - block items only fire a
  right-click event when aimed at a block, which made the powerup unusable
  unless the player was looking at the floor.
- Disable player collision by default (gameplay.disable-player-collision)
  so participants stop shoving each other around on a shrinking floor.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Michael Burgess
2026-08-08 17:15:36 -04:00
co-authored by Claude Sonnet 5
parent cbef1d7a88
commit 039cfab92d
7 changed files with 186 additions and 7 deletions
@@ -26,6 +26,7 @@ import us.tss3.blockparty.logic.TargetColorSelector;
import us.tss3.blockparty.logic.TargetDensityCalculator;
import us.tss3.blockparty.model.PlayerState;
import us.tss3.blockparty.powerup.ActivePowerup;
import us.tss3.blockparty.powerup.PowerupItems;
import us.tss3.blockparty.powerup.PowerupManager;
import us.tss3.blockparty.powerup.PowerupType;
import us.tss3.blockparty.session.PlayerSession;
@@ -186,6 +187,7 @@ public class Arena {
player.setHealth(20.0);
player.setFoodLevel(20);
player.setFireTicks(0);
player.setCollidable(!plugin.getConfigManager().isPlayerCollisionDisabled());
Location target = config.getSpectator() != null ? config.getSpectator() : config.getLobby();
if (target != null) {
player.teleport(target);
@@ -201,6 +203,7 @@ public class Arena {
player.setHealth(20.0);
player.setFoodLevel(20);
player.setFireTicks(0);
player.setCollidable(!plugin.getConfigManager().isPlayerCollisionDisabled());
if (config.getLobby() != null) {
player.teleport(config.getLobby());
}
@@ -219,6 +222,7 @@ public class Arena {
}
if (player != null) {
plugin.getScoreboardManager().detach(player);
player.setCollidable(true);
}
checkCountdownCancel();
checkForWinner();
@@ -503,8 +507,10 @@ public class Arena {
return;
}
powerupManager.clearMarkerBlock(config.getWorld(), key);
applyPowerupEffect(player, powerup.getType());
plugin.getStatsManager().recordPowerupUse(player.getUniqueId());
ItemStack item = PowerupItems.create(plugin, powerup.getType(), powerupManager.itemMaterial(powerup.getType()));
for (ItemStack leftover : player.getInventory().addItem(item).values()) {
player.getWorld().dropItemNaturally(player.getLocation(), leftover);
}
Map<String, String> ph = Map.of("type", powerup.getType().displayName(), "player", player.getName());
player.sendMessage(plugin.getMessages().get("powerup.claimed-self", ph));
for (UUID uuid : players) {
@@ -518,6 +524,29 @@ public class Arena {
}
}
/** Activates a powerup item's effect on right-click use (see PowerupListener) - the deferred
* half of claim-then-use. Returns false (and consumes nothing) if the player isn't currently
* an active participant, so a stale item from a previous match can't be used out of context. */
public boolean usePowerupItem(Player player, PowerupType type) {
if (!players.contains(player.getUniqueId()) || getPhase() != ArenaPhase.RUNNING) {
return false;
}
applyPowerupEffect(player, type);
plugin.getStatsManager().recordPowerupUse(player.getUniqueId());
Map<String, String> ph = Map.of("type", type.displayName(), "player", player.getName());
player.sendMessage(plugin.getMessages().get("powerup.used-self", ph));
for (UUID uuid : players) {
if (uuid.equals(player.getUniqueId())) {
continue;
}
Player other = plugin.getServer().getPlayer(uuid);
if (other != null) {
other.sendMessage(plugin.getMessages().get("powerup.used-other", ph));
}
}
return true;
}
private void applyPowerupEffect(Player player, PowerupType type) {
switch (type) {
case SUPER_SPEED -> {
@@ -1,9 +1,14 @@
package us.tss3.blockparty.config;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.logging.Level;
/** Wraps global config.yml access: UI toggles, sounds, rewards, default floor palette. */
public class ConfigManager {
@@ -15,10 +20,27 @@ public class ConfigManager {
this.plugin = plugin;
}
/** Loads config.yml, then merges in any keys the bundled jar defines that the on-disk file
* is missing (e.g. a config.yml saved before a feature like powerups was added) and persists
* the merge to disk. Bukkit's saveDefaultConfig() only writes the file when it doesn't exist
* yet, so without this, upgrading the plugin on top of an existing config.yml would silently
* leave newer sections absent - every getter still falls back to its own hardcoded default,
* but the feature looks "on" from the docs while actually running on stale settings. */
public void load() {
plugin.saveDefaultConfig();
plugin.reloadConfig();
this.config = plugin.getConfig();
try (InputStream defaultStream = plugin.getResource("config.yml")) {
if (defaultStream != null) {
YamlConfiguration defaults = YamlConfiguration.loadConfiguration(
new InputStreamReader(defaultStream, StandardCharsets.UTF_8));
config.setDefaults(defaults);
config.options().copyDefaults(true);
plugin.saveConfig();
}
} catch (Exception ex) {
plugin.getLogger().log(Level.WARNING, "Failed to merge default config.yml keys", ex);
}
}
public FileConfiguration raw() {
@@ -113,6 +135,12 @@ public class ConfigManager {
return config.getDouble("floor.target-fraction-reduction-per-round", 0.04);
}
/** If true (default), participants and spectators can't push each other around while in an
* arena - handy since BlockParty rounds tend to bunch everyone onto a shrinking floor. */
public boolean isPlayerCollisionDisabled() {
return config.getBoolean("gameplay.disable-player-collision", true);
}
public boolean isPowerupsEnabled() {
return config.getBoolean("powerups.enabled", true);
}
@@ -143,6 +171,17 @@ public class ConfigManager {
return material != null ? material : org.bukkit.Material.TORCH;
}
/** The item a claimed powerup shows up as in the player's inventory. Deliberately a separate
* setting from the floor marker block: block items (TORCH, END_ROD, etc.) only fire a
* right-click interact event when the player is aiming at a block within reach, so using one
* as the held item made the powerup unusable unless the player happened to be looking at the
* floor. Non-block items fire right-click in any direction. */
public org.bukkit.Material getPowerupItemMaterial(String typeKey, org.bukkit.Material fallback) {
String name = config.getString("powerups.types." + typeKey + ".item-material", fallback.name());
org.bukkit.Material material = org.bukkit.Material.matchMaterial(name);
return material != null ? material : fallback;
}
public int getSuperSpeedDurationSeconds() {
return config.getInt("powerups.types.super-speed.duration-seconds", 8);
}
@@ -6,12 +6,16 @@ import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import us.tss3.blockparty.BlockPartyPlugin;
import us.tss3.blockparty.arena.Arena;
import us.tss3.blockparty.powerup.PowerupItems;
import us.tss3.blockparty.powerup.PowerupType;
import us.tss3.blockparty.session.PlayerSession;
/** Lets participants claim a powerup marker by left-clicking it, in addition to just walking
* over it (see Arena's fall-watcher loop). */
/** Lets participants claim a powerup marker off the floor by left-clicking it (in addition to
* just walking over it - see Arena's fall-watcher loop), and use a claimed powerup item by
* right-clicking it later, whenever they choose. */
public class PowerupListener implements Listener {
private final BlockPartyPlugin plugin;
@@ -33,4 +37,31 @@ public class PowerupListener implements Listener {
plugin.getArenaManager().get(session.getArenaName()).ifPresent(arena ->
arena.claimPowerupAt(player, event.getClickedBlock().getLocation()));
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onRightClickUseItem(PlayerInteractEvent event) {
if (event.getHand() != EquipmentSlot.HAND) {
return;
}
if (event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() != Action.RIGHT_CLICK_BLOCK) {
return;
}
ItemStack item = event.getItem();
PowerupType type = PowerupItems.readType(plugin, item);
if (type == null) {
return;
}
Player player = event.getPlayer();
PlayerSession session = plugin.getSessionManager().get(player.getUniqueId());
if (session == null || session.getRole() != PlayerSession.Role.PLAYER) {
return;
}
boolean used = plugin.getArenaManager().get(session.getArenaName())
.map(arena -> arena.usePowerupItem(player, type))
.orElse(false);
if (used) {
event.setCancelled(true);
item.setAmount(item.getAmount() - 1);
}
}
}
@@ -0,0 +1,58 @@
package us.tss3.blockparty.powerup;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.plugin.Plugin;
import java.util.List;
/** Builds/reads the inventory item a player receives when they claim a powerup marker off the
* floor. Claiming only hands over this item - the effect itself is applied later, when the
* player right-clicks it (see PowerupListener), rather than instantly on pickup. */
public final class PowerupItems {
private static final String KEY_NAME = "powerup-type";
private PowerupItems() {
}
public static ItemStack create(Plugin plugin, PowerupType type, Material icon) {
ItemStack stack = new ItemStack(icon);
ItemMeta meta = stack.getItemMeta();
if (meta != null) {
meta.setDisplayName("§b" + type.displayName() + " Powerup");
meta.setLore(List.of("§7Right-click to use"));
meta.getPersistentDataContainer().set(key(plugin), PersistentDataType.STRING, type.name());
stack.setItemMeta(meta);
}
return stack;
}
/** The powerup type this item represents, or null if it isn't one of ours (e.g. a plain
* torch the player picked up elsewhere). */
public static PowerupType readType(Plugin plugin, ItemStack stack) {
if (stack == null || stack.getType().isAir()) {
return null;
}
ItemMeta meta = stack.getItemMeta();
if (meta == null) {
return null;
}
String raw = meta.getPersistentDataContainer().get(key(plugin), PersistentDataType.STRING);
if (raw == null) {
return null;
}
try {
return PowerupType.valueOf(raw);
} catch (IllegalArgumentException ex) {
return null;
}
}
private static NamespacedKey key(Plugin plugin) {
return new NamespacedKey(plugin, KEY_NAME);
}
}
@@ -108,6 +108,15 @@ public class PowerupManager {
return configManager.getPowerupMarkerMaterial(type == PowerupType.SUPER_SPEED ? "super-speed" : "air-blast");
}
/** The material a claimed powerup uses as its held/inventory item - see
* {@link us.tss3.blockparty.config.ConfigManager#getPowerupItemMaterial} for why this is
* intentionally not the same as the floor marker material. */
public Material itemMaterial(PowerupType type) {
return type == PowerupType.SUPER_SPEED
? configManager.getPowerupItemMaterial("super-speed", Material.SUGAR)
: configManager.getPowerupItemMaterial("air-blast", Material.FEATHER);
}
private void placeMarkerBlock(World world, String key, Material material) {
Block block = blockAt(world, key);
if (block != null && block.getType() != material) {
+11
View File
@@ -63,6 +63,11 @@ performance:
# blocks processed per server tick when generating/removing/restoring the floor
floor-blocks-per-tick: 200
gameplay:
# If true (default), players/spectators can't push each other around while in an arena -
# rounds tend to bunch everyone onto a shrinking floor, and collision makes that chaotic.
disable-player-collision: true
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
@@ -96,12 +101,18 @@ powerups:
super-speed:
# Block used to mark a Super Speed powerup's spot on the floor.
marker-block: TORCH
# Item the player holds after claiming it, used later via right-click. Must NOT be a
# placeable block - block items only fire a right-click event when aimed at a nearby
# block, so a claimed powerup would be unusable except while looking at the floor.
item-material: SUGAR
duration-seconds: 8
# Potion amplifier: 0 = Speed I, 1 = Speed II, etc.
amplifier: 1
air-blast:
# Block used to mark an Air Blast powerup's spot on the floor.
marker-block: END_ROD
# See super-speed.item-material above - must not be a placeable block.
item-material: FEATHER
# Radius, in blocks, that nearby players get pushed within when this is claimed.
radius: 4.0
strength: 1.4
+4 -2
View File
@@ -55,8 +55,10 @@ game:
powerup:
spawned: "<aqua>A %type% powerup appeared on the floor!</aqua>"
claimed-self: "<green>You claimed <bold>%type%</bold>!</green>"
claimed-other: "<yellow>%player% claimed a %type% powerup!</yellow>"
claimed-self: "<green>You picked up a <bold>%type%</bold> powerup! Right-click it to use it.</green>"
claimed-other: "<yellow>%player% picked up a %type% powerup!</yellow>"
used-self: "<green>You used <bold>%type%</bold>!</green>"
used-other: "<yellow>%player% used a %type% powerup!</yellow>"
scoreboard:
title: "<gold><bold>BlockParty</bold></gold>"