From 039cfab92da6e729828deabd15e2683293d04852 Mon Sep 17 00:00:00 2001 From: Michael Burgess Date: Sat, 8 Aug 2026 17:15:36 -0400 Subject: [PATCH] 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 --- .../java/us/tss3/blockparty/arena/Arena.java | 33 ++++++++++- .../tss3/blockparty/config/ConfigManager.java | 39 +++++++++++++ .../blockparty/listener/PowerupListener.java | 37 +++++++++++- .../tss3/blockparty/powerup/PowerupItems.java | 58 +++++++++++++++++++ .../blockparty/powerup/PowerupManager.java | 9 +++ src/main/resources/config.yml | 11 ++++ src/main/resources/messages.yml | 6 +- 7 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 src/main/java/us/tss3/blockparty/powerup/PowerupItems.java diff --git a/src/main/java/us/tss3/blockparty/arena/Arena.java b/src/main/java/us/tss3/blockparty/arena/Arena.java index dc8fe4b..29ed9ce 100644 --- a/src/main/java/us/tss3/blockparty/arena/Arena.java +++ b/src/main/java/us/tss3/blockparty/arena/Arena.java @@ -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 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 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 -> { diff --git a/src/main/java/us/tss3/blockparty/config/ConfigManager.java b/src/main/java/us/tss3/blockparty/config/ConfigManager.java index 32de3e6..7d43c9c 100644 --- a/src/main/java/us/tss3/blockparty/config/ConfigManager.java +++ b/src/main/java/us/tss3/blockparty/config/ConfigManager.java @@ -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); } diff --git a/src/main/java/us/tss3/blockparty/listener/PowerupListener.java b/src/main/java/us/tss3/blockparty/listener/PowerupListener.java index daae02d..9727033 100644 --- a/src/main/java/us/tss3/blockparty/listener/PowerupListener.java +++ b/src/main/java/us/tss3/blockparty/listener/PowerupListener.java @@ -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); + } + } } diff --git a/src/main/java/us/tss3/blockparty/powerup/PowerupItems.java b/src/main/java/us/tss3/blockparty/powerup/PowerupItems.java new file mode 100644 index 0000000..e397546 --- /dev/null +++ b/src/main/java/us/tss3/blockparty/powerup/PowerupItems.java @@ -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); + } +} diff --git a/src/main/java/us/tss3/blockparty/powerup/PowerupManager.java b/src/main/java/us/tss3/blockparty/powerup/PowerupManager.java index 51284fd..5624800 100644 --- a/src/main/java/us/tss3/blockparty/powerup/PowerupManager.java +++ b/src/main/java/us/tss3/blockparty/powerup/PowerupManager.java @@ -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) { diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index b7f05d0..60a3ecb 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -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 diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 09638ef..91a1ae3 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -55,8 +55,10 @@ game: powerup: spawned: "A %type% powerup appeared on the floor!" - claimed-self: "You claimed %type%!" - claimed-other: "%player% claimed a %type% powerup!" + claimed-self: "You picked up a %type% powerup! Right-click it to use it." + claimed-other: "%player% picked up a %type% powerup!" + used-self: "You used %type%!" + used-other: "%player% used a %type% powerup!" scoreboard: title: "BlockParty"