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 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 expired = new ArrayList<>(); for (Map.Entry 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 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]; } }