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"