From 0dd69c906956024609fa13096ef3363182cc6b8b Mon Sep 17 00:00:00 2001 From: erikradovan Date: Thu, 5 Mar 2026 01:33:13 +0100 Subject: [PATCH 1/2] Added mine generation logic, mine world handling, basic mine commands. --- .../xyz/soukup/ecoCraftCore/EcoCraftCore.java | 12 +- .../ecoCraftCore/mines/MineCommand.java | 65 +++ .../ecoCraftCore/mines/MineManager.java | 469 ++++++++++++++++++ .../ecoCraftCore/mines/MineWorldManager.java | 275 ++++++++++ src/main/resources/messages.yml | 7 + 5 files changed, 826 insertions(+), 2 deletions(-) create mode 100644 src/main/java/xyz/soukup/ecoCraftCore/mines/MineCommand.java create mode 100644 src/main/java/xyz/soukup/ecoCraftCore/mines/MineManager.java create mode 100644 src/main/java/xyz/soukup/ecoCraftCore/mines/MineWorldManager.java diff --git a/src/main/java/xyz/soukup/ecoCraftCore/EcoCraftCore.java b/src/main/java/xyz/soukup/ecoCraftCore/EcoCraftCore.java index ce8362f..057db04 100644 --- a/src/main/java/xyz/soukup/ecoCraftCore/EcoCraftCore.java +++ b/src/main/java/xyz/soukup/ecoCraftCore/EcoCraftCore.java @@ -9,11 +9,14 @@ import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; +import xyz.soukup.ecoCraftCore.mines.MineCommand; +import xyz.soukup.ecoCraftCore.mines.MineWorldManager; import xyz.soukup.ecoCraftCore.money.MoneyCommand; import xyz.soukup.ecoCraftCore.player.PreparePlayer; import xyz.soukup.ecoCraftCore.positionMarker.RulerCommand; import xyz.soukup.ecoCraftCore.shop.ShopCommand; import xyz.soukup.ecoCraftCore.database.objects.Account; +import xyz.soukup.ecoCraftCore.database.objects.Island; import xyz.soukup.ecoCraftCore.database.objects.Shop; import xyz.soukup.ecoCraftCore.database.objects.Transaction; import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; @@ -46,6 +49,7 @@ public final class EcoCraftCore extends JavaPlugin { prepareDatabase(); registerCommands(); registerEvents(); + MineWorldManager.init(); } catch (SQLException e) { e.printStackTrace(); getLogger().severe("Failed to initialize database."); @@ -74,8 +78,8 @@ public final class EcoCraftCore extends JavaPlugin { } private void prepareDatabase() throws SQLException { - String databaseUrl = "jdbc:mysql://localhost:3306/ecc"; - connectionSource = new JdbcConnectionSource(databaseUrl, "ecc", "ecc"); + String databaseUrl = "jdbc:mysql://u8089_PlbP6lACvk:%3DGz!yBPu%3DGTvywi4ot%40lbEKw@camelot.vagonbrei.eu:3306/s8089_ecc"; + connectionSource = new JdbcConnectionSource(databaseUrl, "u8089_PlbP6lACvk", "=Gz!yBPu=GTvywi4ot@lbEKw"); Logger.getLogger("com.j256.ormlite.table.TableUtils").setLevel(Level.OFF); @@ -83,11 +87,13 @@ public final class EcoCraftCore extends JavaPlugin { TableUtils.createTableIfNotExists(connectionSource, Shop.class); TableUtils.createTableIfNotExists(connectionSource, VirtualChest.class); TableUtils.createTableIfNotExists(connectionSource, Account.class); + TableUtils.createTableIfNotExists(connectionSource, Island.class); DaoRegistry.setTransactionDao(DaoManager.createDao(connectionSource, Transaction.class)); DaoRegistry.setShopDao(DaoManager.createDao(connectionSource, Shop.class)); DaoRegistry.setVirtualChestDao(DaoManager.createDao(connectionSource, VirtualChest.class)); DaoRegistry.setAccountDao(DaoManager.createDao(connectionSource, Account.class)); + DaoRegistry.setIslandDaoo(DaoManager.createDao(connectionSource, Island.class)); } @@ -97,6 +103,7 @@ public final class EcoCraftCore extends JavaPlugin { lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(ShopCommand.createCommand().build())); lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(RulerCommand.createCommand().build())); lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(MoneyCommand.createCommand().build())); + lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(MineCommand.createCommand().build())); } private void registerEvents(){ @@ -106,6 +113,7 @@ public final class EcoCraftCore extends JavaPlugin { pm.registerEvents(new VirtualChestLogic(), this); pm.registerEvents(new ShopLogic(), this); pm.registerEvents(new PreparePlayer(), this); + pm.registerEvents(new MineWorldManager(), this); } diff --git a/src/main/java/xyz/soukup/ecoCraftCore/mines/MineCommand.java b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineCommand.java new file mode 100644 index 0000000..25cd2f4 --- /dev/null +++ b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineCommand.java @@ -0,0 +1,65 @@ +package xyz.soukup.ecoCraftCore.mines; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; +import xyz.soukup.ecoCraftCore.messages.Messages; + +@SuppressWarnings("UnstableApiUsage") +public class MineCommand { + + public static LiteralArgumentBuilder createCommand() { + LiteralArgumentBuilder regenerate = Commands.literal("regenerate") + .executes(MineCommand::regenerateMines); + + LiteralArgumentBuilder tp = Commands.literal("tp") + .requires(source -> source.getSender() instanceof Player) + .executes(MineCommand::teleportToMines); + + return Commands.literal("mine") + .then(regenerate) + .then(tp); + } + + private static int regenerateMines(CommandContext context) { + World world = MineWorldManager.getWorld(); + + if (world == null) { + Messages.send(context.getSource().getSender(), "mine.error.no-world"); + return 0; + } + + Messages.send(context.getSource().getSender(), "mine.regenerating"); + + // Teleport all players out of the mine world first + for (Player p : world.getPlayers()) { + p.teleport(p.getServer().getWorlds().getFirst().getSpawnLocation()); + Messages.send(p, "mine.teleported-out"); + } + + // Delete old world, create fresh one, then generate + MineWorldManager.recreateWorld(newWorld -> MineManager.regenerate(newWorld)); + return 1; + } + + private static int teleportToMines(CommandContext context) { + Player player = (Player) context.getSource().getSender(); + + World world = MineWorldManager.getWorld(); + + if (world == null) { + Messages.send(player, "mine.error.no-world"); + return 0; + } + + Location spawn = MineWorldManager.getSpawnLocation(); + player.teleport(spawn); + Messages.send(player, "mine.teleporting"); + return 1; + } +} + diff --git a/src/main/java/xyz/soukup/ecoCraftCore/mines/MineManager.java b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineManager.java new file mode 100644 index 0000000..2fe15e4 --- /dev/null +++ b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineManager.java @@ -0,0 +1,469 @@ +package xyz.soukup.ecoCraftCore.mines; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.BlockFace; + +import java.util.*; + +import static xyz.soukup.ecoCraftCore.EcoCraftCore.plugin; + +public class MineManager { + + private static final int TUNNEL_RADIUS = 5; // 11x11 = radius 5 from center + private static final int MIN_BRANCH_SIZE = 15; + private static final int FIRST_BRANCH_MIN_SIZE = 12; + private static final int FIRST_BRANCH_SPLIT_DELAY = 12; // first branch: no split chance until after 12 blocks + private static final int MAX_BRANCH_SIZE = 50; + private static final double BASE_SPLIT_INCREMENT = 0.01; + private static final double SPLIT_DECAY_PER_BRANCH = 0.000125; + private static final int BLOCKS_PER_TICK = 800; + + // Biome boundaries (distance thresholds) + private static final int STONE_END = 40; + private static final int TUFF_END = 80; + private static final int DEEPSLATE_END = 120; + private static final int BASALT_END = 160; + // Blackstone: 160+ + + // Transition zone: how many blocks before/after a border we blend + private static final int TRANSITION_RANGE = 8; + + private static final Random random = new Random(); + + private static final BlockFace[] DIRECTIONS = { + BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST, BlockFace.DOWN, BlockFace.UP + }; + + // --- Biome ore tables --- + + private record OreEntry(Material material, double chance) {} + + private static final OreEntry[] STONE_ORES = { + new OreEntry(Material.GRAVEL, 0.08), + new OreEntry(Material.COAL_ORE, 0.05), + new OreEntry(Material.COPPER_ORE, 0.04), + }; + + private static final OreEntry[] TUFF_ORES = { + new OreEntry(Material.IRON_ORE, 0.04), + new OreEntry(Material.GOLD_ORE, 0.025), + new OreEntry(Material.LAPIS_ORE, 0.02), + }; + + private static final OreEntry[] DEEPSLATE_ORES = { + new OreEntry(Material.DEEPSLATE_DIAMOND_ORE, 0.03), + new OreEntry(Material.DEEPSLATE_EMERALD_ORE, 0.015), + new OreEntry(Material.DEEPSLATE_REDSTONE_ORE, 0.02), + }; + + private static final OreEntry[] BASALT_ORES = { + new OreEntry(Material.RAW_IRON_BLOCK, 0.015), + new OreEntry(Material.RAW_COPPER_BLOCK, 0.015), + }; + + private static final OreEntry[] BLACKSTONE_ORES = { + new OreEntry(Material.ANCIENT_DEBRIS, 0.003), + new OreEntry(Material.RAW_GOLD_BLOCK, 0.01), + }; + + // Branch data class + private static class Branch { + final BlockFace direction; + final int startX, startY, startZ; + final int distanceFromStart; + final int maxSize; + final boolean isFirst; + double chanceToSplit; + + Branch(BlockFace direction, int startX, int startY, int startZ, int distanceFromStart, int maxSize, boolean isFirst) { + this.direction = direction; + this.startX = startX; + this.startY = startY; + this.startZ = startZ; + this.distanceFromStart = distanceFromStart; + this.maxSize = maxSize; + this.isFirst = isFirst; + this.chanceToSplit = 0; + } + } + + private static class BlockPlacement { + final int x, y, z; + final Material material; + + BlockPlacement(int x, int y, int z, Material material) { + this.x = x; + this.y = y; + this.z = z; + this.material = material; + } + } + + public static void regenerate(World world) { + int startY = world.getMaxHeight() - 11; + int minY = world.getMinHeight() + 5; + int maxY = world.getMaxHeight(); + int worldMinY = world.getMinHeight(); + + // Run the heavy data generation async, then schedule block placement on main thread + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + Set fillPositions = new HashSet<>(); + List fillPlacements = new ArrayList<>(); + + // Phase 1: Generate tunnel fill data + generateBranches(fillPlacements, fillPositions, startY, minY); + + plugin.getLogger().info("Mine generation phase 1 done: " + fillPlacements.size() + " fill blocks, " + fillPositions.size() + " unique positions."); + + // Phase 2: Generate wall data + List wallPlacements = new ArrayList<>(); + generateWalls(fillPositions, wallPlacements, startY); + + plugin.getLogger().info("Mine generation phase 2 done: " + wallPlacements.size() + " wall blocks."); + + // Phase 3: Schedule block placement on main thread in batches + Bukkit.getScheduler().runTask(plugin, () -> + scheduleBlockPlacements(world, fillPlacements, wallPlacements, startY, worldMinY, maxY)); + }); + } + + private static void generateBranches(List placements, Set fillPositions, int startY, int minY) { + Deque queue = new ArrayDeque<>(); + // Shared mutable counter: [0] = total branches ever created + int[] branchCount = {0}; + + int firstSize = Math.max(FIRST_BRANCH_MIN_SIZE, MIN_BRANCH_SIZE + random.nextInt(MAX_BRANCH_SIZE - MIN_BRANCH_SIZE)); + Branch initial = new Branch(BlockFace.DOWN, 0, startY, 0, 0, firstSize, true); + queue.add(initial); + branchCount[0]++; + + plugin.getLogger().info("[MineGen] Starting generation at Y=" + startY + ", minY=" + minY); + + while (!queue.isEmpty()) { + Branch branch = queue.pollFirst(); + double currentIncrement = Math.max(0, BASE_SPLIT_INCREMENT - (branchCount[0] * SPLIT_DECAY_PER_BRANCH)); + plugin.getLogger().info("[MineGen] Processing branch #" + branchCount[0] + + " dir=" + branch.direction + " start=(" + branch.startX + "," + branch.startY + "," + branch.startZ + ")" + + " dist=" + branch.distanceFromStart + " maxSize=" + branch.maxSize + + " first=" + branch.isFirst + + " queued=" + queue.size() + " fillBlocks=" + fillPositions.size() + + " splitIncrement=" + String.format("%.5f", currentIncrement)); + generateSingleBranch(branch, queue, placements, fillPositions, startY, minY, branchCount); + } + + plugin.getLogger().info("[MineGen] Branch generation complete. Total branches=" + branchCount[0] + " fillBlocks=" + fillPositions.size() + " placements=" + placements.size()); + } + + private static void generateSingleBranch(Branch branch, Deque queue, List placements, + Set fillPositions, int startY, int minY, int[] branchCount) { + int cx = branch.startX; + int cy = branch.startY; + int cz = branch.startZ; + + for (int step = 0; step < branch.maxSize; step++) { + int distance = branch.distanceFromStart + step; + + // Generate the 11x11 cross section at this position + generateCrossSection(cx, cy, cz, branch.direction, distance, placements, fillPositions); + + // First branch: skip split logic for the first 12 blocks + if (branch.isFirst && step < FIRST_BRANCH_SPLIT_DELAY) { + cx += branch.direction.getModX(); + cy += branch.direction.getModY(); + cz += branch.direction.getModZ(); + if (cy < minY || cy > startY + 5) break; + continue; + } + + // Calculate the current split increment based on how many branches exist + double splitIncrement = Math.max(0, BASE_SPLIT_INCREMENT - (branchCount[0] * SPLIT_DECAY_PER_BRANCH)); + + // If increment has reached 0, no more splitting is possible — skip the roll entirely + if (splitIncrement <= 0) { + cx += branch.direction.getModX(); + cy += branch.direction.getModY(); + cz += branch.direction.getModZ(); + if (cy < minY || cy > startY + 5) break; + continue; + } + + branch.chanceToSplit += splitIncrement; + + if (random.nextDouble() < branch.chanceToSplit) { + branch.chanceToSplit = 0; + + int splitCount = 1 + random.nextInt(4); + List available = getAvailableDirections(branch.direction); + Collections.shuffle(available, random); + + int created = 0; + for (BlockFace dir : available) { + if (created >= splitCount) break; + + int branchMaxSize = MIN_BRANCH_SIZE + random.nextInt(MAX_BRANCH_SIZE - MIN_BRANCH_SIZE); + + // Skip UP if it would go above startY + if (dir == BlockFace.UP && cy + branchMaxSize > startY) { + plugin.getLogger().info("[MineGen] Skipped UP branch at Y=" + cy + " (would exceed startY=" + startY + ")"); + continue; + } + + // Skip DOWN if it would go below minY + if (dir == BlockFace.DOWN && cy - branchMaxSize < minY) { + plugin.getLogger().info("[MineGen] Skipped DOWN branch at Y=" + cy + " (would go below minY=" + minY + ")"); + continue; + } + + queue.add(new Branch(dir, cx, cy, cz, distance, branchMaxSize, false)); + branchCount[0]++; + created++; + } + plugin.getLogger().info("[MineGen] Split at step=" + step + " pos=(" + cx + "," + cy + "," + cz + ") created=" + created + " branches, total=" + branchCount[0] + " splitInc=" + String.format("%.5f", splitIncrement)); + } + + // Advance position along the branch direction + cx += branch.direction.getModX(); + cy += branch.direction.getModY(); + cz += branch.direction.getModZ(); + + if (cy < minY || cy > startY + 5) break; + } + } + + private static List getAvailableDirections(BlockFace current) { + List dirs = new ArrayList<>(); + BlockFace opposite = current.getOppositeFace(); + for (BlockFace dir : DIRECTIONS) { + if (dir != opposite && dir != current) { + dirs.add(dir); + } + } + return dirs; + } + + private static void generateCrossSection(int cx, int cy, int cz, BlockFace direction, int distance, + List placements, Set fillPositions) { + int[][] offsets = getPerpOffsets(direction); + + for (int a = -TUNNEL_RADIUS; a <= TUNNEL_RADIUS; a++) { + for (int b = -TUNNEL_RADIUS; b <= TUNNEL_RADIUS; b++) { + int bx = cx + offsets[0][0] * a + offsets[1][0] * b; + int by = cy + offsets[0][1] * a + offsets[1][1] * b; + int bz = cz + offsets[0][2] * a + offsets[1][2] * b; + + long key = posKey(bx, by, bz); + if (!fillPositions.add(key)) continue; + + Material mat = pickBlockForDistance(distance); + placements.add(new BlockPlacement(bx, by, bz, mat)); + } + } + } + + // --- Biome fill + ore + transition logic --- + + private static Material pickBlockForDistance(int distance) { + // Determine primary biome and check if we're in a transition zone + Material primaryFill = getPrimaryFill(distance); + OreEntry[] primaryOres = getOreTable(primaryFill); + + // Check transition: are we near a biome border? + int[] border = getNearestBorder(distance); + // border[0] = border distance threshold, border[1] = signed distance to border (negative = before, positive = after) + + if (border != null && Math.abs(border[1]) <= TRANSITION_RANGE) { + // We're in a transition zone — blend with the neighboring biome + Material neighborFill = (border[1] <= 0) + ? getNextFill(primaryFill) // approaching next biome + : getPrevFill(primaryFill); // just crossed into this biome from previous + OreEntry[] neighborOres = getOreTable(neighborFill); + + // Blend ratio: 0.0 at edge of transition, 0.5 at the border itself + double blendRatio = 0.5 * (1.0 - (double) Math.abs(border[1]) / TRANSITION_RANGE); + + // Roll for neighbor fill block + if (random.nextDouble() < blendRatio) { + // Use neighbor biome's fill + ores + Material ore = rollOreTable(neighborOres); + return (ore != null) ? ore : neighborFill; + } + } + + // Primary biome: roll for ore + Material ore = rollOreTable(primaryOres); + return (ore != null) ? ore : primaryFill; + } + + private static Material rollOreTable(OreEntry[] table) { + double roll = random.nextDouble(); + double cumulative = 0; + for (OreEntry entry : table) { + cumulative += entry.chance; + if (roll < cumulative) return entry.material; + } + return null; + } + + private static Material getPrimaryFill(int distance) { + if (distance < STONE_END) return Material.STONE; + if (distance < TUFF_END) return Material.TUFF; + if (distance < DEEPSLATE_END) return Material.DEEPSLATE; + if (distance < BASALT_END) return Material.SMOOTH_BASALT; + return Material.BLACKSTONE; + } + + private static OreEntry[] getOreTable(Material fill) { + return switch (fill) { + case STONE -> STONE_ORES; + case TUFF -> TUFF_ORES; + case DEEPSLATE -> DEEPSLATE_ORES; + case SMOOTH_BASALT -> BASALT_ORES; + case BLACKSTONE -> BLACKSTONE_ORES; + default -> STONE_ORES; + }; + } + + private static Material getNextFill(Material fill) { + return switch (fill) { + case STONE -> Material.TUFF; + case TUFF -> Material.DEEPSLATE; + case DEEPSLATE -> Material.SMOOTH_BASALT; + case SMOOTH_BASALT -> Material.BLACKSTONE; + default -> Material.BLACKSTONE; // blackstone has no next + }; + } + + private static Material getPrevFill(Material fill) { + return switch (fill) { + case TUFF -> Material.STONE; + case DEEPSLATE -> Material.TUFF; + case SMOOTH_BASALT -> Material.DEEPSLATE; + case BLACKSTONE -> Material.SMOOTH_BASALT; + default -> Material.STONE; // stone has no prev + }; + } + + /** + * Returns the nearest biome border info, or null if not near any. + * Result: [0] = border distance, [1] = signed distance to border (negative = before border, positive = after) + */ + private static int[] getNearestBorder(int distance) { + int[] borders = {STONE_END, TUFF_END, DEEPSLATE_END, BASALT_END}; + int closestDist = Integer.MAX_VALUE; + int closestBorder = -1; + + for (int b : borders) { + int diff = distance - b; + if (Math.abs(diff) < Math.abs(closestDist)) { + closestDist = diff; + closestBorder = b; + } + } + + if (closestBorder == -1 || Math.abs(closestDist) > TRANSITION_RANGE) return null; + return new int[]{closestBorder, closestDist}; + } + + // --- Perpendicular axes --- + + private static int[][] getPerpOffsets(BlockFace dir) { + return switch (dir) { + case UP, DOWN -> new int[][]{{1, 0, 0}, {0, 0, 1}}; + case NORTH, SOUTH -> new int[][]{{1, 0, 0}, {0, 1, 0}}; + case EAST, WEST -> new int[][]{{0, 0, 1}, {0, 1, 0}}; + default -> new int[][]{{1, 0, 0}, {0, 0, 1}}; + }; + } + + // --- Wall generation --- + + private static void generateWalls(Set fillPositions, List wallPlacements, int startY) { + Set wallPositions = new HashSet<>(); + plugin.getLogger().info("[MineGen] Generating walls for " + fillPositions.size() + " fill positions..."); + + for (long key : fillPositions) { + int x = decodeX(key); + int y = decodeY(key); + int z = decodeZ(key); + + for (BlockFace face : DIRECTIONS) { + int nx = x + face.getModX(); + int ny = y + face.getModY(); + int nz = z + face.getModZ(); + + long neighborKey = posKey(nx, ny, nz); + + if (!fillPositions.contains(neighborKey)) { + if (ny > startY) continue; + + if (wallPositions.add(neighborKey)) { + wallPlacements.add(new BlockPlacement(nx, ny, nz, Material.POLISHED_BLACKSTONE)); + } + } + } + } + } + + // --- Block placement scheduling --- + + private static void scheduleBlockPlacements(World world, List fillPlacements, + List wallPlacements, int startY, int minY, int maxY) { + List allPlacements = new ArrayList<>(fillPlacements.size() + wallPlacements.size() + 200); + allPlacements.addAll(fillPlacements); + allPlacements.addAll(wallPlacements); + + // Air blocks above the entrance opening + for (int a = -TUNNEL_RADIUS; a <= TUNNEL_RADIUS; a++) { + for (int b = -TUNNEL_RADIUS; b <= TUNNEL_RADIUS; b++) { + allPlacements.add(new BlockPlacement(a, startY + 1, b, Material.AIR)); + } + } + + int totalBlocks = allPlacements.size(); + int tickDelay = 0; + + plugin.getLogger().info("[MineGen] Scheduling " + totalBlocks + " block placements (" + fillPlacements.size() + " fill + " + wallPlacements.size() + " walls) in batches of " + BLOCKS_PER_TICK); + + for (int i = 0; i < totalBlocks; i += BLOCKS_PER_TICK) { + int from = i; + int to = Math.min(i + BLOCKS_PER_TICK, totalBlocks); + + Bukkit.getScheduler().runTaskLater(plugin, () -> { + for (int j = from; j < to; j++) { + BlockPlacement bp = allPlacements.get(j); + if (bp.y < minY || bp.y >= maxY) continue; + world.getBlockAt(bp.x, bp.y, bp.z).setType(bp.material, false); + } + }, tickDelay); + + tickDelay++; + } + + int finalDelay = tickDelay; + Bukkit.getScheduler().runTaskLater(plugin, + () -> plugin.getLogger().info("Mine regeneration complete. " + totalBlocks + " blocks placed."), + finalDelay + 2); + } + + // --- Position encoding/decoding --- + + private static long posKey(int x, int y, int z) { + return ((long) (x + 30000000) & 0x3FFFFFF) + | (((long) (y + 2048) & 0xFFF) << 26) + | (((long) (z + 30000000) & 0x3FFFFFF) << 38); + } + + private static int decodeX(long key) { + return (int) (key & 0x3FFFFFF) - 30000000; + } + + private static int decodeY(long key) { + return (int) ((key >> 26) & 0xFFF) - 2048; + } + + private static int decodeZ(long key) { + return (int) ((key >> 38) & 0x3FFFFFF) - 30000000; + } +} diff --git a/src/main/java/xyz/soukup/ecoCraftCore/mines/MineWorldManager.java b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineWorldManager.java new file mode 100644 index 0000000..06a10df --- /dev/null +++ b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineWorldManager.java @@ -0,0 +1,275 @@ +package xyz.soukup.ecoCraftCore.mines; + +import com.infernalsuite.asp.api.AdvancedSlimePaperAPI; +import com.infernalsuite.asp.api.world.SlimeWorld; +import com.infernalsuite.asp.api.world.properties.SlimeProperties; +import com.infernalsuite.asp.api.world.properties.SlimePropertyMap; +import org.bukkit.*; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockExplodeEvent; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerItemDamageEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.Damageable; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import xyz.soukup.ecoCraftCore.database.objects.Island; +import xyz.soukup.ecoCraftCore.islands.IslandLoader; + +import java.util.function.Consumer; + +import static xyz.soukup.ecoCraftCore.EcoCraftCore.plugin; + +public class MineWorldManager implements Listener { + + public static final String MINE_WORLD_NAME = "mine_world"; + private static World mineWorld; + + public static void init() { + AdvancedSlimePaperAPI asp = AdvancedSlimePaperAPI.instance(); + IslandLoader loader = new IslandLoader(); + + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + SlimeWorld slimeWorld = null; + + // Try to load existing world + if (loader.worldExists(MINE_WORLD_NAME)) { + try { + slimeWorld = asp.readWorld(loader, MINE_WORLD_NAME, false, new SlimePropertyMap()); + } catch (Exception e) { + plugin.getLogger().warning("Mine world data is corrupted, recreating..."); + loader.deleteWorld(MINE_WORLD_NAME); + } + } + + // Create fresh world if loading failed or didn't exist + if (slimeWorld == null) { + SlimePropertyMap props = new SlimePropertyMap(); + props.setValue(SlimeProperties.ENVIRONMENT, "NORMAL"); + + Island island = new Island("mine", MINE_WORLD_NAME, "Mine World", "Shared mine world", "server", "system", null); + island.save(); + + slimeWorld = asp.createEmptyWorld(MINE_WORLD_NAME, false, props, loader); + } + + SlimeWorld finalWorld = slimeWorld; + Bukkit.getScheduler().runTask(plugin, () -> { + asp.loadWorld(finalWorld, true); + mineWorld = Bukkit.getWorld(MINE_WORLD_NAME); + + if (mineWorld != null) { + mineWorld.setGameRule(GameRule.DO_MOB_SPAWNING, false); + mineWorld.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false); + mineWorld.setGameRule(GameRule.DO_WEATHER_CYCLE, false); + mineWorld.setTime(6000); + plugin.getLogger().info("Mine world loaded successfully."); + } + }); + } catch (Exception e) { + e.printStackTrace(); + plugin.getLogger().severe("Failed to load mine world."); + } + }); + } + + public static World getWorld() { + return mineWorld; + } + + public static Location getSpawnLocation() { + if (mineWorld == null) return null; + int startY = mineWorld.getMaxHeight() - 10; + return new Location(mineWorld, 5, startY + 1, 5); + } + + /** + * Deletes the current mine world and creates a fresh empty one. + * Calls the callback with the new World once it's ready. + * Must be called from the main thread with all players already teleported out. + */ + public static void recreateWorld(Consumer callback) { + AdvancedSlimePaperAPI asp = AdvancedSlimePaperAPI.instance(); + IslandLoader loader = new IslandLoader(); + + plugin.getLogger().info("[MineWorld] Unloading old mine world..."); + + // Unload the old world (no save - we're deleting it anyway) + if (mineWorld != null) { + Bukkit.unloadWorld(mineWorld, false); + mineWorld = null; + } + + // Delete and recreate async + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + // Delete old world data from the loader/DB + if (loader.worldExists(MINE_WORLD_NAME)) { + loader.deleteWorld(MINE_WORLD_NAME); + plugin.getLogger().info("[MineWorld] Old mine world deleted."); + } + + // Create fresh empty world + SlimePropertyMap props = new SlimePropertyMap(); + props.setValue(SlimeProperties.ENVIRONMENT, "NORMAL"); + + Island island = new Island("mine", MINE_WORLD_NAME, "Mine World", "Shared mine world", "server", "system", null); + island.save(); + + SlimeWorld slimeWorld = asp.createEmptyWorld(MINE_WORLD_NAME, false, props, loader); + + // Load the new world on the main thread + Bukkit.getScheduler().runTask(plugin, () -> { + asp.loadWorld(slimeWorld, true); + mineWorld = Bukkit.getWorld(MINE_WORLD_NAME); + + if (mineWorld != null) { + mineWorld.setGameRule(GameRule.DO_MOB_SPAWNING, false); + mineWorld.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false); + mineWorld.setGameRule(GameRule.DO_WEATHER_CYCLE, false); + mineWorld.setTime(6000); + plugin.getLogger().info("[MineWorld] Fresh mine world created and loaded."); + callback.accept(mineWorld); + } else { + plugin.getLogger().severe("[MineWorld] Failed to load fresh mine world."); + } + }); + } catch (Exception e) { + e.printStackTrace(); + plugin.getLogger().severe("[MineWorld] Failed to recreate mine world."); + } + }); + } + + private boolean isInMineWorld(Player player) { + return player.getWorld().equals(mineWorld); + } + + private boolean isInMineWorld(World world) { + return world.equals(mineWorld); + } + + // Apply invisible mining fatigue when entering mine world + @EventHandler + public void onWorldChange(PlayerChangedWorldEvent event) { + Player player = event.getPlayer(); + + // Entering mine world + if (isInMineWorld(player)) { + applyMiningFatigue(player); + } + + // Leaving mine world + if (isInMineWorld(event.getFrom())) { + player.removePotionEffect(PotionEffectType.MINING_FATIGUE); + } + } + + // Re-apply fatigue on join if player is in mine world + @EventHandler + public void onJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + if (isInMineWorld(player)) { + applyMiningFatigue(player); + } + } + + // Re-apply fatigue on respawn in mine world + @EventHandler + public void onDeath(PlayerDeathEvent event) { + Bukkit.getScheduler().runTaskLater(plugin, () -> { + Player player = event.getPlayer(); + if (isInMineWorld(player)) { + applyMiningFatigue(player); + } + }, 1L); + } + + private void applyMiningFatigue(Player player) { + player.addPotionEffect(new PotionEffect( + PotionEffectType.MINING_FATIGUE, + Integer.MAX_VALUE, + 0, // level I (amplifier 0) + true, // ambient + false, // no particles + false // no icon + )); + } + + // Double tool durability damage in mine world + @EventHandler + public void onItemDamage(PlayerItemDamageEvent event) { + if (!isInMineWorld(event.getPlayer())) return; + if (event.getItem().getType().getMaxDurability() <= 0) return; + event.setDamage(event.getDamage() * 2); + } + + // Prevent entity explosions (creepers, tnt, etc.) from destroying polished blackstone + @EventHandler + public void onEntityExplode(EntityExplodeEvent event) { + if (!isInMineWorld(event.getEntity().getWorld())) return; + event.blockList().removeIf(block -> block.getType() == Material.POLISHED_BLACKSTONE); + } + + // Prevent block explosions (beds, respawn anchors, etc.) from destroying polished blackstone + @EventHandler + public void onBlockExplode(BlockExplodeEvent event) { + if (!isInMineWorld(event.getBlock().getWorld())) return; + event.blockList().removeIf(block -> block.getType() == Material.POLISHED_BLACKSTONE); + } + + // Block break logic: prevent polished blackstone, handle mining sequence + @EventHandler + public void onBlockBreak(BlockBreakEvent event) { + if (!isInMineWorld(event.getPlayer())) return; + + Material type = event.getBlock().getType(); + + // Polished blackstone is unbreakable (mine walls) + if (type == Material.POLISHED_BLACKSTONE) { + event.setCancelled(true); + return; + } + + // Mining sequence: each block degrades to the next tier before breaking + Material nextTier = getNextTier(type); + if (nextTier != null) { + event.setCancelled(true); + event.getBlock().setType(nextTier); + event.getBlock().getState().update(true); + + // Damage the tool manually since we cancelled the event (only if the tool has durability) + ItemStack tool = event.getPlayer().getInventory().getItemInMainHand(); + if (!tool.getType().isAir() && tool.getType().getMaxDurability() > 0 && tool.getItemMeta() instanceof Damageable damageable) { + damageable.setDamage(damageable.getDamage() + 2); // doubled durability + tool.setItemMeta(damageable); + + // Break tool if fully damaged + if (damageable.getDamage() >= tool.getType().getMaxDurability()) { + event.getPlayer().getInventory().setItemInMainHand(null); + event.getPlayer().playSound(event.getPlayer().getLocation(), Sound.ENTITY_ITEM_BREAK, 1.0f, 1.0f); + } + } + } + // Stone breaks normally (event not cancelled) + } + + private Material getNextTier(Material material) { + return switch (material) { + case BLACKSTONE -> Material.SMOOTH_BASALT; + case SMOOTH_BASALT -> Material.DEEPSLATE; + case DEEPSLATE -> Material.TUFF; + case TUFF -> Material.STONE; + default -> null; // Stone and ores break normally + }; + } +} + + diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 36219f0..14d8aae 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -65,4 +65,11 @@ gui: title: "Obchod" buy: "Koupit ks za $" sell: "Prodat ks za $" +mine: + regenerating: "Regenerace dolů začala..." + regenerate-complete: "Regenerace dolů dokončena." + teleporting: "Teleportuješ se do dolů." + teleported-out: "Byl jsi teleportován z dolů kvůli regeneraci." + error: + no-world: "Důlní svět není načtený." From 06dcad47abb42c16b80021645e4f1b859597b24b Mon Sep 17 00:00:00 2001 From: erikradovan Date: Sun, 29 Mar 2026 14:43:29 +0200 Subject: [PATCH 2/2] added mine managers --- .../ecoCraftCore/mines/MineManager.java | 561 ++++++++++++------ .../ecoCraftCore/mines/MineWorldManager.java | 87 ++- 2 files changed, 452 insertions(+), 196 deletions(-) diff --git a/src/main/java/xyz/soukup/ecoCraftCore/mines/MineManager.java b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineManager.java index 2fe15e4..614575d 100644 --- a/src/main/java/xyz/soukup/ecoCraftCore/mines/MineManager.java +++ b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineManager.java @@ -3,6 +3,7 @@ package xyz.soukup.ecoCraftCore.mines; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.World; +import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import java.util.*; @@ -12,21 +13,14 @@ import static xyz.soukup.ecoCraftCore.EcoCraftCore.plugin; public class MineManager { private static final int TUNNEL_RADIUS = 5; // 11x11 = radius 5 from center - private static final int MIN_BRANCH_SIZE = 15; - private static final int FIRST_BRANCH_MIN_SIZE = 12; - private static final int FIRST_BRANCH_SPLIT_DELAY = 12; // first branch: no split chance until after 12 blocks - private static final int MAX_BRANCH_SIZE = 50; - private static final double BASE_SPLIT_INCREMENT = 0.01; - private static final double SPLIT_DECAY_PER_BRANCH = 0.000125; + private static final int MIN_BRANCH_SIZE = 30; + private static final int FIRST_BRANCH_MIN_SIZE = 20; + private static final int FIRST_BRANCH_SPLIT_DELAY = 12; + private static final int MAX_BRANCH_SIZE = 80; + private static final double BASE_SPLIT_INCREMENT = 0.004; + private static final double SPLIT_DECAY_PER_BRANCH = 0.0004; private static final int BLOCKS_PER_TICK = 800; - // Biome boundaries (distance thresholds) - private static final int STONE_END = 40; - private static final int TUFF_END = 80; - private static final int DEEPSLATE_END = 120; - private static final int BASALT_END = 160; - // Blackstone: 160+ - // Transition zone: how many blocks before/after a border we blend private static final int TRANSITION_RANGE = 8; @@ -36,48 +30,123 @@ public class MineManager { BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST, BlockFace.DOWN, BlockFace.UP }; - // --- Biome ore tables --- + // --- Biome definitions --- - private record OreEntry(Material material, double chance) {} + public enum MineBiome { + NORMAL, DEEP, SUPER_DEEP + } - private static final OreEntry[] STONE_ORES = { - new OreEntry(Material.GRAVEL, 0.08), - new OreEntry(Material.COAL_ORE, 0.05), - new OreEntry(Material.COPPER_ORE, 0.04), - }; + // Fill progression per biome + private static final Material[] NORMAL_FILLS = {Material.STONE, Material.ANDESITE, Material.COBBLESTONE, Material.TUFF}; + private static final int[] NORMAL_THRESHOLDS = {0, 30, 60, 90}; - private static final OreEntry[] TUFF_ORES = { - new OreEntry(Material.IRON_ORE, 0.04), - new OreEntry(Material.GOLD_ORE, 0.025), - new OreEntry(Material.LAPIS_ORE, 0.02), - }; + private static final Material[] DEEP_FILLS = {Material.DEEPSLATE, Material.COBBLED_DEEPSLATE, Material.CRACKED_DEEPSLATE_TILES}; + private static final int[] DEEP_THRESHOLDS = {0, 40, 80}; - private static final OreEntry[] DEEPSLATE_ORES = { - new OreEntry(Material.DEEPSLATE_DIAMOND_ORE, 0.03), - new OreEntry(Material.DEEPSLATE_EMERALD_ORE, 0.015), - new OreEntry(Material.DEEPSLATE_REDSTONE_ORE, 0.02), - }; + private static final Material[] SUPER_DEEP_FILLS = {Material.SMOOTH_BASALT, Material.BLACKSTONE, Material.CRACKED_POLISHED_BLACKSTONE_BRICKS, Material.GILDED_BLACKSTONE}; + private static final int[] SUPER_DEEP_THRESHOLDS = {0, 40, 80, 120}; - private static final OreEntry[] BASALT_ORES = { - new OreEntry(Material.RAW_IRON_BLOCK, 0.015), - new OreEntry(Material.RAW_COPPER_BLOCK, 0.015), - }; + // Special inline blocks (placed during generation, not ores) + private static final double DIRT_CHANCE = 0.04; + private static final double GRAVEL_CHANCE = 0.05; + private static final double INFESTED_CHANCE = 0.008; + private static final double MUD_CHANCE_SUPER_DEEP = 0.03; + private static final double LAVA_CHANCE_SUPER_DEEP = 0.05; - private static final OreEntry[] BLACKSTONE_ORES = { - new OreEntry(Material.ANCIENT_DEBRIS, 0.003), - new OreEntry(Material.RAW_GOLD_BLOCK, 0.01), - }; + // Deep biome lava: starts low and increases through the fills + private static final double LAVA_CHANCE_DEEP_MIN = 0.005; + private static final double LAVA_CHANCE_DEEP_MAX = 0.04; + + // --- Ore tables per fill type (rolled on break) --- + + public record OreEntry(Material material, double weight) {} + + private static final Map ORE_TABLES = new LinkedHashMap<>(); + + static { + ORE_TABLES.put(Material.STONE, new OreEntry[]{ + new OreEntry(Material.COPPER_ORE, 1.0), + }); + ORE_TABLES.put(Material.ANDESITE, new OreEntry[]{ + new OreEntry(Material.COAL_ORE, 1.0), + }); + ORE_TABLES.put(Material.COBBLESTONE, new OreEntry[]{ + new OreEntry(Material.IRON_ORE, 1.0), + }); + ORE_TABLES.put(Material.TUFF, new OreEntry[]{ + new OreEntry(Material.GOLD_ORE, 1.0), + }); + ORE_TABLES.put(Material.DEEPSLATE, new OreEntry[]{ + new OreEntry(Material.DEEPSLATE_EMERALD_ORE, 0.4), + new OreEntry(Material.DEEPSLATE_LAPIS_ORE, 0.6), + }); + ORE_TABLES.put(Material.COBBLED_DEEPSLATE, new OreEntry[]{ + new OreEntry(Material.DEEPSLATE_REDSTONE_ORE, 0.6), + new OreEntry(Material.RAW_COPPER_BLOCK, 0.4), + }); + ORE_TABLES.put(Material.CRACKED_DEEPSLATE_TILES, new OreEntry[]{ + new OreEntry(Material.DEEPSLATE_DIAMOND_ORE, 0.6), + new OreEntry(Material.RAW_IRON_BLOCK, 0.4), + }); + ORE_TABLES.put(Material.SMOOTH_BASALT, new OreEntry[]{ + new OreEntry(Material.COAL_BLOCK, 1.0), + }); + ORE_TABLES.put(Material.BLACKSTONE, new OreEntry[]{ + new OreEntry(Material.RAW_GOLD_BLOCK, 1.0), + }); + ORE_TABLES.put(Material.CRACKED_POLISHED_BLACKSTONE_BRICKS, new OreEntry[]{ + new OreEntry(Material.ANCIENT_DEBRIS, 1.0), + }); + ORE_TABLES.put(Material.GILDED_BLACKSTONE, new OreEntry[]{ + new OreEntry(Material.GOLD_BLOCK, 1.0), + }); + } + + // Ore chance depending on how far this fill is from the detected zone + // Index 0 = current fill, 1 = previous fill (one step lighter), 2 = three fills back, 3+ = any other + private static final double ORE_CHANCE_CURRENT = 0.05; + private static final double ORE_CHANCE_PREVIOUS = 0.10; + private static final double ORE_CHANCE_THREE_BACK = 0.02; + private static final double ORE_CHANCE_OTHER = 0.01; + + // Ordered list of ALL fills from shallowest to deepest (used for distance calculations) + private static final List ALL_FILLS_ORDERED = new ArrayList<>(); + + static { + Collections.addAll(ALL_FILLS_ORDERED, NORMAL_FILLS); + Collections.addAll(ALL_FILLS_ORDERED, DEEP_FILLS); + Collections.addAll(ALL_FILLS_ORDERED, SUPER_DEEP_FILLS); + } + + // --- All known fill materials (for checking in MineWorldManager) --- + + private static final Set ALL_FILL_MATERIALS = new HashSet<>(ALL_FILLS_ORDERED); + + public static boolean isFillMaterial(Material material) { + return ALL_FILL_MATERIALS.contains(material); + } + + // --- Biome Y tracking --- + + private static int normalStartY = 0; + private static int deepStartY = 0; + private static int superDeepStartY = 0; + + // --- Branch data class --- - // Branch data class private static class Branch { final BlockFace direction; final int startX, startY, startZ; final int distanceFromStart; final int maxSize; final boolean isFirst; + final MineBiome biome; double chanceToSplit; - Branch(BlockFace direction, int startX, int startY, int startZ, int distanceFromStart, int maxSize, boolean isFirst) { + int endX, endY, endZ; + + Branch(BlockFace direction, int startX, int startY, int startZ, + int distanceFromStart, int maxSize, boolean isFirst, MineBiome biome) { this.direction = direction; this.startX = startX; this.startY = startY; @@ -85,7 +154,11 @@ public class MineManager { this.distanceFromStart = distanceFromStart; this.maxSize = maxSize; this.isFirst = isFirst; + this.biome = biome; this.chanceToSplit = 0; + this.endX = startX; + this.endY = startY; + this.endZ = startZ; } } @@ -101,63 +174,105 @@ public class MineManager { } } + // ===================== MAIN ENTRY POINT ===================== + public static void regenerate(World world) { int startY = world.getMaxHeight() - 11; int minY = world.getMinHeight() + 5; int maxY = world.getMaxHeight(); int worldMinY = world.getMinHeight(); - // Run the heavy data generation async, then schedule block placement on main thread + normalStartY = startY; + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Set fillPositions = new HashSet<>(); List fillPlacements = new ArrayList<>(); - // Phase 1: Generate tunnel fill data - generateBranches(fillPlacements, fillPositions, startY, minY); - - plugin.getLogger().info("Mine generation phase 1 done: " + fillPlacements.size() + " fill blocks, " + fillPositions.size() + " unique positions."); - - // Phase 2: Generate wall data + // ===== Biome 1: NORMAL ===== + plugin.getLogger().info("[MineGen] === Starting NORMAL biome generation ==="); + List normalBranches = new ArrayList<>(); + int[] branchCount = {0}; + generateBiomeBranches(MineBiome.NORMAL, 0, startY, 0, startY, minY, + fillPlacements, fillPositions, normalBranches, branchCount); + plugin.getLogger().info("[MineGen] NORMAL done: " + fillPlacements.size() + " fill blocks, branches=" + branchCount[0]); + + // Find lowest Y from NORMAL + Branch lowestNormal = findLowestBranch(normalBranches); + int deepX = lowestNormal != null ? lowestNormal.endX : 0; + int deepY = lowestNormal != null ? lowestNormal.endY : startY - 100; + int deepZ = lowestNormal != null ? lowestNormal.endZ : 0; + deepStartY = deepY; + + // ===== Biome 2: DEEP ===== + plugin.getLogger().info("[MineGen] === Starting DEEP biome generation at Y=" + deepY + " ==="); + List deepBranches = new ArrayList<>(); + generateBiomeBranches(MineBiome.DEEP, deepX, deepY, deepZ, deepY, minY, + fillPlacements, fillPositions, deepBranches, branchCount); + plugin.getLogger().info("[MineGen] DEEP done: " + fillPlacements.size() + " total fill blocks, branches=" + branchCount[0]); + + // Find lowest Y from DEEP + Branch lowestDeep = findLowestBranch(deepBranches); + int sdX = lowestDeep != null ? lowestDeep.endX : deepX; + int sdY = lowestDeep != null ? lowestDeep.endY : deepY - 100; + int sdZ = lowestDeep != null ? lowestDeep.endZ : deepZ; + superDeepStartY = sdY; + + // ===== Biome 3: SUPER_DEEP ===== + plugin.getLogger().info("[MineGen] === Starting SUPER_DEEP biome generation at Y=" + sdY + " ==="); + List superDeepBranches = new ArrayList<>(); + generateBiomeBranches(MineBiome.SUPER_DEEP, sdX, sdY, sdZ, sdY, minY, + fillPlacements, fillPositions, superDeepBranches, branchCount); + plugin.getLogger().info("[MineGen] SUPER_DEEP done: " + fillPlacements.size() + " total fill blocks, branches=" + branchCount[0]); + + // ===== Walls ===== List wallPlacements = new ArrayList<>(); generateWalls(fillPositions, wallPlacements, startY); + plugin.getLogger().info("[MineGen] Walls done: " + wallPlacements.size() + " bedrock blocks."); - plugin.getLogger().info("Mine generation phase 2 done: " + wallPlacements.size() + " wall blocks."); - - // Phase 3: Schedule block placement on main thread in batches + // ===== Schedule placement on main thread ===== Bukkit.getScheduler().runTask(plugin, () -> scheduleBlockPlacements(world, fillPlacements, wallPlacements, startY, worldMinY, maxY)); }); } - private static void generateBranches(List placements, Set fillPositions, int startY, int minY) { + // ===================== BIOME BRANCH GENERATION ===================== + + private static void generateBiomeBranches(MineBiome biome, int startX, int startY, int startZ, + int ceilingY, int minY, + List placements, Set fillPositions, + List completedBranches, int[] branchCount) { Deque queue = new ArrayDeque<>(); - // Shared mutable counter: [0] = total branches ever created - int[] branchCount = {0}; + + // Reset branch count for this biome so split chance starts fresh + int[] biomeBranchCount = {0}; int firstSize = Math.max(FIRST_BRANCH_MIN_SIZE, MIN_BRANCH_SIZE + random.nextInt(MAX_BRANCH_SIZE - MIN_BRANCH_SIZE)); - Branch initial = new Branch(BlockFace.DOWN, 0, startY, 0, 0, firstSize, true); + Branch initial = new Branch(BlockFace.DOWN, startX, startY, startZ, 0, firstSize, true, biome); queue.add(initial); + biomeBranchCount[0]++; branchCount[0]++; - plugin.getLogger().info("[MineGen] Starting generation at Y=" + startY + ", minY=" + minY); + plugin.getLogger().info("[MineGen] [" + biome + "] Starting at (" + startX + "," + startY + "," + startZ + "), minY=" + minY); while (!queue.isEmpty()) { Branch branch = queue.pollFirst(); - double currentIncrement = Math.max(0, BASE_SPLIT_INCREMENT - (branchCount[0] * SPLIT_DECAY_PER_BRANCH)); - plugin.getLogger().info("[MineGen] Processing branch #" + branchCount[0] - + " dir=" + branch.direction + " start=(" + branch.startX + "," + branch.startY + "," + branch.startZ + ")" + double currentIncrement = Math.max(0, BASE_SPLIT_INCREMENT - (biomeBranchCount[0] * SPLIT_DECAY_PER_BRANCH)); + plugin.getLogger().info("[MineGen] [" + biome + "] Processing branch dir=" + branch.direction + + " start=(" + branch.startX + "," + branch.startY + "," + branch.startZ + ")" + " dist=" + branch.distanceFromStart + " maxSize=" + branch.maxSize + " first=" + branch.isFirst + " queued=" + queue.size() + " fillBlocks=" + fillPositions.size() + " splitIncrement=" + String.format("%.5f", currentIncrement)); - generateSingleBranch(branch, queue, placements, fillPositions, startY, minY, branchCount); + + generateSingleBranch(branch, queue, placements, fillPositions, ceilingY, minY, biomeBranchCount); + completedBranches.add(branch); } - plugin.getLogger().info("[MineGen] Branch generation complete. Total branches=" + branchCount[0] + " fillBlocks=" + fillPositions.size() + " placements=" + placements.size()); + plugin.getLogger().info("[MineGen] [" + biome + "] Complete. Branches=" + completedBranches.size()); } private static void generateSingleBranch(Branch branch, Deque queue, List placements, - Set fillPositions, int startY, int minY, int[] branchCount) { + Set fillPositions, int ceilingY, int minY, int[] branchCount) { int cx = branch.startX; int cy = branch.startY; int cz = branch.startZ; @@ -165,27 +280,28 @@ public class MineManager { for (int step = 0; step < branch.maxSize; step++) { int distance = branch.distanceFromStart + step; - // Generate the 11x11 cross section at this position - generateCrossSection(cx, cy, cz, branch.direction, distance, placements, fillPositions); + generateCrossSection(cx, cy, cz, branch.direction, distance, branch.biome, placements, fillPositions); + + branch.endX = cx; + branch.endY = cy; + branch.endZ = cz; // First branch: skip split logic for the first 12 blocks if (branch.isFirst && step < FIRST_BRANCH_SPLIT_DELAY) { cx += branch.direction.getModX(); cy += branch.direction.getModY(); cz += branch.direction.getModZ(); - if (cy < minY || cy > startY + 5) break; + if (cy < minY || cy > ceilingY + 5) break; continue; } - // Calculate the current split increment based on how many branches exist double splitIncrement = Math.max(0, BASE_SPLIT_INCREMENT - (branchCount[0] * SPLIT_DECAY_PER_BRANCH)); - // If increment has reached 0, no more splitting is possible — skip the roll entirely if (splitIncrement <= 0) { cx += branch.direction.getModX(); cy += branch.direction.getModY(); cz += branch.direction.getModZ(); - if (cy < minY || cy > startY + 5) break; + if (cy < minY || cy > ceilingY + 5) break; continue; } @@ -204,34 +320,36 @@ public class MineManager { int branchMaxSize = MIN_BRANCH_SIZE + random.nextInt(MAX_BRANCH_SIZE - MIN_BRANCH_SIZE); - // Skip UP if it would go above startY - if (dir == BlockFace.UP && cy + branchMaxSize > startY) { - plugin.getLogger().info("[MineGen] Skipped UP branch at Y=" + cy + " (would exceed startY=" + startY + ")"); - continue; - } - - // Skip DOWN if it would go below minY - if (dir == BlockFace.DOWN && cy - branchMaxSize < minY) { - plugin.getLogger().info("[MineGen] Skipped DOWN branch at Y=" + cy + " (would go below minY=" + minY + ")"); - continue; - } + if (dir == BlockFace.UP && cy + branchMaxSize > ceilingY) continue; + if (dir == BlockFace.DOWN && cy - branchMaxSize < minY) continue; - queue.add(new Branch(dir, cx, cy, cz, distance, branchMaxSize, false)); + queue.add(new Branch(dir, cx, cy, cz, distance, branchMaxSize, false, branch.biome)); branchCount[0]++; created++; } - plugin.getLogger().info("[MineGen] Split at step=" + step + " pos=(" + cx + "," + cy + "," + cz + ") created=" + created + " branches, total=" + branchCount[0] + " splitInc=" + String.format("%.5f", splitIncrement)); + plugin.getLogger().info("[MineGen] [" + branch.biome + "] Split at step=" + step + + " pos=(" + cx + "," + cy + "," + cz + ") created=" + created + + " total=" + branchCount[0]); } - // Advance position along the branch direction cx += branch.direction.getModX(); cy += branch.direction.getModY(); cz += branch.direction.getModZ(); - if (cy < minY || cy > startY + 5) break; + if (cy < minY || cy > ceilingY + 5) break; } } + private static Branch findLowestBranch(List branches) { + Branch lowest = null; + for (Branch b : branches) { + if (lowest == null || b.endY < lowest.endY) { + lowest = b; + } + } + return lowest; + } + private static List getAvailableDirections(BlockFace current) { List dirs = new ArrayList<>(); BlockFace opposite = current.getOppositeFace(); @@ -243,8 +361,10 @@ public class MineManager { return dirs; } + // ===================== CROSS SECTION GENERATION ===================== + private static void generateCrossSection(int cx, int cy, int cz, BlockFace direction, int distance, - List placements, Set fillPositions) { + MineBiome biome, List placements, Set fillPositions) { int[][] offsets = getPerpOffsets(direction); for (int a = -TUNNEL_RADIUS; a <= TUNNEL_RADIUS; a++) { @@ -256,117 +376,209 @@ public class MineManager { long key = posKey(bx, by, bz); if (!fillPositions.add(key)) continue; - Material mat = pickBlockForDistance(distance); + Material mat = pickFillForDistance(distance, biome); placements.add(new BlockPlacement(bx, by, bz, mat)); } } } - // --- Biome fill + ore + transition logic --- - - private static Material pickBlockForDistance(int distance) { - // Determine primary biome and check if we're in a transition zone - Material primaryFill = getPrimaryFill(distance); - OreEntry[] primaryOres = getOreTable(primaryFill); + // ===================== FILL SELECTION (no ores during generation) ===================== - // Check transition: are we near a biome border? - int[] border = getNearestBorder(distance); - // border[0] = border distance threshold, border[1] = signed distance to border (negative = before, positive = after) + private static Material pickFillForDistance(int distance, MineBiome biome) { + Material[] fills = getFillsForBiome(biome); + int[] thresholds = getThresholdsForBiome(biome); - if (border != null && Math.abs(border[1]) <= TRANSITION_RANGE) { - // We're in a transition zone — blend with the neighboring biome - Material neighborFill = (border[1] <= 0) - ? getNextFill(primaryFill) // approaching next biome - : getPrevFill(primaryFill); // just crossed into this biome from previous - OreEntry[] neighborOres = getOreTable(neighborFill); - - // Blend ratio: 0.0 at edge of transition, 0.5 at the border itself - double blendRatio = 0.5 * (1.0 - (double) Math.abs(border[1]) / TRANSITION_RANGE); + // Determine primary fill + Material primaryFill = fills[0]; + for (int i = thresholds.length - 1; i >= 0; i--) { + if (distance >= thresholds[i]) { + primaryFill = fills[i]; + break; + } + } - // Roll for neighbor fill block + // Check transition zone + Material transitionFill = getTransitionFill(distance, fills, thresholds); + if (transitionFill != null && transitionFill != primaryFill) { + int borderDist = getDistanceToBorder(distance, thresholds); + double blendRatio = 0.5 * (1.0 - (double) Math.abs(borderDist) / TRANSITION_RANGE); if (random.nextDouble() < blendRatio) { - // Use neighbor biome's fill + ores - Material ore = rollOreTable(neighborOres); - return (ore != null) ? ore : neighborFill; + primaryFill = transitionFill; } } - // Primary biome: roll for ore - Material ore = rollOreTable(primaryOres); - return (ore != null) ? ore : primaryFill; + // Roll for special inline blocks (dirt, gravel, lava, mud, infested) + Material special = rollSpecialBlock(biome, primaryFill, distance); + if (special != null) return special; + + return primaryFill; } - private static Material rollOreTable(OreEntry[] table) { + private static Material rollSpecialBlock(MineBiome biome, Material fill, int distance) { double roll = random.nextDouble(); - double cumulative = 0; - for (OreEntry entry : table) { - cumulative += entry.chance; - if (roll < cumulative) return entry.material; + switch (biome) { + case NORMAL: + if (roll < DIRT_CHANCE) return Material.DIRT; + roll -= DIRT_CHANCE; + if (roll < GRAVEL_CHANCE) return Material.GRAVEL; + roll -= GRAVEL_CHANCE; + // Rare infested variants + if (roll < INFESTED_CHANCE) { + return switch (fill) { + case STONE -> Material.INFESTED_STONE; + case COBBLESTONE -> Material.INFESTED_COBBLESTONE; + default -> null; // andesite, tuff don't have infested variants + }; + } + break; + case DEEP: + // Lava in ALL deep fills, chance increases with distance through the biome + // Deep thresholds max out at 80+ (cracked_deepslate_tiles) + // Total deep distance range is roughly 0-120 + double deepProgress = Math.min(1.0, distance / 120.0); + double lavaChance = LAVA_CHANCE_DEEP_MIN + (LAVA_CHANCE_DEEP_MAX - LAVA_CHANCE_DEEP_MIN) * deepProgress; + if (roll < lavaChance) return Material.LAVA; + break; + case SUPER_DEEP: + if (roll < MUD_CHANCE_SUPER_DEEP) return Material.MUD; + roll -= MUD_CHANCE_SUPER_DEEP; + if (roll < LAVA_CHANCE_SUPER_DEEP) return Material.LAVA; + break; } return null; } - private static Material getPrimaryFill(int distance) { - if (distance < STONE_END) return Material.STONE; - if (distance < TUFF_END) return Material.TUFF; - if (distance < DEEPSLATE_END) return Material.DEEPSLATE; - if (distance < BASALT_END) return Material.SMOOTH_BASALT; - return Material.BLACKSTONE; + private static Material getTransitionFill(int distance, Material[] fills, int[] thresholds) { + for (int i = 1; i < thresholds.length; i++) { + int border = thresholds[i]; + int diff = distance - border; + if (Math.abs(diff) <= TRANSITION_RANGE) { + return (diff < 0) ? fills[i] : fills[i - 1]; + } + } + return null; } - private static OreEntry[] getOreTable(Material fill) { - return switch (fill) { - case STONE -> STONE_ORES; - case TUFF -> TUFF_ORES; - case DEEPSLATE -> DEEPSLATE_ORES; - case SMOOTH_BASALT -> BASALT_ORES; - case BLACKSTONE -> BLACKSTONE_ORES; - default -> STONE_ORES; - }; + private static int getDistanceToBorder(int distance, int[] thresholds) { + int closest = Integer.MAX_VALUE; + for (int i = 1; i < thresholds.length; i++) { + int diff = distance - thresholds[i]; + if (Math.abs(diff) < Math.abs(closest)) { + closest = diff; + } + } + return closest; } - private static Material getNextFill(Material fill) { - return switch (fill) { - case STONE -> Material.TUFF; - case TUFF -> Material.DEEPSLATE; - case DEEPSLATE -> Material.SMOOTH_BASALT; - case SMOOTH_BASALT -> Material.BLACKSTONE; - default -> Material.BLACKSTONE; // blackstone has no next + private static Material[] getFillsForBiome(MineBiome biome) { + return switch (biome) { + case NORMAL -> NORMAL_FILLS; + case DEEP -> DEEP_FILLS; + case SUPER_DEEP -> SUPER_DEEP_FILLS; }; } - private static Material getPrevFill(Material fill) { - return switch (fill) { - case TUFF -> Material.STONE; - case DEEPSLATE -> Material.TUFF; - case SMOOTH_BASALT -> Material.DEEPSLATE; - case BLACKSTONE -> Material.SMOOTH_BASALT; - default -> Material.STONE; // stone has no prev + private static int[] getThresholdsForBiome(MineBiome biome) { + return switch (biome) { + case NORMAL -> NORMAL_THRESHOLDS; + case DEEP -> DEEP_THRESHOLDS; + case SUPER_DEEP -> SUPER_DEEP_THRESHOLDS; }; } + // ===================== ORE-ON-BREAK SYSTEM (called from MineWorldManager) ===================== + + /** + * Detects the deepest fill material among the 6 surrounding blocks to determine + * which fill zone the player is in. Then rolls for an ore based on the broken block's + * position in the fill sequence relative to the detected zone. + * + * Ore chances: + * 5% for ores from the current (detected) fill + * 10% for ores from the previous fill (one step lighter) + * 2% for ores from three fills back + * 1% for any other fill's ores + * + * Returns null if no ore was rolled. + */ + public static Material rollOreOnBreak(Block brokenBlock, Material brokenType) { + // Detect the deepest fill in the 6 surrounding blocks + Material detectedFill = detectDeepestSurroundingFill(brokenBlock); + if (detectedFill == null) { + // Fallback: use the broken block itself if it's a fill + detectedFill = isFillMaterial(brokenType) ? brokenType : Material.STONE; + } + + int detectedIndex = ALL_FILLS_ORDERED.indexOf(detectedFill); + int brokenIndex = ALL_FILLS_ORDERED.indexOf(brokenType); + + // If the broken block isn't a fill material, no ore roll + if (brokenIndex < 0) return null; + + // How many steps back from the detected fill is this broken block? + // detectedIndex is the deepest (highest index), brokenIndex is where we are in the sequence + int stepsBack = detectedIndex - brokenIndex; + + // Determine ore chance based on distance from detected fill + double chance; + if (stepsBack == 0) { + chance = ORE_CHANCE_CURRENT; // 5% - breaking the detected fill itself + } else if (stepsBack == 1) { + chance = ORE_CHANCE_PREVIOUS; // 10% - one step lighter + } else if (stepsBack == 2) { + chance = ORE_CHANCE_THREE_BACK; // 2% - two steps lighter + } else { + chance = ORE_CHANCE_OTHER; // 1% - anything else + } + + // Roll for ore + if (random.nextDouble() >= chance) return null; + + // Pick a random ore from the broken block's ore table + OreEntry[] oreTable = ORE_TABLES.get(brokenType); + if (oreTable == null || oreTable.length == 0) return null; + + return pickWeightedOre(oreTable); + } + /** - * Returns the nearest biome border info, or null if not near any. - * Result: [0] = border distance, [1] = signed distance to border (negative = before border, positive = after) + * Looks at the 6 blocks surrounding the broken block and finds the deepest fill material. */ - private static int[] getNearestBorder(int distance) { - int[] borders = {STONE_END, TUFF_END, DEEPSLATE_END, BASALT_END}; - int closestDist = Integer.MAX_VALUE; - int closestBorder = -1; - - for (int b : borders) { - int diff = distance - b; - if (Math.abs(diff) < Math.abs(closestDist)) { - closestDist = diff; - closestBorder = b; + private static Material detectDeepestSurroundingFill(Block block) { + Material deepest = null; + int deepestIndex = -1; + + for (BlockFace face : DIRECTIONS) { + Block neighbor = block.getRelative(face); + Material type = neighbor.getType(); + + int index = ALL_FILLS_ORDERED.indexOf(type); + if (index > deepestIndex) { + deepestIndex = index; + deepest = type; } } - if (closestBorder == -1 || Math.abs(closestDist) > TRANSITION_RANGE) return null; - return new int[]{closestBorder, closestDist}; + return deepest; + } + + /** + * Picks a random ore from the table using weights. + */ + private static Material pickWeightedOre(OreEntry[] table) { + double totalWeight = 0; + for (OreEntry e : table) totalWeight += e.weight; + + double roll = random.nextDouble() * totalWeight; + double cumulative = 0; + for (OreEntry e : table) { + cumulative += e.weight; + if (roll < cumulative) return e.material; + } + return table[table.length - 1].material; } - // --- Perpendicular axes --- + // ===================== PERPENDICULAR AXES ===================== private static int[][] getPerpOffsets(BlockFace dir) { return switch (dir) { @@ -377,11 +589,11 @@ public class MineManager { }; } - // --- Wall generation --- + // ===================== WALL GENERATION ===================== private static void generateWalls(Set fillPositions, List wallPlacements, int startY) { Set wallPositions = new HashSet<>(); - plugin.getLogger().info("[MineGen] Generating walls for " + fillPositions.size() + " fill positions..."); + plugin.getLogger().info("[MineGen] Generating bedrock walls for " + fillPositions.size() + " fill positions..."); for (long key : fillPositions) { int x = decodeX(key); @@ -399,21 +611,34 @@ public class MineManager { if (ny > startY) continue; if (wallPositions.add(neighborKey)) { - wallPlacements.add(new BlockPlacement(nx, ny, nz, Material.POLISHED_BLACKSTONE)); + wallPlacements.add(new BlockPlacement(nx, ny, nz, Material.BEDROCK)); } } } } } - // --- Block placement scheduling --- + // ===================== BLOCK PLACEMENT SCHEDULING ===================== private static void scheduleBlockPlacements(World world, List fillPlacements, - List wallPlacements, int startY, int minY, int maxY) { - List allPlacements = new ArrayList<>(fillPlacements.size() + wallPlacements.size() + 200); - allPlacements.addAll(fillPlacements); + List wallPlacements, + int startY, int minY, int maxY) { + // Three-phase placement: 1) Dirt placeholder 2) Bedrock walls 3) Actual fill + List allPlacements = new ArrayList<>(); + + // Phase A: Dirt placeholders at all fill positions + List dirtPlaceholders = new ArrayList<>(fillPlacements.size()); + for (BlockPlacement bp : fillPlacements) { + dirtPlaceholders.add(new BlockPlacement(bp.x, bp.y, bp.z, Material.DIRT)); + } + allPlacements.addAll(dirtPlaceholders); + + // Phase B: Bedrock walls allPlacements.addAll(wallPlacements); + // Phase C: Actual fill (overwrites dirt) + allPlacements.addAll(fillPlacements); + // Air blocks above the entrance opening for (int a = -TUNNEL_RADIUS; a <= TUNNEL_RADIUS; a++) { for (int b = -TUNNEL_RADIUS; b <= TUNNEL_RADIUS; b++) { @@ -424,7 +649,9 @@ public class MineManager { int totalBlocks = allPlacements.size(); int tickDelay = 0; - plugin.getLogger().info("[MineGen] Scheduling " + totalBlocks + " block placements (" + fillPlacements.size() + " fill + " + wallPlacements.size() + " walls) in batches of " + BLOCKS_PER_TICK); + plugin.getLogger().info("[MineGen] Scheduling " + totalBlocks + " block placements (" + + dirtPlaceholders.size() + " placeholder + " + wallPlacements.size() + " walls + " + + fillPlacements.size() + " fill) in batches of " + BLOCKS_PER_TICK); for (int i = 0; i < totalBlocks; i += BLOCKS_PER_TICK) { int from = i; @@ -447,7 +674,7 @@ public class MineManager { finalDelay + 2); } - // --- Position encoding/decoding --- + // ===================== POSITION ENCODING/DECODING ===================== private static long posKey(int x, int y, int z) { return ((long) (x + 30000000) & 0x3FFFFFF) diff --git a/src/main/java/xyz/soukup/ecoCraftCore/mines/MineWorldManager.java b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineWorldManager.java index 06a10df..38bb233 100644 --- a/src/main/java/xyz/soukup/ecoCraftCore/mines/MineWorldManager.java +++ b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineWorldManager.java @@ -101,22 +101,18 @@ public class MineWorldManager implements Listener { plugin.getLogger().info("[MineWorld] Unloading old mine world..."); - // Unload the old world (no save - we're deleting it anyway) if (mineWorld != null) { Bukkit.unloadWorld(mineWorld, false); mineWorld = null; } - // Delete and recreate async Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { try { - // Delete old world data from the loader/DB if (loader.worldExists(MINE_WORLD_NAME)) { loader.deleteWorld(MINE_WORLD_NAME); plugin.getLogger().info("[MineWorld] Old mine world deleted."); } - // Create fresh empty world SlimePropertyMap props = new SlimePropertyMap(); props.setValue(SlimeProperties.ENVIRONMENT, "NORMAL"); @@ -125,7 +121,6 @@ public class MineWorldManager implements Listener { SlimeWorld slimeWorld = asp.createEmptyWorld(MINE_WORLD_NAME, false, props, loader); - // Load the new world on the main thread Bukkit.getScheduler().runTask(plugin, () -> { asp.loadWorld(slimeWorld, true); mineWorld = Bukkit.getWorld(MINE_WORLD_NAME); @@ -211,65 +206,99 @@ public class MineWorldManager implements Listener { event.setDamage(event.getDamage() * 2); } - // Prevent entity explosions (creepers, tnt, etc.) from destroying polished blackstone + // Prevent entity explosions from destroying bedrock walls @EventHandler public void onEntityExplode(EntityExplodeEvent event) { if (!isInMineWorld(event.getEntity().getWorld())) return; - event.blockList().removeIf(block -> block.getType() == Material.POLISHED_BLACKSTONE); + event.blockList().removeIf(block -> block.getType() == Material.BEDROCK); } - // Prevent block explosions (beds, respawn anchors, etc.) from destroying polished blackstone + // Prevent block explosions from destroying bedrock walls @EventHandler public void onBlockExplode(BlockExplodeEvent event) { if (!isInMineWorld(event.getBlock().getWorld())) return; - event.blockList().removeIf(block -> block.getType() == Material.POLISHED_BLACKSTONE); + event.blockList().removeIf(block -> block.getType() == Material.BEDROCK); } - // Block break logic: prevent polished blackstone, handle mining sequence + // Block break logic: bedrock unbreakable, ore-on-break for ALL fills, mining sequence @EventHandler public void onBlockBreak(BlockBreakEvent event) { if (!isInMineWorld(event.getPlayer())) return; Material type = event.getBlock().getType(); - // Polished blackstone is unbreakable (mine walls) - if (type == Material.POLISHED_BLACKSTONE) { + // Bedrock is completely unbreakable (mine walls) + if (type == Material.BEDROCK) { event.setCancelled(true); return; } - // Mining sequence: each block degrades to the next tier before breaking + // For any fill material (at any stage of the mining sequence), roll for ore first. + // The ore replaces the block as another mining step — player then breaks the ore normally. + if (MineManager.isFillMaterial(type)) { + Material ore = MineManager.rollOreOnBreak(event.getBlock(), type); + if (ore != null) { + event.setCancelled(true); + event.getBlock().setType(ore); + event.getBlock().getState().update(true); + applyToolDamage(event); + return; + } + } + + // No ore rolled — apply normal mining sequence (deeper fills degrade into lighter fills) Material nextTier = getNextTier(type); if (nextTier != null) { event.setCancelled(true); event.getBlock().setType(nextTier); event.getBlock().getState().update(true); + applyToolDamage(event); + return; + } - // Damage the tool manually since we cancelled the event (only if the tool has durability) - ItemStack tool = event.getPlayer().getInventory().getItemInMainHand(); - if (!tool.getType().isAir() && tool.getType().getMaxDurability() > 0 && tool.getItemMeta() instanceof Damageable damageable) { - damageable.setDamage(damageable.getDamage() + 2); // doubled durability - tool.setItemMeta(damageable); + // Terminal fill (stone) or ores — break normally + } - // Break tool if fully damaged - if (damageable.getDamage() >= tool.getType().getMaxDurability()) { - event.getPlayer().getInventory().setItemInMainHand(null); - event.getPlayer().playSound(event.getPlayer().getLocation(), Sound.ENTITY_ITEM_BREAK, 1.0f, 1.0f); - } - } + /** + * Applies doubled durability damage to the player's tool when we cancel a break event. + * Only applies if the held item actually has durability. + */ + private void applyToolDamage(BlockBreakEvent event) { + ItemStack tool = event.getPlayer().getInventory().getItemInMainHand(); + if (tool.getType().isAir() || tool.getType().getMaxDurability() <= 0) return; + if (!(tool.getItemMeta() instanceof Damageable damageable)) return; + + damageable.setDamage(damageable.getDamage() + 2); // doubled durability + tool.setItemMeta(damageable); + + if (damageable.getDamage() >= tool.getType().getMaxDurability()) { + event.getPlayer().getInventory().setItemInMainHand(null); + event.getPlayer().playSound(event.getPlayer().getLocation(), Sound.ENTITY_ITEM_BREAK, 1.0f, 1.0f); } - // Stone breaks normally (event not cancelled) } + /** + * Mining sequence: deeper fills degrade into lighter fills. + * Returns null when the block should break normally (terminal fill = stone). + * + * Chain: gilded_blackstone -> cracked_polished_blackstone_bricks -> blackstone -> smooth_basalt + * -> cracked_deepslate_tiles -> cobbled_deepslate -> deepslate -> tuff -> cobblestone + * -> andesite -> stone -> null (breaks normally) + */ private Material getNextTier(Material material) { return switch (material) { + case GILDED_BLACKSTONE -> Material.CRACKED_POLISHED_BLACKSTONE_BRICKS; + case CRACKED_POLISHED_BLACKSTONE_BRICKS -> Material.BLACKSTONE; case BLACKSTONE -> Material.SMOOTH_BASALT; - case SMOOTH_BASALT -> Material.DEEPSLATE; + case SMOOTH_BASALT -> Material.CRACKED_DEEPSLATE_TILES; + case CRACKED_DEEPSLATE_TILES -> Material.COBBLED_DEEPSLATE; + case COBBLED_DEEPSLATE -> Material.DEEPSLATE; case DEEPSLATE -> Material.TUFF; - case TUFF -> Material.STONE; - default -> null; // Stone and ores break normally + case TUFF -> Material.COBBLESTONE; + case COBBLESTONE -> Material.ANDESITE; + case ANDESITE -> Material.STONE; + default -> null; // Stone and non-fill blocks break normally }; } } -