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:
George
2026-06-07 20:24:04 +01:00
parent 67a1e07b82
commit 61abff52dc
4 changed files with 297 additions and 52 deletions
@@ -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,13 +65,8 @@ 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 WIND_BOOST = 0.5;
private static final double RAIN_DROUGHT_RELIEF = 0.15; private static final double WIND_DRIFT_MAX = 0.05; // radians per sim cycle
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_DRIFT_MAX = 0.05; // radians per sim cycle
private double windAngle = 0.0; private double windAngle = 0.0;
private final Random windRandom = new Random(); private final Random windRandom = new Random();
@@ -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 + "}";
}
}