From c9f927b265372e4e5d8c9f85820ead9e7d9944e4 Mon Sep 17 00:00:00 2001 From: George Date: Sun, 7 Jun 2026 19:57:09 +0100 Subject: [PATCH] Add mob spawning, HUD, agriculture, acid rain, biome caps, directional wind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../java/com/livingworld/LivingWorldMod.java | 154 +++++++++++++++++- .../bootstrap/LivingWorldBootstrap.java | 112 ++++++++++++- .../modules/recovery/RecoveryRegionData.java | 38 ++++- 3 files changed, 289 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/livingworld/LivingWorldMod.java b/src/main/java/com/livingworld/LivingWorldMod.java index 9ebba7f..a199b04 100644 --- a/src/main/java/com/livingworld/LivingWorldMod.java +++ b/src/main/java/com/livingworld/LivingWorldMod.java @@ -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 playerRegionCache = new HashMap<>(); + private final Set 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 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 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 = 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)); + } } diff --git a/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java b/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java index 017393c..b6b36ba 100644 --- a/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java +++ b/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java @@ -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 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 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 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); } diff --git a/src/main/java/com/livingworld/modules/recovery/RecoveryRegionData.java b/src/main/java/com/livingworld/modules/recovery/RecoveryRegionData.java index 6d1d04f..4200b43 100644 --- a/src/main/java/com/livingworld/modules/recovery/RecoveryRegionData.java +++ b/src/main/java/com/livingworld/modules/recovery/RecoveryRegionData.java @@ -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 + "}"; } }