diff --git a/src/main/java/com/livingworld/modules/worldeffects/WorldEffectType.java b/src/main/java/com/livingworld/modules/worldeffects/WorldEffectType.java index 27e4022..0b062f8 100644 --- a/src/main/java/com/livingworld/modules/worldeffects/WorldEffectType.java +++ b/src/main/java/com/livingworld/modules/worldeffects/WorldEffectType.java @@ -46,6 +46,13 @@ public enum WorldEffectType { */ WILDFIRE, + /** + * Sustained bad conditions (low soil quality or high pollution) cause living + * vegetation to die visibly: leaves are removed, upper log sections stripped to + * stumps, and surface plants replaced with dead material. + */ + VEGETATION_DIES, + /** * Heavy regional rain on low-succession (barren / sparse-grass) terrain causes * water to pool in low-lying spots. The platform adapter places water source blocks diff --git a/src/main/java/com/livingworld/modules/worldeffects/WorldEffectsModule.java b/src/main/java/com/livingworld/modules/worldeffects/WorldEffectsModule.java index 0989561..13f49d6 100644 --- a/src/main/java/com/livingworld/modules/worldeffects/WorldEffectsModule.java +++ b/src/main/java/com/livingworld/modules/worldeffects/WorldEffectsModule.java @@ -63,6 +63,9 @@ public final class WorldEffectsModule implements SimulationModule { // Soil defaults to 60 so the gate is set at 75 to avoid blocking early effects. private static final double GRASS_DEGRADE_POLLUTION_MIN = 15.0; private static final double GRASS_DEGRADE_SOIL_MAX = 75.0; + // Vegetation die-off: mirrors VegetationModule's bad-conditions thresholds. + private static final double DIEOFF_SOIL_MAX = 20.0; + private static final double DIEOFF_POLLUTION_MIN = 30.0; private static final double VEG_SPREAD_VEG_MIN = 60.0; private static final double VEG_SPREAD_SOIL_MIN = 50.0; private static final double SAPLING_SLOW_LOGGING_MIN = 50.0; @@ -206,7 +209,26 @@ public final class WorldEffectsModule implements SimulationModule { emitted = true; } - // --- Effect 7: rain pools in arid low-succession terrain --- + // --- Effect 7: vegetation dies under bad soil or heavy pollution --- + // Mirrors the bad-conditions gate in VegetationModule so block changes + // stay in sync with the data-layer die-off. + boolean badSoil = m.getSoilQuality() < DIEOFF_SOIL_MAX; + boolean badPollution = m.getPollutionScore() > DIEOFF_POLLUTION_MIN; + if (badSoil || badPollution) { + double intensitySoil = badSoil + ? computeIntensity(DIEOFF_SOIL_MAX - m.getSoilQuality(), DIEOFF_SOIL_MAX) + : 0.0; + double intensityPoll = badPollution + ? computeIntensity(m.getPollutionScore() - DIEOFF_POLLUTION_MIN, 70.0) + : 0.0; + double dieoffIntensity = Math.max(intensitySoil, intensityPoll); + emit(new WorldEffectRequest( + WorldEffectType.VEGETATION_DIES, region.getCoordinate(), + Math.max(0.1, dieoffIntensity))); + emitted = true; + } + + // --- Effect 8: rain pools in arid low-succession terrain --- // Heavy regional rain on barren/sparse-grass land cannot drain into vegetation; // water collects in depressions, forming puddles visible as actual water blocks. if (recovery != null && atm != null diff --git a/src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java b/src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java index 19d0c60..de4e539 100644 --- a/src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java +++ b/src/main/java/com/livingworld/platform/neoforge/NeoForgeWorldEffectExecutor.java @@ -70,6 +70,8 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer { case SAPLING_GROWTH_SLOWED -> {} // requires mixin; deferred case WILDFIRE -> igniteVegetation(level, baseX, baseZ, request.intensity()); + case VEGETATION_DIES -> + killVegetation(level, baseX, baseZ, request.intensity()); case WATER_POOL_FORMS -> formWaterPool(level, baseX, baseZ, request.intensity()); } @@ -188,6 +190,55 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer { } } + /** + * Strips leaves and upper trunk sections from trees, and removes surface plants. + * Uses MOTION_BLOCKING (includes canopy) vs MOTION_BLOCKING_NO_LEAVES (ground) to + * detect trees — when the two heights differ, a canopy is present. + * At low intensity: leaves only. At high intensity: leaves + upper log sections. + */ + private void killVegetation(ServerLevel level, int baseX, int baseZ, double intensity) { + int attempts = Math.max(2, (int)(intensity * BLOCK_ATTEMPTS * 2)); + for (int i = 0; i < attempts; i++) { + int x = baseX + random.nextInt(REGION_BLOCKS); + int z = baseZ + random.nextInt(REGION_BLOCKS); + + int canopyY = level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z) - 1; + int groundY = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1; + + if (canopyY > groundY) { + // A tree canopy is present — strip downward from canopy. + for (int y = canopyY; y > groundY; y--) { + BlockPos pos = new BlockPos(x, y, z); + if (!level.isLoaded(pos)) break; + var state = level.getBlockState(pos); + if (state.is(BlockTags.LEAVES)) { + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + } else if (state.is(BlockTags.LOGS) && y > groundY + 1 && intensity > 0.3) { + // Remove upper trunk, leave bottom two blocks as a stump. + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + } + } + } else { + // No canopy — clear surface plants (grass, flowers, saplings). + if (groundY < level.getMinBuildHeight()) continue; + BlockPos surfacePos = new BlockPos(x, groundY, z); + if (!level.isLoaded(surfacePos)) continue; + BlockPos abovePos = surfacePos.above(); + if (!level.isLoaded(abovePos)) continue; + var above = level.getBlockState(abovePos); + if (above.is(Blocks.SHORT_GRASS) || above.is(Blocks.FERN) + || above.is(Blocks.TALL_GRASS) || above.is(Blocks.LARGE_FERN) + || above.is(BlockTags.SAPLINGS) + || above.is(Blocks.DANDELION) || above.is(Blocks.POPPY)) { + level.setBlock(abovePos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + } + } + } + LivingWorldLogger.info(DiagnosticCategory.SIMULATION, + "WorldEffect VEGETATION_DIES intensity=" + String.format("%.2f", intensity) + + " at (" + baseX + "," + baseZ + ")"); + } + /** * Places water source blocks in the lowest depression found among random surface samples. * Fires ~5% of times it is called so pools build gradually rather than flooding the region.