From 8601626b1607dd59e34113a4a93de66576947e49 Mon Sep 17 00:00:00 2001 From: tiko Date: Wed, 4 Feb 2026 00:05:54 +0100 Subject: [PATCH] REWRITE INIT. --- .../tiko/battletower/BattleTowerPlugin.java | 29 - .../battletower/ChunkPopulateListener.java | 22 - .../battletower/OriginalTowerGenerator.java | 296 ---------- .../fyi/tiko/battletower/TowerGenerator.java | 96 ---- .../tiko/battletower/TowerSpawnManager.java | 64 --- .../java/fyi/tiko/battletower/TowerType.java | 39 -- .../tiko/battletowers/BattleTowersPlugin.java | 54 ++ .../battletowers/TowerBossDeathListener.java | 27 + .../tiko/battletowers/TowerBossSpawner.java | 50 ++ .../tiko/battletowers/TowerChestListener.java | 31 + .../fyi/tiko/battletowers/TowerDestroyer.java | 40 ++ .../fyi/tiko/battletowers/TowerGenerator.java | 528 ++++++++++++++++++ .../tiko/battletowers/TowerLootManager.java | 36 ++ .../fyi/tiko/battletowers/TowerPopulator.java | 24 + .../battletowers/TowerStageItemManager.java | 69 +++ .../java/fyi/tiko/battletowers/TowerType.java | 30 + src/main/resources/config.yml | 17 +- src/main/resources/plugin.yml | 2 +- 18 files changed, 906 insertions(+), 548 deletions(-) delete mode 100644 src/main/java/fyi/tiko/battletower/BattleTowerPlugin.java delete mode 100644 src/main/java/fyi/tiko/battletower/ChunkPopulateListener.java delete mode 100644 src/main/java/fyi/tiko/battletower/OriginalTowerGenerator.java delete mode 100644 src/main/java/fyi/tiko/battletower/TowerGenerator.java delete mode 100644 src/main/java/fyi/tiko/battletower/TowerSpawnManager.java delete mode 100644 src/main/java/fyi/tiko/battletower/TowerType.java create mode 100644 src/main/java/fyi/tiko/battletowers/BattleTowersPlugin.java create mode 100644 src/main/java/fyi/tiko/battletowers/TowerBossDeathListener.java create mode 100644 src/main/java/fyi/tiko/battletowers/TowerBossSpawner.java create mode 100644 src/main/java/fyi/tiko/battletowers/TowerChestListener.java create mode 100644 src/main/java/fyi/tiko/battletowers/TowerDestroyer.java create mode 100644 src/main/java/fyi/tiko/battletowers/TowerGenerator.java create mode 100644 src/main/java/fyi/tiko/battletowers/TowerLootManager.java create mode 100644 src/main/java/fyi/tiko/battletowers/TowerPopulator.java create mode 100644 src/main/java/fyi/tiko/battletowers/TowerStageItemManager.java create mode 100644 src/main/java/fyi/tiko/battletowers/TowerType.java diff --git a/src/main/java/fyi/tiko/battletower/BattleTowerPlugin.java b/src/main/java/fyi/tiko/battletower/BattleTowerPlugin.java deleted file mode 100644 index 90de822..0000000 --- a/src/main/java/fyi/tiko/battletower/BattleTowerPlugin.java +++ /dev/null @@ -1,29 +0,0 @@ -package fyi.tiko.battletower; - -import org.bukkit.plugin.java.JavaPlugin; - -/** - * @author Tiko - * @since 03.02.2026, 19:33 - */ -public class BattleTowerPlugin extends JavaPlugin { - private TowerSpawnManager spawnManager; - - @Override - public void onEnable() { - saveDefaultConfig(); - - spawnManager = new TowerSpawnManager(this); - - getServer().getPluginManager().registerEvents( - new ChunkPopulateListener(spawnManager), - this - ); - - getLogger().info("Enabled BattleTowerPlugin"); - } - - public TowerSpawnManager spawnManager() { - return spawnManager; - } -} diff --git a/src/main/java/fyi/tiko/battletower/ChunkPopulateListener.java b/src/main/java/fyi/tiko/battletower/ChunkPopulateListener.java deleted file mode 100644 index aed6aab..0000000 --- a/src/main/java/fyi/tiko/battletower/ChunkPopulateListener.java +++ /dev/null @@ -1,22 +0,0 @@ -package fyi.tiko.battletower; - -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.world.ChunkPopulateEvent; - -/** - * @author Tiko - * @since 03.02.2026, 19:37 - */ -public class ChunkPopulateListener implements Listener { - private final TowerSpawnManager spawnManager; - - public ChunkPopulateListener(final TowerSpawnManager spawnManager) { - this.spawnManager = spawnManager; - } - - @EventHandler - public void handleChunkPopulate(final ChunkPopulateEvent event) { - spawnManager.attemptSpawn(event.getChunk()); - } -} diff --git a/src/main/java/fyi/tiko/battletower/OriginalTowerGenerator.java b/src/main/java/fyi/tiko/battletower/OriginalTowerGenerator.java deleted file mode 100644 index e028784..0000000 --- a/src/main/java/fyi/tiko/battletower/OriginalTowerGenerator.java +++ /dev/null @@ -1,296 +0,0 @@ -package fyi.tiko.battletower; -import org.bukkit.*; -import org.bukkit.block.Block; -import org.bukkit.block.Chest; -import org.bukkit.block.CreatureSpawner; -import org.bukkit.entity.EntityType; -import org.bukkit.inventory.ItemStack; - -import java.util.Random; - -public class OriginalTowerGenerator { - - private int floor = 1; - private int floorIterator = 0; - private boolean topFloor; - - public boolean generate(World world, Random random, int ix, int surfaceY, int kz) { - - TowerType type = TowerType.COBBLE; - - Material WALL = type.wall(); - Material FLOOR = type.floor(); - Material PILLAR = type.pillar(); - Material LIGHT = type.light(); - Material STAIRS = type.stairs(); - - int startY = surfaceY - 6; - int maxHeight = 120; - - floor = 1; - - for (int builderHeight = startY; builderHeight < maxHeight; builderHeight += 7) { - - topFloor = builderHeight + 7 >= maxHeight; - - // ✅ FULL Forge Pattern Segment - buildSegment(world, random, ix, builderHeight, kz, - WALL, FLOOR, PILLAR, STAIRS); - - // ✅ Spawner/Floor - if (!topFloor) { - placeSpawner(world, ix + 2, builderHeight + 6, kz + 2, random); - placeSpawner(world, ix - 3, builderHeight + 6, kz + 2, random); - } - - // ✅ Chests - placeChests(world, ix, builderHeight + 7, kz); - - // ✅ Lights - placeLights(world, ix, builderHeight, kz, LIGHT); - - // ✅ Random Damage Holes (Original feel) - randomDamage(world, random, ix, builderHeight + 5, kz, FLOOR); - - floor++; - } - - // ✅ Boss Placeholder - spawnBoss(world, ix, maxHeight, kz); - - return true; - } - - /* ========================================================= */ - - private void buildSegment(World world, Random random, - int ix, int baseY, int kz, - Material WALL, - Material FLOOR, - Material PILLAR, - Material STAIRS) { - - for (floorIterator = 0; floorIterator < 7; floorIterator++) { - - if (floor == 1 && floorIterator < 4) { - continue; - } - - for (int xIt = -7; xIt < 7; xIt++) { - for (int zIt = -7; zIt < 7; zIt++) { - - int x = ix + xIt; - int y = baseY + floorIterator; - int z = kz + zIt; - - // ==== EXACT FORGE SHAPE ==== - - if (zIt == -7) { - if (xIt > -5 && xIt < 4) { - wallPiece(world, x, y, z, WALL); - } - continue; - } - - // Main interior - if (zIt != -6 && zIt != -5) { - - // Window rows excluded - if (zIt != -4 && zIt != -3 && zIt != 2 && zIt != 3) { - - if (zIt > -3 && zIt < 2) { - - if (xIt != -7 && xIt != 6) { - - if (floorIterator == 5) { - place(world, x, y, z, FLOOR); - } else { - place(world, x, y, z, Material.AIR); - } - - } else { - wallPiece(world, x, y, z, WALL); - } - - } - - else if (zIt == 4) { - - if (xIt != -5 && xIt != 4) { - if (floorIterator == 5) { - place(world, x, y, z, FLOOR); - } else { - place(world, x, y, z, Material.AIR); - } - } else { - wallPiece(world, x, y, z, WALL); - } - } - - else if (zIt == 5) { - - if (xIt != -4 && xIt != -3 && xIt != 2 && xIt != 3) { - - if (xIt > -3 && xIt < 2) { - if (floorIterator == 5) { - place(world, x, y, z, FLOOR); - } else { - wallPiece(world, x, y, z, WALL); - } - } - - } else { - wallPiece(world, x, y, z, WALL); - } - } - - else if (zIt == 6 && xIt > -3 && xIt < 2) { - wallPiece(world, x, y, z, WALL); - } - } - - // Window rows (-4,-3,2,3) - else { - - if (xIt != -6 && xIt != 5) { - if (floorIterator == 5) { - place(world, x, y, z, FLOOR); - } else { - place(world, x, y, z, Material.AIR); - } - } else { - wallPiece(world, x, y, z, WALL); - } - } - - } - - // Stair gap - else { - - if (xIt == -5 || xIt == 4) { - wallPiece(world, x, y, z, WALL); - continue; - } - - if (zIt == -6) { - - int stairX = (floorIterator + 1) % 7 - 3; - - if (xIt == stairX) { - place(world, x, y, z, STAIRS); - } else if (xIt > -5 && xIt < 4) { - place(world, x, y, z, Material.AIR); - } - } - - if (zIt == -5) { - wallPiece(world, x, y, z, WALL); - } - } - } - } - } - } - - /* ========================================================= */ - - private void wallPiece(World world, int x, int y, int z, Material wall) { - - // Mossy pillar accents - if ((x % 5 == 0) && (z % 5 == 0)) { - place(world, x, y, z, Material.MOSSY_COBBLESTONE); - } else { - place(world, x, y, z, wall); - } - - // Fill base into ground - if (floor == 1 && floorIterator == 4) { - fillDown(world, x, y, z, wall); - } - } - - private void fillDown(World world, int x, int y, int z, Material mat) { - for (int yy = y - 1; yy > 0; yy--) { - Block b = world.getBlockAt(x, yy, z); - if (b.getType().isSolid()) break; - b.setType(mat, false); - } - } - - /* ========================================================= */ - - private void randomDamage(World world, Random random, - int ix, int y, int kz, Material floorMat) { - - if (topFloor) return; - - int holes = floor * 2; - - for (int i = 0; i < holes; i++) { - - int dx = random.nextInt(10) - 5; - int dz = random.nextInt(10) - 5; - - if (Math.abs(dx) < 2 && Math.abs(dz) < 2) continue; - - Block b = world.getBlockAt(ix + dx, y, kz + dz); - - if (b.getType() == floorMat) { - b.setType(Material.AIR, false); - } - } - } - - /* ================= Spawner ================= */ - - private void placeSpawner(World world, int x, int y, int z, Random random) { - - Block b = world.getBlockAt(x, y, z); - b.setType(Material.SPAWNER); - - CreatureSpawner spawner = (CreatureSpawner) b.getState(); - spawner.setSpawnedType(EntityType.ZOMBIE); - spawner.update(); - } - - /* ================= Loot ================= */ - - private void placeChests(World world, int ix, int y, int kz) { - - for (int i = 0; i < 2; i++) { - - Block chestBlock = world.getBlockAt(ix - i, y, kz + 3); - chestBlock.setType(Material.CHEST); - - Chest chest = (Chest) chestBlock.getState(); - chest.getInventory().addItem(new ItemStack(Material.BREAD, 2)); - chest.getInventory().addItem(new ItemStack(Material.IRON_INGOT, 2)); - chest.update(); - } - } - - /* ================= Lights ================= */ - - private void placeLights(World world, int ix, int y, int kz, Material light) { - - place(world, ix + 3, y + 1, kz - 6, light); - place(world, ix - 4, y + 1, kz - 6, light); - } - - /* ================= Boss ================= */ - - private void spawnBoss(World world, int ix, int y, int kz) { - - world.spawnEntity( - new Location(world, ix + 0.5, y + 2, kz + 0.5), - EntityType.IRON_GOLEM - ); - } - - /* ========================================================= */ - - private void place(World world, int x, int y, int z, Material mat) { - world.getBlockAt(x, y, z).setType(mat, false); - } -} \ No newline at end of file diff --git a/src/main/java/fyi/tiko/battletower/TowerGenerator.java b/src/main/java/fyi/tiko/battletower/TowerGenerator.java deleted file mode 100644 index a7b435b..0000000 --- a/src/main/java/fyi/tiko/battletower/TowerGenerator.java +++ /dev/null @@ -1,96 +0,0 @@ -package fyi.tiko.battletower; - -import org.bukkit.Location; -import org.bukkit.World; - -/** - * @author Tiko - * @since 03.02.2026, 19:41 - */ -public class TowerGenerator { - // 7 blöcke hoch pro etage - private static final int FLOOR_HEIGHT = 7; - - // tower radius in blöcken - private static final int RADIUS = 7; - - public boolean generate(final Location location) { - final var world = location.getWorld(); - final var x = location.getBlockX(); - final var z = location.getBlockZ(); - - final var surfaceY = world.getHighestBlockYAt(x, z); - - if (surfaceY < 60) { - return false; // zu niedrig - } - - // original: tower soll leicht im boden starten - final var startY = surfaceY - 6; - - TowerType type = TowerType.COBBLE; - - // 6 etagen hoch (zusammenbasteln hier) - for (var floor = 0; floor < 6; floor++) { - final var floorBaseY = startY + (floor * FLOOR_HEIGHT); - buildFloor(world, x, floorBaseY, z, type); - buildWalls(world, x, floorBaseY, z, type); - } - - // top zusammenbasteln hier - buildTop(world, x, startY + (6 * FLOOR_HEIGHT), z, type); - return true; - } - - private void buildFloor( - final World world, - final int centerX, - final int y, - final int centerZ, - final TowerType type - ) { - for (var dx = -6; dx <= 6; dx++) { - for (var dz = -6; dz <= 6; dz++) { - // nur drinnen - if (Math.abs(dx) < 6 && Math.abs(dz) < 6) { - world.getBlockAt(centerX + dx, y, centerZ + dz).setType(type.floor()); - } - } - } - } - - private void buildWalls( - final World world, - final int centerX, - final int y, - final int centerZ, - final TowerType type - ) { - for (var dy = 0; dy < FLOOR_HEIGHT; dy++) { - final var currentY = y + dy; - - for (int dx = -RADIUS; dx <= RADIUS; dx++) { - for (int dz = -RADIUS; dz <= RADIUS; dz++) { - // outer border = wall - if (Math.abs(dx) == RADIUS || Math.abs(dz) == RADIUS) { - world.getBlockAt(centerX + dx, currentY, centerZ + dz).setType(type.wall()); - } - } - } - } - } - - private void buildTop( - final World world, - final int centerX, - final int y, - final int centerZ, - final TowerType type - ) { - for(var dx = -6; dx <= 6; dx++) { - for(var dz = -6; dz <= 6; dz++) { - world.getBlockAt(centerX + dx, y, centerZ + dz).setType(type.floor()); - } - } - } -} diff --git a/src/main/java/fyi/tiko/battletower/TowerSpawnManager.java b/src/main/java/fyi/tiko/battletower/TowerSpawnManager.java deleted file mode 100644 index 3fbdf58..0000000 --- a/src/main/java/fyi/tiko/battletower/TowerSpawnManager.java +++ /dev/null @@ -1,64 +0,0 @@ -package fyi.tiko.battletower; - -import java.util.HashSet; -import java.util.Random; -import java.util.Set; -import org.bukkit.Chunk; -import org.bukkit.Location; -import org.bukkit.plugin.java.JavaPlugin; - -/** - * @author Tiko - * @since 03.02.2026, 19:37 - */ -public class TowerSpawnManager { - private final JavaPlugin plugin; - private final Random random = new Random(); - private final Set towerPositions = new HashSet<>(); - - public TowerSpawnManager(final JavaPlugin plugin) { - this.plugin = plugin; - } - - public void attemptSpawn(final Chunk chunk) { - final var chance = plugin.getConfig().getInt("spawn.chance-per-chunk"); - - if (random.nextInt(chance) != 0) { - return; - } - - final var world = chunk.getWorld(); - final var x = chunk.getX() * 16 + 8; - final var z = chunk.getZ() * 16 + 8; - final var y = world.getHighestBlockYAt(x, z); - - final var baseLocation = new Location(world, x, y, z); - - if (!canSpawnAt(baseLocation)) { - return; - } - - final var generator = new OriginalTowerGenerator(); - final var success = generator.generate(world, random, x, y, z); - - if (success) { - towerPositions.add(baseLocation); - plugin.getLogger().info("Spawned tower at " + baseLocation); - } - } - - private boolean canSpawnAt(final Location location) { - final var minDistance = plugin.getConfig().getInt("spawn.min-distance"); - - for (final var other : towerPositions) { - if (other.getWorld() != location.getWorld()) { - continue; - } - - if (other.distanceSquared(location) < minDistance) { - return false; - } - } - return true; - } -} diff --git a/src/main/java/fyi/tiko/battletower/TowerType.java b/src/main/java/fyi/tiko/battletower/TowerType.java deleted file mode 100644 index 361b5cf..0000000 --- a/src/main/java/fyi/tiko/battletower/TowerType.java +++ /dev/null @@ -1,39 +0,0 @@ -package fyi.tiko.battletower; - -import org.bukkit.Material; - -public enum TowerType { - - COBBLE( - Material.COBBLESTONE, - Material.STONE_BRICKS, - Material.MOSSY_COBBLESTONE, // ✅ Pillars - Material.TORCH, - Material.COBBLESTONE_STAIRS - ); - - private final Material wall; - private final Material floor; - private final Material pillar; - private final Material light; - private final Material stairs; - - TowerType(Material wall, - Material floor, - Material pillar, - Material light, - Material stairs) { - - this.wall = wall; - this.floor = floor; - this.pillar = pillar; - this.light = light; - this.stairs = stairs; - } - - public Material wall() { return wall; } - public Material floor() { return floor; } - public Material pillar() { return pillar; } - public Material light() { return light; } - public Material stairs() { return stairs; } -} diff --git a/src/main/java/fyi/tiko/battletowers/BattleTowersPlugin.java b/src/main/java/fyi/tiko/battletowers/BattleTowersPlugin.java new file mode 100644 index 0000000..4214ac5 --- /dev/null +++ b/src/main/java/fyi/tiko/battletowers/BattleTowersPlugin.java @@ -0,0 +1,54 @@ +package fyi.tiko.battletowers; + +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.plugin.java.JavaPlugin; + +public final class BattleTowersPlugin extends JavaPlugin { + + private static BattleTowersPlugin instance; + + public static BattleTowersPlugin instance() { + return instance; + } + + private TowerLootManager lootManager; + private NamespacedKey towerKey; + + public NamespacedKey towerKey() { + return towerKey; + } + + public TowerLootManager getLootManager() { + return lootManager; + } + + @Override + public void onEnable() { + instance = this; + + saveDefaultConfig(); + + towerKey = new NamespacedKey(this, "tower_location"); + lootManager = new TowerLootManager(); + + Bukkit.getWorlds().forEach(world -> { + world.getPopulators().add(new TowerPopulator()); + }); + + getServer().getPluginManager().registerEvents( + new TowerChestListener(), this + ); + + getServer().getPluginManager().registerEvents( + new TowerBossDeathListener(), this + ); + + getLogger().info("BattleTowers enabled!"); + } + + @Override + public void onDisable() { + getLogger().info("BattleTowers disabled!"); + } +} diff --git a/src/main/java/fyi/tiko/battletowers/TowerBossDeathListener.java b/src/main/java/fyi/tiko/battletowers/TowerBossDeathListener.java new file mode 100644 index 0000000..828e38a --- /dev/null +++ b/src/main/java/fyi/tiko/battletowers/TowerBossDeathListener.java @@ -0,0 +1,27 @@ +package fyi.tiko.battletowers; + +import org.bukkit.entity.IronGolem; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDeathEvent; + +import static fyi.tiko.battletowers.BattleTowersPlugin.instance; + +public class TowerBossDeathListener implements Listener { + + @EventHandler + public void onBossDeath(EntityDeathEvent event) { + + if (!(event.getEntity() instanceof IronGolem golem)) return; + + var data = golem.getPersistentDataContainer().get( + instance().towerKey(), + org.bukkit.persistence.PersistentDataType.STRING + ); + + if (data == null) return; + + // Tower Collapse Start + TowerDestroyer.startCollapse(golem.getLocation(), event.getEntity().getKiller()); + } +} diff --git a/src/main/java/fyi/tiko/battletowers/TowerBossSpawner.java b/src/main/java/fyi/tiko/battletowers/TowerBossSpawner.java new file mode 100644 index 0000000..4afabdd --- /dev/null +++ b/src/main/java/fyi/tiko/battletowers/TowerBossSpawner.java @@ -0,0 +1,50 @@ +package fyi.tiko.battletowers; + +import org.bukkit.Location; +import org.bukkit.attribute.Attribute; +import org.bukkit.entity.IronGolem; +import org.bukkit.persistence.PersistentDataType; + +import static fyi.tiko.battletowers.BattleTowersPlugin.instance; + +public class TowerBossSpawner { + + public static void spawnDormantBoss(org.bukkit.World world, + Location loc, + int towerX, int towerY, int towerZ) { + + IronGolem golem = world.spawn(loc, IronGolem.class); + + // ===== Boss Stats like Forge ===== + golem.getAttribute(Attribute.MAX_HEALTH).setBaseValue(300.0); + golem.setHealth(300.0); + + golem.getAttribute(Attribute.ATTACK_DAMAGE).setBaseValue(12.0); + golem.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(0.30); + + golem.setCustomName("§6Battletower Guardian"); + golem.setCustomNameVisible(true); + + // ===== Dormant Mode ===== + golem.setAI(false); + + // Save tower coords inside entity + golem.getPersistentDataContainer().set( + instance().towerKey(), + PersistentDataType.STRING, + towerX + ";" + towerY + ";" + towerZ + ); + } + + public static void wakeUp(IronGolem golem) { + + if (golem.hasAI()) return; + + golem.setAI(true); + golem.getWorld().playSound( + golem.getLocation(), + "entity.iron_golem.repair", + 3f, 0.6f + ); + } +} diff --git a/src/main/java/fyi/tiko/battletowers/TowerChestListener.java b/src/main/java/fyi/tiko/battletowers/TowerChestListener.java new file mode 100644 index 0000000..fa27b35 --- /dev/null +++ b/src/main/java/fyi/tiko/battletowers/TowerChestListener.java @@ -0,0 +1,31 @@ +package fyi.tiko.battletowers; + +import org.bukkit.entity.IronGolem; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryOpenEvent; + +public class TowerChestListener implements Listener { + + @EventHandler + public void onChestOpen(InventoryOpenEvent event) { + + if (!(event.getInventory().getHolder() instanceof org.bukkit.block.Chest)) { + return; + } + + var player = event.getPlayer(); + + // Search nearby golems + for (var entity : player.getNearbyEntities(8, 8, 8)) { + + if (entity instanceof IronGolem golem) { + + if (!golem.hasAI()) { + TowerBossSpawner.wakeUp(golem); + golem.setTarget(player); + } + } + } + } +} diff --git a/src/main/java/fyi/tiko/battletowers/TowerDestroyer.java b/src/main/java/fyi/tiko/battletowers/TowerDestroyer.java new file mode 100644 index 0000000..f315d5e --- /dev/null +++ b/src/main/java/fyi/tiko/battletowers/TowerDestroyer.java @@ -0,0 +1,40 @@ +package fyi.tiko.battletowers; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; + +import static fyi.tiko.battletowers.BattleTowersPlugin.instance; + +public class TowerDestroyer { + + // TODO: die einzelnen floors werden nicht kaputt gemacht, sondern nur ganz oben + nachricht nur an spielern in reichweite von X blöcken... + public static void startCollapse(Location center, Player killer) { + + World world = center.getWorld(); + + Bukkit.broadcastMessage("§cA Battletower's Guardian has fallen! The Tower will collapse..."); + + new BukkitRunnable() { + + int floor = 6; + + @Override + public void run() { + + if (floor <= 0) { + cancel(); + return; + } + + double y = center.getY() - (floor * 7); + + world.createExplosion(center, 10f); + floor--; + } + + }.runTaskTimer(instance(), 20 * 15, 20 * 5); + } +} diff --git a/src/main/java/fyi/tiko/battletowers/TowerGenerator.java b/src/main/java/fyi/tiko/battletowers/TowerGenerator.java new file mode 100644 index 0000000..d7cef30 --- /dev/null +++ b/src/main/java/fyi/tiko/battletowers/TowerGenerator.java @@ -0,0 +1,528 @@ +package fyi.tiko.battletowers; + +import java.util.Random; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.CreatureSpawner; +import org.bukkit.entity.EntityType; +import org.bukkit.inventory.ItemStack; + +public class TowerGenerator { + + private static final int MAX_HOLE_DEPTH = 22; + + private static final int[][] CANDIDATES = { + {4, -5}, {4, 0}, {4, 5}, + {0, -5}, {0, 0}, {0, 5}, + {-4, -5}, {-4, 0}, {-4, 5} + }; + + private final Random random = new Random(); + + public void trySpawnTower(World world, Random random, int x, int z) { + + int surfaceY = getSurfaceY(world, x, z); + if (surfaceY < 60) { + return; + } + + TowerType type = pickTowerType(world, random, x, z, surfaceY); + if (type == null) { + return; + } + + boolean underground = random.nextInt(100) < 15; + + generate(world, x, surfaceY, z, type, underground); + + Bukkit.getLogger().info("Spawned BattleTower at " + x + " " + z + " type=" + type); + } + + private TowerType pickTowerType(World world, Random random, int x, int z, int centerY) { + + int water = 0, snow = 0, sand = 0, foliage = 0, other = 0; + + for (int[] pair : CANDIDATES) { + + int checkY = getSurfaceY(world, x + pair[0], z + pair[1]); + Block block = world.getBlockAt(x + pair[0], checkY, z + pair[1]); + + Material mat = block.getType(); + + if (mat == Material.SNOW || mat == Material.ICE) { + snow++; + } else if (mat == Material.SAND || mat == Material.SANDSTONE) { + sand++; + } else if (mat == Material.WATER) { + water++; + } else if (mat.name().contains("LEAVES") || mat.name().contains("LOG")) { + foliage++; + } else { + other++; + } + + if (Math.abs(checkY - centerY) > MAX_HOLE_DEPTH) { + return null; + } + } + + if (sand >= snow && sand >= water && sand >= foliage) { + return TowerType.SANDSTONE; + } + if (snow >= water && snow >= foliage) { + return TowerType.ICE; + } + if (water >= foliage) { + return TowerType.MOSSY; + } + if (random.nextInt(10) == 0) { + return TowerType.NETHER; + } + + return random.nextInt(5) == 0 ? TowerType.SMOOTH : TowerType.COBBLE; + } + + private int getSurfaceY(World world, int x, int z) { + int y = world.getHighestBlockYAt(x, z); + return y; + } + + // ✅ 1:1 Block Placement Core + public void generate(World world, int ix, int jy, int kz, TowerType towerChosen, boolean underground) { + + Material towerWallBlock = towerChosen.wall(); + Material towerLightBlock = towerChosen.light(); + Material towerFloorBlock = towerChosen.floor(); + Material towerStairBlock = towerChosen.stair(); + + int startingHeight = underground ? Math.max(jy - 70, 15) : jy - 6; + int maximumHeight = underground ? jy + 7 : 120; + + int floor = 1; + + for (int builderHeight = startingHeight; builderHeight < maximumHeight; builderHeight += 7) { + + boolean topFloor = builderHeight + 7 >= maximumHeight; + + // ============================ + // BUILD FLOOR (7 BLOCK HIGH) + // ============================ + for (int floorIterator = 0; floorIterator < 7; floorIterator++) { + + // initial floor skip like Forge + if (floor == 1 && floorIterator < 4) { + continue; + } + + for (int xIterator = -7; xIterator < 7; xIterator++) { + for (int zIterator = -7; zIterator < 7; zIterator++) { + + int x = ix + xIterator; + int y = builderHeight + floorIterator; + int z = kz + zIterator; + + // ============================ + // ORIGINAL FORGE SHAPE LOGIC + // ============================ + + // ---- Back wall row z=-7 + if (zIterator == -7) { + if (xIterator > -5 && xIterator < 4) { + buildWallPiece(world, x, y, z, towerWallBlock, floor, floorIterator); + } + continue; + } + + // ---- Stairwell rows z=-6,-5 + if (zIterator == -6 || zIterator == -5) { + + // Outer wall supports + if (xIterator == -5 || xIterator == 4) { + buildWallPiece(world, x, y, z, towerWallBlock, floor, floorIterator); + continue; + } + + // Stairwell special row z=-6 + if (zIterator == -6) { + + // Stairwell column formula + if (xIterator == (floorIterator + 1) % 7 - 3) { + + if (!(underground && floor == 1)) { + placeStair(world, x, y, z, towerStairBlock, floorIterator); + } + + // floorIterator=5 → platform extension + if (floorIterator == 5) { + setBlock(world, x - 7, y, z, towerFloorBlock); + } + + // Top ledge + if (floorIterator == 6 && topFloor) { + buildWallPiece(world, x, y, z, towerWallBlock, floor, floorIterator); + } + + continue; + } + + // Tower inside air clearing + if (xIterator < 4 && xIterator > -5) { + setBlock(world, x, y, z, Material.AIR); + } + + continue; + } + + // row z=-5 outer bounds + if (xIterator <= -5 || xIterator >= 5) { + continue; + } + + // under stairwell blocks + if ((floorIterator != 0 && floorIterator != 6) + || (xIterator != -4 && xIterator != 3)) { + + if (floorIterator == 5 && (xIterator == 3 || xIterator == -4)) { + buildFloorPiece(world, x, y, z, towerFloorBlock); + } else { + buildWallPiece(world, x, y, z, towerWallBlock, floor, floorIterator); + } + + } else { + // Stairwell space + setBlock(world, x, y, z, Material.AIR); + } + + continue; + } + + // ---- Side rows z=-4,-3,z=2,3 + if (zIterator == -4 || zIterator == -3 || zIterator == 2 || zIterator == 3) { + + if (xIterator == -6 || xIterator == 5) { + buildWallPiece(world, x, y, z, towerWallBlock, floor, floorIterator); + continue; + } + + if (xIterator <= -6 || xIterator >= 5) { + continue; + } + + if (floorIterator == 5) { + buildFloorPiece(world, x, y, z, towerFloorBlock); + continue; + } + + // NEU: SCHUTZ FÜR DIE KISTEN + // Wenn wir im neuen Stockwerk ganz unten sind (floorIterator == 0), + // dürfen wir an der Stelle x=0 und x=-1 bei z=3 die Luft nicht löschen, + // weil dort die Kisten vom Stockwerk darunter stehen. + if (floorIterator == 0 && zIterator == 3 && (xIterator == 0 || xIterator == -1)) { + continue; + } + + setBlock(world, x, y, z, Material.AIR); + continue; + } + + // ---- Middle rows z=-2,-1,0,1 + if (zIterator > -3 && zIterator < 2) { + + if (xIterator == -7 || xIterator == 6) { + + // Window logic + if (floorIterator < 0 || floorIterator > 3 + || underground + || (zIterator != -1 && zIterator != 0)) { + buildWallPiece(world, x, y, z, towerWallBlock, floor, floorIterator); + } else { + setBlock(world, x, y, z, Material.AIR); + } + + continue; + } + + if (xIterator <= -7 || xIterator >= 6) { + continue; + } + + if (floorIterator == 5) { + buildFloorPiece(world, x, y, z, towerFloorBlock); + } else { + setBlock(world, x, y, z, Material.AIR); + } + + continue; + } + + // ---- Front row z=4 + if (zIterator == 4) { + + if (xIterator == -5 || xIterator == 4) { + buildWallPiece(world, x, y, z, towerWallBlock, floor, floorIterator); + continue; + } + + if (xIterator <= -5 || xIterator >= 4) { + continue; + } + + if (floorIterator == 5) { + buildFloorPiece(world, x, y, z, towerFloorBlock); + } else { + setBlock(world, x, y, z, Material.AIR); + } + + continue; + } + + // ---- Row z=5 entrance ring + if (zIterator == 5) { + + if (xIterator == -4 || xIterator == -3 || xIterator == 2 || xIterator == 3) { + buildWallPiece(world, x, y, z, towerWallBlock, floor, floorIterator); + continue; + } + + if (xIterator <= -3 || xIterator >= 2) { + continue; + } + + if (floorIterator == 5) { + buildFloorPiece(world, x, y, z, towerFloorBlock); + } else { + buildWallPiece(world, x, y, z, towerWallBlock, floor, floorIterator); + } + + continue; + } + + // ---- Final row z=6 + if (zIterator == 6 && xIterator > -3 && xIterator < 2) { + + buildWallPiece(world, x, y, z, towerWallBlock, floor, floorIterator); + } + } + } + } + + // ============================ + // FLOOR LIGHTS (EXACT) + // ============================ + setBlock(world, ix + 3, builderHeight + 2, kz - 6, towerLightBlock); + setBlock(world, ix - 4, builderHeight + 2, kz - 6, towerLightBlock); + + setBlock(world, ix + 1, builderHeight + 2, kz - 4, towerLightBlock); + setBlock(world, ix - 2, builderHeight + 2, kz - 4, towerLightBlock); + + // ============================ + // SPAWNERS OR BOSS + // ============================ + + if ((!underground && topFloor) || (underground && floor == 1)) { + + // ===== TOP FLOOR → BOSS GOLEM ===== + TowerBossSpawner.spawnDormantBoss(world, + new Location(world, ix + 0.5, builderHeight + 6, kz + 0.5), + ix, builderHeight + 6, kz); + + } else { + + // ===== NORMAL FLOOR → 2 SPAWNERS ===== + spawnMobSpawner(world, ix + 2, builderHeight + 6, kz + 2); + spawnMobSpawner(world, ix - 3, builderHeight + 6, kz + 2); + } + + // ============================ + // ALWAYS PLACE FLOOR CHESTS + // ============================ + // ============================ + // FIXED: PLACE FLOOR CHESTS + // ============================ + + // Wir übergeben 'false' für topFloor, damit wir die Logik in der Methode selbst regeln können + // oder nutzen einfach deine Logik weiter, aber rufen es sicher auf. + placeFloorChests(world, ix, builderHeight, kz, floor, underground, topFloor); + + + // ============================ + // CHEST PETAL (PODEST) - WIEDER EINGEFÜGT + // ============================ + // Das Podest kommt auf Höhe +6 (einen Block über dem normalen Boden) + setBlock(world, ix, builderHeight + 6, kz + 3, towerFloorBlock); + setBlock(world, ix - 1, builderHeight + 6, kz + 3, towerFloorBlock); + + floor++; + } + } + + private void placeStair(World world, int x, int y, int z, Material stairMat, int floorIterator) { + var stair = (org.bukkit.block.data.type.Stairs) stairMat.createBlockData(); + + // KORREKTUR: + // Da deine Treppen sich in der `generate`-Methode nur entlang der X-Achse bewegen + // (Z ist fest auf -6), ist es eine gerade Treppe. + // Die X-Werte steigen an (-2, -1, 0...), also laufen wir nach OSTEN hoch. + // Deshalb müssen alle Treppen stur nach Osten zeigen. + + stair.setFacing(BlockFace.EAST); + + stair.setHalf(org.bukkit.block.data.type.Stairs.Half.BOTTOM); + + world.getBlockAt(x, y, z).setBlockData(stair, false); + } + + private void placeFloorChests(World world, + int ix, + int builderHeight, + int kz, + int floor, + boolean underground, + boolean topFloor + ) { + TowerLootManager loot = BattleTowersPlugin.instance().getLootManager(); + TowerStageItemManager floorChestManager; + + // 1. Loot-Logik (Original Forge Style) + if (!underground) { + floorChestManager = topFloor + ? loot.getManagerForFloor(10, random) + : loot.getManagerForFloor(floor, random); + } else { + floorChestManager = (floor == 1) + ? loot.getManagerForFloor(10, random) + : loot.getManagerForFloor(Math.abs(11 - floor), random); + } + + // 2. Koordinaten berechnen + // cy = builderHeight + 7 stellt die Kisten auf das Podest (welches auf +6 ist) + int cx = ix; + int cy = builderHeight + 7; + int cz = kz + 3; + + // 3. Blöcke initial auf Kiste setzen + world.getBlockAt(cx, cy, cz).setType(Material.CHEST, false); + world.getBlockAt(cx - 1, cy, cz).setType(Material.CHEST, false); + + // 4. Doppelkisten-Daten anwenden (Verbindung korrigieren) + // Von vorne (Norden) betrachtet: + // cx - 1 ist LINKS (Westen) -> Type.LEFT + // cx ist RECHTS (Osten) -> Type.RIGHT + + // Linke Hälfte (cx - 1) + var leftData = (org.bukkit.block.data.type.Chest) Material.CHEST.createBlockData(); + leftData.setFacing(BlockFace.NORTH); + leftData.setType(org.bukkit.block.data.type.Chest.Type.LEFT); + + // Rechte Hälfte (cx) + var rightData = (org.bukkit.block.data.type.Chest) Material.CHEST.createBlockData(); + rightData.setFacing(BlockFace.NORTH); + rightData.setType(org.bukkit.block.data.type.Chest.Type.RIGHT); + + // Daten auf die Blöcke schreiben + world.getBlockAt(cx - 1, cy, cz).setBlockData(leftData, false); + world.getBlockAt(cx, cy, cz).setBlockData(rightData, false); + + // 5. Inventar füllen + fillChest(world, cx, cy, cz, floorChestManager, underground); + fillChest(world, cx - 1, cy, cz, floorChestManager, underground); + } + + private void fillChest(World world, + int x, + int y, + int z, + TowerStageItemManager manager, + boolean underground) { + + var state = world.getBlockAt(x, y, z).getState(); + if (!(state instanceof org.bukkit.block.Chest chest)) return; + + int attempts = BattleTowersPlugin.instance() + .getConfig() + .getInt("tower.itemGenerateAttemptsPerFloor"); + + if (underground) attempts *= 2; + + for (int i = 0; i < attempts; i++) { + + ItemStack item = manager.getStageItem(random); + if (item == null) continue; + + chest.getInventory().setItem( + random.nextInt(chest.getInventory().getSize()), + item + ); + } + } + + + private void spawnMobSpawner(World world, int x, int y, int z) { + + Block block = world.getBlockAt(x, y, z); + block.setType(Material.SPAWNER, false); + + if (block.getState() instanceof CreatureSpawner spawner) { + + EntityType mob = switch (new Random().nextInt(4)) { + case 0 -> EntityType.SKELETON; + case 1 -> EntityType.ZOMBIE; + case 2 -> EntityType.SPIDER; + default -> EntityType.CAVE_SPIDER; + }; + + spawner.setSpawnedType(mob); + spawner.update(true); + } + } + + + private void buildFloorPiece(World world, int x, int y, int z, Material floor) { + setBlock(world, x, y, z, floor); + } + + private void buildWallPiece(World world, int x, int y, int z, + Material wall, int floor, int floorIterator) { + + setBlock(world, x, y, z, wall); + + // Forge: fill base downwards + if (floor == 1 && floorIterator == 4) { + fillTowerBaseToGround(world, x, y, z, wall); + } + } + + private void fillTowerBaseToGround(World world, int x, int y, int z, Material wall) { + + int yy = y - 1; + + while (yy > 0) { + + Material below = world.getBlockAt(x, yy, z).getType(); + + if (isBuildableBlock(below)) { + break; + } + + setBlock(world, x, yy, z, wall); + yy--; + } + } + + private boolean isBuildableBlock(Material mat) { + return mat == Material.STONE + || mat == Material.GRASS_BLOCK + || mat == Material.DIRT + || mat == Material.SAND + || mat == Material.SANDSTONE + || mat == Material.GRAVEL; + } + + private void setBlock(World world, int x, int y, int z, Material mat) { + world.getBlockAt(x, y, z).setType(mat, false); + } +} diff --git a/src/main/java/fyi/tiko/battletowers/TowerLootManager.java b/src/main/java/fyi/tiko/battletowers/TowerLootManager.java new file mode 100644 index 0000000..edd182d --- /dev/null +++ b/src/main/java/fyi/tiko/battletowers/TowerLootManager.java @@ -0,0 +1,36 @@ +package fyi.tiko.battletowers; + +import fyi.tiko.battletowers.BattleTowersPlugin; + +import java.util.Random; + +public class TowerLootManager { + + private final TowerStageItemManager[] floors = new TowerStageItemManager[10]; + + public TowerLootManager() { + + var cfg = BattleTowersPlugin.instance().getConfig(); + + floors[0] = new TowerStageItemManager(cfg.getString("loot.floor1")); + floors[1] = new TowerStageItemManager(cfg.getString("loot.floor2")); + floors[2] = new TowerStageItemManager(cfg.getString("loot.floor3")); + floors[3] = new TowerStageItemManager(cfg.getString("loot.floor4")); + floors[4] = new TowerStageItemManager(cfg.getString("loot.floor5")); + floors[5] = new TowerStageItemManager(cfg.getString("loot.floor6")); + floors[6] = new TowerStageItemManager(cfg.getString("loot.floor7")); + floors[7] = new TowerStageItemManager(cfg.getString("loot.floor8")); + floors[8] = new TowerStageItemManager(cfg.getString("loot.floor9")); + floors[9] = new TowerStageItemManager(cfg.getString("loot.top")); + } + + public TowerStageItemManager getManagerForFloor(int floor, Random rand) { + + floor--; // Forge subtract + + if (floor < 0) floor = 0; + if (floor >= floors.length) floor = floors.length - 1; + + return new TowerStageItemManager(floors[floor]); + } +} diff --git a/src/main/java/fyi/tiko/battletowers/TowerPopulator.java b/src/main/java/fyi/tiko/battletowers/TowerPopulator.java new file mode 100644 index 0000000..b432066 --- /dev/null +++ b/src/main/java/fyi/tiko/battletowers/TowerPopulator.java @@ -0,0 +1,24 @@ +package fyi.tiko.battletowers; + +import org.bukkit.Chunk; +import org.bukkit.World; +import org.bukkit.generator.BlockPopulator; + +import java.util.Random; + +public class TowerPopulator extends BlockPopulator { + + private final TowerGenerator generator = new TowerGenerator(); + + @Override + public void populate(World world, Random random, Chunk chunk) { + + // Chance pro Chunk (erstmal test) + if (random.nextInt(250) != 0) return; + + int x = chunk.getX() * 16 + 8; + int z = chunk.getZ() * 16 + 8; + + generator.trySpawnTower(world, random, x, z); + } +} diff --git a/src/main/java/fyi/tiko/battletowers/TowerStageItemManager.java b/src/main/java/fyi/tiko/battletowers/TowerStageItemManager.java new file mode 100644 index 0000000..7a109c5 --- /dev/null +++ b/src/main/java/fyi/tiko/battletowers/TowerStageItemManager.java @@ -0,0 +1,69 @@ +package fyi.tiko.battletowers; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class TowerStageItemManager { + + private final List entries = new ArrayList<>(); + private int curIndex = 0; + + public TowerStageItemManager(String configString) { + + String[] elements = configString.split(";"); + + for (String element : elements) { + + String[] data = element.trim().split("-"); + + if (data.length < 5) { + System.err.println("Invalid loot entry: " + element); + continue; + } + + Material mat = Material.matchMaterial(data[0].toUpperCase()); + if (mat == null) { + System.err.println("Unknown material: " + data[0]); + continue; + } + + int damage = Integer.parseInt(data[1]); // ignored in modern MC + int chance = Integer.parseInt(data[2]); + int min = Integer.parseInt(data[3]); + int max = Integer.parseInt(data[4]); + + entries.add(new LootEntry(mat, chance, min, max)); + } + } + + public TowerStageItemManager(TowerStageItemManager copy) { + entries.addAll(copy.entries); + } + + public boolean floorHasItemsLeft() { + return curIndex < entries.size(); + } + + public ItemStack getStageItem(Random rand) { + + if (!floorHasItemsLeft()) return null; + + LootEntry entry = entries.get(curIndex); + + ItemStack result = null; + + if (rand.nextInt(100) < entry.chance) { + int amount = entry.min + rand.nextInt(entry.max - entry.min + 1); + result = new ItemStack(entry.material, amount); + } + + curIndex++; + return result; + } + + private record LootEntry(Material material, int chance, int min, int max) {} +} diff --git a/src/main/java/fyi/tiko/battletowers/TowerType.java b/src/main/java/fyi/tiko/battletowers/TowerType.java new file mode 100644 index 0000000..ea40be1 --- /dev/null +++ b/src/main/java/fyi/tiko/battletowers/TowerType.java @@ -0,0 +1,30 @@ +package fyi.tiko.battletowers; + +import org.bukkit.Material; + +public enum TowerType { + + COBBLE(Material.COBBLESTONE, Material.STONE_BRICKS, Material.TORCH, Material.STONE_STAIRS), + MOSSY(Material.MOSSY_COBBLESTONE, Material.STONE_BRICKS, Material.TORCH, Material.STONE_STAIRS), + SANDSTONE(Material.SANDSTONE, Material.CUT_SANDSTONE, Material.TORCH, Material.SANDSTONE_STAIRS), + ICE(Material.ICE, Material.CLAY, Material.AIR, Material.OAK_STAIRS), + SMOOTH(Material.STONE, Material.SMOOTH_STONE, Material.TORCH, Material.STONE_STAIRS), + NETHER(Material.NETHERRACK, Material.SOUL_SAND, Material.GLOWSTONE, Material.NETHER_BRICK_STAIRS); + + private final Material wall; + private final Material floor; + private final Material light; + private final Material stair; + + TowerType(Material wall, Material floor, Material light, Material stair) { + this.wall = wall; + this.floor = floor; + this.light = light; + this.stair = stair; + } + + public Material wall() { return wall; } + public Material floor() { return floor; } + public Material light() { return light; } + public Material stair() { return stair; } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 2593cc8..5f576a3 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -3,4 +3,19 @@ spawn: min-distance: 196 # wie original max-y-difference: 22 -debug: true \ No newline at end of file +debug: true + +tower: + itemGenerateAttemptsPerFloor: 7 + +loot: + floor1: "stick-0-75-5-6;wheat_seeds-0-75-3-5;oak_planks-0-75-5-6;sugar_cane-0-75-3-5" + floor2: "stone_pickaxe-0-50-1-1;stone_axe-0-50-1-1;torch-0-80-3-3;stone_button-0-50-2-2" + floor3: "bowl-0-75-2-4;coal-0-90-4-4;string-0-80-5-5;white_wool-0-75-2-2" + floor4: "glass-0-75-3-3;feather-0-75-4-4;bread-0-75-2-2;apple-0-75-2-2" + floor5: "brown_mushroom-0-75-2-2;red_mushroom-0-75-2-2;oak_sapling-0-90-3-3;wheat-0-75-4-4" + floor6: "oak_sign-0-50-1-2;fishing_rod-0-75-1-1;pumpkin_seeds-0-60-2-2;melon_seeds-0-60-3-3" + floor7: "iron_sword-0-60-1-1;gunpowder-0-75-3-3;leather-0-75-4-4;cod-0-75-3-3;blue_dye-0-60-1-2" + floor8: "chainmail_helmet-0-40-1-1;chainmail_chestplate-0-40-1-1;chainmail_leggings-0-40-1-1;chainmail_boots-0-40-1-1" + floor9: "bookshelf-0-70-1-3;redstone_lamp-0-60-2-2;lily_pad-0-75-3-3;brewing_stand-0-50-1-1" + top: "ender_pearl-0-50-2-2;diamond-0-70-2-2;redstone-0-75-5-5;gold_ingot-0-90-8-8" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 0f8e973..6ba8291 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ name: battle-towers-revamped version: 0.0.1 -main: fyi.tiko.battletower.BattleTowerPlugin +main: fyi.tiko.battletowers.BattleTowersPlugin api-version: 1.21 author: tiko description: A revamped version of the old battle towers mod for the old modpack "hexxit". Ported from 1.5.2 to paper 1.21