Files
BlockParty/src/main/java/us/tss3/blockparty/powerup/PowerupManager.java
T
Michael BurgessandClaude Sonnet 5 039cfab92d 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]>
2026-08-08 17:15:36 -04:00

151 lines
6.3 KiB
Java

package us.tss3.blockparty.powerup;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import us.tss3.blockparty.config.ConfigManager;
import us.tss3.blockparty.floor.FloorManager;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
/** Owns the powerups currently sitting on a single arena's floor: spawning new ones each round,
* ageing out unclaimed ones, and rendering/clearing their markers in the world. A powerup's
* marker is a non-solid block floating directly above its floor cell (the player's foot space
* when standing there) rather than replacing the floor block itself, so it never removes a
* player's footing and never has to be reconciled with the floor's own color each round.
* Claiming (i.e. what happens when a player actually gets one) is handled by the caller (see
* Arena) since it needs to apply effects and record stats. */
public class PowerupManager {
private final ConfigManager configManager;
private final FloorManager floorManager;
/** marker position key ("x,y,z", one block above the floor cell) -> the powerup there */
private final Map<String, ActivePowerup> active = new LinkedHashMap<>();
public PowerupManager(ConfigManager configManager, FloorManager floorManager) {
this.configManager = configManager;
this.floorManager = floorManager;
}
public boolean hasActiveAt(String markerKey) {
return active.containsKey(markerKey);
}
/** Removes and returns the powerup at a marker position (e.g. a player walked over or
* clicked it), or null if nothing is there. Does not touch the world block - the caller is
* expected to clear it via {@link #clearMarkerBlock}. */
public ActivePowerup claim(String markerKey) {
return active.remove(markerKey);
}
/** Ages out any powerup that's sat unclaimed too long, clearing its marker. */
public void expireOld(World world, int currentRound) {
List<String> expired = new ArrayList<>();
for (Map.Entry<String, ActivePowerup> entry : active.entrySet()) {
if (entry.getValue().isExpired(currentRound)) {
expired.add(entry.getKey());
}
}
for (String key : expired) {
active.remove(key);
clearMarkerBlock(world, key);
}
}
/** Rolls whether a new powerup should appear this round and, if so, places one above a
* random free floor cell. No-op while powerups are disabled or the floor has no free cells. */
public void maybeSpawn(World world, Random random, int currentRound) {
if (!configManager.isPowerupsEnabled() || world == null) {
return;
}
if (active.size() >= configManager.getPowerupMaxActive()) {
return;
}
if (random.nextDouble() >= configManager.getPowerupSpawnChance()) {
return;
}
List<String> floorKeys = floorManager.layoutKeys();
if (floorKeys.isEmpty()) {
return;
}
String floorKey = floorKeys.get(random.nextInt(floorKeys.size()));
String markerKey = aboveKey(floorKey);
if (markerKey == null || active.containsKey(markerKey)) {
return;
}
PowerupType[] types = PowerupType.values();
PowerupType type = types[random.nextInt(types.length)];
int min = Math.min(configManager.getPowerupMinLifetimeRounds(), configManager.getPowerupMaxLifetimeRounds());
int max = Math.max(configManager.getPowerupMinLifetimeRounds(), configManager.getPowerupMaxLifetimeRounds());
int lifetime = min + (max > min ? random.nextInt(max - min + 1) : 0);
active.put(markerKey, new ActivePowerup(type, markerKey, currentRound, lifetime));
placeMarkerBlock(world, markerKey, markerMaterial(type));
}
/** (Re-)places every currently active powerup's marker block in the world. Only strictly
* needed for freshly-spawned ones (the floor restore each round never touches the marker's
* above-floor position), but cheap and safe to call for all of them defensively. */
public void applyMarkers(World world) {
for (ActivePowerup powerup : active.values()) {
placeMarkerBlock(world, powerup.getPositionKey(), markerMaterial(powerup.getType()));
}
}
/** Clears every active powerup's marker - used when a match ends/resets so no stray blocks
* are left floating over the arena. */
public void clearAll(World world) {
for (String key : new ArrayList<>(active.keySet())) {
clearMarkerBlock(world, key);
}
active.clear();
}
public Material markerMaterial(PowerupType type) {
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) {
block.setType(material, false);
}
}
public void clearMarkerBlock(World world, String key) {
Block block = blockAt(world, key);
if (block != null) {
block.setType(Material.AIR, false);
}
}
private Block blockAt(World world, String key) {
if (world == null || key == null) {
return null;
}
String[] parts = key.split(",");
return world.getBlockAt(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2]));
}
private String aboveKey(String floorKey) {
String[] parts = floorKey.split(",");
if (parts.length != 3) {
return null;
}
int y = Integer.parseInt(parts[1]);
return parts[0] + "," + (y + 1) + "," + parts[2];
}
}