From 38ee028651533f63fed29db38114a5a2897c1cfa Mon Sep 17 00:00:00 2001 From: George Date: Thu, 11 Jun 2026 18:11:42 +0100 Subject: [PATCH] Complete phase 4 hydrology expansion --- .../java/com/livingworld/LivingWorldMod.java | 6 +- .../bootstrap/LivingWorldBootstrap.java | 172 +++++++++++++++++- .../livingworld/climate/ClimateEventType.java | 3 +- .../livingworld/config/EcosystemTuning.java | 4 + .../modules/worldeffects/WorldEffectType.java | 21 +++ .../platform/neoforge/NeoForgeModConfig.java | 8 + .../neoforge/NeoForgeWorldEffectExecutor.java | 156 ++++++++++++++++ 7 files changed, 365 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/livingworld/LivingWorldMod.java b/src/main/java/com/livingworld/LivingWorldMod.java index 579be97..6a05b06 100644 --- a/src/main/java/com/livingworld/LivingWorldMod.java +++ b/src/main/java/com/livingworld/LivingWorldMod.java @@ -291,7 +291,7 @@ public class LivingWorldMod { RegionCoordinate spawnCoord = RegionCoordinate.fromBlock( dimId, (int) event.getX(), (int) event.getZ(), LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS); - if (bootstrap.isDeadZone(spawnCoord) + if (bootstrap.isAquaticSpawnSuppressed(spawnCoord) && (event.getEntity() instanceof AbstractFish || event.getEntity() instanceof Squid || event.getEntity() instanceof Dolphin)) { @@ -563,7 +563,7 @@ public class LivingWorldMod { // -- Pre-compute tidal current for this tick -- ServerLevel overworld = minecraftServer.overworld(); long dayTime = overworld.getDayTime(); - int seaLevel = overworld.getSeaLevel(); + int seaLevel = bootstrap.getDynamicSeaLevel(); int moonPhase = overworld.getMoonPhase(); double springNeap = 1.0 + 0.40 * Math.cos(moonPhase * Math.PI / 4.0); // Tidal current magnitude = derivative of tide height (max at mid-tide, zero at high/low) @@ -696,7 +696,7 @@ public class LivingWorldMod { if (minecraftServer == null) return; ServerLevel overworld = minecraftServer.overworld(); long dayTime = overworld.getDayTime(); - int seaLevel = overworld.getSeaLevel(); + int seaLevel = bootstrap.getDynamicSeaLevel(); // Moon phase (0=full, 4=new); full moon = maximum spring tides int moonPhase = overworld.getMoonPhase(); diff --git a/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java b/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java index 33bef3f..b1e13e2 100644 --- a/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java +++ b/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java @@ -140,6 +140,11 @@ public final class LivingWorldBootstrap { private record GeyserSite(int cycleTick, boolean active) {} private final Map geyserSites = new HashMap<>(); private int geologicalActivityTick = 0; + private final Map dryCycles = new HashMap<>(); + private final Map previousEffectiveTemperature = new HashMap<>(); + private final Map blizzardHistory = new HashMap<>(); + private int hydrologyCycleTick = 0; + private int dynamicSeaLevel = 62; private PlatformAdapter platformAdapter; private Path worldSaveDirectory; @@ -273,7 +278,10 @@ public final class LivingWorldBootstrap { */ /** Sets the ecosystem tuning loaded from the server config. Must be called before onServerStarting(). */ public void setEcosystemTuning(EcosystemTuning tuning) { - if (tuning != null) ecosystemTuning = tuning; + if (tuning != null) { + ecosystemTuning = tuning; + dynamicSeaLevel = tuning.getSeaLevel(); + } } public void onServerStopping() { @@ -309,6 +317,11 @@ public final class LivingWorldBootstrap { volcanicActivityTick = 0; geyserSites.clear(); geologicalActivityTick = 0; + dryCycles.clear(); + previousEffectiveTemperature.clear(); + blizzardHistory.clear(); + hydrologyCycleTick = 0; + dynamicSeaLevel = ecosystemTuning.getSeaLevel(); simSpeedMultiplier = 1; serverReady = false; LivingWorldLogger.info( @@ -474,6 +487,147 @@ public final class LivingWorldBootstrap { if (++geologicalActivityTick % 4 == 0) { applyGeologicalActivity(); } + applyHydrologyExpansion(); + } + + private void applyHydrologyExpansion() { + Collection active = regionManager.getActiveRegions(); + if (active.isEmpty() || worldEffectsModule == null) return; + hydrologyCycleTick++; + double warming = climateTracker.getWarmingLevel(); + int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + Map byCoord = new HashMap<>(); + for (Region region : active) byCoord.put(region.getCoordinate(), region); + + double totalRain = 0.0; + int rainSamples = 0; + for (Region region : active) { + RegionCoordinate coord = region.getCoordinate(); + AtmosphereRegionData atmosphere = region.getModuleData() + .get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class).orElse(null); + WaterRegionData water = region.getModuleData() + .get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null); + RecoveryRegionData recovery = region.getModuleData() + .get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElse(null); + if (atmosphere == null || water == null || recovery == null) continue; + totalRain += atmosphere.getRainLevel() * 100.0; + rainSamples++; + + double elevation = regionElevations.getOrDefault(coord, 64.0); + double effectiveTemperature = regionTemperatures.getOrDefault(coord, 0.8f) + warming * 0.2; + double previousTemperature = previousEffectiveTemperature.getOrDefault( + coord, effectiveTemperature); + previousEffectiveTemperature.put(coord, effectiveTemperature); + + if (previousTemperature < 0.15 && effectiveTemperature >= 0.15) { + worldEffectsModule.queueEffect(new WorldEffectRequest( + WorldEffectType.ICE_MELTS, coord, 0.8)); + worldEffectsModule.queueEffect(new WorldEffectRequest( + WorldEffectType.SNOW_MELTS, coord, 0.8)); + groundwaterLevel.merge(coord, 12.0, Double::sum); + for (int[] offset : offsets) { + RegionCoordinate neighbour = new RegionCoordinate( + coord.dimensionId(), coord.x() + offset[0], coord.z() + offset[1]); + Double neighbourElevation = regionElevations.get(neighbour); + if (neighbourElevation != null && neighbourElevation < elevation - 3.0) { + worldEffectsModule.queueEffect(new WorldEffectRequest( + WorldEffectType.GROUND_SPRING_EMERGES, neighbour, 0.7)); + } + } + } + + int dry = atmosphere.getRainLevel() < 0.15 + ? dryCycles.merge(coord, 1, Integer::sum) : 0; + if (dry == 0) dryCycles.remove(coord); + if (dry >= 20) { + worldEffectsModule.queueEffect(new WorldEffectRequest( + WorldEffectType.RIVERBED_DRIES, coord, + Math.min(1.0, (dry - 15) / 30.0))); + groundwaterLevel.compute(coord, + (ignored, level) -> Math.max(0, (level == null ? 0.0 : level) - 2.0)); + } + + boolean lowSuccession = recovery.getSuccessionStage() == SuccessionStage.BARREN + || recovery.getSuccessionStage() == SuccessionStage.SPARSE_GRASS; + double maxSlope = 0.0; + RegionCoordinate lowestNeighbour = null; + for (int[] offset : offsets) { + RegionCoordinate neighbour = new RegionCoordinate( + coord.dimensionId(), coord.x() + offset[0], coord.z() + offset[1]); + Double neighbourElevation = regionElevations.get(neighbour); + if (neighbourElevation != null && elevation - neighbourElevation > maxSlope) { + maxSlope = elevation - neighbourElevation; + lowestNeighbour = neighbour; + } + } + if (atmosphere.getRainLevel() > 0.80 && lowSuccession && maxSlope > 10.0 + && windRandom.nextDouble() < 0.02 + && !isClimateEventActive(coord, ClimateEventType.FLASH_FLOOD)) { + ClimateEvent flood = new ClimateEvent( + ClimateEventType.FLASH_FLOOD, coord, + simulationManager.getSimulationTickCounter(), 0.8); + activeClimateEvents.add(flood); + pendingEventMessages.add("[LW] Flash flood at region (" + + coord.x() + "," + coord.z() + ")."); + } + + double flow = riverFlowIntensity.getOrDefault(coord, 0.0); + if (flow > 20.0 && lowestNeighbour != null) { + if (oceanicRegions.contains(lowestNeighbour)) { + worldEffectsModule.queueEffect(new WorldEffectRequest( + WorldEffectType.SEDIMENT_DEPOSIT, lowestNeighbour, + Math.min(1.0, flow / 100.0))); + pendingElevationResample.add(lowestNeighbour); + } + if (maxSlope > 10.0) { + worldEffectsModule.queueEffect(new WorldEffectRequest( + WorldEffectType.PLUNGE_POOL_DEEPENS, lowestNeighbour, + Math.min(1.0, flow / 100.0))); + } + } + + if (isClimateEventActive(coord, ClimateEventType.BLIZZARD)) { + blizzardHistory.merge(coord, 1, Integer::sum); + } + int snowHistory = blizzardHistory.getOrDefault(coord, 0); + if (effectiveTemperature < 0.0 && elevation > 90.0 && snowHistory >= 10 + && hydrologyCycleTick % 50 == 0) { + worldEffectsModule.queueEffect(new WorldEffectRequest( + WorldEffectType.GLACIER_ADVANCE, coord, 0.7)); + worldEffectsModule.queueEffect(new WorldEffectRequest( + WorldEffectType.GLACIER_POLISH, coord, 0.7)); + } + + if (isClimateEventActive(coord, ClimateEventType.ACID_RAIN) && flow > 10.0 + && lowestNeighbour != null && oceanicRegions.contains(lowestNeighbour)) { + Region downstream = byCoord.get(lowestNeighbour); + if (downstream != null) { + PollutionRegionData downstreamPollution = downstream.getModuleData() + .get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null); + if (downstreamPollution != null) { + double spike = geothermalRegions.contains(coord) ? 0.2 : 1.0; + downstreamPollution.addPollution(0, 0, spike); + downstream.getModuleData().put(PollutionModule.MODULE_ID, downstreamPollution); + regionManager.markDirty(downstream); + } + } + } + } + + if (hydrologyCycleTick % 500 == 0 && rainSamples > 0) { + double averageRain = totalRain / rainSamples; + int baseline = ecosystemTuning.getSeaLevel(); + int oldSeaLevel = dynamicSeaLevel; + if (averageRain > 65.0 && dynamicSeaLevel < baseline + 3) dynamicSeaLevel++; + if (averageRain < 15.0 && dynamicSeaLevel > baseline - 3) dynamicSeaLevel--; + if (oldSeaLevel != dynamicSeaLevel) { + pendingEventMessages.add("[LW] Sea level changed to Y=" + dynamicSeaLevel + "."); + } + } + } + + public int getDynamicSeaLevel() { + return dynamicSeaLevel; } private void applyGeologicalActivity() { @@ -983,6 +1137,15 @@ public final class LivingWorldBootstrap { return coord != null && deadZoneRegions.contains(coord); } + public boolean isAquaticSpawnSuppressed(RegionCoordinate coord) { + if (coord == null || deadZoneRegions.contains(coord)) return coord != null; + return regionManager != null && regionManager.resolve(coord) + .flatMap(region -> region.getModuleData() + .get(PollutionModule.MODULE_ID, PollutionRegionData.class)) + .map(pollution -> pollution.getWaterPollution() > 50.0) + .orElse(false); + } + /** Returns whether a region is known to be oceanic. */ public boolean isOceanicRegion(RegionCoordinate coord) { return coord != null && oceanicRegions.contains(coord); @@ -1795,6 +1958,13 @@ public final class LivingWorldBootstrap { } if (ev.getTicksActive() >= 2) shouldResolve = true; } + case FLASH_FLOOD -> { + if (worldEffectsModule != null) { + worldEffectsModule.queueEffect(new WorldEffectRequest( + WorldEffectType.FLASH_FLOOD, coord, ev.getSeverity())); + } + if (ev.getTicksActive() >= 5) shouldResolve = true; + } } } diff --git a/src/main/java/com/livingworld/climate/ClimateEventType.java b/src/main/java/com/livingworld/climate/ClimateEventType.java index 923752d..2e56d31 100644 --- a/src/main/java/com/livingworld/climate/ClimateEventType.java +++ b/src/main/java/com/livingworld/climate/ClimateEventType.java @@ -12,7 +12,8 @@ public enum ClimateEventType { SANDSTORM("Sandstorm", "Dry high winds stripping plants and depositing sand"), LIGHTNING_STORM("Lightning Storm", "Dry thunderstorm producing isolated ignition strikes"), EARTHQUAKE("Earthquake", "Seismic rupture opening cracks and unstable slopes"), - SINKHOLE("Sinkhole", "Groundwater-driven collapse into a shallow cave"); + SINKHOLE("Sinkhole", "Groundwater-driven collapse into a shallow cave"), + FLASH_FLOOD("Flash Flood", "Rapid runoff surging across barren steep terrain"); private final String displayName; private final String description; diff --git a/src/main/java/com/livingworld/config/EcosystemTuning.java b/src/main/java/com/livingworld/config/EcosystemTuning.java index 5650c1f..1b96d63 100644 --- a/src/main/java/com/livingworld/config/EcosystemTuning.java +++ b/src/main/java/com/livingworld/config/EcosystemTuning.java @@ -43,6 +43,7 @@ public final class EcosystemTuning { private double corridorBoostMultiplier = 3.5; /** Seeds blocked per unit of pollution score in the target region. */ private double seedPollutionBlock = 0.015; + private int seaLevel = 62; public EcosystemTuning() {} @@ -91,4 +92,7 @@ public final class EcosystemTuning { public void setSeedEmissionRate(double v) { seedEmissionRate = v; } public void setCorridorBoostMultiplier(double v) { corridorBoostMultiplier = v; } public void setSeedPollutionBlock(double v) { seedPollutionBlock = v; } + + public int getSeaLevel() { return seaLevel; } + public void setSeaLevel(int seaLevel) { this.seaLevel = Math.max(1, Math.min(320, seaLevel)); } } diff --git a/src/main/java/com/livingworld/modules/worldeffects/WorldEffectType.java b/src/main/java/com/livingworld/modules/worldeffects/WorldEffectType.java index 6fca053..99e0aea 100644 --- a/src/main/java/com/livingworld/modules/worldeffects/WorldEffectType.java +++ b/src/main/java/com/livingworld/modules/worldeffects/WorldEffectType.java @@ -166,4 +166,25 @@ public enum WorldEffectType { /** A cooling volcanic roof collapses to reveal a lava-adjacent cavity. */ LAVA_TUBE_COLLAPSE, + + /** A temporary surge places water along a low terrain path. */ + FLASH_FLOOD, + + /** Seasonal thaw converts exposed ice back into water. */ + ICE_MELTS, + + /** Sustained drought drains exposed river sources and hardens the bed. */ + RIVERBED_DRIES, + + /** River sediment accumulates below sea level at an ocean mouth. */ + SEDIMENT_DEPOSIT, + + /** A glacier toe pushes loose rock downhill. */ + GLACIER_ADVANCE, + + /** Moving ice leaves polished andesite behind. */ + GLACIER_POLISH, + + /** Falling river water deepens its impact basin. */ + PLUNGE_POOL_DEEPENS, } diff --git a/src/main/java/com/livingworld/platform/neoforge/NeoForgeModConfig.java b/src/main/java/com/livingworld/platform/neoforge/NeoForgeModConfig.java index 8310518..8264430 100644 --- a/src/main/java/com/livingworld/platform/neoforge/NeoForgeModConfig.java +++ b/src/main/java/com/livingworld/platform/neoforge/NeoForgeModConfig.java @@ -46,6 +46,7 @@ public final class NeoForgeModConfig { private static final ModConfigSpec.DoubleValue SEED_EMISSION_RATE; private static final ModConfigSpec.DoubleValue CORRIDOR_BOOST_MULTIPLIER; private static final ModConfigSpec.DoubleValue SEED_POLLUTION_BLOCK; + private static final ModConfigSpec.IntValue SEA_LEVEL; static { ModConfigSpec.Builder b = new ModConfigSpec.Builder(); @@ -65,6 +66,12 @@ public final class NeoForgeModConfig { .defineInRange("wind_boost", 0.5, 0.0, 2.0); b.pop(); + b.comment("Long-timescale hydrology").push("hydrology"); + SEA_LEVEL = b + .comment("Baseline simulated sea level. Long climate cycles may drift this by +/-3 blocks.") + .defineInRange("sea_level", 62, 1, 320); + b.pop(); + b.comment("Vegetation growth rates (per soil-quality unit above threshold per tick)").push("vegetation"); GRASS_GROWTH_RATE = b.comment("Grass growth rate. Default: 0.06") .defineInRange("grass_growth", 0.06, 0.001, 2.0); @@ -134,6 +141,7 @@ public final class NeoForgeModConfig { t.setSeedEmissionRate(SEED_EMISSION_RATE.get()); t.setCorridorBoostMultiplier(CORRIDOR_BOOST_MULTIPLIER.get()); t.setSeedPollutionBlock(SEED_POLLUTION_BLOCK.get()); + t.setSeaLevel(SEA_LEVEL.get()); return t; } } diff --git a/src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java b/src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java index 064921c..22dd343 100644 --- a/src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java +++ b/src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java @@ -44,6 +44,7 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer { private final Random random = new Random(); private record TimedWater(BlockPos pos, long expiresAt) {} private final Map, List> geyserWater = new HashMap<>(); + private final Map, List> floodWater = new HashMap<>(); public NeoForgeWorldEffectExecutor(Supplier serverSupplier) { this.serverSupplier = serverSupplier; @@ -65,6 +66,7 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer { int baseX = request.region().x() * REGION_BLOCKS; int baseZ = request.region().z() * REGION_BLOCKS; cleanExpiredGeyserWater(level, dimensionKey); + cleanExpiredFloodWater(level, dimensionKey); switch (request.type()) { case GRASS_DEGRADES_TO_DIRT -> @@ -134,6 +136,20 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer { landslide(level, baseX, baseZ, request.intensity()); case LAVA_TUBE_COLLAPSE -> collapseLavaTube(level, baseX, baseZ, request.intensity()); + case FLASH_FLOOD -> + flashFlood(level, dimensionKey, baseX, baseZ, request.intensity()); + case ICE_MELTS -> + meltIce(level, baseX, baseZ, request.intensity()); + case RIVERBED_DRIES -> + dryRiverbed(level, baseX, baseZ, request.intensity()); + case SEDIMENT_DEPOSIT -> + depositSediment(level, baseX, baseZ, request.intensity()); + case GLACIER_ADVANCE -> + advanceGlacier(level, baseX, baseZ, request.intensity()); + case GLACIER_POLISH -> + polishGlacierRock(level, baseX, baseZ, request.intensity()); + case PLUNGE_POOL_DEEPENS -> + deepenPlungePool(level, baseX, baseZ, request.intensity()); } } @@ -991,6 +1007,146 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer { || state.is(Blocks.BASALT); } + private void cleanExpiredFloodWater(ServerLevel level, ResourceKey dimensionKey) { + List entries = floodWater.get(dimensionKey); + if (entries == null) return; + long now = level.getGameTime(); + entries.removeIf(entry -> { + if (entry.expiresAt() > now) return false; + if (level.isLoaded(entry.pos()) && level.getBlockState(entry.pos()).is(Blocks.WATER)) { + level.setBlock(entry.pos(), Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + } + return true; + }); + if (entries.isEmpty()) floodWater.remove(dimensionKey); + } + + private void flashFlood(ServerLevel level, ResourceKey dimensionKey, + int baseX, int baseZ, double intensity) { + if (random.nextDouble() > 0.35) return; + List entries = floodWater.computeIfAbsent(dimensionKey, ignored -> new ArrayList<>()); + BlockPos lowest = null; + for (int i = 0; i < 10; i++) { + BlockPos sample = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS), + baseZ + random.nextInt(REGION_BLOCKS)); + if (sample != null && (lowest == null || sample.getY() < lowest.getY())) lowest = sample; + } + if (lowest == null) return; + int writes = 0; + for (Direction direction : Direction.Plane.HORIZONTAL) { + BlockPos pos = lowest.relative(direction).above(); + if (writes >= 5) break; + if (level.isLoaded(pos) && level.getBlockState(pos).isAir()) { + level.setBlock(pos, Blocks.WATER.defaultBlockState(), Block.UPDATE_ALL); + entries.add(new TimedWater(pos, level.getGameTime() + 6000)); + writes++; + } + } + } + + private void meltIce(ServerLevel level, int baseX, int baseZ, double intensity) { + for (int i = 0; i < Math.max(2, (int) (intensity * 8)); i++) { + int x = baseX + random.nextInt(REGION_BLOCKS); + int z = baseZ + random.nextInt(REGION_BLOCKS); + int y = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z) - 1; + BlockPos pos = new BlockPos(x, y, z); + if (level.isLoaded(pos) && level.getBlockState(pos).is(Blocks.ICE)) { + level.setBlock(pos, Blocks.WATER.defaultBlockState(), Block.UPDATE_ALL); + } + } + } + + private void dryRiverbed(ServerLevel level, int baseX, int baseZ, double intensity) { + if (random.nextDouble() > 0.25) return; + for (int i = 0; i < Math.max(2, (int) (intensity * 8)); i++) { + int x = baseX + random.nextInt(REGION_BLOCKS); + int z = baseZ + random.nextInt(REGION_BLOCKS); + int y = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z) - 1; + BlockPos pos = new BlockPos(x, y, z); + if (!level.isLoaded(pos) || !level.getBlockState(pos).is(Blocks.WATER) + || !level.getFluidState(pos).isSource()) continue; + BlockPos below = pos.below(); + var belowState = level.getBlockState(below); + if (belowState.is(Blocks.SAND) || belowState.is(Blocks.DIRT) + || belowState.is(Blocks.GRAVEL) || belowState.is(Blocks.STONE)) { + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + level.setBlock(below, random.nextBoolean() ? Blocks.COARSE_DIRT.defaultBlockState() + : Blocks.GRAVEL.defaultBlockState(), Block.UPDATE_ALL); + } + } + } + + private void depositSediment(ServerLevel level, int baseX, int baseZ, double intensity) { + if (random.nextDouble() > 0.25) return; + for (int i = 0; i < Math.max(1, (int) (intensity * 5)); i++) { + int x = baseX + random.nextInt(REGION_BLOCKS); + int z = baseZ + random.nextInt(REGION_BLOCKS); + int y = level.getHeight(Heightmap.Types.OCEAN_FLOOR, x, z); + if (y >= level.getSeaLevel()) continue; + BlockPos pos = new BlockPos(x, y, z); + if (level.isLoaded(pos) && level.getFluidState(pos).is(FluidTags.WATER)) { + level.setBlock(pos, random.nextBoolean() ? Blocks.SAND.defaultBlockState() + : Blocks.GRAVEL.defaultBlockState(), Block.UPDATE_ALL); + } + } + } + + private void advanceGlacier(ServerLevel level, int baseX, int baseZ, double intensity) { + BlockPos snow = null; + BlockPos low = null; + for (int i = 0; i < 10; i++) { + BlockPos sample = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS), + baseZ + random.nextInt(REGION_BLOCKS)); + if (sample == null) continue; + if ((level.getBlockState(sample).is(Blocks.SNOW_BLOCK) + || level.getBlockState(sample).is(Blocks.ICE)) + && (snow == null || sample.getY() < snow.getY())) snow = sample; + if (low == null || sample.getY() < low.getY()) low = sample; + } + if (snow == null || low == null) return; + int dx = Integer.compare(low.getX(), snow.getX()); + int dz = Integer.compare(low.getZ(), snow.getZ()); + for (Direction direction : Direction.Plane.HORIZONTAL) { + BlockPos source = snow.relative(direction); + var sourceState = level.getBlockState(source); + if (!sourceState.is(Blocks.GRAVEL) && !sourceState.is(Blocks.STONE)) continue; + BlockPos target = source.offset(dx, 0, dz); + if (level.isLoaded(target) && level.getBlockState(target).isAir()) { + level.setBlock(source, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + level.setBlock(target, sourceState, Block.UPDATE_ALL); + return; + } + } + } + + private void polishGlacierRock(ServerLevel level, int baseX, int baseZ, double intensity) { + for (int i = 0; i < 5; i++) { + BlockPos pos = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS), + baseZ + random.nextInt(REGION_BLOCKS)); + if (pos != null && (level.getBlockState(pos).is(Blocks.STONE) + || level.getBlockState(pos).is(Blocks.ANDESITE))) { + level.setBlock(pos, Blocks.POLISHED_ANDESITE.defaultBlockState(), Block.UPDATE_ALL); + } + } + } + + private void deepenPlungePool(ServerLevel level, int baseX, int baseZ, double intensity) { + if (random.nextDouble() > 0.25) return; + for (int i = 0; i < 8; i++) { + int x = baseX + random.nextInt(REGION_BLOCKS); + int z = baseZ + random.nextInt(REGION_BLOCKS); + int surfaceY = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z) - 1; + BlockPos water = new BlockPos(x, surfaceY, z); + if (!level.isLoaded(water) || !level.getFluidState(water).is(FluidTags.WATER)) continue; + BlockPos base = water.below(); + var state = level.getBlockState(base); + if (state.is(Blocks.STONE) || state.is(Blocks.GRAVEL) || state.is(Blocks.SAND)) { + level.setBlock(base, Blocks.WATER.defaultBlockState(), 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()) {