Add regional atmosphere: vegetation scrub, ecosystem-driven rain, per-player weather
- New AtmosphereModule (9th in pipeline) derives per-region rain/thunder from ecosystem health and pollution score, smoothly interpolating each sim cycle. - Tree canopy scrubs air pollution: treePressure × 0.003 removed per cycle, creating a direct incentive to maintain forests. - Rain level (0–1) targets ecosystem health / 100 × 0.85; global MC rain adds a 0.15 bias so natural weather still matters. - Thunder level targets pollutionScore / 100 × 0.8; acid rain fires when thunder > 0.4 AND pollution > 20, draining soil fertility and water. - Regional drought: rain < 0.2 raises drought risk proportionally. - Per-player ClientboundGameEventPacket (RAIN_LEVEL_CHANGE, THUNDER_LEVEL_CHANGE, START_RAINING, STOP_RAINING) sent each player-check cycle so each region has an independent sky without client-side mixins. - HUD extended with Rain% and Storm% fields when atmosphere data is present. - Removed global applyWeatherFeedback() from bootstrap; AtmosphereModule owns all rain/drought/acid-rain simulation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,9 +33,11 @@ import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
|||||||
import net.minecraft.world.level.chunk.LevelChunk;
|
import net.minecraft.world.level.chunk.LevelChunk;
|
||||||
import net.minecraft.world.level.levelgen.Heightmap;
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
import net.minecraft.world.level.storage.LevelResource;
|
import net.minecraft.world.level.storage.LevelResource;
|
||||||
|
import net.minecraft.network.protocol.game.ClientboundGameEventPacket;
|
||||||
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||||
|
|
||||||
import com.livingworld.bootstrap.LivingWorldBootstrap;
|
import com.livingworld.bootstrap.LivingWorldBootstrap;
|
||||||
|
import com.livingworld.modules.atmosphere.AtmosphereRegionData;
|
||||||
import com.livingworld.core.LivingWorldConstants;
|
import com.livingworld.core.LivingWorldConstants;
|
||||||
import com.livingworld.debug.DiagnosticCategory;
|
import com.livingworld.debug.DiagnosticCategory;
|
||||||
import com.livingworld.debug.LivingWorldLogger;
|
import com.livingworld.debug.LivingWorldLogger;
|
||||||
@@ -85,6 +87,7 @@ public class LivingWorldMod {
|
|||||||
private int furnaceScanTick = 0;
|
private int furnaceScanTick = 0;
|
||||||
private int playerCheckTick = 0;
|
private int playerCheckTick = 0;
|
||||||
private final Map<UUID, RegionCoordinate> playerRegionCache = new HashMap<>();
|
private final Map<UUID, RegionCoordinate> playerRegionCache = new HashMap<>();
|
||||||
|
private final Map<UUID, Boolean> playerRainState = new HashMap<>();
|
||||||
private final Set<RegionCoordinate> biomeInitialized = new HashSet<>();
|
private final Set<RegionCoordinate> biomeInitialized = new HashSet<>();
|
||||||
|
|
||||||
public LivingWorldMod(IEventBus eventBus) {
|
public LivingWorldMod(IEventBus eventBus) {
|
||||||
@@ -115,6 +118,7 @@ public class LivingWorldMod {
|
|||||||
bootstrap.onServerStopping();
|
bootstrap.onServerStopping();
|
||||||
bootstrap.setOverworldRaining(null);
|
bootstrap.setOverworldRaining(null);
|
||||||
playerRegionCache.clear();
|
playerRegionCache.clear();
|
||||||
|
playerRainState.clear();
|
||||||
biomeInitialized.clear();
|
biomeInitialized.clear();
|
||||||
this.minecraftServer = null;
|
this.minecraftServer = null;
|
||||||
});
|
});
|
||||||
@@ -235,13 +239,33 @@ public class LivingWorldMod {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Compass HUD — display region health while holding a compass, or debug HUD is on.
|
// Regional weather — send per-player rain/thunder packets so each region
|
||||||
|
// has its own sky independently of global Minecraft weather.
|
||||||
|
AtmosphereRegionData atm = bootstrap.getRegionalWeather(coord).orElse(null);
|
||||||
|
if (atm != null) {
|
||||||
|
float rainLevel = (float) atm.getRainLevel();
|
||||||
|
float thunderLevel = (float) atm.getThunderLevel();
|
||||||
|
boolean wasRaining = playerRainState.getOrDefault(player.getUUID(), false);
|
||||||
|
boolean nowRaining = rainLevel > 0.1f;
|
||||||
|
if (nowRaining != wasRaining) {
|
||||||
|
player.connection.send(nowRaining
|
||||||
|
? new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0f)
|
||||||
|
: new ClientboundGameEventPacket(ClientboundGameEventPacket.STOP_RAINING, 0f));
|
||||||
|
playerRainState.put(player.getUUID(), nowRaining);
|
||||||
|
}
|
||||||
|
player.connection.send(new ClientboundGameEventPacket(
|
||||||
|
ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, rainLevel));
|
||||||
|
player.connection.send(new ClientboundGameEventPacket(
|
||||||
|
ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, thunderLevel));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compass HUD — display region health while holding a compass, or debug HUD is on.
|
||||||
boolean showHud = player.getMainHandItem().is(Items.COMPASS)
|
boolean showHud = player.getMainHandItem().is(Items.COMPASS)
|
||||||
|| player.getOffhandItem().is(Items.COMPASS)
|
|| player.getOffhandItem().is(Items.COMPASS)
|
||||||
|| bootstrap.isHudEnabled(player.getUUID());
|
|| bootstrap.isHudEnabled(player.getUUID());
|
||||||
if (showHud) {
|
if (showHud) {
|
||||||
Optional<RegionMetrics> metricsOpt = bootstrap.getMetricsAt(dimId, player.getX(), player.getZ());
|
Optional<RegionMetrics> metricsOpt = bootstrap.getMetricsAt(dimId, player.getX(), player.getZ());
|
||||||
metricsOpt.ifPresent(m -> player.displayClientMessage(buildHud(coord, m), true));
|
metricsOpt.ifPresent(m -> player.displayClientMessage(buildHud(coord, m, atm), true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,8 +296,8 @@ public class LivingWorldMod {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Step 2: Builds the action-bar HUD component for a player holding a compass. */
|
/** Builds the action-bar HUD component for a player holding a compass or with /lw hud on. */
|
||||||
private Component buildHud(RegionCoordinate coord, RegionMetrics m) {
|
private Component buildHud(RegionCoordinate coord, RegionMetrics m, AtmosphereRegionData atm) {
|
||||||
double eco = m.getEcosystemHealth();
|
double eco = m.getEcosystemHealth();
|
||||||
double poll = m.getPollutionScore();
|
double poll = m.getPollutionScore();
|
||||||
double soil = m.getSoilQuality();
|
double soil = m.getSoilQuality();
|
||||||
@@ -284,7 +308,7 @@ public class LivingWorldMod {
|
|||||||
ChatFormatting soilCol = soil > 50 ? ChatFormatting.GREEN : soil > 25 ? ChatFormatting.YELLOW : ChatFormatting.RED;
|
ChatFormatting soilCol = soil > 50 ? ChatFormatting.GREEN : soil > 25 ? ChatFormatting.YELLOW : ChatFormatting.RED;
|
||||||
ChatFormatting watCol = wat > 50 ? ChatFormatting.GREEN : wat > 25 ? ChatFormatting.YELLOW : ChatFormatting.RED;
|
ChatFormatting watCol = wat > 50 ? ChatFormatting.GREEN : wat > 25 ? ChatFormatting.YELLOW : ChatFormatting.RED;
|
||||||
|
|
||||||
return Component.empty()
|
var line = Component.empty()
|
||||||
.append(Component.literal("[LW] ").withStyle(ChatFormatting.GOLD))
|
.append(Component.literal("[LW] ").withStyle(ChatFormatting.GOLD))
|
||||||
.append(Component.literal(String.format("(%d,%d) ", coord.x(), coord.z())).withStyle(ChatFormatting.GRAY))
|
.append(Component.literal(String.format("(%d,%d) ", coord.x(), coord.z())).withStyle(ChatFormatting.GRAY))
|
||||||
.append(Component.literal("Eco:").withStyle(ChatFormatting.WHITE))
|
.append(Component.literal("Eco:").withStyle(ChatFormatting.WHITE))
|
||||||
@@ -295,5 +319,19 @@ public class LivingWorldMod {
|
|||||||
.append(Component.literal(String.format("%.0f ", soil)).withStyle(soilCol))
|
.append(Component.literal(String.format("%.0f ", soil)).withStyle(soilCol))
|
||||||
.append(Component.literal("Wat:").withStyle(ChatFormatting.WHITE))
|
.append(Component.literal("Wat:").withStyle(ChatFormatting.WHITE))
|
||||||
.append(Component.literal(String.format("%.0f", wat)).withStyle(watCol));
|
.append(Component.literal(String.format("%.0f", wat)).withStyle(watCol));
|
||||||
|
|
||||||
|
if (atm != null) {
|
||||||
|
double rain = atm.getRainLevel() * 100;
|
||||||
|
double thunder = atm.getThunderLevel() * 100;
|
||||||
|
ChatFormatting rainCol = rain > 50 ? ChatFormatting.AQUA : rain > 20 ? ChatFormatting.YELLOW : ChatFormatting.RED;
|
||||||
|
line.append(Component.literal(" Rain:").withStyle(ChatFormatting.WHITE))
|
||||||
|
.append(Component.literal(String.format("%.0f%%", rain)).withStyle(rainCol));
|
||||||
|
if (thunder > 15) {
|
||||||
|
ChatFormatting stormCol = thunder > 50 ? ChatFormatting.RED : ChatFormatting.YELLOW;
|
||||||
|
line.append(Component.literal(" Storm:").withStyle(ChatFormatting.WHITE))
|
||||||
|
.append(Component.literal(String.format("%.0f%%", thunder)).withStyle(stormCol));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return line;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ import com.livingworld.modules.vegetation.VegetationModule;
|
|||||||
import com.livingworld.modules.vegetation.VegetationRegionData;
|
import com.livingworld.modules.vegetation.VegetationRegionData;
|
||||||
import com.livingworld.modules.water.WaterModule;
|
import com.livingworld.modules.water.WaterModule;
|
||||||
import com.livingworld.modules.water.WaterRegionData;
|
import com.livingworld.modules.water.WaterRegionData;
|
||||||
|
import com.livingworld.modules.atmosphere.AtmosphereModule;
|
||||||
|
import com.livingworld.modules.atmosphere.AtmosphereRegionData;
|
||||||
import com.livingworld.modules.worldeffects.WorldEffectsModule;
|
import com.livingworld.modules.worldeffects.WorldEffectsModule;
|
||||||
import com.livingworld.platform.BlockBreakInfo;
|
import com.livingworld.platform.BlockBreakInfo;
|
||||||
import com.livingworld.platform.PlatformAdapter;
|
import com.livingworld.platform.PlatformAdapter;
|
||||||
@@ -63,11 +65,6 @@ import net.minecraft.commands.CommandSourceStack;
|
|||||||
*/
|
*/
|
||||||
public final class LivingWorldBootstrap {
|
public final class LivingWorldBootstrap {
|
||||||
|
|
||||||
private static final double RAIN_MOISTURE_GAIN = 0.3;
|
|
||||||
private static final double RAIN_DROUGHT_RELIEF = 0.15;
|
|
||||||
private static final double DRY_DROUGHT_INCREASE = 0.05;
|
|
||||||
private static final double ACID_RAIN_THRESHOLD = 20.0;
|
|
||||||
private static final double ACID_FERTILITY_DRAIN = 0.0005;
|
|
||||||
private static final double WIND_BOOST = 0.5;
|
private static final double WIND_BOOST = 0.5;
|
||||||
private static final double WIND_DRIFT_MAX = 0.05; // radians per sim cycle
|
private static final double WIND_DRIFT_MAX = 0.05; // radians per sim cycle
|
||||||
|
|
||||||
@@ -82,6 +79,7 @@ public final class LivingWorldBootstrap {
|
|||||||
private ModuleRegistry moduleRegistry;
|
private ModuleRegistry moduleRegistry;
|
||||||
private SimulationManager simulationManager;
|
private SimulationManager simulationManager;
|
||||||
private WorldEffectsModule worldEffectsModule;
|
private WorldEffectsModule worldEffectsModule;
|
||||||
|
private AtmosphereModule atmosphereModule;
|
||||||
private BooleanSupplier overworldRaining = () -> false;
|
private BooleanSupplier overworldRaining = () -> false;
|
||||||
private boolean initialized;
|
private boolean initialized;
|
||||||
private boolean serverReady;
|
private boolean serverReady;
|
||||||
@@ -327,7 +325,6 @@ public final class LivingWorldBootstrap {
|
|||||||
long previousSimulationTick = simulationManager.getSimulationTickCounter();
|
long previousSimulationTick = simulationManager.getSimulationTickCounter();
|
||||||
simulationManager.onMinecraftServerTick();
|
simulationManager.onMinecraftServerTick();
|
||||||
if (simulationManager.getSimulationTickCounter() != previousSimulationTick) {
|
if (simulationManager.getSimulationTickCounter() != previousSimulationTick) {
|
||||||
applyWeatherFeedback();
|
|
||||||
spreadPollutionAcrossRegions();
|
spreadPollutionAcrossRegions();
|
||||||
regionManager.saveDirtyRegions();
|
regionManager.saveDirtyRegions();
|
||||||
}
|
}
|
||||||
@@ -379,43 +376,11 @@ public final class LivingWorldBootstrap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyWeatherFeedback() {
|
/** Returns the current atmospheric state for a region, if it has been initialised. */
|
||||||
boolean raining = overworldRaining.getAsBoolean();
|
public Optional<AtmosphereRegionData> getRegionalWeather(RegionCoordinate coord) {
|
||||||
for (Region region : regionManager.getActiveRegions()) {
|
if (!serverReady || coord == null) return Optional.empty();
|
||||||
if (!"minecraft:overworld".equals(region.getCoordinate().dimensionId())) {
|
return regionManager.resolve(coord)
|
||||||
continue;
|
.flatMap(r -> r.getModuleData().get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class));
|
||||||
}
|
|
||||||
WaterRegionData water = region.getModuleData()
|
|
||||||
.get(WaterModule.MODULE_ID, WaterRegionData.class)
|
|
||||||
.orElse(null);
|
|
||||||
if (water == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (raining) {
|
|
||||||
water.setWaterAvailability(water.getWaterAvailability() + RAIN_MOISTURE_GAIN);
|
|
||||||
water.setDroughtRisk(water.getDroughtRisk() - RAIN_DROUGHT_RELIEF);
|
|
||||||
|
|
||||||
// Acid rain: polluted rainfall degrades soil and water quality.
|
|
||||||
double pollutionScore = region.getMetrics().getPollutionScore();
|
|
||||||
if (pollutionScore > ACID_RAIN_THRESHOLD) {
|
|
||||||
double acidStrength = (pollutionScore - ACID_RAIN_THRESHOLD) * ACID_FERTILITY_DRAIN;
|
|
||||||
SoilRegionData soil = region.getModuleData()
|
|
||||||
.get(SoilModule.MODULE_ID, SoilRegionData.class)
|
|
||||||
.orElse(null);
|
|
||||||
if (soil != null) {
|
|
||||||
soil.setFertility(Math.max(0, soil.getFertility() - acidStrength));
|
|
||||||
soil.setContamination(Math.min(100, soil.getContamination() + acidStrength * 0.5));
|
|
||||||
region.getModuleData().put(SoilModule.MODULE_ID, soil);
|
|
||||||
}
|
|
||||||
water.setWaterAvailability(
|
|
||||||
Math.max(0, water.getWaterAvailability() - acidStrength * 0.3));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
water.setDroughtRisk(water.getDroughtRisk() + DRY_DROUGHT_INCREASE);
|
|
||||||
}
|
|
||||||
region.getModuleData().put(WaterModule.MODULE_ID, water);
|
|
||||||
regionManager.markDirty(region);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Toggles the debug HUD for the given player. Returns true if HUD is now enabled. */
|
/** Toggles the debug HUD for the given player. Returns true if HUD is now enabled. */
|
||||||
@@ -665,6 +630,18 @@ public final class LivingWorldBootstrap {
|
|||||||
r.readDouble("stress", 20.0),
|
r.readDouble("stress", 20.0),
|
||||||
r.readDouble("resilience", 50.0),
|
r.readDouble("resilience", 50.0),
|
||||||
r.readDouble("recoveryRate", 5.0))));
|
r.readDouble("recoveryRate", 5.0))));
|
||||||
|
|
||||||
|
service.registerModuleCodec(
|
||||||
|
AtmosphereModule.MODULE_ID,
|
||||||
|
(data, w) -> {
|
||||||
|
AtmosphereRegionData d = data.get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class)
|
||||||
|
.orElseGet(AtmosphereRegionData::defaults);
|
||||||
|
w.writeDouble("rainLevel", d.getRainLevel());
|
||||||
|
w.writeDouble("thunderLevel", d.getThunderLevel());
|
||||||
|
},
|
||||||
|
(r, data) -> data.put(AtmosphereModule.MODULE_ID, new AtmosphereRegionData(
|
||||||
|
r.readDouble("rainLevel", 0.4),
|
||||||
|
r.readDouble("thunderLevel", 0.0))));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -684,10 +661,12 @@ public final class LivingWorldBootstrap {
|
|||||||
registry.register(new EcosystemModule());
|
registry.register(new EcosystemModule());
|
||||||
worldEffectsModule = new WorldEffectsModule();
|
worldEffectsModule = new WorldEffectsModule();
|
||||||
registry.register(worldEffectsModule);
|
registry.register(worldEffectsModule);
|
||||||
|
atmosphereModule = new AtmosphereModule(() -> overworldRaining.getAsBoolean());
|
||||||
|
registry.register(atmosphereModule);
|
||||||
LivingWorldLogger.info(
|
LivingWorldLogger.info(
|
||||||
DiagnosticCategory.BOOTSTRAP,
|
DiagnosticCategory.BOOTSTRAP,
|
||||||
"Registered 8 ecosystem modules (pollution → soil → water → vegetation"
|
"Registered 9 ecosystem modules (pollution → soil → water → vegetation"
|
||||||
+ " → resources → recovery → ecosystem → worldeffects).");
|
+ " → resources → recovery → ecosystem → worldeffects → atmosphere).");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requireInitialized() {
|
private void requireInitialized() {
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package com.livingworld.modules.atmosphere;
|
||||||
|
|
||||||
|
import com.livingworld.data.serialization.PersistenceReader;
|
||||||
|
import com.livingworld.data.serialization.PersistenceWriter;
|
||||||
|
import com.livingworld.events.LivingWorldEvent;
|
||||||
|
import com.livingworld.modules.ModuleContext;
|
||||||
|
import com.livingworld.modules.ModuleMetadata;
|
||||||
|
import com.livingworld.modules.ModuleUpdateResult;
|
||||||
|
import com.livingworld.modules.RegionUpdateContext;
|
||||||
|
import com.livingworld.modules.ServerContext;
|
||||||
|
import com.livingworld.modules.SimulationModule;
|
||||||
|
import com.livingworld.modules.pollution.PollutionModule;
|
||||||
|
import com.livingworld.modules.pollution.PollutionRegionData;
|
||||||
|
import com.livingworld.modules.soil.SoilModule;
|
||||||
|
import com.livingworld.modules.soil.SoilRegionData;
|
||||||
|
import com.livingworld.modules.vegetation.VegetationModule;
|
||||||
|
import com.livingworld.modules.vegetation.VegetationRegionData;
|
||||||
|
import com.livingworld.modules.water.WaterModule;
|
||||||
|
import com.livingworld.modules.water.WaterRegionData;
|
||||||
|
import com.livingworld.regions.Region;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drives per-region weather from ecosystem state and applies atmospheric feedback
|
||||||
|
* to other simulation layers.
|
||||||
|
*
|
||||||
|
* <h3>Pipeline position</h3>
|
||||||
|
* Runs last (after WorldEffects) so ecosystem health and pollution scores are final
|
||||||
|
* before weather targets are computed.
|
||||||
|
*
|
||||||
|
* <h3>Per-cycle rules</h3>
|
||||||
|
* <ol>
|
||||||
|
* <li>Tree canopy scrubs air pollution: {@code airPollution -= treePressure × TREE_SCRUB_RATE}.
|
||||||
|
* <li>Target rain level is derived from ecosystem health (healthy = wet, degraded = drought).
|
||||||
|
* Global Minecraft rain adds a small bias so natural weather still matters.
|
||||||
|
* <li>Target thunder level is derived from pollution (polluted = stormy).
|
||||||
|
* <li>Rain and thunder smoothly interpolate toward their targets each cycle.
|
||||||
|
* <li>Rain fills water availability; drought raises drought risk.
|
||||||
|
* <li>High thunder + high pollution triggers acid rain (drains soil fertility).
|
||||||
|
* </ol>
|
||||||
|
*/
|
||||||
|
public final class AtmosphereModule implements SimulationModule {
|
||||||
|
|
||||||
|
public static final String MODULE_ID = "atmosphere";
|
||||||
|
|
||||||
|
// Tree scrub: treePressure (0-100) × rate removed from air pollution per cycle.
|
||||||
|
private static final double TREE_SCRUB_RATE = 0.003;
|
||||||
|
|
||||||
|
// Rain target ceiling when ecosystem is pristine (no global rain).
|
||||||
|
private static final double MAX_RAIN_FROM_ECO = 0.85;
|
||||||
|
// Additive bias when Minecraft's overworld is actually raining.
|
||||||
|
private static final double GLOBAL_RAIN_BIAS = 0.15;
|
||||||
|
|
||||||
|
// Smoothing rates — weather changes slowly, not instantly.
|
||||||
|
private static final double RAIN_SMOOTHING = 0.05;
|
||||||
|
private static final double THUNDER_SMOOTHING = 0.03;
|
||||||
|
|
||||||
|
// Rain < this → drought conditions worsen each cycle.
|
||||||
|
private static final double DROUGHT_THRESHOLD = 0.2;
|
||||||
|
private static final double RAIN_MOISTURE_GAIN = 0.3;
|
||||||
|
private static final double RAIN_DROUGHT_RELIEF = 0.15;
|
||||||
|
private static final double DRY_DROUGHT_INCREASE = 0.05;
|
||||||
|
|
||||||
|
// Acid rain fires when BOTH thunder and pollution exceed these thresholds.
|
||||||
|
private static final double ACID_THUNDER_THRESHOLD = 0.4;
|
||||||
|
private static final double ACID_POLL_THRESHOLD = 20.0;
|
||||||
|
private static final double ACID_FERTILITY_DRAIN = 0.0005;
|
||||||
|
|
||||||
|
private static final ModuleMetadata METADATA = new ModuleMetadata(
|
||||||
|
MODULE_ID,
|
||||||
|
"Atmosphere",
|
||||||
|
"1.0.0",
|
||||||
|
"Per-region weather driven by ecosystem health and pollution. Trees scrub air pollution.",
|
||||||
|
"9",
|
||||||
|
List.of(),
|
||||||
|
List.of(),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false);
|
||||||
|
|
||||||
|
private final BooleanSupplier globalRaining;
|
||||||
|
|
||||||
|
public AtmosphereModule(BooleanSupplier globalRaining) {
|
||||||
|
this.globalRaining = globalRaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public String getModuleId() { return MODULE_ID; }
|
||||||
|
@Override public ModuleMetadata getMetadata() { return METADATA; }
|
||||||
|
@Override public void initialize(ModuleContext ctx) {}
|
||||||
|
@Override public void onServerStarted(ServerContext ctx) {}
|
||||||
|
@Override public void onLivingWorldEvent(LivingWorldEvent event) {}
|
||||||
|
@Override public void saveModuleData(PersistenceWriter w) {}
|
||||||
|
@Override public void loadModuleData(PersistenceReader r) {}
|
||||||
|
@Override public void shutdown() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void createDefaultRegionData(Region region) {
|
||||||
|
if (!region.getModuleData().contains(MODULE_ID)) {
|
||||||
|
region.getModuleData().put(MODULE_ID, AtmosphereRegionData.defaults());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ModuleUpdateResult updateRegion(RegionUpdateContext context) {
|
||||||
|
Region region = context.getRegion();
|
||||||
|
|
||||||
|
AtmosphereRegionData data = region.getModuleData()
|
||||||
|
.get(MODULE_ID, AtmosphereRegionData.class)
|
||||||
|
.orElseGet(AtmosphereRegionData::defaults);
|
||||||
|
|
||||||
|
// 1. Vegetation scrubs air pollution.
|
||||||
|
VegetationRegionData veg = region.getModuleData()
|
||||||
|
.get(VegetationModule.MODULE_ID, VegetationRegionData.class)
|
||||||
|
.orElse(null);
|
||||||
|
PollutionRegionData pollution = region.getModuleData()
|
||||||
|
.get(PollutionModule.MODULE_ID, PollutionRegionData.class)
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (veg != null && pollution != null) {
|
||||||
|
double scrub = veg.getTreePressure() * TREE_SCRUB_RATE;
|
||||||
|
pollution.addPollution(-scrub, 0.0, 0.0);
|
||||||
|
region.getModuleData().put(PollutionModule.MODULE_ID, pollution);
|
||||||
|
// Recompute pollution score now that trees have scrubbed the air.
|
||||||
|
double pollScore = pollution.getAirPollution() * 0.40
|
||||||
|
+ pollution.getGroundPollution() * 0.35
|
||||||
|
+ pollution.getWaterPollution() * 0.25;
|
||||||
|
region.getMetrics().setPollutionScore(pollScore);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Compute targets.
|
||||||
|
double ecosystemHealth = region.getMetrics().getEcosystemHealth();
|
||||||
|
double pollScore = region.getMetrics().getPollutionScore();
|
||||||
|
boolean mcRaining = globalRaining.getAsBoolean();
|
||||||
|
|
||||||
|
double targetRain = clamp01(ecosystemHealth / 100.0 * MAX_RAIN_FROM_ECO
|
||||||
|
+ (mcRaining ? GLOBAL_RAIN_BIAS : 0.0));
|
||||||
|
double targetThunder = clamp01(pollScore / 100.0 * 0.8);
|
||||||
|
|
||||||
|
// 3. Smooth toward targets.
|
||||||
|
double rain = data.getRainLevel() + (targetRain - data.getRainLevel()) * RAIN_SMOOTHING;
|
||||||
|
double thunder = data.getThunderLevel() + (targetThunder - data.getThunderLevel()) * THUNDER_SMOOTHING;
|
||||||
|
data.setRainLevel(rain);
|
||||||
|
data.setThunderLevel(thunder);
|
||||||
|
|
||||||
|
// 4. Apply rain effects to water availability and drought risk.
|
||||||
|
WaterRegionData water = region.getModuleData()
|
||||||
|
.get(WaterModule.MODULE_ID, WaterRegionData.class)
|
||||||
|
.orElse(null);
|
||||||
|
if (water != null) {
|
||||||
|
water.setWaterAvailability(
|
||||||
|
Math.min(100, water.getWaterAvailability() + rain * RAIN_MOISTURE_GAIN));
|
||||||
|
if (rain < DROUGHT_THRESHOLD) {
|
||||||
|
water.setDroughtRisk(
|
||||||
|
Math.min(100, water.getDroughtRisk() + DRY_DROUGHT_INCREASE * (1.0 - rain)));
|
||||||
|
} else {
|
||||||
|
water.setDroughtRisk(
|
||||||
|
Math.max(0, water.getDroughtRisk() - RAIN_DROUGHT_RELIEF * rain));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Acid rain: stormy + polluted atmosphere corrodes soil and water.
|
||||||
|
if (thunder > ACID_THUNDER_THRESHOLD && pollScore > ACID_POLL_THRESHOLD) {
|
||||||
|
double acidStrength = thunder * (pollScore - ACID_POLL_THRESHOLD) * ACID_FERTILITY_DRAIN;
|
||||||
|
SoilRegionData soil = region.getModuleData()
|
||||||
|
.get(SoilModule.MODULE_ID, SoilRegionData.class)
|
||||||
|
.orElse(null);
|
||||||
|
if (soil != null) {
|
||||||
|
soil.setFertility(Math.max(0, soil.getFertility() - acidStrength));
|
||||||
|
soil.setContamination(Math.min(100, soil.getContamination() + acidStrength * 0.5));
|
||||||
|
region.getModuleData().put(SoilModule.MODULE_ID, soil);
|
||||||
|
}
|
||||||
|
water.setWaterAvailability(
|
||||||
|
Math.max(0, water.getWaterAvailability() - acidStrength * 0.3));
|
||||||
|
}
|
||||||
|
region.getModuleData().put(WaterModule.MODULE_ID, water);
|
||||||
|
}
|
||||||
|
|
||||||
|
region.getModuleData().put(MODULE_ID, data);
|
||||||
|
return ModuleUpdateResult.changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double clamp01(double v) { return Math.min(1.0, Math.max(0.0, v)); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.livingworld.modules.atmosphere;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-region atmospheric state tracked by {@link AtmosphereModule}.
|
||||||
|
*
|
||||||
|
* <p>Both values smoothly interpolate toward ecosystem-derived targets each
|
||||||
|
* simulation cycle and are used to drive per-player weather packets.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li><b>rainLevel</b> – 0.0 = drought, 1.0 = heavy rainfall
|
||||||
|
* <li><b>thunderLevel</b> – 0.0 = calm, 1.0 = heavy storm (acid rain above 0.4)
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public final class AtmosphereRegionData {
|
||||||
|
|
||||||
|
private double rainLevel;
|
||||||
|
private double thunderLevel;
|
||||||
|
|
||||||
|
public AtmosphereRegionData(double rainLevel, double thunderLevel) {
|
||||||
|
this.rainLevel = clamp(rainLevel);
|
||||||
|
this.thunderLevel = clamp(thunderLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AtmosphereRegionData defaults() {
|
||||||
|
return new AtmosphereRegionData(0.4, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getRainLevel() { return rainLevel; }
|
||||||
|
public double getThunderLevel() { return thunderLevel; }
|
||||||
|
|
||||||
|
public void setRainLevel(double v) { this.rainLevel = clamp(v); }
|
||||||
|
public void setThunderLevel(double v) { this.thunderLevel = clamp(v); }
|
||||||
|
|
||||||
|
public AtmosphereRegionData copy() {
|
||||||
|
return new AtmosphereRegionData(rainLevel, thunderLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double clamp(double v) { return Math.min(1.0, Math.max(0.0, v)); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "AtmosphereRegionData{rain=" + rainLevel + ", thunder=" + thunderLevel + "}";
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user