diff --git a/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java b/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java index 43db1d3..8a9f3c5 100644 --- a/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java +++ b/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java @@ -154,6 +154,11 @@ public final class LivingWorldBootstrap { private final Map rewildingBoostCycles = new HashMap<>(); private final Map bogWetCycles = new HashMap<>(); private final Set waterloggedRegions = new HashSet<>(); + private final Map previousSeasonalTemperature = new HashMap<>(); + private final Map leafColourCycles = new HashMap<>(); + private final Map bareDryCycles = new HashMap<>(); + private final Map permafrostWarmCycles = new HashMap<>(); + private final Set thawedPermafrostRegions = new HashSet<>(); private PlatformAdapter platformAdapter; private Path worldSaveDirectory; @@ -340,6 +345,11 @@ public final class LivingWorldBootstrap { rewildingBoostCycles.clear(); bogWetCycles.clear(); waterloggedRegions.clear(); + previousSeasonalTemperature.clear(); + leafColourCycles.clear(); + bareDryCycles.clear(); + permafrostWarmCycles.clear(); + thawedPermafrostRegions.clear(); simSpeedMultiplier = 1; serverReady = false; LivingWorldLogger.info( @@ -508,6 +518,92 @@ public final class LivingWorldBootstrap { } applyHydrologyExpansion(); applyEcologyExpansion(); + applyLongCycleEffects(); + } + + private void applyLongCycleEffects() { + if (worldEffectsModule == null) return; + Season season = getCurrentSeason(); + double seasonalOffset = switch (season) { + case SPRING -> 0.05; + case SUMMER -> 0.15; + case AUTUMN -> -0.05; + case WINTER -> -0.20; + }; + double warming = climateTracker.getWarmingLevel() * 0.25; + + for (Region region : regionManager.getActiveRegions()) { + RegionCoordinate coord = region.getCoordinate(); + double baseTemperature = regionTemperatures.getOrDefault(coord, 0.8f); + double seasonalTemperature = baseTemperature + seasonalOffset + warming; + double previous = previousSeasonalTemperature.getOrDefault( + coord, seasonalTemperature); + previousSeasonalTemperature.put(coord, seasonalTemperature); + + VegetationRegionData vegetation = region.getModuleData() + .get(VegetationModule.MODULE_ID, VegetationRegionData.class).orElse(null); + SoilRegionData soil = region.getModuleData() + .get(SoilModule.MODULE_ID, SoilRegionData.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); + AtmosphereRegionData atmosphere = region.getModuleData() + .get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class).orElse(null); + if (vegetation == null || soil == null || water == null + || recovery == null || atmosphere == null) continue; + + if (previous >= 0.2 && seasonalTemperature < 0.2 + && vegetation.getTreePressure() > 10.0) { + worldEffectsModule.queueEffect(new WorldEffectRequest( + WorldEffectType.LEAVES_CHANGE_COLOUR, coord, 0.8)); + leafColourCycles.put(coord, 5 + windRandom.nextInt(6)); + } + Integer colourCycles = leafColourCycles.get(coord); + if (colourCycles != null) { + if (colourCycles <= 1) { + worldEffectsModule.queueEffect(new WorldEffectRequest( + WorldEffectType.LEAVES_FALL, coord, 0.8)); + leafColourCycles.remove(coord); + } else { + leafColourCycles.put(coord, colourCycles - 1); + } + } + + boolean bareAndDry = dryCycles.getOrDefault(coord, 0) >= 20 + && vegetation.getGrassPressure() < 10.0 + && soil.getMoisture() < 20.0; + if (bareAndDry) { + int exposed = bareDryCycles.merge(coord, 1, Integer::sum); + if (exposed >= 15) { + worldEffectsModule.queueEffect(new WorldEffectRequest( + WorldEffectType.SOIL_CRUSTS, coord, + Math.min(1.0, exposed / 40.0))); + water.setWaterAvailability(Math.max(0, water.getWaterAvailability() - 0.1)); + } + } else if (atmosphere.getRainLevel() > 0.65) { + bareDryCycles.remove(coord); + worldEffectsModule.queueEffect(new WorldEffectRequest( + WorldEffectType.SOIL_CRUST_BREAKS, coord, atmosphere.getRainLevel())); + } + + if (baseTemperature < 0.0 && warming > 0.03 + && !thawedPermafrostRegions.contains(coord)) { + int warmCycles = permafrostWarmCycles.merge(coord, 1, Integer::sum); + if (warmCycles >= 20) { + worldEffectsModule.queueEffect(new WorldEffectRequest( + WorldEffectType.PERMAFROST_THAWS, coord, 0.8)); + groundwaterLevel.merge(coord, 20.0, Double::sum); + recovery.raiseCapOneStage(); + thawedPermafrostRegions.add(coord); + pendingElevationResample.add(coord); + } + } + + region.getModuleData().put(WaterModule.MODULE_ID, water); + region.getModuleData().put(RecoveryModule.MODULE_ID, recovery); + regionManager.markDirty(region); + } } public void recordPassiveMobSpawn(RegionCoordinate coord) { diff --git a/src/main/java/com/livingworld/modules/recovery/RecoveryRegionData.java b/src/main/java/com/livingworld/modules/recovery/RecoveryRegionData.java index afd0c79..6e408af 100644 --- a/src/main/java/com/livingworld/modules/recovery/RecoveryRegionData.java +++ b/src/main/java/com/livingworld/modules/recovery/RecoveryRegionData.java @@ -82,6 +82,13 @@ public final class RecoveryRegionData { } } + /** Permanently raises the ecological ceiling by one stage. */ + public void raiseCapOneStage() { + if (maxSuccessionStage.hasNext()) { + maxSuccessionStage = maxSuccessionStage.next(); + } + } + // ------------------------------------------------------------------ // Mutation // ------------------------------------------------------------------ diff --git a/src/main/java/com/livingworld/modules/worldeffects/WorldEffectType.java b/src/main/java/com/livingworld/modules/worldeffects/WorldEffectType.java index db984c2..21630af 100644 --- a/src/main/java/com/livingworld/modules/worldeffects/WorldEffectType.java +++ b/src/main/java/com/livingworld/modules/worldeffects/WorldEffectType.java @@ -190,4 +190,19 @@ public enum WorldEffectType { /** Waterlogged wetland soil slowly develops a persistent peat profile. */ PEAT_FORMS, + + /** Deciduous leaves enter a temporary autumn colour stage. */ + LEAVES_CHANGE_COLOUR, + + /** Coloured deciduous leaves fall after the seasonal display. */ + LEAVES_FALL, + + /** Exposed drought-baked dirt develops an impermeable clay crust. */ + SOIL_CRUSTS, + + /** Heavy rain breaks clay crust back through gravel toward soil. */ + SOIL_CRUST_BREAKS, + + /** Warming frozen subsoil collapses into wet mud and meltwater. */ + PERMAFROST_THAWS, } diff --git a/src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java b/src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java index 0ee3d2a..3639489 100644 --- a/src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java +++ b/src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java @@ -162,6 +162,16 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer { deepenPlungePool(level, baseX, baseZ, request.intensity()); case PEAT_FORMS -> formPeat(level, baseX, baseZ, request.intensity()); + case LEAVES_CHANGE_COLOUR -> + changeLeafColour(level, baseX, baseZ, request.intensity()); + case LEAVES_FALL -> + dropLeaves(level, baseX, baseZ, request.intensity()); + case SOIL_CRUSTS -> + crustSoil(level, baseX, baseZ, request.intensity()); + case SOIL_CRUST_BREAKS -> + breakSoilCrust(level, baseX, baseZ, request.intensity()); + case PERMAFROST_THAWS -> + thawPermafrost(level, baseX, baseZ, request.intensity()); } } @@ -1177,6 +1187,93 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer { } } + private void changeLeafColour(ServerLevel level, int baseX, int baseZ, double intensity) { + for (int i = 0; i < Math.max(3, (int) (intensity * 10)); i++) { + int x = baseX + random.nextInt(REGION_BLOCKS); + int z = baseZ + random.nextInt(REGION_BLOCKS); + int topY = level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z) - 1; + for (int y = topY; y >= topY - 6; y--) { + BlockPos pos = new BlockPos(x, y, z); + if (!level.isLoaded(pos)) break; + var state = level.getBlockState(pos); + if (state.is(Blocks.OAK_LEAVES) || state.is(Blocks.BIRCH_LEAVES)) { + level.setBlock(pos, random.nextBoolean() + ? Blocks.ORANGE_TERRACOTTA.defaultBlockState() + : Blocks.BROWN_TERRACOTTA.defaultBlockState(), Block.UPDATE_ALL); + break; + } + } + } + } + + private void dropLeaves(ServerLevel level, int baseX, int baseZ, double intensity) { + for (int i = 0; i < Math.max(3, (int) (intensity * 10)); i++) { + int x = baseX + random.nextInt(REGION_BLOCKS); + int z = baseZ + random.nextInt(REGION_BLOCKS); + int topY = level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z) - 1; + for (int y = topY; y >= topY - 6; y--) { + BlockPos pos = new BlockPos(x, y, z); + if (!level.isLoaded(pos)) break; + if (level.getBlockState(pos).is(Blocks.ORANGE_TERRACOTTA) + || level.getBlockState(pos).is(Blocks.BROWN_TERRACOTTA)) { + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + level.sendParticles(ParticleTypes.FALLING_SPORE_BLOSSOM, + x + 0.5, y + 0.5, z + 0.5, 2, 0.4, 0.3, 0.4, 0.01); + break; + } + } + } + } + + private void crustSoil(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++) { + BlockPos pos = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS), + baseZ + random.nextInt(REGION_BLOCKS)); + if (pos != null && level.canSeeSky(pos.above()) + && (level.getBlockState(pos).is(Blocks.DIRT) + || level.getBlockState(pos).is(Blocks.COARSE_DIRT))) { + level.setBlock(pos, Blocks.TERRACOTTA.defaultBlockState(), Block.UPDATE_ALL); + } + } + } + + private void breakSoilCrust(ServerLevel level, int baseX, int baseZ, double intensity) { + for (int i = 0; i < Math.max(2, (int) (intensity * 8)); i++) { + BlockPos pos = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS), + baseZ + random.nextInt(REGION_BLOCKS)); + if (pos == null) continue; + if (level.getBlockState(pos).is(Blocks.TERRACOTTA)) { + level.setBlock(pos, Blocks.GRAVEL.defaultBlockState(), Block.UPDATE_ALL); + } else if (level.getBlockState(pos).is(Blocks.GRAVEL)) { + level.setBlock(pos, Blocks.DIRT.defaultBlockState(), Block.UPDATE_ALL); + } + } + } + + private void thawPermafrost(ServerLevel level, int baseX, int baseZ, double intensity) { + int writes = 0; + for (int i = 0; i < 10 && writes < 10; i++) { + 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 = 1; depth <= 3 && writes < 10; depth++) { + BlockPos pos = new BlockPos(x, surfaceY - depth, z); + if (!level.isLoaded(pos)) continue; + var state = level.getBlockState(pos); + if (state.is(Blocks.STONE) || state.is(Blocks.GRAVEL)) { + level.setBlock(pos, Blocks.MUD.defaultBlockState(), Block.UPDATE_ALL); + writes++; + } + } + BlockPos surface = new BlockPos(x, surfaceY, z); + if (writes < 10 && isNaturalTerrain(level.getBlockState(surface))) { + level.setBlock(surface, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + writes++; + } + } + } + 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()) {