Add VEGETATION_DIES world effect — trees and plants visibly die under bad conditions
WorldEffectType: new VEGETATION_DIES variant. WorldEffectsModule: emits VEGETATION_DIES whenever soilQuality < 20 OR pollutionScore > 30, mirroring VegetationModule's bad-conditions gate so block changes stay in sync with the data-layer die-off. Intensity is the max of the soil-shortage and pollution-excess fractions. NeoForgeWorldEffectExecutor.killVegetation(): - Detects tree canopy by comparing MOTION_BLOCKING (includes leaves/logs) height vs MOTION_BLOCKING_NO_LEAVES (ground) height. - When canopy is present: strips all leaf blocks downward from the canopy top; at intensity > 0.3 also removes upper trunk log sections, leaving bottom two blocks as a visible stump. - When no canopy: clears surface plants (short grass, fern, tall grass, large fern, saplings, dandelion, poppy). Previously the simulation correctly tracked vegetation pressure dropping to zero but the Minecraft world remained a full forest, so there was no visible feedback for degradation. /lw demo degrade now visibly kills the forest canopy while the succession stage regresses. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user