Add mob spawning, HUD, agriculture, acid rain, biome caps, directional wind
Step 1 – Mob spawn feedback: FinalizeSpawnEvent suppresses passive mobs (Animal) in regions with ecosystem health < 30 (up to 70% cancellation) and hostile mobs (Monster) in regions > 60 health (up to 50% cancellation). Step 2 – Compass HUD: holding a compass shows a colour-coded action-bar overlay with Eco/Poll/Soil/Wat scores for the current region (green/yellow/red banded). Step 3 – Resource loop closure: logging depletion recovery is already tied to vegetation pressure in ResourceDepletionModule; sapling-to-tree growth continues to close the loop via the existing vegetation feedback path. Step 4 – Agriculture: bone meal (PlayerInteractEvent.RightClickBlock) adds +2 soil fertility; harvesting fully-grown (age=7) crops (BlockEvent.BreakEvent) drains -0.5 fertility and adds 0.3 farming depletion. Step 5 – Acid rain: when raining AND region pollution > 20, rainfall drains soil fertility, raises soil contamination, and reduces water availability proportional to excess pollution. Implemented in applyWeatherFeedback(). Step 6 – Biome-aware succession: RecoveryRegionData gains a maxSuccessionStage cap (default MATURE_FOREST). deriveBiomeCap() samples the biome at the region centre on first player entry and calls setRegionBiomeCap(); deserts/badlands → SPARSE_GRASS, savannas/mountains/cold → SCRUBLAND, beaches/oceans → GRASSLAND, forests/taiga/jungle → MATURE_FOREST, plains/swamp → YOUNG_WOODLAND. Step 7 – Directional wind: spreadPollutionAcrossRegions() drifts a wind angle by ±0.05 rad per sim cycle; each cardinal neighbour's spread rate is multiplied by (1 + alignment×0.5) where alignment = cos(windAngle − offsetAngle), creating downwind (×1.5) and upwind (×0.5) asymmetry. All 400 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,34 +4,54 @@ import net.neoforged.fml.common.Mod;
|
||||
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.neoforge.common.NeoForge;
|
||||
import net.neoforged.neoforge.event.entity.living.FinalizeSpawnEvent;
|
||||
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.neoforged.neoforge.event.level.BlockEvent;
|
||||
import net.neoforged.neoforge.event.server.ServerStartedEvent;
|
||||
import net.neoforged.neoforge.event.server.ServerStartingEvent;
|
||||
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.tags.BiomeTags;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.world.entity.animal.Animal;
|
||||
import net.minecraft.world.entity.monster.Monster;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.CampfireBlockEntity;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||
|
||||
import com.livingworld.bootstrap.LivingWorldBootstrap;
|
||||
import com.livingworld.core.LivingWorldConstants;
|
||||
import com.livingworld.debug.DiagnosticCategory;
|
||||
import com.livingworld.debug.LivingWorldLogger;
|
||||
import com.livingworld.modules.recovery.SuccessionStage;
|
||||
import com.livingworld.platform.neoforge.NeoForgePlatformAdapter;
|
||||
import com.livingworld.platform.neoforge.NeoForgeWorldEffectExecutor;
|
||||
import com.livingworld.regions.Region;
|
||||
import com.livingworld.regions.RegionCoordinate;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||
import com.livingworld.regions.RegionMetrics;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@@ -54,11 +74,18 @@ public class LivingWorldMod {
|
||||
|
||||
private static final int PLAYER_CHECK_INTERVAL = 20;
|
||||
|
||||
/** Passive mobs suppressed in regions with ecosystem health below this. */
|
||||
private static final double PASSIVE_SUPPRESS_HEALTH = 30.0;
|
||||
/** Hostile mobs suppressed in regions with ecosystem health above this. */
|
||||
private static final double HOSTILE_SUPPRESS_HEALTH = 60.0;
|
||||
|
||||
private final LivingWorldBootstrap bootstrap;
|
||||
private final Random random = new Random();
|
||||
private MinecraftServer minecraftServer;
|
||||
private int furnaceScanTick = 0;
|
||||
private int playerCheckTick = 0;
|
||||
private final Map<UUID, RegionCoordinate> playerRegionCache = new HashMap<>();
|
||||
private final Set<RegionCoordinate> biomeInitialized = new HashSet<>();
|
||||
|
||||
public LivingWorldMod(IEventBus eventBus) {
|
||||
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World mod starting...");
|
||||
@@ -88,6 +115,7 @@ public class LivingWorldMod {
|
||||
bootstrap.onServerStopping();
|
||||
bootstrap.setOverworldRaining(null);
|
||||
playerRegionCache.clear();
|
||||
biomeInitialized.clear();
|
||||
this.minecraftServer = null;
|
||||
});
|
||||
|
||||
@@ -101,6 +129,50 @@ public class LivingWorldMod {
|
||||
}
|
||||
});
|
||||
|
||||
// Step 1: Mob spawn feedback — passive mobs suppressed in degraded regions,
|
||||
// hostile mobs suppressed in healthy ones.
|
||||
NeoForge.EVENT_BUS.addListener(FinalizeSpawnEvent.class, event -> {
|
||||
if (!bootstrap.isServerReady()) return;
|
||||
if (!(event.getLevel() instanceof ServerLevel level)) return;
|
||||
String dimId = level.dimension().location().toString();
|
||||
Optional<RegionMetrics> metricsOpt =
|
||||
bootstrap.getMetricsAt(dimId, event.getX(), event.getZ());
|
||||
if (metricsOpt.isEmpty()) return;
|
||||
double health = metricsOpt.get().getEcosystemHealth();
|
||||
if (event.getEntity() instanceof Animal) {
|
||||
if (health < PASSIVE_SUPPRESS_HEALTH) {
|
||||
double chance = (PASSIVE_SUPPRESS_HEALTH - health) / PASSIVE_SUPPRESS_HEALTH * 0.7;
|
||||
if (random.nextDouble() < chance) event.setSpawnCancelled(true);
|
||||
}
|
||||
} else if (event.getEntity() instanceof Monster) {
|
||||
if (health > HOSTILE_SUPPRESS_HEALTH) {
|
||||
double chance = (health - HOSTILE_SUPPRESS_HEALTH) / (100.0 - HOSTILE_SUPPRESS_HEALTH) * 0.5;
|
||||
if (random.nextDouble() < chance) event.setSpawnCancelled(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Step 4: Agriculture — bone meal boosts soil fertility.
|
||||
NeoForge.EVENT_BUS.addListener(PlayerInteractEvent.RightClickBlock.class, event -> {
|
||||
if (!bootstrap.isServerReady()) return;
|
||||
if (!event.getItemStack().is(Items.BONE_MEAL)) return;
|
||||
if (!(event.getLevel() instanceof ServerLevel level)) return;
|
||||
BlockPos pos = event.getPos();
|
||||
bootstrap.handleBoneMeal(level.dimension().location().toString(), pos.getX(), pos.getZ());
|
||||
});
|
||||
|
||||
// Step 4: Agriculture — harvesting fully-grown crops drains soil fertility.
|
||||
NeoForge.EVENT_BUS.addListener(BlockEvent.BreakEvent.class, event -> {
|
||||
if (!bootstrap.isServerReady()) return;
|
||||
if (!(event.getLevel() instanceof ServerLevel level)) return;
|
||||
var state = event.getState();
|
||||
if (!state.is(BlockTags.CROPS)) return;
|
||||
if (!state.hasProperty(BlockStateProperties.AGE_7)) return;
|
||||
if (state.getValue(BlockStateProperties.AGE_7) < 7) return;
|
||||
BlockPos pos = event.getPos();
|
||||
bootstrap.handleCropHarvest(level.dimension().location().toString(), pos.getX(), pos.getZ());
|
||||
});
|
||||
|
||||
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World Bootstrap initialized successfully.");
|
||||
}
|
||||
|
||||
@@ -150,9 +222,75 @@ public class LivingWorldMod {
|
||||
int regionZ = (int) Math.floor(player.getZ() / (LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16.0));
|
||||
RegionCoordinate coord = new RegionCoordinate(dimId, regionX, regionZ);
|
||||
RegionCoordinate previous = playerRegionCache.put(player.getUUID(), coord);
|
||||
|
||||
if (!coord.equals(previous)) {
|
||||
bootstrap.notifyPlayerInRegion(coord);
|
||||
|
||||
// Step 6: Biome-aware succession — derive and apply cap once per region.
|
||||
if (!biomeInitialized.contains(coord)
|
||||
&& player.level() instanceof ServerLevel serverLevel) {
|
||||
SuccessionStage cap = deriveBiomeCap(serverLevel, coord);
|
||||
bootstrap.setRegionBiomeCap(coord, cap);
|
||||
biomeInitialized.add(coord);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Compass HUD — display region health while holding a compass.
|
||||
if (player.getMainHandItem().is(Items.COMPASS) || player.getOffhandItem().is(Items.COMPASS)) {
|
||||
Optional<RegionMetrics> metricsOpt = bootstrap.getMetricsAt(dimId, player.getX(), player.getZ());
|
||||
metricsOpt.ifPresent(m -> player.displayClientMessage(buildHud(coord, m), true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Step 6: Samples the dominant biome at the region centre and returns a succession ceiling. */
|
||||
private SuccessionStage deriveBiomeCap(ServerLevel level, RegionCoordinate coord) {
|
||||
int cx = coord.x() * LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16 + 64;
|
||||
int cz = coord.z() * LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16 + 64;
|
||||
int cy = level.getHeight(Heightmap.Types.WORLD_SURFACE, cx, cz);
|
||||
Holder<Biome> biome = level.getBiome(new BlockPos(cx, cy, cz));
|
||||
|
||||
// Temperature ≥ 2.0 = desert or badlands (no IS_DESERT tag in MC 1.21).
|
||||
float temp = biome.value().getBaseTemperature();
|
||||
if (biome.is(BiomeTags.IS_BADLANDS) || temp >= 2.0f) {
|
||||
return SuccessionStage.SPARSE_GRASS;
|
||||
} else if (biome.is(BiomeTags.IS_SAVANNA)) {
|
||||
return SuccessionStage.SCRUBLAND;
|
||||
} else if (biome.is(BiomeTags.IS_MOUNTAIN) || temp < 0.15f) {
|
||||
return SuccessionStage.SCRUBLAND;
|
||||
} else if (biome.is(BiomeTags.IS_BEACH) || biome.is(BiomeTags.IS_OCEAN)
|
||||
|| biome.is(BiomeTags.IS_DEEP_OCEAN)) {
|
||||
return SuccessionStage.GRASSLAND;
|
||||
} else if (biome.is(BiomeTags.IS_FOREST) || biome.is(BiomeTags.IS_TAIGA)
|
||||
|| biome.is(BiomeTags.IS_JUNGLE)) {
|
||||
return SuccessionStage.MATURE_FOREST;
|
||||
} else {
|
||||
return SuccessionStage.YOUNG_WOODLAND;
|
||||
}
|
||||
}
|
||||
|
||||
/** Step 2: Builds the action-bar HUD component for a player holding a compass. */
|
||||
private Component buildHud(RegionCoordinate coord, RegionMetrics m) {
|
||||
double eco = m.getEcosystemHealth();
|
||||
double poll = m.getPollutionScore();
|
||||
double soil = m.getSoilQuality();
|
||||
double wat = m.getWaterQuality();
|
||||
|
||||
ChatFormatting ecoCol = eco > 60 ? ChatFormatting.GREEN : eco > 30 ? ChatFormatting.YELLOW : ChatFormatting.RED;
|
||||
ChatFormatting pollCol = poll < 15 ? ChatFormatting.GREEN : poll < 40 ? 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;
|
||||
|
||||
return Component.empty()
|
||||
.append(Component.literal("[LW] ").withStyle(ChatFormatting.GOLD))
|
||||
.append(Component.literal(String.format("(%d,%d) ", coord.x(), coord.z())).withStyle(ChatFormatting.GRAY))
|
||||
.append(Component.literal("Eco:").withStyle(ChatFormatting.WHITE))
|
||||
.append(Component.literal(String.format("%.0f ", eco)).withStyle(ecoCol))
|
||||
.append(Component.literal("Poll:").withStyle(ChatFormatting.WHITE))
|
||||
.append(Component.literal(String.format("%.1f ", poll)).withStyle(pollCol))
|
||||
.append(Component.literal("Soil:").withStyle(ChatFormatting.WHITE))
|
||||
.append(Component.literal(String.format("%.0f ", soil)).withStyle(soilCol))
|
||||
.append(Component.literal("Wat:").withStyle(ChatFormatting.WHITE))
|
||||
.append(Component.literal(String.format("%.0f", wat)).withStyle(watCol));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import com.livingworld.regions.RegionMetrics;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
|
||||
/**
|
||||
@@ -57,9 +60,16 @@ import net.minecraft.commands.CommandSourceStack;
|
||||
*/
|
||||
public final class LivingWorldBootstrap {
|
||||
|
||||
private static final double RAIN_MOISTURE_GAIN = 0.3;
|
||||
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_DRIFT_MAX = 0.05; // radians per sim cycle
|
||||
|
||||
private double windAngle = 0.0;
|
||||
private final Random windRandom = new Random();
|
||||
|
||||
private PlatformAdapter platformAdapter;
|
||||
private Path worldSaveDirectory;
|
||||
@@ -221,6 +231,80 @@ public final class LivingWorldBootstrap {
|
||||
regionManager.markDirty(region);
|
||||
}
|
||||
|
||||
/** Returns the live metrics for the region containing the given world position. */
|
||||
public Optional<RegionMetrics> getMetricsAt(String dimensionId, double x, double z) {
|
||||
if (!serverReady) return Optional.empty();
|
||||
RegionCoordinate coord = RegionCoordinate.fromBlock(
|
||||
dimensionId, (int) x, (int) z, LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
|
||||
return regionManager.resolve(coord).map(Region::getMetrics);
|
||||
}
|
||||
|
||||
/** Called when a sapling grows into a tree — reduces logging depletion in that region. */
|
||||
public void handleTreeGrowth(RegionCoordinate coord) {
|
||||
if (!serverReady || coord == null) return;
|
||||
regionManager.resolve(coord).ifPresent(region -> {
|
||||
ResourceRegionData res = region.getModuleData()
|
||||
.get(ResourceDepletionModule.MODULE_ID, ResourceRegionData.class)
|
||||
.orElse(null);
|
||||
if (res == null) return;
|
||||
res.setLoggingDepletion(Math.max(0, res.getLoggingDepletion() - 2.0));
|
||||
region.getModuleData().put(ResourceDepletionModule.MODULE_ID, res);
|
||||
regionManager.markDirty(region);
|
||||
});
|
||||
}
|
||||
|
||||
/** Called when a player applies bone meal — boosts soil fertility. */
|
||||
public void handleBoneMeal(String dimensionId, int x, int z) {
|
||||
if (!serverReady) return;
|
||||
RegionCoordinate coord = RegionCoordinate.fromBlock(
|
||||
dimensionId, x, z, LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
|
||||
regionManager.resolve(coord).ifPresent(region -> {
|
||||
SoilRegionData soil = region.getModuleData()
|
||||
.get(SoilModule.MODULE_ID, SoilRegionData.class)
|
||||
.orElse(null);
|
||||
if (soil == null) return;
|
||||
soil.setFertility(Math.min(100, soil.getFertility() + 2.0));
|
||||
region.getModuleData().put(SoilModule.MODULE_ID, soil);
|
||||
regionManager.markDirty(region);
|
||||
});
|
||||
}
|
||||
|
||||
/** Called when a player harvests a fully-grown crop — drains soil fertility. */
|
||||
public void handleCropHarvest(String dimensionId, int x, int z) {
|
||||
if (!serverReady) return;
|
||||
RegionCoordinate coord = RegionCoordinate.fromBlock(
|
||||
dimensionId, x, z, LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
|
||||
regionManager.resolve(coord).ifPresent(region -> {
|
||||
SoilRegionData soil = region.getModuleData()
|
||||
.get(SoilModule.MODULE_ID, SoilRegionData.class)
|
||||
.orElse(null);
|
||||
if (soil == null) return;
|
||||
soil.setFertility(Math.max(0, soil.getFertility() - 0.5));
|
||||
ResourceRegionData res = region.getModuleData()
|
||||
.get(ResourceDepletionModule.MODULE_ID, ResourceRegionData.class)
|
||||
.orElse(null);
|
||||
if (res != null) {
|
||||
res.recordFarming(0.3);
|
||||
region.getModuleData().put(ResourceDepletionModule.MODULE_ID, res);
|
||||
}
|
||||
region.getModuleData().put(SoilModule.MODULE_ID, soil);
|
||||
regionManager.markDirty(region);
|
||||
});
|
||||
}
|
||||
|
||||
/** Sets the biome-derived succession ceiling for a region. */
|
||||
public void setRegionBiomeCap(RegionCoordinate coord, SuccessionStage maxStage) {
|
||||
if (!serverReady || coord == null || maxStage == null) return;
|
||||
regionManager.resolve(coord).ifPresent(region -> {
|
||||
RecoveryRegionData recovery = region.getModuleData()
|
||||
.get(RecoveryModule.MODULE_ID, RecoveryRegionData.class)
|
||||
.orElseGet(RecoveryRegionData::defaults);
|
||||
recovery.setMaxSuccessionStage(maxStage);
|
||||
region.getModuleData().put(RecoveryModule.MODULE_ID, recovery);
|
||||
regionManager.markDirty(region);
|
||||
});
|
||||
}
|
||||
|
||||
public void notifyPlayerInRegion(RegionCoordinate coordinate) {
|
||||
if (!serverReady || coordinate == null) return;
|
||||
regionManager.getOrCreateRegion(coordinate);
|
||||
@@ -250,9 +334,13 @@ public final class LivingWorldBootstrap {
|
||||
Collection<Region> active = regionManager.getActiveRegions();
|
||||
if (active.size() < 2) return;
|
||||
|
||||
// Wind drifts slowly each sim cycle.
|
||||
windAngle += (windRandom.nextDouble() * 2.0 - 1.0) * WIND_DRIFT_MAX;
|
||||
|
||||
Map<RegionCoordinate, Region> byCoord = new HashMap<>();
|
||||
for (Region r : active) byCoord.put(r.getCoordinate(), r);
|
||||
|
||||
// dx, dz, and the angle that offset points in (atan2(dz, dx))
|
||||
int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
for (Region region : active) {
|
||||
RegionCoordinate coord = region.getCoordinate();
|
||||
@@ -271,7 +359,11 @@ public final class LivingWorldBootstrap {
|
||||
if (neighbourData == null) continue;
|
||||
double diff = data.getAirPollution() - neighbourData.getAirPollution();
|
||||
if (diff <= 0) continue;
|
||||
double transfer = diff * POLLUTION_SPREAD_RATE;
|
||||
// Wind alignment: positive = downwind (faster spread), negative = upwind (slower).
|
||||
double offsetAngle = Math.atan2(off[1], off[0]);
|
||||
double alignment = Math.cos(windAngle - offsetAngle);
|
||||
double rate = POLLUTION_SPREAD_RATE * (1.0 + alignment * WIND_BOOST);
|
||||
double transfer = diff * rate;
|
||||
data.addPollution(-transfer, 0, 0);
|
||||
neighbourData.addPollution(transfer, 0, 0);
|
||||
neighbour.getModuleData().put(PollutionModule.MODULE_ID, neighbourData);
|
||||
@@ -297,6 +389,22 @@ public final class LivingWorldBootstrap {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -21,17 +21,31 @@ public final class RecoveryRegionData {
|
||||
private SuccessionStage successionStage;
|
||||
private double recoveryProgress;
|
||||
private double damageAccumulation;
|
||||
/** Biome-derived ceiling — region cannot advance past this stage. */
|
||||
private SuccessionStage maxSuccessionStage;
|
||||
|
||||
public RecoveryRegionData(
|
||||
SuccessionStage successionStage,
|
||||
double recoveryProgress,
|
||||
double damageAccumulation) {
|
||||
this(successionStage, recoveryProgress, damageAccumulation, SuccessionStage.MATURE_FOREST);
|
||||
}
|
||||
|
||||
public RecoveryRegionData(
|
||||
SuccessionStage successionStage,
|
||||
double recoveryProgress,
|
||||
double damageAccumulation,
|
||||
SuccessionStage maxSuccessionStage) {
|
||||
if (successionStage == null) {
|
||||
throw new IllegalArgumentException("successionStage must not be null");
|
||||
}
|
||||
this.successionStage = successionStage;
|
||||
this.recoveryProgress = clamp(recoveryProgress);
|
||||
if (maxSuccessionStage == null) {
|
||||
throw new IllegalArgumentException("maxSuccessionStage must not be null");
|
||||
}
|
||||
this.successionStage = successionStage;
|
||||
this.recoveryProgress = clamp(recoveryProgress);
|
||||
this.damageAccumulation = clamp(damageAccumulation);
|
||||
this.maxSuccessionStage = maxSuccessionStage;
|
||||
}
|
||||
|
||||
/** Returns a region at grassland stage with no accumulated damage. */
|
||||
@@ -43,9 +57,20 @@ public final class RecoveryRegionData {
|
||||
// Getters
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
public SuccessionStage getSuccessionStage() { return successionStage; }
|
||||
public SuccessionStage getSuccessionStage() { return successionStage; }
|
||||
public double getRecoveryProgress() { return recoveryProgress; }
|
||||
public double getDamageAccumulation() { return damageAccumulation; }
|
||||
public SuccessionStage getMaxSuccessionStage() { return maxSuccessionStage; }
|
||||
|
||||
public void setMaxSuccessionStage(SuccessionStage cap) {
|
||||
if (cap == null) throw new IllegalArgumentException("cap must not be null");
|
||||
this.maxSuccessionStage = cap;
|
||||
// If current stage already exceeds the new cap, clamp it.
|
||||
if (successionStage.ordinal() > cap.ordinal()) {
|
||||
successionStage = cap;
|
||||
recoveryProgress = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Mutation
|
||||
@@ -67,6 +92,8 @@ public final class RecoveryRegionData {
|
||||
if (amount < 0) throw new IllegalArgumentException("amount must be >= 0");
|
||||
recoveryProgress = clamp(recoveryProgress + amount);
|
||||
if (recoveryProgress >= 100.0
|
||||
&& successionStage.hasNext()
|
||||
&& successionStage.next().ordinal() <= maxSuccessionStage.ordinal()
|
||||
&& successionStage.conditionsMetForAdvancement(soilQuality, pollutionScore, vegetationPressure)) {
|
||||
successionStage = successionStage.next();
|
||||
recoveryProgress = 0.0;
|
||||
@@ -105,7 +132,7 @@ public final class RecoveryRegionData {
|
||||
|
||||
/** Returns an independent copy. */
|
||||
public RecoveryRegionData copy() {
|
||||
return new RecoveryRegionData(successionStage, recoveryProgress, damageAccumulation);
|
||||
return new RecoveryRegionData(successionStage, recoveryProgress, damageAccumulation, maxSuccessionStage);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
@@ -119,6 +146,7 @@ public final class RecoveryRegionData {
|
||||
return "RecoveryRegionData{"
|
||||
+ "stage=" + successionStage
|
||||
+ ", progress=" + recoveryProgress
|
||||
+ ", damage=" + damageAccumulation + "}";
|
||||
+ ", damage=" + damageAccumulation
|
||||
+ ", maxStage=" + maxSuccessionStage + "}";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user