Complete phase 5 ecology systems
This commit is contained in:
@@ -7,6 +7,8 @@ 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.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.entity.player.PlayerInteractEvent;
|
||||||
import net.neoforged.neoforge.event.level.BlockEvent;
|
import net.neoforged.neoforge.event.level.BlockEvent;
|
||||||
import net.neoforged.neoforge.event.level.ChunkEvent;
|
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.AbstractFish;
|
||||||
import net.minecraft.world.entity.animal.Dolphin;
|
import net.minecraft.world.entity.animal.Dolphin;
|
||||||
import net.minecraft.world.entity.animal.Squid;
|
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.monster.Monster;
|
||||||
|
import net.minecraft.world.entity.MobSpawnType;
|
||||||
import net.minecraft.world.item.Items;
|
import net.minecraft.world.item.Items;
|
||||||
import net.minecraft.world.level.biome.Biome;
|
import net.minecraft.world.level.biome.Biome;
|
||||||
import net.minecraft.world.level.biome.Biomes;
|
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. */
|
/** Stores playerCheckTick value when a region was last water-body-scanned. */
|
||||||
private final Map<RegionCoordinate, Integer> waterBodyLastScan = new HashMap<>();
|
private final Map<RegionCoordinate, Integer> waterBodyLastScan = new HashMap<>();
|
||||||
private final Set<RegionCoordinate> elevationInitialized = new HashSet<>();
|
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). */
|
/** Stores playerCheckTick when a region last played an ambient sound (per-region throttle). */
|
||||||
private final Map<RegionCoordinate, Integer> regionSoundLastTick = new HashMap<>();
|
private final Map<RegionCoordinate, Integer> regionSoundLastTick = new HashMap<>();
|
||||||
|
|
||||||
@@ -153,7 +159,9 @@ public class LivingWorldMod {
|
|||||||
this.minecraftServer = event.getServer();
|
this.minecraftServer = event.getServer();
|
||||||
bootstrap.onServerStarted();
|
bootstrap.onServerStarted();
|
||||||
bootstrap.getWorldEffectsModule().registerConsumer(
|
bootstrap.getWorldEffectsModule().registerConsumer(
|
||||||
new NeoForgeWorldEffectExecutor(() -> minecraftServer));
|
new NeoForgeWorldEffectExecutor(
|
||||||
|
() -> minecraftServer,
|
||||||
|
coord -> !bootstrap.isPollinatorCollapse(coord)));
|
||||||
bootstrap.setOverworldRaining(
|
bootstrap.setOverworldRaining(
|
||||||
() -> minecraftServer != null && minecraftServer.overworld().isRaining());
|
() -> minecraftServer != null && minecraftServer.overworld().isRaining());
|
||||||
bootstrap.setAbsoluteDaySupplier(
|
bootstrap.setAbsoluteDaySupplier(
|
||||||
@@ -240,6 +248,7 @@ public class LivingWorldMod {
|
|||||||
waterBodyLastScan.clear();
|
waterBodyLastScan.clear();
|
||||||
elevationInitialized.clear();
|
elevationInitialized.clear();
|
||||||
regionSoundLastTick.clear();
|
regionSoundLastTick.clear();
|
||||||
|
trackedPassiveMobs.clear();
|
||||||
tideTick = 0;
|
tideTick = 0;
|
||||||
lastTideLevel = 0.0;
|
lastTideLevel = 0.0;
|
||||||
riverCurrentTick = 0;
|
riverCurrentTick = 0;
|
||||||
@@ -298,19 +307,67 @@ public class LivingWorldMod {
|
|||||||
event.setSpawnCancelled(true);
|
event.setSpawnCancelled(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (event.getEntity() instanceof Bee && bootstrap.isPollinatorCollapse(spawnCoord)) {
|
||||||
|
event.setSpawnCancelled(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.getEntity() instanceof Animal) {
|
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;
|
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) {
|
} 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) {
|
if (health > HOSTILE_SUPPRESS_HEALTH) {
|
||||||
double chance = (health - HOSTILE_SUPPRESS_HEALTH) / (100.0 - HOSTILE_SUPPRESS_HEALTH) * 0.5;
|
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);
|
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.
|
// Step 4: Agriculture — bone meal boosts soil fertility.
|
||||||
NeoForge.EVENT_BUS.addListener(PlayerInteractEvent.RightClickBlock.class, event -> {
|
NeoForge.EVENT_BUS.addListener(PlayerInteractEvent.RightClickBlock.class, event -> {
|
||||||
if (!bootstrap.isServerReady()) return;
|
if (!bootstrap.isServerReady()) return;
|
||||||
@@ -339,6 +396,15 @@ public class LivingWorldMod {
|
|||||||
BlockPos pos = event.getPos();
|
BlockPos pos = event.getPos();
|
||||||
bootstrap.handleCropHarvest(level.dimension().location().toString(), pos.getX(), pos.getZ());
|
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.");
|
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 final Map<RegionCoordinate, Integer> blizzardHistory = new HashMap<>();
|
||||||
private int hydrologyCycleTick = 0;
|
private int hydrologyCycleTick = 0;
|
||||||
private int dynamicSeaLevel = 62;
|
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 PlatformAdapter platformAdapter;
|
||||||
private Path worldSaveDirectory;
|
private Path worldSaveDirectory;
|
||||||
@@ -322,6 +331,15 @@ public final class LivingWorldBootstrap {
|
|||||||
blizzardHistory.clear();
|
blizzardHistory.clear();
|
||||||
hydrologyCycleTick = 0;
|
hydrologyCycleTick = 0;
|
||||||
dynamicSeaLevel = ecosystemTuning.getSeaLevel();
|
dynamicSeaLevel = ecosystemTuning.getSeaLevel();
|
||||||
|
passiveMobPressure.clear();
|
||||||
|
wildlifeAbsentCycles.clear();
|
||||||
|
extinctRegions.clear();
|
||||||
|
pollinatorCollapseRegions.clear();
|
||||||
|
saplingPlacementDay.clear();
|
||||||
|
saplingPlacementCount.clear();
|
||||||
|
rewildingBoostCycles.clear();
|
||||||
|
bogWetCycles.clear();
|
||||||
|
waterloggedRegions.clear();
|
||||||
simSpeedMultiplier = 1;
|
simSpeedMultiplier = 1;
|
||||||
serverReady = false;
|
serverReady = false;
|
||||||
LivingWorldLogger.info(
|
LivingWorldLogger.info(
|
||||||
@@ -387,7 +405,8 @@ public final class LivingWorldBootstrap {
|
|||||||
.get(SoilModule.MODULE_ID, SoilRegionData.class)
|
.get(SoilModule.MODULE_ID, SoilRegionData.class)
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
if (soil == null) return;
|
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);
|
region.getModuleData().put(SoilModule.MODULE_ID, soil);
|
||||||
regionManager.markDirty(region);
|
regionManager.markDirty(region);
|
||||||
});
|
});
|
||||||
@@ -488,6 +507,136 @@ public final class LivingWorldBootstrap {
|
|||||||
applyGeologicalActivity();
|
applyGeologicalActivity();
|
||||||
}
|
}
|
||||||
applyHydrologyExpansion();
|
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() {
|
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. */
|
/** Applies a water quality and purification bonus to regions adjacent to or containing water bodies. */
|
||||||
public void applyWaterBodyBoost(RegionCoordinate coord) {
|
public void applyWaterBodyBoost(RegionCoordinate coord) {
|
||||||
if (!serverReady || coord == null) return;
|
if (!serverReady || coord == null) return;
|
||||||
|
waterloggedRegions.add(coord);
|
||||||
regionManager.resolve(coord).ifPresent(region -> {
|
regionManager.resolve(coord).ifPresent(region -> {
|
||||||
WaterRegionData water = region.getModuleData()
|
WaterRegionData water = region.getModuleData()
|
||||||
.get(WaterModule.MODULE_ID, WaterRegionData.class)
|
.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
|
// Mutation
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|||||||
@@ -187,4 +187,7 @@ public enum WorldEffectType {
|
|||||||
|
|
||||||
/** Falling river water deepens its impact basin. */
|
/** Falling river water deepens its impact basin. */
|
||||||
PLUNGE_POOL_DEEPENS,
|
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.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
import java.util.function.Predicate;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.core.Direction;
|
import net.minecraft.core.Direction;
|
||||||
import net.minecraft.core.particles.ParticleTypes;
|
import net.minecraft.core.particles.ParticleTypes;
|
||||||
@@ -42,12 +43,20 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
|
|||||||
|
|
||||||
private final Supplier<MinecraftServer> serverSupplier;
|
private final Supplier<MinecraftServer> serverSupplier;
|
||||||
private final Random random = new Random();
|
private final Random random = new Random();
|
||||||
|
private final Predicate<com.livingworld.regions.RegionCoordinate> flowersAllowed;
|
||||||
private record TimedWater(BlockPos pos, long expiresAt) {}
|
private record TimedWater(BlockPos pos, long expiresAt) {}
|
||||||
private final Map<ResourceKey<Level>, List<TimedWater>> geyserWater = new HashMap<>();
|
private final Map<ResourceKey<Level>, List<TimedWater>> geyserWater = new HashMap<>();
|
||||||
private final Map<ResourceKey<Level>, List<TimedWater>> floodWater = new HashMap<>();
|
private final Map<ResourceKey<Level>, List<TimedWater>> floodWater = new HashMap<>();
|
||||||
|
|
||||||
public NeoForgeWorldEffectExecutor(Supplier<MinecraftServer> serverSupplier) {
|
public NeoForgeWorldEffectExecutor(Supplier<MinecraftServer> serverSupplier) {
|
||||||
|
this(serverSupplier, ignored -> true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NeoForgeWorldEffectExecutor(
|
||||||
|
Supplier<MinecraftServer> serverSupplier,
|
||||||
|
Predicate<com.livingworld.regions.RegionCoordinate> flowersAllowed) {
|
||||||
this.serverSupplier = serverSupplier;
|
this.serverSupplier = serverSupplier;
|
||||||
|
this.flowersAllowed = flowersAllowed != null ? flowersAllowed : ignored -> true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -72,7 +81,8 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
|
|||||||
case GRASS_DEGRADES_TO_DIRT ->
|
case GRASS_DEGRADES_TO_DIRT ->
|
||||||
degradeGrass(level, baseX, baseZ, request.intensity());
|
degradeGrass(level, baseX, baseZ, request.intensity());
|
||||||
case VEGETATION_SPREADS ->
|
case VEGETATION_SPREADS ->
|
||||||
spreadVegetation(level, baseX, baseZ, request.intensity());
|
spreadVegetation(level, baseX, baseZ, request.intensity(),
|
||||||
|
flowersAllowed.test(request.region()));
|
||||||
case POLLUTION_VISUAL_INDICATOR ->
|
case POLLUTION_VISUAL_INDICATOR ->
|
||||||
spawnPollutionParticles(level, baseX, baseZ, request.intensity());
|
spawnPollutionParticles(level, baseX, baseZ, request.intensity());
|
||||||
case SAPLING_GROWTH_BOOSTED ->
|
case SAPLING_GROWTH_BOOSTED ->
|
||||||
@@ -150,6 +160,8 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
|
|||||||
polishGlacierRock(level, baseX, baseZ, request.intensity());
|
polishGlacierRock(level, baseX, baseZ, request.intensity());
|
||||||
case PLUNGE_POOL_DEEPENS ->
|
case PLUNGE_POOL_DEEPENS ->
|
||||||
deepenPlungePool(level, baseX, baseZ, request.intensity());
|
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));
|
int attempts = Math.max(1, (int) (intensity * BLOCK_ATTEMPTS));
|
||||||
for (int i = 0; i < attempts; i++) {
|
for (int i = 0; i < attempts; i++) {
|
||||||
BlockPos pos = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS),
|
BlockPos pos = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS),
|
||||||
@@ -197,7 +210,7 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
|
|||||||
"WorldEffect VEGETATION_SPREADS at " + pos);
|
"WorldEffect VEGETATION_SPREADS at " + pos);
|
||||||
} else if (state.is(Blocks.GRASS_BLOCK) && brightEnough) {
|
} else if (state.is(Blocks.GRASS_BLOCK) && brightEnough) {
|
||||||
// Add surface plants — flowers 1-in-5 chance, short grass otherwise
|
// 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)
|
? (random.nextBoolean() ? Blocks.DANDELION : Blocks.POPPY)
|
||||||
: Blocks.SHORT_GRASS;
|
: Blocks.SHORT_GRASS;
|
||||||
level.setBlock(above, plant.defaultBlockState(), Block.UPDATE_ALL);
|
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) {
|
private BlockPos surfaceAt(ServerLevel level, int x, int z) {
|
||||||
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1;
|
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1;
|
||||||
if (y < level.getMinBuildHeight()) {
|
if (y < level.getMinBuildHeight()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user