Complete phase 5 ecology systems

This commit is contained in:
George
2026-06-11 18:17:05 +01:00
parent 38ee028651
commit 2304266544
5 changed files with 266 additions and 7 deletions
@@ -7,6 +7,8 @@ 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.living.LivingDeathEvent;
import net.neoforged.neoforge.event.entity.EntityLeaveLevelEvent;
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
import net.neoforged.neoforge.event.level.BlockEvent;
import net.neoforged.neoforge.event.level.ChunkEvent;
@@ -30,7 +32,9 @@ import net.minecraft.world.entity.animal.Animal;
import net.minecraft.world.entity.animal.AbstractFish;
import net.minecraft.world.entity.animal.Dolphin;
import net.minecraft.world.entity.animal.Squid;
import net.minecraft.world.entity.animal.Bee;
import net.minecraft.world.entity.monster.Monster;
import net.minecraft.world.entity.MobSpawnType;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.Biomes;
@@ -124,6 +128,8 @@ public class LivingWorldMod {
/** Stores playerCheckTick value when a region was last water-body-scanned. */
private final Map<RegionCoordinate, Integer> waterBodyLastScan = new HashMap<>();
private final Set<RegionCoordinate> elevationInitialized = new HashSet<>();
private final Map<UUID, RegionCoordinate> trackedPassiveMobs = new HashMap<>();
private boolean migrationCompanionSpawn;
/** Stores playerCheckTick when a region last played an ambient sound (per-region throttle). */
private final Map<RegionCoordinate, Integer> regionSoundLastTick = new HashMap<>();
@@ -153,7 +159,9 @@ public class LivingWorldMod {
this.minecraftServer = event.getServer();
bootstrap.onServerStarted();
bootstrap.getWorldEffectsModule().registerConsumer(
new NeoForgeWorldEffectExecutor(() -> minecraftServer));
new NeoForgeWorldEffectExecutor(
() -> minecraftServer,
coord -> !bootstrap.isPollinatorCollapse(coord)));
bootstrap.setOverworldRaining(
() -> minecraftServer != null && minecraftServer.overworld().isRaining());
bootstrap.setAbsoluteDaySupplier(
@@ -240,6 +248,7 @@ public class LivingWorldMod {
waterBodyLastScan.clear();
elevationInitialized.clear();
regionSoundLastTick.clear();
trackedPassiveMobs.clear();
tideTick = 0;
lastTideLevel = 0.0;
riverCurrentTick = 0;
@@ -298,19 +307,67 @@ public class LivingWorldMod {
event.setSpawnCancelled(true);
return;
}
if (event.getEntity() instanceof Bee && bootstrap.isPollinatorCollapse(spawnCoord)) {
event.setSpawnCancelled(true);
return;
}
if (event.getEntity() instanceof Animal) {
if (health < PASSIVE_SUPPRESS_HEALTH) {
if (migrationCompanionSpawn) {
trackedPassiveMobs.put(event.getEntity().getUUID(), spawnCoord);
bootstrap.recordPassiveMobSpawn(spawnCoord);
return;
}
if (bootstrap.isLocallyExtinct(spawnCoord)) {
event.setSpawnCancelled(true);
return;
}
double attraction = bootstrap.getMigrationAttraction(spawnCoord);
if (health < PASSIVE_SUPPRESS_HEALTH || attraction < 0.0) {
double chance = (PASSIVE_SUPPRESS_HEALTH - health) / PASSIVE_SUPPRESS_HEALTH * 0.7;
if (random.nextDouble() < chance) event.setSpawnCancelled(true);
chance = Math.max(chance, -attraction * 0.5);
if (random.nextDouble() < chance) {
event.setSpawnCancelled(true);
return;
}
}
trackedPassiveMobs.put(event.getEntity().getUUID(), spawnCoord);
bootstrap.recordPassiveMobSpawn(spawnCoord);
if (attraction >= 0.5 && bootstrap.getPassiveMobPressure(spawnCoord) < 12
&& random.nextDouble() < 0.50) {
migrationCompanionSpawn = true;
try {
event.getEntity().getType().spawn(
level,
event.getEntity().blockPosition().offset(
random.nextInt(5) - 2, 0, random.nextInt(5) - 2),
MobSpawnType.NATURAL);
} finally {
migrationCompanionSpawn = false;
}
}
} else if (event.getEntity() instanceof Monster) {
int preyPressure = bootstrap.getPassiveMobPressure(spawnCoord);
if (preyPressure == 0 && random.nextDouble() < 0.55) {
event.setSpawnCancelled(true);
return;
}
if (health > HOSTILE_SUPPRESS_HEALTH) {
double chance = (health - HOSTILE_SUPPRESS_HEALTH) / (100.0 - HOSTILE_SUPPRESS_HEALTH) * 0.5;
if (preyPressure > 8) chance *= 0.5;
if (random.nextDouble() < chance) event.setSpawnCancelled(true);
}
}
});
NeoForge.EVENT_BUS.addListener(LivingDeathEvent.class, event -> {
RegionCoordinate coord = trackedPassiveMobs.remove(event.getEntity().getUUID());
if (coord != null) bootstrap.recordPassiveMobDeparture(coord);
});
NeoForge.EVENT_BUS.addListener(EntityLeaveLevelEvent.class, event -> {
RegionCoordinate coord = trackedPassiveMobs.remove(event.getEntity().getUUID());
if (coord != null) bootstrap.recordPassiveMobDeparture(coord);
});
// Step 4: Agriculture — bone meal boosts soil fertility.
NeoForge.EVENT_BUS.addListener(PlayerInteractEvent.RightClickBlock.class, event -> {
if (!bootstrap.isServerReady()) return;
@@ -339,6 +396,15 @@ public class LivingWorldMod {
BlockPos pos = event.getPos();
bootstrap.handleCropHarvest(level.dimension().location().toString(), pos.getX(), pos.getZ());
});
NeoForge.EVENT_BUS.addListener(BlockEvent.EntityPlaceEvent.class, event -> {
if (!bootstrap.isServerReady() || !(event.getLevel() instanceof ServerLevel level)) return;
if (!event.getPlacedBlock().is(BlockTags.SAPLINGS)) return;
RegionCoordinate coord = RegionCoordinate.fromBlock(
level.dimension().location().toString(),
event.getPos().getX(), event.getPos().getZ(),
LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
bootstrap.handleSaplingPlaced(coord, level.getGameTime() / 24000L);
});
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World Bootstrap initialized successfully.");
}
@@ -145,6 +145,15 @@ public final class LivingWorldBootstrap {
private final Map<RegionCoordinate, Integer> blizzardHistory = new HashMap<>();
private int hydrologyCycleTick = 0;
private int dynamicSeaLevel = 62;
private final Map<RegionCoordinate, Integer> passiveMobPressure = new HashMap<>();
private final Map<RegionCoordinate, Integer> wildlifeAbsentCycles = new HashMap<>();
private final Set<RegionCoordinate> extinctRegions = new HashSet<>();
private final Set<RegionCoordinate> pollinatorCollapseRegions = new HashSet<>();
private final Map<RegionCoordinate, Long> saplingPlacementDay = new HashMap<>();
private final Map<RegionCoordinate, Integer> saplingPlacementCount = new HashMap<>();
private final Map<RegionCoordinate, Integer> rewildingBoostCycles = new HashMap<>();
private final Map<RegionCoordinate, Integer> bogWetCycles = new HashMap<>();
private final Set<RegionCoordinate> waterloggedRegions = new HashSet<>();
private PlatformAdapter platformAdapter;
private Path worldSaveDirectory;
@@ -322,6 +331,15 @@ public final class LivingWorldBootstrap {
blizzardHistory.clear();
hydrologyCycleTick = 0;
dynamicSeaLevel = ecosystemTuning.getSeaLevel();
passiveMobPressure.clear();
wildlifeAbsentCycles.clear();
extinctRegions.clear();
pollinatorCollapseRegions.clear();
saplingPlacementDay.clear();
saplingPlacementCount.clear();
rewildingBoostCycles.clear();
bogWetCycles.clear();
waterloggedRegions.clear();
simSpeedMultiplier = 1;
serverReady = false;
LivingWorldLogger.info(
@@ -387,7 +405,8 @@ public final class LivingWorldBootstrap {
.get(SoilModule.MODULE_ID, SoilRegionData.class)
.orElse(null);
if (soil == null) return;
soil.setFertility(Math.min(100, soil.getFertility() + 2.0));
double gain = isPollinatorCollapse(coord) ? 1.0 : 2.0;
soil.setFertility(Math.min(100, soil.getFertility() + gain));
region.getModuleData().put(SoilModule.MODULE_ID, soil);
regionManager.markDirty(region);
});
@@ -488,6 +507,136 @@ public final class LivingWorldBootstrap {
applyGeologicalActivity();
}
applyHydrologyExpansion();
applyEcologyExpansion();
}
public void recordPassiveMobSpawn(RegionCoordinate coord) {
if (coord != null) passiveMobPressure.merge(coord, 1, Integer::sum);
}
public void recordPassiveMobDeparture(RegionCoordinate coord) {
if (coord != null) {
passiveMobPressure.compute(coord,
(ignored, pressure) -> Math.max(0, (pressure == null ? 0 : pressure) - 1));
}
}
public int getPassiveMobPressure(RegionCoordinate coord) {
return passiveMobPressure.getOrDefault(coord, 0);
}
public double getMigrationAttraction(RegionCoordinate coord) {
if (coord == null || regionManager == null) return 0.0;
Region region = regionManager.resolve(coord).orElse(null);
if (region == null) return 0.0;
double ownHealth = region.getMetrics().getEcosystemHealth();
double bestNeighbour = ownHealth;
int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (int[] offset : offsets) {
RegionCoordinate neighbour = new RegionCoordinate(
coord.dimensionId(), coord.x() + offset[0], coord.z() + offset[1]);
Region adjacent = regionManager.resolve(neighbour).orElse(null);
if (adjacent != null) {
bestNeighbour = Math.max(bestNeighbour, adjacent.getMetrics().getEcosystemHealth());
}
}
return Math.max(-1.0, Math.min(1.0, (ownHealth - bestNeighbour + 25.0) / 50.0));
}
public boolean isLocallyExtinct(RegionCoordinate coord) {
return coord != null && extinctRegions.contains(coord);
}
public boolean isPollinatorCollapse(RegionCoordinate coord) {
return coord != null && pollinatorCollapseRegions.contains(coord);
}
public void handleSaplingPlaced(RegionCoordinate coord, long absoluteDay) {
if (!serverReady || coord == null) return;
long previousDay = saplingPlacementDay.getOrDefault(coord, Long.MIN_VALUE);
if (previousDay != absoluteDay) {
saplingPlacementDay.put(coord, absoluteDay);
saplingPlacementCount.put(coord, 0);
}
int count = saplingPlacementCount.merge(coord, 1, Integer::sum);
if (count < 10 || rewildingBoostCycles.containsKey(coord)) return;
Region region = regionManager.resolve(coord).orElse(null);
if (region == null) return;
RecoveryRegionData recovery = region.getModuleData()
.get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElse(null);
if (recovery == null || recovery.getSuccessionStage().ordinal()
> SuccessionStage.SPARSE_GRASS.ordinal()) return;
rewildingBoostCycles.put(coord, 20);
pendingEventMessages.add("[LW] Rewilding effort detected in region ("
+ coord.x() + "," + coord.z() + ") - ecosystem recovering!");
}
private void applyEcologyExpansion() {
for (Region region : regionManager.getActiveRegions()) {
RegionCoordinate coord = region.getCoordinate();
RecoveryRegionData recovery = region.getModuleData()
.get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElse(null);
PollutionRegionData pollution = region.getModuleData()
.get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
VegetationRegionData vegetation = region.getModuleData()
.get(VegetationModule.MODULE_ID, VegetationRegionData.class).orElse(null);
WaterRegionData water = region.getModuleData()
.get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null);
if (recovery == null || pollution == null || vegetation == null || water == null) continue;
int pressure = passiveMobPressure.getOrDefault(coord, 0);
if (pressure == 0) {
int absent = wildlifeAbsentCycles.merge(coord, 1, Integer::sum);
if (absent >= 100) extinctRegions.add(coord);
} else {
wildlifeAbsentCycles.remove(coord);
}
if (extinctRegions.contains(coord)
&& recovery.getSuccessionStage().ordinal()
>= SuccessionStage.YOUNG_WOODLAND.ordinal()) {
extinctRegions.remove(coord);
wildlifeAbsentCycles.remove(coord);
pendingEventMessages.add("[LW] Wildlife returning to region ("
+ coord.x() + "," + coord.z() + ").");
}
if (pollution.getGroundPollution() > 50.0
&& vegetation.getFlowerPressure() > 5.0) {
pollinatorCollapseRegions.add(coord);
} else if (pollution.getGroundPollution() < 20.0) {
pollinatorCollapseRegions.remove(coord);
}
Integer boost = rewildingBoostCycles.get(coord);
if (boost != null) {
if (boost == 20 || boost == 10) recovery.boostOneStage();
pollution.addPollution(-1.5, -1.5, -1.5);
if (boost <= 1) rewildingBoostCycles.remove(coord);
else rewildingBoostCycles.put(coord, boost - 1);
}
boolean bogCandidate = coastalWetlandRegions.contains(coord)
&& waterloggedRegions.contains(coord)
&& water.getWaterAvailability() > 75.0
&& recovery.getSuccessionStage().ordinal() <= SuccessionStage.GRASSLAND.ordinal();
if (bogCandidate) {
int wetCycles = bogWetCycles.merge(coord, 1, Integer::sum);
if (wetCycles >= 30) {
recovery.setMaxSuccessionStage(SuccessionStage.SCRUBLAND);
if (worldEffectsModule != null) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.PEAT_FORMS, coord,
Math.min(1.0, wetCycles / 100.0)));
}
}
} else {
bogWetCycles.remove(coord);
}
region.getModuleData().put(RecoveryModule.MODULE_ID, recovery);
region.getModuleData().put(PollutionModule.MODULE_ID, pollution);
regionManager.markDirty(region);
}
}
private void applyHydrologyExpansion() {
@@ -2080,6 +2229,7 @@ public final class LivingWorldBootstrap {
/** Applies a water quality and purification bonus to regions adjacent to or containing water bodies. */
public void applyWaterBodyBoost(RegionCoordinate coord) {
if (!serverReady || coord == null) return;
waterloggedRegions.add(coord);
regionManager.resolve(coord).ifPresent(region -> {
WaterRegionData water = region.getModuleData()
.get(WaterModule.MODULE_ID, WaterRegionData.class)
@@ -72,6 +72,16 @@ public final class RecoveryRegionData {
}
}
/** Advances one restoration stage without bypassing the region's biome cap. */
public void boostOneStage() {
if (successionStage.hasNext()
&& successionStage.next().ordinal() <= maxSuccessionStage.ordinal()) {
successionStage = successionStage.next();
recoveryProgress = 0.0;
damageAccumulation = Math.max(0.0, damageAccumulation - 20.0);
}
}
// ------------------------------------------------------------------
// Mutation
// ------------------------------------------------------------------
@@ -187,4 +187,7 @@ public enum WorldEffectType {
/** Falling river water deepens its impact basin. */
PLUNGE_POOL_DEEPENS,
/** Waterlogged wetland soil slowly develops a persistent peat profile. */
PEAT_FORMS,
}
@@ -11,6 +11,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.function.Predicate;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.particles.ParticleTypes;
@@ -42,12 +43,20 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
private final Supplier<MinecraftServer> serverSupplier;
private final Random random = new Random();
private final Predicate<com.livingworld.regions.RegionCoordinate> flowersAllowed;
private record TimedWater(BlockPos pos, long expiresAt) {}
private final Map<ResourceKey<Level>, List<TimedWater>> geyserWater = new HashMap<>();
private final Map<ResourceKey<Level>, List<TimedWater>> floodWater = new HashMap<>();
public NeoForgeWorldEffectExecutor(Supplier<MinecraftServer> serverSupplier) {
this(serverSupplier, ignored -> true);
}
public NeoForgeWorldEffectExecutor(
Supplier<MinecraftServer> serverSupplier,
Predicate<com.livingworld.regions.RegionCoordinate> flowersAllowed) {
this.serverSupplier = serverSupplier;
this.flowersAllowed = flowersAllowed != null ? flowersAllowed : ignored -> true;
}
@Override
@@ -72,7 +81,8 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
case GRASS_DEGRADES_TO_DIRT ->
degradeGrass(level, baseX, baseZ, request.intensity());
case VEGETATION_SPREADS ->
spreadVegetation(level, baseX, baseZ, request.intensity());
spreadVegetation(level, baseX, baseZ, request.intensity(),
flowersAllowed.test(request.region()));
case POLLUTION_VISUAL_INDICATOR ->
spawnPollutionParticles(level, baseX, baseZ, request.intensity());
case SAPLING_GROWTH_BOOSTED ->
@@ -150,6 +160,8 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
polishGlacierRock(level, baseX, baseZ, request.intensity());
case PLUNGE_POOL_DEEPENS ->
deepenPlungePool(level, baseX, baseZ, request.intensity());
case PEAT_FORMS ->
formPeat(level, baseX, baseZ, request.intensity());
}
}
@@ -180,7 +192,8 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
}
}
private void spreadVegetation(ServerLevel level, int baseX, int baseZ, double intensity) {
private void spreadVegetation(
ServerLevel level, int baseX, int baseZ, double intensity, boolean allowFlowers) {
int attempts = Math.max(1, (int) (intensity * BLOCK_ATTEMPTS));
for (int i = 0; i < attempts; i++) {
BlockPos pos = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS),
@@ -197,7 +210,7 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
"WorldEffect VEGETATION_SPREADS at " + pos);
} else if (state.is(Blocks.GRASS_BLOCK) && brightEnough) {
// Add surface plants — flowers 1-in-5 chance, short grass otherwise
Block plant = random.nextInt(5) == 0
Block plant = allowFlowers && random.nextInt(5) == 0
? (random.nextBoolean() ? Blocks.DANDELION : Blocks.POPPY)
: Blocks.SHORT_GRASS;
level.setBlock(above, plant.defaultBlockState(), Block.UPDATE_ALL);
@@ -1147,6 +1160,23 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
}
}
private void formPeat(ServerLevel level, int baseX, int baseZ, double intensity) {
if (random.nextDouble() > 0.20) return;
for (int i = 0; i < Math.max(2, (int) (intensity * 8)); i++) {
BlockPos pos = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS),
baseZ + random.nextInt(REGION_BLOCKS));
if (pos == null) continue;
var state = level.getBlockState(pos);
if (state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.DIRT)) {
level.setBlock(pos, Blocks.MUD.defaultBlockState(), Block.UPDATE_ALL);
} else if (state.is(Blocks.MUD)) {
level.setBlock(pos, Blocks.PACKED_MUD.defaultBlockState(), Block.UPDATE_ALL);
} else if (state.is(Blocks.PACKED_MUD) && intensity > 0.7) {
level.setBlock(pos, Blocks.BROWN_TERRACOTTA.defaultBlockState(), Block.UPDATE_ALL);
}
}
}
private BlockPos surfaceAt(ServerLevel level, int x, int z) {
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1;
if (y < level.getMinBuildHeight()) {