added mine managers

mines
erikradovan 1 month ago
parent 0dd69c9069
commit 06dcad47ab
  1. 559
      src/main/java/xyz/soukup/ecoCraftCore/mines/MineManager.java
  2. 75
      src/main/java/xyz/soukup/ecoCraftCore/mines/MineWorldManager.java

@ -3,6 +3,7 @@ package xyz.soukup.ecoCraftCore.mines;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace; import org.bukkit.block.BlockFace;
import java.util.*; import java.util.*;
@ -12,21 +13,14 @@ import static xyz.soukup.ecoCraftCore.EcoCraftCore.plugin;
public class MineManager { public class MineManager {
private static final int TUNNEL_RADIUS = 5; // 11x11 = radius 5 from center private static final int TUNNEL_RADIUS = 5; // 11x11 = radius 5 from center
private static final int MIN_BRANCH_SIZE = 15; private static final int MIN_BRANCH_SIZE = 30;
private static final int FIRST_BRANCH_MIN_SIZE = 12; private static final int FIRST_BRANCH_MIN_SIZE = 20;
private static final int FIRST_BRANCH_SPLIT_DELAY = 12; // first branch: no split chance until after 12 blocks private static final int FIRST_BRANCH_SPLIT_DELAY = 12;
private static final int MAX_BRANCH_SIZE = 50; private static final int MAX_BRANCH_SIZE = 80;
private static final double BASE_SPLIT_INCREMENT = 0.01; private static final double BASE_SPLIT_INCREMENT = 0.004;
private static final double SPLIT_DECAY_PER_BRANCH = 0.000125; private static final double SPLIT_DECAY_PER_BRANCH = 0.0004;
private static final int BLOCKS_PER_TICK = 800; 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 // Transition zone: how many blocks before/after a border we blend
private static final int TRANSITION_RANGE = 8; 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 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 = { // Fill progression per biome
new OreEntry(Material.GRAVEL, 0.08), private static final Material[] NORMAL_FILLS = {Material.STONE, Material.ANDESITE, Material.COBBLESTONE, Material.TUFF};
new OreEntry(Material.COAL_ORE, 0.05), private static final int[] NORMAL_THRESHOLDS = {0, 30, 60, 90};
new OreEntry(Material.COPPER_ORE, 0.04),
};
private static final OreEntry[] TUFF_ORES = { private static final Material[] DEEP_FILLS = {Material.DEEPSLATE, Material.COBBLED_DEEPSLATE, Material.CRACKED_DEEPSLATE_TILES};
new OreEntry(Material.IRON_ORE, 0.04), private static final int[] DEEP_THRESHOLDS = {0, 40, 80};
new OreEntry(Material.GOLD_ORE, 0.025),
new OreEntry(Material.LAPIS_ORE, 0.02),
};
private static final OreEntry[] DEEPSLATE_ORES = { private static final Material[] SUPER_DEEP_FILLS = {Material.SMOOTH_BASALT, Material.BLACKSTONE, Material.CRACKED_POLISHED_BLACKSTONE_BRICKS, Material.GILDED_BLACKSTONE};
new OreEntry(Material.DEEPSLATE_DIAMOND_ORE, 0.03), private static final int[] SUPER_DEEP_THRESHOLDS = {0, 40, 80, 120};
new OreEntry(Material.DEEPSLATE_EMERALD_ORE, 0.015),
new OreEntry(Material.DEEPSLATE_REDSTONE_ORE, 0.02),
};
private static final OreEntry[] BASALT_ORES = { // Special inline blocks (placed during generation, not ores)
new OreEntry(Material.RAW_IRON_BLOCK, 0.015), private static final double DIRT_CHANCE = 0.04;
new OreEntry(Material.RAW_COPPER_BLOCK, 0.015), 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 = { // Deep biome lava: starts low and increases through the fills
new OreEntry(Material.ANCIENT_DEBRIS, 0.003), private static final double LAVA_CHANCE_DEEP_MIN = 0.005;
new OreEntry(Material.RAW_GOLD_BLOCK, 0.01), 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<Material, OreEntry[]> 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<Material> 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<Material> 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 { private static class Branch {
final BlockFace direction; final BlockFace direction;
final int startX, startY, startZ; final int startX, startY, startZ;
final int distanceFromStart; final int distanceFromStart;
final int maxSize; final int maxSize;
final boolean isFirst; final boolean isFirst;
final MineBiome biome;
double chanceToSplit; 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.direction = direction;
this.startX = startX; this.startX = startX;
this.startY = startY; this.startY = startY;
@ -85,7 +154,11 @@ public class MineManager {
this.distanceFromStart = distanceFromStart; this.distanceFromStart = distanceFromStart;
this.maxSize = maxSize; this.maxSize = maxSize;
this.isFirst = isFirst; this.isFirst = isFirst;
this.biome = biome;
this.chanceToSplit = 0; 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) { public static void regenerate(World world) {
int startY = world.getMaxHeight() - 11; int startY = world.getMaxHeight() - 11;
int minY = world.getMinHeight() + 5; int minY = world.getMinHeight() + 5;
int maxY = world.getMaxHeight(); int maxY = world.getMaxHeight();
int worldMinY = world.getMinHeight(); int worldMinY = world.getMinHeight();
// Run the heavy data generation async, then schedule block placement on main thread normalStartY = startY;
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
Set<Long> fillPositions = new HashSet<>(); Set<Long> fillPositions = new HashSet<>();
List<BlockPlacement> fillPlacements = new ArrayList<>(); List<BlockPlacement> fillPlacements = new ArrayList<>();
// Phase 1: Generate tunnel fill data // ===== Biome 1: NORMAL =====
generateBranches(fillPlacements, fillPositions, startY, minY); plugin.getLogger().info("[MineGen] === Starting NORMAL biome generation ===");
List<Branch> normalBranches = new ArrayList<>();
plugin.getLogger().info("Mine generation phase 1 done: " + fillPlacements.size() + " fill blocks, " + fillPositions.size() + " unique positions."); int[] branchCount = {0};
generateBiomeBranches(MineBiome.NORMAL, 0, startY, 0, startY, minY,
// Phase 2: Generate wall data 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<Branch> 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<Branch> 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<BlockPlacement> wallPlacements = new ArrayList<>(); List<BlockPlacement> wallPlacements = new ArrayList<>();
generateWalls(fillPositions, wallPlacements, startY); 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."); // ===== Schedule placement on main thread =====
// Phase 3: Schedule block placement on main thread in batches
Bukkit.getScheduler().runTask(plugin, () -> Bukkit.getScheduler().runTask(plugin, () ->
scheduleBlockPlacements(world, fillPlacements, wallPlacements, startY, worldMinY, maxY)); scheduleBlockPlacements(world, fillPlacements, wallPlacements, startY, worldMinY, maxY));
}); });
} }
private static void generateBranches(List<BlockPlacement> placements, Set<Long> 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<BlockPlacement> placements, Set<Long> fillPositions,
List<Branch> completedBranches, int[] branchCount) {
Deque<Branch> queue = new ArrayDeque<>(); Deque<Branch> 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)); 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); queue.add(initial);
biomeBranchCount[0]++;
branchCount[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()) { while (!queue.isEmpty()) {
Branch branch = queue.pollFirst(); Branch branch = queue.pollFirst();
double currentIncrement = Math.max(0, BASE_SPLIT_INCREMENT - (branchCount[0] * SPLIT_DECAY_PER_BRANCH)); double currentIncrement = Math.max(0, BASE_SPLIT_INCREMENT - (biomeBranchCount[0] * SPLIT_DECAY_PER_BRANCH));
plugin.getLogger().info("[MineGen] Processing branch #" + branchCount[0] plugin.getLogger().info("[MineGen] [" + biome + "] Processing branch dir=" + branch.direction
+ " dir=" + branch.direction + " start=(" + branch.startX + "," + branch.startY + "," + branch.startZ + ")" + " start=(" + branch.startX + "," + branch.startY + "," + branch.startZ + ")"
+ " dist=" + branch.distanceFromStart + " maxSize=" + branch.maxSize + " dist=" + branch.distanceFromStart + " maxSize=" + branch.maxSize
+ " first=" + branch.isFirst + " first=" + branch.isFirst
+ " queued=" + queue.size() + " fillBlocks=" + fillPositions.size() + " queued=" + queue.size() + " fillBlocks=" + fillPositions.size()
+ " splitIncrement=" + String.format("%.5f", currentIncrement)); + " 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<Branch> queue, List<BlockPlacement> placements, private static void generateSingleBranch(Branch branch, Deque<Branch> queue, List<BlockPlacement> placements,
Set<Long> fillPositions, int startY, int minY, int[] branchCount) { Set<Long> fillPositions, int ceilingY, int minY, int[] branchCount) {
int cx = branch.startX; int cx = branch.startX;
int cy = branch.startY; int cy = branch.startY;
int cz = branch.startZ; int cz = branch.startZ;
@ -165,27 +280,28 @@ public class MineManager {
for (int step = 0; step < branch.maxSize; step++) { for (int step = 0; step < branch.maxSize; step++) {
int distance = branch.distanceFromStart + step; int distance = branch.distanceFromStart + step;
// Generate the 11x11 cross section at this position generateCrossSection(cx, cy, cz, branch.direction, distance, branch.biome, placements, fillPositions);
generateCrossSection(cx, cy, cz, branch.direction, distance, placements, fillPositions);
branch.endX = cx;
branch.endY = cy;
branch.endZ = cz;
// First branch: skip split logic for the first 12 blocks // First branch: skip split logic for the first 12 blocks
if (branch.isFirst && step < FIRST_BRANCH_SPLIT_DELAY) { if (branch.isFirst && step < FIRST_BRANCH_SPLIT_DELAY) {
cx += branch.direction.getModX(); cx += branch.direction.getModX();
cy += branch.direction.getModY(); cy += branch.direction.getModY();
cz += branch.direction.getModZ(); cz += branch.direction.getModZ();
if (cy < minY || cy > startY + 5) break; if (cy < minY || cy > ceilingY + 5) break;
continue; 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)); 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) { if (splitIncrement <= 0) {
cx += branch.direction.getModX(); cx += branch.direction.getModX();
cy += branch.direction.getModY(); cy += branch.direction.getModY();
cz += branch.direction.getModZ(); cz += branch.direction.getModZ();
if (cy < minY || cy > startY + 5) break; if (cy < minY || cy > ceilingY + 5) break;
continue; continue;
} }
@ -204,34 +320,36 @@ public class MineManager {
int branchMaxSize = MIN_BRANCH_SIZE + random.nextInt(MAX_BRANCH_SIZE - MIN_BRANCH_SIZE); 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 > ceilingY) continue;
if (dir == BlockFace.UP && cy + branchMaxSize > startY) { if (dir == BlockFace.DOWN && cy - branchMaxSize < minY) continue;
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)); queue.add(new Branch(dir, cx, cy, cz, distance, branchMaxSize, false, branch.biome));
branchCount[0]++; branchCount[0]++;
created++; 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(); cx += branch.direction.getModX();
cy += branch.direction.getModY(); cy += branch.direction.getModY();
cz += branch.direction.getModZ(); cz += branch.direction.getModZ();
if (cy < minY || cy > startY + 5) break; if (cy < minY || cy > ceilingY + 5) break;
} }
} }
private static Branch findLowestBranch(List<Branch> branches) {
Branch lowest = null;
for (Branch b : branches) {
if (lowest == null || b.endY < lowest.endY) {
lowest = b;
}
}
return lowest;
}
private static List<BlockFace> getAvailableDirections(BlockFace current) { private static List<BlockFace> getAvailableDirections(BlockFace current) {
List<BlockFace> dirs = new ArrayList<>(); List<BlockFace> dirs = new ArrayList<>();
BlockFace opposite = current.getOppositeFace(); BlockFace opposite = current.getOppositeFace();
@ -243,8 +361,10 @@ public class MineManager {
return dirs; return dirs;
} }
// ===================== CROSS SECTION GENERATION =====================
private static void generateCrossSection(int cx, int cy, int cz, BlockFace direction, int distance, private static void generateCrossSection(int cx, int cy, int cz, BlockFace direction, int distance,
List<BlockPlacement> placements, Set<Long> fillPositions) { MineBiome biome, List<BlockPlacement> placements, Set<Long> fillPositions) {
int[][] offsets = getPerpOffsets(direction); int[][] offsets = getPerpOffsets(direction);
for (int a = -TUNNEL_RADIUS; a <= TUNNEL_RADIUS; a++) { for (int a = -TUNNEL_RADIUS; a <= TUNNEL_RADIUS; a++) {
@ -256,117 +376,209 @@ public class MineManager {
long key = posKey(bx, by, bz); long key = posKey(bx, by, bz);
if (!fillPositions.add(key)) continue; if (!fillPositions.add(key)) continue;
Material mat = pickBlockForDistance(distance); Material mat = pickFillForDistance(distance, biome);
placements.add(new BlockPlacement(bx, by, bz, mat)); placements.add(new BlockPlacement(bx, by, bz, mat));
} }
} }
} }
// --- Biome fill + ore + transition logic --- // ===================== FILL SELECTION (no ores during generation) =====================
private static Material pickBlockForDistance(int distance) { private static Material pickFillForDistance(int distance, MineBiome biome) {
// Determine primary biome and check if we're in a transition zone Material[] fills = getFillsForBiome(biome);
Material primaryFill = getPrimaryFill(distance); int[] thresholds = getThresholdsForBiome(biome);
OreEntry[] primaryOres = getOreTable(primaryFill);
// Check transition: are we near a biome border? // Determine primary fill
int[] border = getNearestBorder(distance); Material primaryFill = fills[0];
// border[0] = border distance threshold, border[1] = signed distance to border (negative = before, positive = after) for (int i = thresholds.length - 1; i >= 0; i--) {
if (distance >= thresholds[i]) {
if (border != null && Math.abs(border[1]) <= TRANSITION_RANGE) { primaryFill = fills[i];
// We're in a transition zone — blend with the neighboring biome break;
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 // 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) { if (random.nextDouble() < blendRatio) {
// Use neighbor biome's fill + ores primaryFill = transitionFill;
Material ore = rollOreTable(neighborOres);
return (ore != null) ? ore : neighborFill;
} }
} }
// Primary biome: roll for ore // Roll for special inline blocks (dirt, gravel, lava, mud, infested)
Material ore = rollOreTable(primaryOres); Material special = rollSpecialBlock(biome, primaryFill, distance);
return (ore != null) ? ore : primaryFill; 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 roll = random.nextDouble();
double cumulative = 0; switch (biome) {
for (OreEntry entry : table) { case NORMAL:
cumulative += entry.chance; if (roll < DIRT_CHANCE) return Material.DIRT;
if (roll < cumulative) return entry.material; 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; return null;
} }
private static Material getPrimaryFill(int distance) { private static Material getTransitionFill(int distance, Material[] fills, int[] thresholds) {
if (distance < STONE_END) return Material.STONE; for (int i = 1; i < thresholds.length; i++) {
if (distance < TUFF_END) return Material.TUFF; int border = thresholds[i];
if (distance < DEEPSLATE_END) return Material.DEEPSLATE; int diff = distance - border;
if (distance < BASALT_END) return Material.SMOOTH_BASALT; if (Math.abs(diff) <= TRANSITION_RANGE) {
return Material.BLACKSTONE; return (diff < 0) ? fills[i] : fills[i - 1];
}
}
return null;
} }
private static OreEntry[] getOreTable(Material fill) { private static int getDistanceToBorder(int distance, int[] thresholds) {
return switch (fill) { int closest = Integer.MAX_VALUE;
case STONE -> STONE_ORES; for (int i = 1; i < thresholds.length; i++) {
case TUFF -> TUFF_ORES; int diff = distance - thresholds[i];
case DEEPSLATE -> DEEPSLATE_ORES; if (Math.abs(diff) < Math.abs(closest)) {
case SMOOTH_BASALT -> BASALT_ORES; closest = diff;
case BLACKSTONE -> BLACKSTONE_ORES; }
default -> STONE_ORES; }
}; return closest;
} }
private static Material getNextFill(Material fill) { private static Material[] getFillsForBiome(MineBiome biome) {
return switch (fill) { return switch (biome) {
case STONE -> Material.TUFF; case NORMAL -> NORMAL_FILLS;
case TUFF -> Material.DEEPSLATE; case DEEP -> DEEP_FILLS;
case DEEPSLATE -> Material.SMOOTH_BASALT; case SUPER_DEEP -> SUPER_DEEP_FILLS;
case SMOOTH_BASALT -> Material.BLACKSTONE;
default -> Material.BLACKSTONE; // blackstone has no next
}; };
} }
private static Material getPrevFill(Material fill) { private static int[] getThresholdsForBiome(MineBiome biome) {
return switch (fill) { return switch (biome) {
case TUFF -> Material.STONE; case NORMAL -> NORMAL_THRESHOLDS;
case DEEPSLATE -> Material.TUFF; case DEEP -> DEEP_THRESHOLDS;
case SMOOTH_BASALT -> Material.DEEPSLATE; case SUPER_DEEP -> SUPER_DEEP_THRESHOLDS;
case BLACKSTONE -> Material.SMOOTH_BASALT;
default -> Material.STONE; // stone has no prev
}; };
} }
// ===================== ORE-ON-BREAK SYSTEM (called from MineWorldManager) =====================
/** /**
* Returns the nearest biome border info, or null if not near any. * Detects the deepest fill material among the 6 surrounding blocks to determine
* Result: [0] = border distance, [1] = signed distance to border (negative = before border, positive = after) * 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.
*/ */
private static int[] getNearestBorder(int distance) { public static Material rollOreOnBreak(Block brokenBlock, Material brokenType) {
int[] borders = {STONE_END, TUFF_END, DEEPSLATE_END, BASALT_END}; // Detect the deepest fill in the 6 surrounding blocks
int closestDist = Integer.MAX_VALUE; Material detectedFill = detectDeepestSurroundingFill(brokenBlock);
int closestBorder = -1; 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;
for (int b : borders) { // How many steps back from the detected fill is this broken block?
int diff = distance - b; // detectedIndex is the deepest (highest index), brokenIndex is where we are in the sequence
if (Math.abs(diff) < Math.abs(closestDist)) { int stepsBack = detectedIndex - brokenIndex;
closestDist = diff;
closestBorder = b; // 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);
} }
if (closestBorder == -1 || Math.abs(closestDist) > TRANSITION_RANGE) return null; /**
return new int[]{closestBorder, closestDist}; * Looks at the 6 blocks surrounding the broken block and finds the deepest fill material.
*/
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;
}
} }
// --- Perpendicular axes --- 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 =====================
private static int[][] getPerpOffsets(BlockFace dir) { private static int[][] getPerpOffsets(BlockFace dir) {
return switch (dir) { return switch (dir) {
@ -377,11 +589,11 @@ public class MineManager {
}; };
} }
// --- Wall generation --- // ===================== WALL GENERATION =====================
private static void generateWalls(Set<Long> fillPositions, List<BlockPlacement> wallPlacements, int startY) { private static void generateWalls(Set<Long> fillPositions, List<BlockPlacement> wallPlacements, int startY) {
Set<Long> wallPositions = new HashSet<>(); Set<Long> 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) { for (long key : fillPositions) {
int x = decodeX(key); int x = decodeX(key);
@ -399,21 +611,34 @@ public class MineManager {
if (ny > startY) continue; if (ny > startY) continue;
if (wallPositions.add(neighborKey)) { 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<BlockPlacement> fillPlacements, private static void scheduleBlockPlacements(World world, List<BlockPlacement> fillPlacements,
List<BlockPlacement> wallPlacements, int startY, int minY, int maxY) { List<BlockPlacement> wallPlacements,
List<BlockPlacement> allPlacements = new ArrayList<>(fillPlacements.size() + wallPlacements.size() + 200); int startY, int minY, int maxY) {
allPlacements.addAll(fillPlacements); // Three-phase placement: 1) Dirt placeholder 2) Bedrock walls 3) Actual fill
List<BlockPlacement> allPlacements = new ArrayList<>();
// Phase A: Dirt placeholders at all fill positions
List<BlockPlacement> 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); allPlacements.addAll(wallPlacements);
// Phase C: Actual fill (overwrites dirt)
allPlacements.addAll(fillPlacements);
// Air blocks above the entrance opening // Air blocks above the entrance opening
for (int a = -TUNNEL_RADIUS; a <= TUNNEL_RADIUS; a++) { for (int a = -TUNNEL_RADIUS; a <= TUNNEL_RADIUS; a++) {
for (int b = -TUNNEL_RADIUS; b <= TUNNEL_RADIUS; b++) { for (int b = -TUNNEL_RADIUS; b <= TUNNEL_RADIUS; b++) {
@ -424,7 +649,9 @@ public class MineManager {
int totalBlocks = allPlacements.size(); int totalBlocks = allPlacements.size();
int tickDelay = 0; 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) { for (int i = 0; i < totalBlocks; i += BLOCKS_PER_TICK) {
int from = i; int from = i;
@ -447,7 +674,7 @@ public class MineManager {
finalDelay + 2); finalDelay + 2);
} }
// --- Position encoding/decoding --- // ===================== POSITION ENCODING/DECODING =====================
private static long posKey(int x, int y, int z) { private static long posKey(int x, int y, int z) {
return ((long) (x + 30000000) & 0x3FFFFFF) return ((long) (x + 30000000) & 0x3FFFFFF)

@ -101,22 +101,18 @@ public class MineWorldManager implements Listener {
plugin.getLogger().info("[MineWorld] Unloading old mine world..."); plugin.getLogger().info("[MineWorld] Unloading old mine world...");
// Unload the old world (no save - we're deleting it anyway)
if (mineWorld != null) { if (mineWorld != null) {
Bukkit.unloadWorld(mineWorld, false); Bukkit.unloadWorld(mineWorld, false);
mineWorld = null; mineWorld = null;
} }
// Delete and recreate async
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try { try {
// Delete old world data from the loader/DB
if (loader.worldExists(MINE_WORLD_NAME)) { if (loader.worldExists(MINE_WORLD_NAME)) {
loader.deleteWorld(MINE_WORLD_NAME); loader.deleteWorld(MINE_WORLD_NAME);
plugin.getLogger().info("[MineWorld] Old mine world deleted."); plugin.getLogger().info("[MineWorld] Old mine world deleted.");
} }
// Create fresh empty world
SlimePropertyMap props = new SlimePropertyMap(); SlimePropertyMap props = new SlimePropertyMap();
props.setValue(SlimeProperties.ENVIRONMENT, "NORMAL"); props.setValue(SlimeProperties.ENVIRONMENT, "NORMAL");
@ -125,7 +121,6 @@ public class MineWorldManager implements Listener {
SlimeWorld slimeWorld = asp.createEmptyWorld(MINE_WORLD_NAME, false, props, loader); SlimeWorld slimeWorld = asp.createEmptyWorld(MINE_WORLD_NAME, false, props, loader);
// Load the new world on the main thread
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
asp.loadWorld(slimeWorld, true); asp.loadWorld(slimeWorld, true);
mineWorld = Bukkit.getWorld(MINE_WORLD_NAME); mineWorld = Bukkit.getWorld(MINE_WORLD_NAME);
@ -211,65 +206,99 @@ public class MineWorldManager implements Listener {
event.setDamage(event.getDamage() * 2); event.setDamage(event.getDamage() * 2);
} }
// Prevent entity explosions (creepers, tnt, etc.) from destroying polished blackstone // Prevent entity explosions from destroying bedrock walls
@EventHandler @EventHandler
public void onEntityExplode(EntityExplodeEvent event) { public void onEntityExplode(EntityExplodeEvent event) {
if (!isInMineWorld(event.getEntity().getWorld())) return; 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 @EventHandler
public void onBlockExplode(BlockExplodeEvent event) { public void onBlockExplode(BlockExplodeEvent event) {
if (!isInMineWorld(event.getBlock().getWorld())) return; 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 @EventHandler
public void onBlockBreak(BlockBreakEvent event) { public void onBlockBreak(BlockBreakEvent event) {
if (!isInMineWorld(event.getPlayer())) return; if (!isInMineWorld(event.getPlayer())) return;
Material type = event.getBlock().getType(); Material type = event.getBlock().getType();
// Polished blackstone is unbreakable (mine walls) // Bedrock is completely unbreakable (mine walls)
if (type == Material.POLISHED_BLACKSTONE) { if (type == Material.BEDROCK) {
event.setCancelled(true); event.setCancelled(true);
return; 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); Material nextTier = getNextTier(type);
if (nextTier != null) { if (nextTier != null) {
event.setCancelled(true); event.setCancelled(true);
event.getBlock().setType(nextTier); event.getBlock().setType(nextTier);
event.getBlock().getState().update(true); event.getBlock().getState().update(true);
applyToolDamage(event);
return;
}
// Terminal fill (stone) or ores — break normally
}
// Damage the tool manually since we cancelled the event (only if the tool has durability) /**
* 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(); ItemStack tool = event.getPlayer().getInventory().getItemInMainHand();
if (!tool.getType().isAir() && tool.getType().getMaxDurability() > 0 && tool.getItemMeta() instanceof Damageable damageable) { if (tool.getType().isAir() || tool.getType().getMaxDurability() <= 0) return;
if (!(tool.getItemMeta() instanceof Damageable damageable)) return;
damageable.setDamage(damageable.getDamage() + 2); // doubled durability damageable.setDamage(damageable.getDamage() + 2); // doubled durability
tool.setItemMeta(damageable); tool.setItemMeta(damageable);
// Break tool if fully damaged
if (damageable.getDamage() >= tool.getType().getMaxDurability()) { if (damageable.getDamage() >= tool.getType().getMaxDurability()) {
event.getPlayer().getInventory().setItemInMainHand(null); event.getPlayer().getInventory().setItemInMainHand(null);
event.getPlayer().playSound(event.getPlayer().getLocation(), Sound.ENTITY_ITEM_BREAK, 1.0f, 1.0f); 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) { private Material getNextTier(Material material) {
return switch (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 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 DEEPSLATE -> Material.TUFF;
case TUFF -> Material.STONE; case TUFF -> Material.COBBLESTONE;
default -> null; // Stone and ores break normally case COBBLESTONE -> Material.ANDESITE;
case ANDESITE -> Material.STONE;
default -> null; // Stone and non-fill blocks break normally
}; };
} }
} }

Loading…
Cancel
Save