Complete phase 2 atmospheric events

This commit is contained in:
George
2026-06-11 18:06:17 +01:00
parent 7802e4b603
commit f8995392eb
5 changed files with 326 additions and 1 deletions
@@ -216,6 +216,13 @@ public class LivingWorldMod {
if (biome.is(BiomeTags.IS_OCEAN) || biome.is(BiomeTags.IS_DEEP_OCEAN)) {
bootstrap.markOceanicRegion(coord);
}
if (biome.is(BiomeTags.IS_BADLANDS) || temp >= 2.0f) {
bootstrap.markDesertRegion(coord);
}
if (biome.is(BiomeTags.IS_BEACH) || biome.is(BiomeTags.IS_OCEAN)
|| biome.is(BiomeTags.IS_DEEP_OCEAN) || downfall > 0.8f) {
bootstrap.markCoastalOrWetlandRegion(coord);
}
}
});
NeoForge.EVENT_BUS.addListener(ServerStoppingEvent.class, event -> {
@@ -488,6 +495,36 @@ public class LivingWorldMod {
player.getX(), player.getY() + 1.0, player.getZ(),
4, 5.0, 2.0, 5.0, 0.01);
}
double humidity = bootstrap.getRegionHumidity(coord);
double temperature = bootstrap.getRegionTemperature(coord);
double pollution = metricsOpt.map(RegionMetrics::getPollutionScore).orElse(100.0);
long localTime = ambLevel.getDayTime() % 24000L;
if (bootstrap.isCoastalOrWetlandRegion(coord) && humidity > 0.70
&& (atm == null || atm.getThunderLevel() < 0.15)) {
ambLevel.sendParticles(player, ParticleTypes.CLOUD, false,
player.getX(), player.getY() + 0.5, player.getZ(),
5, 10.0, 1.5, 10.0, 0.005);
}
if ((bootstrap.isDesertRegion(coord) || pollution > 60.0)
&& localTime < 12000L) {
ambLevel.sendParticles(player, ParticleTypes.FLAME, false,
player.getX(), player.getY() + 0.2, player.getZ(),
2, 5.0, 0.1, 5.0, 0.01);
}
if (temperature < 0.15 && localTime > 13000L
&& pollution < 10.0 && (atm == null || atm.getRainLevel() < 0.1)) {
ambLevel.sendParticles(player, ParticleTypes.END_ROD, false,
player.getX(), Math.min(220.0, Math.max(180.0, player.getY() + 100.0)),
player.getZ(), 5, 24.0, 4.0, 24.0, 0.01);
}
if (bootstrap.isClimateEventActive(coord, com.livingworld.climate.ClimateEventType.BLIZZARD)) {
double angle = bootstrap.getWindAngle();
var movement = player.getDeltaMovement();
player.setDeltaMovement(movement.x + Math.cos(angle) * 0.025,
movement.y, movement.z + Math.sin(angle) * 0.025);
player.addEffect(new MobEffectInstance(
MobEffects.MOVEMENT_SLOWDOWN, 60, 0, true, false));
}
}
}
}
@@ -737,6 +774,16 @@ public class LivingWorldMod {
float temp = biome.value().getBaseTemperature();
float downfall = biome.value().getModifiedClimateSettings().downfall();
bootstrap.initializeRegionBiomeValues(coord, temp, downfall);
if (biome.is(BiomeTags.IS_OCEAN) || biome.is(BiomeTags.IS_DEEP_OCEAN)) {
bootstrap.markOceanicRegion(coord);
}
if (biome.is(BiomeTags.IS_BADLANDS) || temp >= 2.0f) {
bootstrap.markDesertRegion(coord);
}
if (biome.is(BiomeTags.IS_BEACH) || biome.is(BiomeTags.IS_OCEAN)
|| biome.is(BiomeTags.IS_DEEP_OCEAN) || downfall > 0.8f) {
bootstrap.markCoastalOrWetlandRegion(coord);
}
}
private static final int REGION_BLOCKS =
@@ -127,6 +127,9 @@ public final class LivingWorldBootstrap {
/** Ocean-floor regions; can develop submarine volcanoes regardless of surface elevation. */
private final Set<RegionCoordinate> oceanicRegions = new HashSet<>();
private final Map<RegionCoordinate, Float> regionTemperatures = new HashMap<>();
private final Map<RegionCoordinate, Float> regionDownfall = new HashMap<>();
private final Set<RegionCoordinate> desertRegions = new HashSet<>();
private final Set<RegionCoordinate> coastalWetlandRegions = new HashSet<>();
private final Map<RegionCoordinate, Integer> oceanPollutionCycles = new HashMap<>();
private final Set<RegionCoordinate> deadZoneRegions = new HashSet<>();
private final Set<RegionCoordinate> ventedRegions = new HashSet<>();
@@ -291,6 +294,9 @@ public final class LivingWorldBootstrap {
volcanicRegions.clear();
oceanicRegions.clear();
regionTemperatures.clear();
regionDownfall.clear();
desertRegions.clear();
coastalWetlandRegions.clear();
oceanPollutionCycles.clear();
deadZoneRegions.clear();
ventedRegions.clear();
@@ -886,6 +892,38 @@ public final class LivingWorldBootstrap {
return coord != null && oceanicRegions.contains(coord);
}
public void markDesertRegion(RegionCoordinate coord) {
if (coord != null) desertRegions.add(coord);
}
public void markCoastalOrWetlandRegion(RegionCoordinate coord) {
if (coord != null) coastalWetlandRegions.add(coord);
}
public boolean isDesertRegion(RegionCoordinate coord) {
return coord != null && desertRegions.contains(coord);
}
public boolean isCoastalOrWetlandRegion(RegionCoordinate coord) {
return coord != null && coastalWetlandRegions.contains(coord);
}
public float getRegionTemperature(RegionCoordinate coord) {
return regionTemperatures.getOrDefault(coord, 0.8f);
}
public float getRegionHumidity(RegionCoordinate coord) {
return regionDownfall.getOrDefault(coord, 0.4f);
}
public boolean isClimateEventActive(RegionCoordinate coord, ClimateEventType type) {
if (coord == null || type == null) return false;
for (ClimateEvent event : activeClimateEvents) {
if (event.getType() == type && event.getAffectedRegions().contains(coord)) return true;
}
return false;
}
/**
* Advances reefs, kelp forests, blooms and dead zones. All physical work is queued
* through the bounded world-effect executor.
@@ -1374,6 +1412,8 @@ public final class LivingWorldBootstrap {
RecoveryRegionData recovery = region.getModuleData().get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElse(null);
PollutionRegionData poll = region.getModuleData().get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
if (water == null || atm == null || recovery == null) continue;
float temperature = regionTemperatures.getOrDefault(coord, 0.8f);
float humidity = regionDownfall.getOrDefault(coord, 0.4f);
// --- DROUGHT trigger ---
if (water.getWaterAvailability() < 20 && water.getDroughtRisk() > 65) {
@@ -1426,6 +1466,52 @@ public final class LivingWorldBootstrap {
+ coord.x() + "," + coord.z() + ") overflowing into lowlands.");
}
}
if (poll != null && poll.getAirPollution() > 70.0 && atm.getRainLevel() > 0.25) {
ClimateEvent acidRain = new ClimateEvent(
ClimateEventType.ACID_RAIN, coord, simTick,
Math.min(1.0, poll.getAirPollution() / 100.0));
activeClimateEvents.add(acidRain);
coveredByEvent.add(coord);
pendingEventMessages.add("[LW] Acid rain falling at region ("
+ coord.x() + "," + coord.z() + ").");
continue;
}
if (temperature < 0.15f && humidity > 0.6f
&& atm.getRainLevel() > 0.45 && atm.getThunderLevel() > 0.25) {
ClimateEvent blizzard = new ClimateEvent(
ClimateEventType.BLIZZARD, coord, simTick,
Math.min(1.0, 0.4 + atm.getThunderLevel()));
activeClimateEvents.add(blizzard);
coveredByEvent.add(coord);
pendingEventMessages.add("[LW] Blizzard conditions at region ("
+ coord.x() + "," + coord.z() + ").");
continue;
}
if (desertRegions.contains(coord) && humidity < 0.25f
&& atm.getThunderLevel() > 0.25) {
ClimateEvent sandstorm = new ClimateEvent(
ClimateEventType.SANDSTORM, coord, simTick,
Math.min(1.0, 0.35 + atm.getThunderLevel()));
activeClimateEvents.add(sandstorm);
coveredByEvent.add(coord);
pendingEventMessages.add("[LW] Sandstorm crossing region ("
+ coord.x() + "," + coord.z() + ").");
continue;
}
if (humidity < 0.30f && atm.getThunderLevel() > 0.55
&& windRandom.nextDouble() < 0.20) {
ClimateEvent lightning = new ClimateEvent(
ClimateEventType.LIGHTNING_STORM, coord, simTick,
Math.min(1.0, atm.getThunderLevel()));
activeClimateEvents.add(lightning);
coveredByEvent.add(coord);
pendingEventMessages.add("[LW] Dry lightning storm at region ("
+ coord.x() + "," + coord.z() + ").");
}
}
}
@@ -1517,6 +1603,78 @@ public final class LivingWorldBootstrap {
WorldEffectType.ALGAE_BLOOM, coord, ev.getSeverity()));
}
}
case ACID_RAIN -> {
AtmosphereRegionData atm = region.getModuleData()
.get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class).orElse(null);
PollutionRegionData pollution = region.getModuleData()
.get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
SoilRegionData soil = region.getModuleData()
.get(SoilModule.MODULE_ID, SoilRegionData.class).orElse(null);
if (atm == null || pollution == null
|| atm.getRainLevel() < 0.1 || pollution.getAirPollution() < 70.0) {
shouldResolve = true;
break;
}
if (soil != null) {
soil.setContamination(Math.min(100, soil.getContamination() + 5.0));
region.getModuleData().put(SoilModule.MODULE_ID, soil);
}
if (worldEffectsModule != null) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.ACID_RAIN_DAMAGE, coord, ev.getSeverity()));
}
regionManager.markDirty(region);
}
case BLIZZARD -> {
AtmosphereRegionData atm = region.getModuleData()
.get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class).orElse(null);
float temperature = regionTemperatures.getOrDefault(coord, 0.8f);
if (temperature > 0.15f || atm == null || atm.getRainLevel() < 0.2) {
shouldResolve = true;
if (temperature > 0.0f && worldEffectsModule != null) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.SNOW_MELTS, coord, 0.7));
}
break;
}
if (worldEffectsModule != null) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.SNOW_ACCUMULATION, coord, ev.getSeverity()));
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.WATER_FREEZES, coord, ev.getSeverity()));
}
}
case SANDSTORM -> {
AtmosphereRegionData atm = region.getModuleData()
.get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class).orElse(null);
if (!desertRegions.contains(coord) || atm == null || atm.getThunderLevel() < 0.15) {
shouldResolve = true;
break;
}
if (worldEffectsModule != null) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.SAND_DEPOSIT, coord, ev.getSeverity()));
}
SoilRegionData soil = region.getModuleData()
.get(SoilModule.MODULE_ID, SoilRegionData.class).orElse(null);
if (soil != null) {
soil.setContamination(Math.min(100, soil.getContamination() + 0.1));
region.getModuleData().put(SoilModule.MODULE_ID, soil);
regionManager.markDirty(region);
}
}
case LIGHTNING_STORM -> {
AtmosphereRegionData atm = region.getModuleData()
.get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class).orElse(null);
if (atm == null || atm.getThunderLevel() < 0.25) {
shouldResolve = true;
break;
}
if (worldEffectsModule != null && windRandom.nextDouble() < 0.45) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.WILDFIRE, coord, 0.25));
}
}
}
}
@@ -1568,6 +1726,7 @@ public final class LivingWorldBootstrap {
public void initializeRegionBiomeValues(RegionCoordinate coord, float temp, float downfall) {
if (!serverReady || coord == null) return;
regionTemperatures.put(coord, temp);
regionDownfall.put(coord, downfall);
regionManager.resolve(coord).ifPresent(region -> {
SoilRegionData soil = region.getModuleData()
.get(SoilModule.MODULE_ID, SoilRegionData.class).orElse(null);
@@ -6,7 +6,11 @@ public enum ClimateEventType {
WILDFIRE("Wildfire", "Fire spreading through drought-stressed forest regions"),
FLOOD("Flood", "Heavy rainfall overwhelming low-lying terrain"),
VOLCANIC_ERUPTION("Volcanic Eruption", "Lava flows and ash clouds from an active volcano"),
ALGAE_BLOOM("Algae Bloom", "Nutrient pollution causing a bloom and aquatic dead zone");
ALGAE_BLOOM("Algae Bloom", "Nutrient pollution causing a bloom and aquatic dead zone"),
ACID_RAIN("Acid Rain", "Polluted rainfall damaging soil, stone and vegetation"),
BLIZZARD("Blizzard", "Wind-driven snow accumulation and freezing water"),
SANDSTORM("Sandstorm", "Dry high winds stripping plants and depositing sand"),
LIGHTNING_STORM("Lightning Storm", "Dry thunderstorm producing isolated ignition strikes");
private final String displayName;
private final String description;
@@ -130,4 +130,19 @@ public enum WorldEffectType {
/** Volcanic seafloor activity creates a permanent bubbling vent field. */
HYDROTHERMAL_VENT,
/** Polluted rainfall weathers exposed rock and kills surface vegetation. */
ACID_RAIN_DAMAGE,
/** Blizzard snowfall builds bounded snow layers on exposed terrain. */
SNOW_ACCUMULATION,
/** Blizzard conditions freeze exposed standing water. */
WATER_FREEZES,
/** Warm conditions remove accumulated seasonal snow one layer at a time. */
SNOW_MELTS,
/** Dry high winds deposit sand and strip fragile surface plants. */
SAND_DEPOSIT,
}
@@ -103,6 +103,16 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
deadZone(level, baseX, baseZ, request.intensity());
case HYDROTHERMAL_VENT ->
hydrothermalVent(level, baseX, baseZ, request.intensity());
case ACID_RAIN_DAMAGE ->
acidRainDamage(level, baseX, baseZ, request.intensity());
case SNOW_ACCUMULATION ->
accumulateSnow(level, baseX, baseZ, request.intensity());
case WATER_FREEZES ->
freezeWater(level, baseX, baseZ, request.intensity());
case SNOW_MELTS ->
meltSnow(level, baseX, baseZ, request.intensity());
case SAND_DEPOSIT ->
depositSand(level, baseX, baseZ, request.intensity());
}
}
@@ -711,6 +721,96 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
}
}
private void acidRainDamage(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())) continue;
var state = level.getBlockState(pos);
if (state.is(Blocks.STONE) || state.is(Blocks.COBBLESTONE)) {
level.setBlock(pos, Blocks.GRAVEL.defaultBlockState(), Block.UPDATE_ALL);
} else if (state.is(Blocks.GRASS_BLOCK)) {
level.setBlock(pos, Blocks.DIRT.defaultBlockState(), Block.UPDATE_ALL);
}
BlockPos above = pos.above();
if (level.getBlockState(above).is(BlockTags.FLOWERS)
|| level.getBlockState(above).is(Blocks.SHORT_GRASS)
|| level.getBlockState(above).is(BlockTags.SAPLINGS)) {
level.setBlock(above, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
}
}
}
private void accumulateSnow(ServerLevel level, int baseX, int baseZ, double intensity) {
if (random.nextDouble() > 0.35) return;
for (int i = 0; i < Math.max(2, (int) (intensity * 8)); i++) {
BlockPos surface = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS),
baseZ + random.nextInt(REGION_BLOCKS));
if (surface == null) continue;
BlockPos above = surface.above();
if (!level.canSeeSky(above)) continue;
var state = level.getBlockState(above);
if (state.isAir()) {
level.setBlock(above, Blocks.SNOW.defaultBlockState(), Block.UPDATE_ALL);
} else if (state.is(Blocks.SNOW)
&& state.getValue(net.minecraft.world.level.block.SnowLayerBlock.LAYERS) < 4) {
level.setBlock(above, state.setValue(
net.minecraft.world.level.block.SnowLayerBlock.LAYERS,
state.getValue(net.minecraft.world.level.block.SnowLayerBlock.LAYERS) + 1),
Block.UPDATE_ALL);
}
}
}
private void freezeWater(ServerLevel level, int baseX, int baseZ, double intensity) {
if (random.nextDouble() > 0.25) return;
for (int i = 0; i < Math.max(1, (int) (intensity * 6)); 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() && level.canSeeSky(pos.above())) {
level.setBlock(pos, Blocks.ICE.defaultBlockState(), Block.UPDATE_ALL);
}
}
}
private void meltSnow(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 || !level.getBlockState(pos).is(Blocks.SNOW)) continue;
var state = level.getBlockState(pos);
int layers = state.getValue(net.minecraft.world.level.block.SnowLayerBlock.LAYERS);
level.setBlock(pos, layers > 1
? state.setValue(net.minecraft.world.level.block.SnowLayerBlock.LAYERS, layers - 1)
: Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
}
}
private void depositSand(ServerLevel level, int baseX, int baseZ, double intensity) {
if (random.nextDouble() > 0.30) return;
for (int i = 0; i < Math.max(2, (int) (intensity * 8)); i++) {
BlockPos surface = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS),
baseZ + random.nextInt(REGION_BLOCKS));
if (surface == null) continue;
BlockPos above = surface.above();
var aboveState = level.getBlockState(above);
if (aboveState.is(BlockTags.FLOWERS) || aboveState.is(Blocks.SHORT_GRASS)
|| aboveState.is(Blocks.DEAD_BUSH)) {
level.setBlock(above, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
} else if (aboveState.isAir() && level.canSeeSky(above)
&& (level.getBlockState(surface).is(Blocks.SAND)
|| level.getBlockState(surface).is(Blocks.RED_SAND)
|| level.getBlockState(surface).is(Blocks.SANDSTONE)
|| level.getBlockState(surface).is(Blocks.TERRACOTTA))) {
level.setBlock(above, Blocks.SAND.defaultBlockState(), Block.UPDATE_ALL);
}
}
}
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()) {