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.fml.event.lifecycle.FMLCommonSetupEvent;
|
||||||
import net.neoforged.bus.api.IEventBus;
|
import net.neoforged.bus.api.IEventBus;
|
||||||
import net.neoforged.neoforge.common.NeoForge;
|
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.ServerStartedEvent;
|
||||||
import net.neoforged.neoforge.event.server.ServerStartingEvent;
|
import net.neoforged.neoforge.event.server.ServerStartingEvent;
|
||||||
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
|
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.AbstractFurnaceBlockEntity;
|
||||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||||
import net.minecraft.world.level.block.entity.CampfireBlockEntity;
|
import net.minecraft.world.level.block.entity.CampfireBlockEntity;
|
||||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
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.server.level.ServerLevel;
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.world.level.storage.LevelResource;
|
||||||
import net.minecraft.resources.ResourceKey;
|
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
|
||||||
import net.minecraft.core.registries.Registries;
|
|
||||||
|
|
||||||
import com.livingworld.bootstrap.LivingWorldBootstrap;
|
import com.livingworld.bootstrap.LivingWorldBootstrap;
|
||||||
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;
|
||||||
|
import com.livingworld.modules.recovery.SuccessionStage;
|
||||||
import com.livingworld.platform.neoforge.NeoForgePlatformAdapter;
|
import com.livingworld.platform.neoforge.NeoForgePlatformAdapter;
|
||||||
import com.livingworld.platform.neoforge.NeoForgeWorldEffectExecutor;
|
import com.livingworld.platform.neoforge.NeoForgeWorldEffectExecutor;
|
||||||
import com.livingworld.regions.Region;
|
import com.livingworld.regions.Region;
|
||||||
import com.livingworld.regions.RegionCoordinate;
|
import com.livingworld.regions.RegionCoordinate;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import com.livingworld.regions.RegionMetrics;
|
||||||
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,11 +74,18 @@ public class LivingWorldMod {
|
|||||||
|
|
||||||
private static final int PLAYER_CHECK_INTERVAL = 20;
|
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 LivingWorldBootstrap bootstrap;
|
||||||
|
private final Random random = new Random();
|
||||||
private MinecraftServer minecraftServer;
|
private MinecraftServer minecraftServer;
|
||||||
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 Set<RegionCoordinate> biomeInitialized = new HashSet<>();
|
||||||
|
|
||||||
public LivingWorldMod(IEventBus eventBus) {
|
public LivingWorldMod(IEventBus eventBus) {
|
||||||
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World mod starting...");
|
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World mod starting...");
|
||||||
@@ -88,6 +115,7 @@ public class LivingWorldMod {
|
|||||||
bootstrap.onServerStopping();
|
bootstrap.onServerStopping();
|
||||||
bootstrap.setOverworldRaining(null);
|
bootstrap.setOverworldRaining(null);
|
||||||
playerRegionCache.clear();
|
playerRegionCache.clear();
|
||||||
|
biomeInitialized.clear();
|
||||||
this.minecraftServer = null;
|
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.");
|
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));
|
int regionZ = (int) Math.floor(player.getZ() / (LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16.0));
|
||||||
RegionCoordinate coord = new RegionCoordinate(dimId, regionX, regionZ);
|
RegionCoordinate coord = new RegionCoordinate(dimId, regionX, regionZ);
|
||||||
RegionCoordinate previous = playerRegionCache.put(player.getUUID(), coord);
|
RegionCoordinate previous = playerRegionCache.put(player.getUUID(), coord);
|
||||||
|
|
||||||
if (!coord.equals(previous)) {
|
if (!coord.equals(previous)) {
|
||||||
bootstrap.notifyPlayerInRegion(coord);
|
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.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Random;
|
||||||
|
import com.livingworld.regions.RegionMetrics;
|
||||||
import net.minecraft.commands.CommandSourceStack;
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,9 +60,16 @@ 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_MOISTURE_GAIN = 0.3;
|
||||||
private static final double RAIN_DROUGHT_RELIEF = 0.15;
|
private static final double RAIN_DROUGHT_RELIEF = 0.15;
|
||||||
private static final double DRY_DROUGHT_INCREASE = 0.05;
|
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 PlatformAdapter platformAdapter;
|
||||||
private Path worldSaveDirectory;
|
private Path worldSaveDirectory;
|
||||||
@@ -221,6 +231,80 @@ public final class LivingWorldBootstrap {
|
|||||||
regionManager.markDirty(region);
|
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) {
|
public void notifyPlayerInRegion(RegionCoordinate coordinate) {
|
||||||
if (!serverReady || coordinate == null) return;
|
if (!serverReady || coordinate == null) return;
|
||||||
regionManager.getOrCreateRegion(coordinate);
|
regionManager.getOrCreateRegion(coordinate);
|
||||||
@@ -250,9 +334,13 @@ public final class LivingWorldBootstrap {
|
|||||||
Collection<Region> active = regionManager.getActiveRegions();
|
Collection<Region> active = regionManager.getActiveRegions();
|
||||||
if (active.size() < 2) return;
|
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<>();
|
Map<RegionCoordinate, Region> byCoord = new HashMap<>();
|
||||||
for (Region r : active) byCoord.put(r.getCoordinate(), r);
|
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}};
|
int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||||
for (Region region : active) {
|
for (Region region : active) {
|
||||||
RegionCoordinate coord = region.getCoordinate();
|
RegionCoordinate coord = region.getCoordinate();
|
||||||
@@ -271,7 +359,11 @@ public final class LivingWorldBootstrap {
|
|||||||
if (neighbourData == null) continue;
|
if (neighbourData == null) continue;
|
||||||
double diff = data.getAirPollution() - neighbourData.getAirPollution();
|
double diff = data.getAirPollution() - neighbourData.getAirPollution();
|
||||||
if (diff <= 0) continue;
|
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);
|
data.addPollution(-transfer, 0, 0);
|
||||||
neighbourData.addPollution(transfer, 0, 0);
|
neighbourData.addPollution(transfer, 0, 0);
|
||||||
neighbour.getModuleData().put(PollutionModule.MODULE_ID, neighbourData);
|
neighbour.getModuleData().put(PollutionModule.MODULE_ID, neighbourData);
|
||||||
@@ -297,6 +389,22 @@ public final class LivingWorldBootstrap {
|
|||||||
if (raining) {
|
if (raining) {
|
||||||
water.setWaterAvailability(water.getWaterAvailability() + RAIN_MOISTURE_GAIN);
|
water.setWaterAvailability(water.getWaterAvailability() + RAIN_MOISTURE_GAIN);
|
||||||
water.setDroughtRisk(water.getDroughtRisk() - RAIN_DROUGHT_RELIEF);
|
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 {
|
} else {
|
||||||
water.setDroughtRisk(water.getDroughtRisk() + DRY_DROUGHT_INCREASE);
|
water.setDroughtRisk(water.getDroughtRisk() + DRY_DROUGHT_INCREASE);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,17 +21,31 @@ public final class RecoveryRegionData {
|
|||||||
private SuccessionStage successionStage;
|
private SuccessionStage successionStage;
|
||||||
private double recoveryProgress;
|
private double recoveryProgress;
|
||||||
private double damageAccumulation;
|
private double damageAccumulation;
|
||||||
|
/** Biome-derived ceiling — region cannot advance past this stage. */
|
||||||
|
private SuccessionStage maxSuccessionStage;
|
||||||
|
|
||||||
public RecoveryRegionData(
|
public RecoveryRegionData(
|
||||||
SuccessionStage successionStage,
|
SuccessionStage successionStage,
|
||||||
double recoveryProgress,
|
double recoveryProgress,
|
||||||
double damageAccumulation) {
|
double damageAccumulation) {
|
||||||
|
this(successionStage, recoveryProgress, damageAccumulation, SuccessionStage.MATURE_FOREST);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecoveryRegionData(
|
||||||
|
SuccessionStage successionStage,
|
||||||
|
double recoveryProgress,
|
||||||
|
double damageAccumulation,
|
||||||
|
SuccessionStage maxSuccessionStage) {
|
||||||
if (successionStage == null) {
|
if (successionStage == null) {
|
||||||
throw new IllegalArgumentException("successionStage must not be null");
|
throw new IllegalArgumentException("successionStage must not be null");
|
||||||
}
|
}
|
||||||
this.successionStage = successionStage;
|
if (maxSuccessionStage == null) {
|
||||||
this.recoveryProgress = clamp(recoveryProgress);
|
throw new IllegalArgumentException("maxSuccessionStage must not be null");
|
||||||
|
}
|
||||||
|
this.successionStage = successionStage;
|
||||||
|
this.recoveryProgress = clamp(recoveryProgress);
|
||||||
this.damageAccumulation = clamp(damageAccumulation);
|
this.damageAccumulation = clamp(damageAccumulation);
|
||||||
|
this.maxSuccessionStage = maxSuccessionStage;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns a region at grassland stage with no accumulated damage. */
|
/** Returns a region at grassland stage with no accumulated damage. */
|
||||||
@@ -43,9 +57,20 @@ public final class RecoveryRegionData {
|
|||||||
// Getters
|
// Getters
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
public SuccessionStage getSuccessionStage() { return successionStage; }
|
public SuccessionStage getSuccessionStage() { return successionStage; }
|
||||||
public double getRecoveryProgress() { return recoveryProgress; }
|
public double getRecoveryProgress() { return recoveryProgress; }
|
||||||
public double getDamageAccumulation() { return damageAccumulation; }
|
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
|
// Mutation
|
||||||
@@ -67,6 +92,8 @@ public final class RecoveryRegionData {
|
|||||||
if (amount < 0) throw new IllegalArgumentException("amount must be >= 0");
|
if (amount < 0) throw new IllegalArgumentException("amount must be >= 0");
|
||||||
recoveryProgress = clamp(recoveryProgress + amount);
|
recoveryProgress = clamp(recoveryProgress + amount);
|
||||||
if (recoveryProgress >= 100.0
|
if (recoveryProgress >= 100.0
|
||||||
|
&& successionStage.hasNext()
|
||||||
|
&& successionStage.next().ordinal() <= maxSuccessionStage.ordinal()
|
||||||
&& successionStage.conditionsMetForAdvancement(soilQuality, pollutionScore, vegetationPressure)) {
|
&& successionStage.conditionsMetForAdvancement(soilQuality, pollutionScore, vegetationPressure)) {
|
||||||
successionStage = successionStage.next();
|
successionStage = successionStage.next();
|
||||||
recoveryProgress = 0.0;
|
recoveryProgress = 0.0;
|
||||||
@@ -105,7 +132,7 @@ public final class RecoveryRegionData {
|
|||||||
|
|
||||||
/** Returns an independent copy. */
|
/** Returns an independent copy. */
|
||||||
public RecoveryRegionData 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{"
|
return "RecoveryRegionData{"
|
||||||
+ "stage=" + successionStage
|
+ "stage=" + successionStage
|
||||||
+ ", progress=" + recoveryProgress
|
+ ", progress=" + recoveryProgress
|
||||||
+ ", damage=" + damageAccumulation + "}";
|
+ ", damage=" + damageAccumulation
|
||||||
|
+ ", maxStage=" + maxSuccessionStage + "}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user