diff --git a/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java b/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java index 3a8c877..20f07b7 100644 --- a/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java +++ b/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java @@ -164,6 +164,7 @@ public final class LivingWorldBootstrap { private final Set farmlandRegions = new HashSet<>(); private final Set exhaustedFarmlandRegions = new HashSet<>(); private final Map farmingInactiveCycles = new HashMap<>(); + private int undergroundCycleTick = 0; private PlatformAdapter platformAdapter; private Path worldSaveDirectory; @@ -360,6 +361,7 @@ public final class LivingWorldBootstrap { farmlandRegions.clear(); exhaustedFarmlandRegions.clear(); farmingInactiveCycles.clear(); + undergroundCycleTick = 0; simSpeedMultiplier = 1; serverReady = false; LivingWorldLogger.info( @@ -533,6 +535,31 @@ public final class LivingWorldBootstrap { applyEcologyExpansion(); applyLongCycleEffects(); applyPlayerFeedbackLoops(); + applyUndergroundSystems(); + } + + private void applyUndergroundSystems() { + if (worldEffectsModule == null) return; + undergroundCycleTick++; + for (Region region : regionManager.getActiveRegions()) { + RegionCoordinate coord = region.getCoordinate(); + double groundwater = groundwaterLevel.getOrDefault(coord, 0.0); + if (groundwater > 80.0 && windRandom.nextDouble() < 0.01) { + worldEffectsModule.queueEffect(new WorldEffectRequest( + WorldEffectType.CAVE_FLOODS, coord, + Math.min(1.0, groundwater / 100.0))); + } + if (volcanoPhase.get(coord) == VolcanoPhase.FERTILE + && windRandom.nextDouble() < 0.01) { + worldEffectsModule.queueEffect(new WorldEffectRequest( + WorldEffectType.VEIN_SHIFTS, coord, 0.7)); + } + if (geothermalRegions.contains(coord) && groundwater > 60.0 + && undergroundCycleTick % 100 == 0) { + worldEffectsModule.queueEffect(new WorldEffectRequest( + WorldEffectType.STALACTITE_GROWS, coord, 0.6)); + } + } } private void applyLongCycleEffects() { diff --git a/src/main/java/com/livingworld/modules/worldeffects/WorldEffectType.java b/src/main/java/com/livingworld/modules/worldeffects/WorldEffectType.java index 9ef3d1e..dfdc5ab 100644 --- a/src/main/java/com/livingworld/modules/worldeffects/WorldEffectType.java +++ b/src/main/java/com/livingworld/modules/worldeffects/WorldEffectType.java @@ -211,4 +211,13 @@ public enum WorldEffectType { /** Exhausted farmland fails into coarse dirt and loses its crop. */ CROPLAND_EXHAUSTS, + + /** Saturated groundwater enters a natural cave opening. */ + CAVE_FLOODS, + + /** Volcanic mineralisation buries old ore and exposes a new shallow vein. */ + VEIN_SHIFTS, + + /** Wet dripstone cave ceilings grow a pointed stalactite segment. */ + STALACTITE_GROWS, } diff --git a/src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java b/src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java index 5908341..ee8de1d 100644 --- a/src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java +++ b/src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java @@ -25,6 +25,7 @@ import net.minecraft.tags.BlockTags; import net.minecraft.tags.FluidTags; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.minecraft.world.level.levelgen.Heightmap; /** @@ -176,6 +177,12 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer { siltRiver(level, baseX, baseZ, request.intensity()); case CROPLAND_EXHAUSTS -> exhaustCropland(level, baseX, baseZ, request.intensity()); + case CAVE_FLOODS -> + floodCave(level, baseX, baseZ, request.intensity()); + case VEIN_SHIFTS -> + shiftMineralVein(level, baseX, baseZ, request.intensity()); + case STALACTITE_GROWS -> + growStalactite(level, baseX, baseZ, request.intensity()); } } @@ -1034,6 +1041,17 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer { || state.is(Blocks.BASALT); } + private boolean isOre(net.minecraft.world.level.block.state.BlockState state) { + return state.is(Blocks.COAL_ORE) || state.is(Blocks.DEEPSLATE_COAL_ORE) + || state.is(Blocks.IRON_ORE) || state.is(Blocks.DEEPSLATE_IRON_ORE) + || state.is(Blocks.COPPER_ORE) || state.is(Blocks.DEEPSLATE_COPPER_ORE) + || state.is(Blocks.GOLD_ORE) || state.is(Blocks.DEEPSLATE_GOLD_ORE) + || state.is(Blocks.REDSTONE_ORE) || state.is(Blocks.DEEPSLATE_REDSTONE_ORE) + || state.is(Blocks.LAPIS_ORE) || state.is(Blocks.DEEPSLATE_LAPIS_ORE) + || state.is(Blocks.DIAMOND_ORE) || state.is(Blocks.DEEPSLATE_DIAMOND_ORE) + || state.is(Blocks.EMERALD_ORE) || state.is(Blocks.DEEPSLATE_EMERALD_ORE); + } + private void cleanExpiredFloodWater(ServerLevel level, ResourceKey dimensionKey) { List entries = floodWater.get(dimensionKey); if (entries == null) return; @@ -1307,6 +1325,78 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer { } } + private void floodCave(ServerLevel level, int baseX, int baseZ, double intensity) { + for (int attempt = 0; attempt < 10; attempt++) { + int x = baseX + random.nextInt(REGION_BLOCKS); + int z = baseZ + random.nextInt(REGION_BLOCKS); + int surfaceY = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1; + for (int depth = 2; depth <= 10; depth++) { + BlockPos cave = new BlockPos(x, surfaceY - depth, z); + if (!level.isLoaded(cave) || !level.getBlockState(cave).isAir()) continue; + if (level.getBlockState(cave.above()).isSolid()) { + level.setBlock(cave, Blocks.WATER.defaultBlockState(), Block.UPDATE_ALL); + return; + } + } + } + } + + private void shiftMineralVein(ServerLevel level, int baseX, int baseZ, double intensity) { + int writes = 0; + for (int attempt = 0; attempt < 10 && writes < 10; attempt++) { + int x = baseX + random.nextInt(REGION_BLOCKS); + int z = baseZ + random.nextInt(REGION_BLOCKS); + int surfaceY = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1; + for (int depth = 5; depth <= 20 && writes < 10; depth++) { + BlockPos pos = new BlockPos(x, surfaceY - depth, z); + if (!level.isLoaded(pos)) continue; + var state = level.getBlockState(pos); + if (isOre(state)) { + for (Direction direction : Direction.values()) { + BlockPos adjacent = pos.relative(direction); + if (writes >= 6) break; + if (level.getBlockState(adjacent).is(Blocks.STONE)) { + level.setBlock(adjacent, random.nextBoolean() + ? Blocks.TUFF.defaultBlockState() + : Blocks.BASALT.defaultBlockState(), Block.UPDATE_ALL); + writes++; + } + } + break; + } + } + int shallowDepth = 5 + random.nextInt(6); + BlockPos exposure = new BlockPos(x, surfaceY - shallowDepth, z); + if (writes < 10 && level.isLoaded(exposure) + && level.getBlockState(exposure).is(Blocks.STONE)) { + level.setBlock(exposure, random.nextInt(4) == 0 + ? Blocks.RAW_GOLD_BLOCK.defaultBlockState() + : Blocks.COPPER_ORE.defaultBlockState(), Block.UPDATE_ALL); + writes++; + } + } + } + + private void growStalactite(ServerLevel level, int baseX, int baseZ, double intensity) { + for (int attempt = 0; attempt < 10; attempt++) { + int x = baseX + random.nextInt(REGION_BLOCKS); + int z = baseZ + random.nextInt(REGION_BLOCKS); + int surfaceY = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1; + for (int depth = 5; depth <= 30; depth++) { + BlockPos air = new BlockPos(x, surfaceY - depth, z); + if (!level.isLoaded(air) || !level.getBlockState(air).isAir()) continue; + BlockPos ceiling = air.above(); + if (level.getBlockState(ceiling).is(Blocks.DRIPSTONE_BLOCK) + || level.getBlockState(ceiling).is(Blocks.STONE)) { + var dripstone = Blocks.POINTED_DRIPSTONE.defaultBlockState() + .setValue(BlockStateProperties.VERTICAL_DIRECTION, Direction.DOWN); + level.setBlock(air, dripstone, Block.UPDATE_ALL); + return; + } + } + } + } + private BlockPos surfaceAt(ServerLevel level, int x, int z) { int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1; if (y < level.getMinBuildHeight()) {