Add TOML config, region borders, seed dispersal corridors, wind, and ambient sound
1. TOML config (livingworld-server.toml) — NeoForgeModConfig registers a server-side config spec exposing all major tuning constants: pollution decay/spread, vegetation growth/dieoff rates, recovery rates, and seed dispersal params. EcosystemTuning wires these into the service registry (CoreServices.TUNING); PollutionModule, VegetationModule, RecoveryModule all read from it so admins can tune without recompiling. 2. Persistence audit — CodecKeys confirmed complete and correct for all 8 data-bearing modules. The 4-arg RecoveryRegionData constructor (from the prior session) correctly preserves maxSuccessionStage on every path. No structural issues found. 3. /lw region borders [regionX regionZ] — draws cyan DustParticle lines along all 4 edges of the region at surface height (32 samples per edge, ~4 blocks apart on the 128-block edge). 4. Pollution spreading — already in place via wind-based air diffusion. Extended: applyWaterRunoff now also transports ground pollution downstream (15 % of ground pollution × runoff rate) with a small water bleed, so contaminated upland regions poison their downhill neighbours. 5. Sound ambience — per-region throttled (≥8 check-intervals between plays). Forest (YOUNG_WOODLAND+, health>50): azalea leaves rustling. Barren (SPARSE_GRASS-): dry sand step. Heavy pollution (score>50): basalt deltas mood. Direct ClientboundSoundPacket per player so other nearby players aren't spammed. 6. Seed dispersal & ecological corridors — applySeedDispersal() runs each post-sim cycle. Healthy regions (YOUNG_WOODLAND+) stochastically emit seeds to all 4 neighbours proportional to vegetationPressure. Pollution in the target blocks seeds. Corridor effect: if the target has 2+ healthy neighbours, seed strength × corridorBoostMultiplier (default 3.5×). When accumulated seed rain ≥ 5.0 the target's succession cap is pioneered one stage ahead of what physical conditions alone would allow. /lw wind shows current wind angle and spread rate. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
package com.livingworld;
|
package com.livingworld;
|
||||||
|
|
||||||
|
import net.neoforged.fml.ModContainer;
|
||||||
import net.neoforged.fml.common.Mod;
|
import net.neoforged.fml.common.Mod;
|
||||||
|
import net.neoforged.fml.config.ModConfig;
|
||||||
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;
|
||||||
@@ -33,8 +35,13 @@ 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.world.level.levelgen.Heightmap;
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
import net.minecraft.world.level.storage.LevelResource;
|
import net.minecraft.world.level.storage.LevelResource;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
import net.minecraft.core.particles.ParticleTypes;
|
import net.minecraft.core.particles.ParticleTypes;
|
||||||
import net.minecraft.network.protocol.game.ClientboundGameEventPacket;
|
import net.minecraft.network.protocol.game.ClientboundGameEventPacket;
|
||||||
|
import net.minecraft.network.protocol.game.ClientboundSoundPacket;
|
||||||
|
import net.minecraft.sounds.SoundEvent;
|
||||||
|
import net.minecraft.sounds.SoundEvents;
|
||||||
|
import net.minecraft.sounds.SoundSource;
|
||||||
import net.minecraft.tags.FluidTags;
|
import net.minecraft.tags.FluidTags;
|
||||||
import net.minecraft.world.effect.MobEffectInstance;
|
import net.minecraft.world.effect.MobEffectInstance;
|
||||||
import net.minecraft.world.effect.MobEffects;
|
import net.minecraft.world.effect.MobEffects;
|
||||||
@@ -46,6 +53,7 @@ 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.modules.recovery.SuccessionStage;
|
||||||
|
import com.livingworld.platform.neoforge.NeoForgeModConfig;
|
||||||
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;
|
||||||
@@ -98,10 +106,15 @@ 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<>();
|
||||||
|
/** Stores playerCheckTick when a region last played an ambient sound (per-region throttle). */
|
||||||
|
private final Map<RegionCoordinate, Integer> regionSoundLastTick = new HashMap<>();
|
||||||
|
|
||||||
public LivingWorldMod(IEventBus eventBus) {
|
public LivingWorldMod(IEventBus eventBus, ModContainer modContainer) {
|
||||||
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World mod starting...");
|
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World mod starting...");
|
||||||
|
|
||||||
|
// Register server-side TOML config (written to world/serverconfig/livingworld-server.toml).
|
||||||
|
modContainer.registerConfig(ModConfig.Type.SERVER, NeoForgeModConfig.SPEC);
|
||||||
|
|
||||||
this.bootstrap = new LivingWorldBootstrap();
|
this.bootstrap = new LivingWorldBootstrap();
|
||||||
NeoForgePlatformAdapter platformAdapter = new NeoForgePlatformAdapter(
|
NeoForgePlatformAdapter platformAdapter = new NeoForgePlatformAdapter(
|
||||||
bootstrap::getWorldSaveDirectory,
|
bootstrap::getWorldSaveDirectory,
|
||||||
@@ -113,8 +126,11 @@ public class LivingWorldMod {
|
|||||||
eventBus.addListener(FMLCommonSetupEvent.class, event -> bootstrap.onCommonSetup());
|
eventBus.addListener(FMLCommonSetupEvent.class, event -> bootstrap.onCommonSetup());
|
||||||
NeoForge.EVENT_BUS.addListener(
|
NeoForge.EVENT_BUS.addListener(
|
||||||
ServerStartingEvent.class,
|
ServerStartingEvent.class,
|
||||||
event -> bootstrap.onServerStarting(
|
event -> {
|
||||||
event.getServer().getWorldPath(LevelResource.ROOT)));
|
// Config is loaded before ServerStartingEvent; extract tuning values now.
|
||||||
|
bootstrap.setEcosystemTuning(NeoForgeModConfig.createTuning());
|
||||||
|
bootstrap.onServerStarting(event.getServer().getWorldPath(LevelResource.ROOT));
|
||||||
|
});
|
||||||
NeoForge.EVENT_BUS.addListener(ServerStartedEvent.class, event -> {
|
NeoForge.EVENT_BUS.addListener(ServerStartedEvent.class, event -> {
|
||||||
this.minecraftServer = event.getServer();
|
this.minecraftServer = event.getServer();
|
||||||
bootstrap.onServerStarted();
|
bootstrap.onServerStarted();
|
||||||
@@ -134,6 +150,7 @@ public class LivingWorldMod {
|
|||||||
biomeInitialized.clear();
|
biomeInitialized.clear();
|
||||||
waterBodyLastScan.clear();
|
waterBodyLastScan.clear();
|
||||||
elevationInitialized.clear();
|
elevationInitialized.clear();
|
||||||
|
regionSoundLastTick.clear();
|
||||||
this.minecraftServer = null;
|
this.minecraftServer = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -334,8 +351,61 @@ public class LivingWorldMod {
|
|||||||
if (showHud) {
|
if (showHud) {
|
||||||
metricsOpt.ifPresent(m -> player.displayClientMessage(buildHud(coord, m, atm), true));
|
metricsOpt.ifPresent(m -> player.displayClientMessage(buildHud(coord, m, atm), true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sound ambience — throttled per region so only one sound fires per region per
|
||||||
|
// ~8 seconds regardless of how many players are standing in it.
|
||||||
|
if (player.level() instanceof ServerLevel ambLevel) {
|
||||||
|
Integer lastSound = regionSoundLastTick.get(coord);
|
||||||
|
if (lastSound == null || (playerCheckTick - lastSound) >= 8) {
|
||||||
|
SuccessionStage stage = bootstrap.getSuccessionStageAt(dimId, player.getX(), player.getZ())
|
||||||
|
.orElse(null);
|
||||||
|
double health = metricsOpt.map(RegionMetrics::getEcosystemHealth).orElse(0.0);
|
||||||
|
double poll = metricsOpt.map(RegionMetrics::getPollutionScore).orElse(0.0);
|
||||||
|
if (tryPlayRegionAmbience(player, stage, health, poll)) {
|
||||||
|
regionSoundLastTick.put(coord, playerCheckTick);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays a single ambient sound appropriate for the region's ecological state.
|
||||||
|
* Each sound fires at low volume and random pitch so it blends naturally.
|
||||||
|
* Returns true if a sound was played (so the per-region cooldown can be set).
|
||||||
|
*/
|
||||||
|
private boolean tryPlayRegionAmbience(
|
||||||
|
ServerPlayer player, SuccessionStage stage, double health, double pollution) {
|
||||||
|
float pitch = 0.75f + random.nextFloat() * 0.5f;
|
||||||
|
|
||||||
|
if (stage != null && stage.ordinal() >= SuccessionStage.YOUNG_WOODLAND.ordinal()
|
||||||
|
&& health > 50.0 && random.nextInt(10) == 0) {
|
||||||
|
// Rustling leaves — healthy forest ambience.
|
||||||
|
playAmbientSound(player, Holder.direct(SoundEvents.AZALEA_LEAVES_STEP), 0.18f, pitch);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stage != null && stage.ordinal() <= SuccessionStage.SPARSE_GRASS.ordinal()
|
||||||
|
&& random.nextInt(15) == 0) {
|
||||||
|
// Dry, shifting sand — barren/arid ambience.
|
||||||
|
playAmbientSound(player, Holder.direct(SoundEvents.SAND_STEP), 0.10f, 0.55f + random.nextFloat() * 0.2f);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (pollution > 50.0 && random.nextInt(18) == 0) {
|
||||||
|
// Industrial mood — heavy-pollution ambience (already a Holder.Reference).
|
||||||
|
playAmbientSound(player, SoundEvents.AMBIENT_BASALT_DELTAS_MOOD, 0.12f, 1.0f + random.nextFloat() * 0.15f);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sends an ambient sound packet directly to one player so others nearby don't hear it. */
|
||||||
|
private static void playAmbientSound(ServerPlayer player, Holder<SoundEvent> sound, float vol, float pitch) {
|
||||||
|
player.connection.send(new ClientboundSoundPacket(
|
||||||
|
sound,
|
||||||
|
SoundSource.AMBIENT,
|
||||||
|
player.getX(), player.getEyeY(), player.getZ(),
|
||||||
|
vol, pitch, player.getRandom().nextLong()));
|
||||||
|
}
|
||||||
|
|
||||||
private static final int REGION_BLOCKS =
|
private static final int REGION_BLOCKS =
|
||||||
LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16;
|
LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.livingworld.bootstrap;
|
|||||||
|
|
||||||
import com.livingworld.commands.LivingWorldCommandRoot;
|
import com.livingworld.commands.LivingWorldCommandRoot;
|
||||||
import com.livingworld.config.DefaultConfigService;
|
import com.livingworld.config.DefaultConfigService;
|
||||||
|
import com.livingworld.config.EcosystemTuning;
|
||||||
import com.livingworld.config.SimulationConfig;
|
import com.livingworld.config.SimulationConfig;
|
||||||
import com.livingworld.core.LivingWorldConstants;
|
import com.livingworld.core.LivingWorldConstants;
|
||||||
import com.livingworld.core.services.CoreServices;
|
import com.livingworld.core.services.CoreServices;
|
||||||
@@ -69,11 +70,18 @@ import net.minecraft.commands.CommandSourceStack;
|
|||||||
*/
|
*/
|
||||||
public final class LivingWorldBootstrap {
|
public final class LivingWorldBootstrap {
|
||||||
|
|
||||||
private static final double WIND_BOOST = 0.5;
|
|
||||||
private static final double WIND_DRIFT_MAX = 0.05; // radians per sim cycle
|
private static final double WIND_DRIFT_MAX = 0.05; // radians per sim cycle
|
||||||
|
|
||||||
private double windAngle = 0.0;
|
private double windAngle = 0.0;
|
||||||
private final Random windRandom = new Random();
|
private final Random windRandom = new Random();
|
||||||
|
/** Ecosystem tuning constants — loaded from TOML config at server start. */
|
||||||
|
private EcosystemTuning ecosystemTuning = new EcosystemTuning();
|
||||||
|
/**
|
||||||
|
* Per-region accumulated incoming seed strength from healthy neighbours.
|
||||||
|
* Populated each post-sim cycle by {@link #applySeedDispersal()}.
|
||||||
|
* Transient — not persisted; re-accumulates within a few sim cycles on restart.
|
||||||
|
*/
|
||||||
|
private final Map<RegionCoordinate, Double> accumulatedSeedRain = new HashMap<>();
|
||||||
private final Set<UUID> hudEnabledPlayers = new HashSet<>();
|
private final Set<UUID> hudEnabledPlayers = new HashSet<>();
|
||||||
|
|
||||||
private PlatformAdapter platformAdapter;
|
private PlatformAdapter platformAdapter;
|
||||||
@@ -206,6 +214,11 @@ public final class LivingWorldBootstrap {
|
|||||||
/**
|
/**
|
||||||
* Called when the server is stopping.
|
* Called when the server is stopping.
|
||||||
*/
|
*/
|
||||||
|
/** Sets the ecosystem tuning loaded from the server config. Must be called before onServerStarting(). */
|
||||||
|
public void setEcosystemTuning(EcosystemTuning tuning) {
|
||||||
|
if (tuning != null) ecosystemTuning = tuning;
|
||||||
|
}
|
||||||
|
|
||||||
public void onServerStopping() {
|
public void onServerStopping() {
|
||||||
if (!serverReady) {
|
if (!serverReady) {
|
||||||
return;
|
return;
|
||||||
@@ -215,6 +228,7 @@ public final class LivingWorldBootstrap {
|
|||||||
climateTracker.save(worldSaveDirectory.resolve("living_world/global_climate.dat"));
|
climateTracker.save(worldSaveDirectory.resolve("living_world/global_climate.dat"));
|
||||||
hudEnabledPlayers.clear();
|
hudEnabledPlayers.clear();
|
||||||
regionElevations.clear();
|
regionElevations.clear();
|
||||||
|
accumulatedSeedRain.clear();
|
||||||
simSpeedMultiplier = 1;
|
simSpeedMultiplier = 1;
|
||||||
serverReady = false;
|
serverReady = false;
|
||||||
LivingWorldLogger.info(
|
LivingWorldLogger.info(
|
||||||
@@ -367,6 +381,7 @@ public final class LivingWorldBootstrap {
|
|||||||
applyClimateWarmingEffects();
|
applyClimateWarmingEffects();
|
||||||
applyWaterRunoff();
|
applyWaterRunoff();
|
||||||
applyDynamicCapUpdate();
|
applyDynamicCapUpdate();
|
||||||
|
applySeedDispersal();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the simulation speed multiplier (1 = real-time, max 100). */
|
/** Sets the simulation speed multiplier (1 = real-time, max 100). */
|
||||||
@@ -379,8 +394,6 @@ public final class LivingWorldBootstrap {
|
|||||||
|
|
||||||
public int getSimSpeedMultiplier() { return simSpeedMultiplier; }
|
public int getSimSpeedMultiplier() { return simSpeedMultiplier; }
|
||||||
|
|
||||||
private static final double POLLUTION_SPREAD_RATE = 0.02;
|
|
||||||
|
|
||||||
private void spreadPollutionAcrossRegions() {
|
private void spreadPollutionAcrossRegions() {
|
||||||
Collection<Region> active = regionManager.getActiveRegions();
|
Collection<Region> active = regionManager.getActiveRegions();
|
||||||
if (active.size() < 2) return;
|
if (active.size() < 2) return;
|
||||||
@@ -413,7 +426,8 @@ public final class LivingWorldBootstrap {
|
|||||||
// Wind alignment: positive = downwind (faster spread), negative = upwind (slower).
|
// Wind alignment: positive = downwind (faster spread), negative = upwind (slower).
|
||||||
double offsetAngle = Math.atan2(off[1], off[0]);
|
double offsetAngle = Math.atan2(off[1], off[0]);
|
||||||
double alignment = Math.cos(windAngle - offsetAngle);
|
double alignment = Math.cos(windAngle - offsetAngle);
|
||||||
double rate = POLLUTION_SPREAD_RATE * (1.0 + alignment * WIND_BOOST);
|
double rate = ecosystemTuning.getPollutionSpreadRate()
|
||||||
|
* (1.0 + alignment * ecosystemTuning.getWindBoost());
|
||||||
double transfer = diff * rate;
|
double transfer = diff * rate;
|
||||||
data.addPollution(-transfer, 0, 0);
|
data.addPollution(-transfer, 0, 0);
|
||||||
neighbourData.addPollution(transfer, 0, 0);
|
neighbourData.addPollution(transfer, 0, 0);
|
||||||
@@ -523,6 +537,20 @@ public final class LivingWorldBootstrap {
|
|||||||
nWater.setWaterAvailability(Math.min(100, nWater.getWaterAvailability() + runoff));
|
nWater.setWaterAvailability(Math.min(100, nWater.getWaterAvailability() + runoff));
|
||||||
nWater.setDroughtRisk(Math.max(0, nWater.getDroughtRisk() - runoff * 0.5));
|
nWater.setDroughtRisk(Math.max(0, nWater.getDroughtRisk() - runoff * 0.5));
|
||||||
neighbour.getModuleData().put(WaterModule.MODULE_ID, nWater);
|
neighbour.getModuleData().put(WaterModule.MODULE_ID, nWater);
|
||||||
|
|
||||||
|
// Runoff also carries ground pollution downstream — contaminants travel with water.
|
||||||
|
PollutionRegionData srcPoll = region.getModuleData()
|
||||||
|
.get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
|
||||||
|
PollutionRegionData dstPoll = neighbour.getModuleData()
|
||||||
|
.get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
|
||||||
|
if (srcPoll != null && dstPoll != null && srcPoll.getGroundPollution() > 0.5) {
|
||||||
|
double pollTransfer = runoff * 0.15 * srcPoll.getGroundPollution();
|
||||||
|
srcPoll.addPollution(0, -pollTransfer, 0);
|
||||||
|
dstPoll.addPollution(0, pollTransfer, pollTransfer * 0.1); // small bleed to water
|
||||||
|
region.getModuleData().put(PollutionModule.MODULE_ID, srcPoll);
|
||||||
|
neighbour.getModuleData().put(PollutionModule.MODULE_ID, dstPoll);
|
||||||
|
}
|
||||||
|
|
||||||
regionManager.markDirty(neighbour);
|
regionManager.markDirty(neighbour);
|
||||||
}
|
}
|
||||||
region.getModuleData().put(WaterModule.MODULE_ID, myWater);
|
region.getModuleData().put(WaterModule.MODULE_ID, myWater);
|
||||||
@@ -578,6 +606,130 @@ public final class LivingWorldBootstrap {
|
|||||||
return SuccessionStage.SPARSE_GRASS;
|
return SuccessionStage.SPARSE_GRASS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spreads seeds from healthy regions (YOUNG_WOODLAND+) to their neighbours, allowing
|
||||||
|
* degraded regions to recover faster — especially when flanked by multiple healthy regions
|
||||||
|
* (corridor effect).
|
||||||
|
*
|
||||||
|
* <p>Seeds are blocked by pollution in the target region. When a target region receives
|
||||||
|
* sufficient seeds it can have its succession cap temporarily pioneered one stage ahead of
|
||||||
|
* what physical conditions would normally allow, representing natural recolonisation.
|
||||||
|
*/
|
||||||
|
private void applySeedDispersal() {
|
||||||
|
Collection<Region> active = regionManager.getActiveRegions();
|
||||||
|
if (active.size() < 2) return;
|
||||||
|
|
||||||
|
Map<RegionCoordinate, Region> byCoord = new HashMap<>();
|
||||||
|
for (Region r : active) byCoord.put(r.getCoordinate(), r);
|
||||||
|
|
||||||
|
// First pass: collect seed emissions from healthy source regions.
|
||||||
|
Map<RegionCoordinate, Double> incomingSeeds = new HashMap<>();
|
||||||
|
int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||||
|
|
||||||
|
for (Region region : active) {
|
||||||
|
RecoveryRegionData recovery = region.getModuleData()
|
||||||
|
.get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElse(null);
|
||||||
|
if (recovery == null) continue;
|
||||||
|
if (recovery.getSuccessionStage().ordinal() < SuccessionStage.YOUNG_WOODLAND.ordinal()) continue;
|
||||||
|
|
||||||
|
double vegPressure = region.getMetrics().getVegetationPressure();
|
||||||
|
double emissionChance = vegPressure * ecosystemTuning.getSeedEmissionRate();
|
||||||
|
if (windRandom.nextDouble() > emissionChance) continue; // stochastic emission
|
||||||
|
|
||||||
|
double seedStrength = vegPressure * 0.1;
|
||||||
|
RegionCoordinate coord = region.getCoordinate();
|
||||||
|
for (int[] off : offsets) {
|
||||||
|
RegionCoordinate nCoord = new RegionCoordinate(
|
||||||
|
coord.dimensionId(), coord.x() + off[0], coord.z() + off[1]);
|
||||||
|
if (byCoord.containsKey(nCoord)) {
|
||||||
|
incomingSeeds.merge(nCoord, seedStrength, Double::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (incomingSeeds.isEmpty()) return;
|
||||||
|
|
||||||
|
// Second pass: apply incoming seeds to target regions.
|
||||||
|
for (var entry : incomingSeeds.entrySet()) {
|
||||||
|
RegionCoordinate targetCoord = entry.getKey();
|
||||||
|
Region target = byCoord.get(targetCoord);
|
||||||
|
if (target == null) continue;
|
||||||
|
|
||||||
|
// Pollution in the target region blocks seed germination.
|
||||||
|
double pollution = target.getMetrics().getPollutionScore();
|
||||||
|
double pollBlock = Math.min(1.0, pollution * ecosystemTuning.getSeedPollutionBlock());
|
||||||
|
double effectiveSeeds = entry.getValue() * (1.0 - pollBlock);
|
||||||
|
if (effectiveSeeds <= 0) continue;
|
||||||
|
|
||||||
|
// Corridor boost: count how many neighbours are themselves healthy.
|
||||||
|
int healthyNeighbours = 0;
|
||||||
|
for (int[] off : offsets) {
|
||||||
|
RegionCoordinate nCoord = new RegionCoordinate(
|
||||||
|
targetCoord.dimensionId(), targetCoord.x() + off[0], targetCoord.z() + off[1]);
|
||||||
|
Region neighbour = byCoord.get(nCoord);
|
||||||
|
if (neighbour == null) continue;
|
||||||
|
RecoveryRegionData nRec = neighbour.getModuleData()
|
||||||
|
.get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElse(null);
|
||||||
|
if (nRec != null && nRec.getSuccessionStage().ordinal()
|
||||||
|
>= SuccessionStage.YOUNG_WOODLAND.ordinal()) {
|
||||||
|
healthyNeighbours++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (healthyNeighbours >= 2) {
|
||||||
|
effectiveSeeds *= ecosystemTuning.getCorridorBoostMultiplier();
|
||||||
|
}
|
||||||
|
|
||||||
|
accumulatedSeedRain.merge(targetCoord, effectiveSeeds, Double::sum);
|
||||||
|
|
||||||
|
// Attempt to pioneer the succession cap one stage ahead of what conditions currently
|
||||||
|
// support, representing natural recolonisation via seed rain.
|
||||||
|
RecoveryRegionData targetRec = target.getModuleData()
|
||||||
|
.get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElse(null);
|
||||||
|
WaterRegionData water = target.getModuleData()
|
||||||
|
.get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null);
|
||||||
|
SoilRegionData soil = target.getModuleData()
|
||||||
|
.get(SoilModule.MODULE_ID, SoilRegionData.class).orElse(null);
|
||||||
|
if (targetRec == null || water == null || soil == null) continue;
|
||||||
|
|
||||||
|
SuccessionStage conditionCap = computeDynamicCap(water, soil);
|
||||||
|
// Seeds pioneer only one stage ahead and only if accumulated rain is significant.
|
||||||
|
double totalRain = accumulatedSeedRain.getOrDefault(targetCoord, 0.0);
|
||||||
|
if (conditionCap.hasNext() && totalRain >= 5.0) {
|
||||||
|
SuccessionStage pioneered = conditionCap.next();
|
||||||
|
if (pioneered.ordinal() > targetRec.getMaxSuccessionStage().ordinal()) {
|
||||||
|
targetRec.setMaxSuccessionStage(pioneered);
|
||||||
|
target.getModuleData().put(RecoveryModule.MODULE_ID, targetRec);
|
||||||
|
regionManager.markDirty(target);
|
||||||
|
LivingWorldLogger.info(DiagnosticCategory.SIMULATION,
|
||||||
|
"Region " + targetCoord + " seed-rain pioneered cap to " + pioneered
|
||||||
|
+ (healthyNeighbours >= 2 ? " [corridor x" + String.format("%.1f",
|
||||||
|
ecosystemTuning.getCorridorBoostMultiplier()) + "]" : ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a formatted wind status string for the {@code /lw wind} command. */
|
||||||
|
public String getWindInfo() {
|
||||||
|
// Map angle to 8-point compass (N=0°, E=90°, S=180°, W=270°).
|
||||||
|
String[] compass = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"};
|
||||||
|
double normalised = ((windAngle % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
|
||||||
|
int idx = (int) Math.round(normalised / (Math.PI / 4)) % 8;
|
||||||
|
return String.format("[LW] Wind: %s (%.0f°) | Spread rate: %.3f | Speed: %dx",
|
||||||
|
compass[idx], Math.toDegrees(normalised),
|
||||||
|
ecosystemTuning.getPollutionSpreadRate(), simSpeedMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the succession stage for the region at the given world position, if loaded. */
|
||||||
|
public Optional<SuccessionStage> getSuccessionStageAt(String dimId, double x, double z) {
|
||||||
|
if (!serverReady) return Optional.empty();
|
||||||
|
RegionCoordinate coord = RegionCoordinate.fromBlock(
|
||||||
|
dimId, (int) x, (int) z, LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
|
||||||
|
return regionManager.resolve(coord)
|
||||||
|
.flatMap(r -> r.getModuleData().get(RecoveryModule.MODULE_ID, RecoveryRegionData.class))
|
||||||
|
.map(RecoveryRegionData::getSuccessionStage);
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns the current atmospheric state for a region, if it has been initialised. */
|
/** Returns the current atmospheric state for a region, if it has been initialised. */
|
||||||
public Optional<AtmosphereRegionData> getRegionalWeather(RegionCoordinate coord) {
|
public Optional<AtmosphereRegionData> getRegionalWeather(RegionCoordinate coord) {
|
||||||
if (!serverReady || coord == null) return Optional.empty();
|
if (!serverReady || coord == null) return Optional.empty();
|
||||||
@@ -662,7 +814,8 @@ public final class LivingWorldBootstrap {
|
|||||||
this::toggleHud,
|
this::toggleHud,
|
||||||
this::getAtmosphereStatusFor,
|
this::getAtmosphereStatusFor,
|
||||||
this::getClimateStatusFor,
|
this::getClimateStatusFor,
|
||||||
this::setSimSpeedMultiplier);
|
this::setSimSpeedMultiplier,
|
||||||
|
this::getWindInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Path getWorldSaveDirectory() {
|
public Path getWorldSaveDirectory() {
|
||||||
@@ -712,6 +865,7 @@ public final class LivingWorldBootstrap {
|
|||||||
|
|
||||||
services = new ServiceRegistry();
|
services = new ServiceRegistry();
|
||||||
services.register(CoreServices.CONFIG, configService);
|
services.register(CoreServices.CONFIG, configService);
|
||||||
|
services.register(CoreServices.TUNING, ecosystemTuning);
|
||||||
services.register(CoreServices.REGIONS, regionManager);
|
services.register(CoreServices.REGIONS, regionManager);
|
||||||
services.register(CoreServices.SIMULATION, simulationManager);
|
services.register(CoreServices.SIMULATION, simulationManager);
|
||||||
services.register(CoreServices.PERSISTENCE, persistenceService);
|
services.register(CoreServices.PERSISTENCE, persistenceService);
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ public final class LivingWorldCommandRoot {
|
|||||||
uuid -> false,
|
uuid -> false,
|
||||||
css -> "Atmosphere not available.",
|
css -> "Atmosphere not available.",
|
||||||
css -> "Climate not available.",
|
css -> "Climate not available.",
|
||||||
n -> n);
|
n -> n,
|
||||||
|
() -> "Wind not available.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void registerDeferred(
|
public static void registerDeferred(
|
||||||
@@ -54,7 +55,8 @@ public final class LivingWorldCommandRoot {
|
|||||||
Function<UUID, Boolean> hudToggle,
|
Function<UUID, Boolean> hudToggle,
|
||||||
Function<CommandSourceStack, String> atmosphereStatus,
|
Function<CommandSourceStack, String> atmosphereStatus,
|
||||||
Function<CommandSourceStack, String> climateStatus,
|
Function<CommandSourceStack, String> climateStatus,
|
||||||
IntUnaryOperator speedSetter) {
|
IntUnaryOperator speedSetter,
|
||||||
|
Supplier<String> windStatus) {
|
||||||
if (dispatcher == null) {
|
if (dispatcher == null) {
|
||||||
throw new IllegalArgumentException("dispatcher must not be null");
|
throw new IllegalArgumentException("dispatcher must not be null");
|
||||||
}
|
}
|
||||||
@@ -92,7 +94,8 @@ public final class LivingWorldCommandRoot {
|
|||||||
.executes(context -> ForceUpdateCommand.executeAtSelf(
|
.executes(context -> ForceUpdateCommand.executeAtSelf(
|
||||||
context.getSource(),
|
context.getSource(),
|
||||||
requireService(regionManager, "regionManager"),
|
requireService(regionManager, "regionManager"),
|
||||||
requireService(simulationManager, "simulationManager")))))
|
requireService(simulationManager, "simulationManager"))))
|
||||||
|
.then(RegionBordersCommand.build()))
|
||||||
.then(Commands.literal("modules")
|
.then(Commands.literal("modules")
|
||||||
.then(Commands.literal("list")
|
.then(Commands.literal("list")
|
||||||
.executes(context -> listModules(
|
.executes(context -> listModules(
|
||||||
@@ -125,6 +128,12 @@ public final class LivingWorldCommandRoot {
|
|||||||
context.getSource().sendSuccess(() -> Component.literal(info), false);
|
context.getSource().sendSuccess(() -> Component.literal(info), false);
|
||||||
return 1;
|
return 1;
|
||||||
}))
|
}))
|
||||||
|
.then(Commands.literal("wind")
|
||||||
|
.executes(context -> {
|
||||||
|
String info = windStatus.get();
|
||||||
|
context.getSource().sendSuccess(() -> Component.literal(info), false);
|
||||||
|
return 1;
|
||||||
|
}))
|
||||||
.then(Commands.literal("speed")
|
.then(Commands.literal("speed")
|
||||||
.then(Commands.argument("multiplier", IntegerArgumentType.integer(1, 100))
|
.then(Commands.argument("multiplier", IntegerArgumentType.integer(1, 100))
|
||||||
.executes(context -> {
|
.executes(context -> {
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package com.livingworld.commands;
|
||||||
|
|
||||||
|
import com.livingworld.core.LivingWorldConstants;
|
||||||
|
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||||
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
import net.minecraft.commands.Commands;
|
||||||
|
import net.minecraft.core.particles.DustParticleOptions;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import org.joml.Vector3f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements {@code /lw region borders [regionX regionZ]}.
|
||||||
|
*
|
||||||
|
* <p>Draws temporary cyan dust-particle lines along all four edges of the
|
||||||
|
* specified region (or the player's current region if no coordinates are given).
|
||||||
|
* Each edge is sampled at the world surface so borders follow terrain.</p>
|
||||||
|
*/
|
||||||
|
public final class RegionBordersCommand {
|
||||||
|
|
||||||
|
private static final int REGION_BLOCKS = LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16;
|
||||||
|
private static final int EDGE_SAMPLES = 32; // particles per edge (~4 blocks apart on 128-block edge)
|
||||||
|
|
||||||
|
// Cyan-teal colour for region borders.
|
||||||
|
private static final DustParticleOptions BORDER_PARTICLE =
|
||||||
|
new DustParticleOptions(new Vector3f(0.0f, 0.95f, 0.85f), 1.5f);
|
||||||
|
|
||||||
|
private RegionBordersCommand() {}
|
||||||
|
|
||||||
|
public static LiteralArgumentBuilder<CommandSourceStack> build() {
|
||||||
|
return Commands.literal("borders")
|
||||||
|
.executes(ctx -> execute(ctx.getSource(), null, null))
|
||||||
|
.then(Commands.argument("regionX", IntegerArgumentType.integer())
|
||||||
|
.then(Commands.argument("regionZ", IntegerArgumentType.integer())
|
||||||
|
.executes(ctx -> execute(
|
||||||
|
ctx.getSource(),
|
||||||
|
IntegerArgumentType.getInteger(ctx, "regionX"),
|
||||||
|
IntegerArgumentType.getInteger(ctx, "regionZ")))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int execute(CommandSourceStack source, Integer regionX, Integer regionZ) {
|
||||||
|
ServerPlayer player;
|
||||||
|
try {
|
||||||
|
player = source.getPlayerOrException();
|
||||||
|
} catch (CommandSyntaxException e) {
|
||||||
|
source.sendFailure(Component.literal("/lw region borders requires a player"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rx = regionX != null ? regionX
|
||||||
|
: (int) Math.floor(player.getX() / REGION_BLOCKS);
|
||||||
|
int rz = regionZ != null ? regionZ
|
||||||
|
: (int) Math.floor(player.getZ() / REGION_BLOCKS);
|
||||||
|
|
||||||
|
ServerLevel level = player.serverLevel();
|
||||||
|
int baseX = rx * REGION_BLOCKS;
|
||||||
|
int baseZ = rz * REGION_BLOCKS;
|
||||||
|
|
||||||
|
for (int i = 0; i <= EDGE_SAMPLES; i++) {
|
||||||
|
int offset = (int) ((double) i / EDGE_SAMPLES * REGION_BLOCKS);
|
||||||
|
// North edge: z = baseZ, vary x
|
||||||
|
spawnEdgeParticles(level, baseX + offset, baseZ);
|
||||||
|
// South edge: z = baseZ + REGION_BLOCKS, vary x
|
||||||
|
spawnEdgeParticles(level, baseX + offset, baseZ + REGION_BLOCKS);
|
||||||
|
// West edge: x = baseX, vary z
|
||||||
|
spawnEdgeParticles(level, baseX, baseZ + offset);
|
||||||
|
// East edge: x = baseX + REGION_BLOCKS, vary z
|
||||||
|
spawnEdgeParticles(level, baseX + REGION_BLOCKS, baseZ + offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
source.sendSuccess(() -> Component.literal(String.format(
|
||||||
|
"[LW] Region (%d,%d) borders — blocks X[%d..%d] Z[%d..%d]",
|
||||||
|
rx, rz, baseX, baseX + REGION_BLOCKS - 1, baseZ, baseZ + REGION_BLOCKS - 1)),
|
||||||
|
false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void spawnEdgeParticles(ServerLevel level, int x, int z) {
|
||||||
|
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z) + 1;
|
||||||
|
// Use count=3 so the vertical column is visible even in high terrain.
|
||||||
|
level.sendParticles(BORDER_PARTICLE,
|
||||||
|
x + 0.5, y + 0.5, z + 0.5,
|
||||||
|
3, // count
|
||||||
|
0.0, 0.8, 0.0, // spread x/y/z
|
||||||
|
0.0); // speed (0 = stationary)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package com.livingworld.config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tuning constants for the Living World ecosystem simulation.
|
||||||
|
*
|
||||||
|
* <p>Populated from {@code config/livingworld-server.toml} at server startup via
|
||||||
|
* {@link com.livingworld.platform.neoforge.NeoForgeModConfig}. Modules access this
|
||||||
|
* via {@link com.livingworld.core.services.CoreServices#TUNING} from the service
|
||||||
|
* registry during {@code initialize()}.
|
||||||
|
*
|
||||||
|
* <p>Default values match the hardcoded constants they replaced, so a missing
|
||||||
|
* config file produces identical simulation behaviour to earlier versions.
|
||||||
|
*/
|
||||||
|
public final class EcosystemTuning {
|
||||||
|
|
||||||
|
// -- Pollution --
|
||||||
|
private double pollutionDecayRate = 0.002;
|
||||||
|
private double groundToWaterLeach = 0.005;
|
||||||
|
private double pollutionSpreadRate = 0.02;
|
||||||
|
private double windBoost = 0.5;
|
||||||
|
|
||||||
|
// -- Vegetation growth (per soil-quality unit above threshold per tick) --
|
||||||
|
private double grassGrowthRate = 0.06;
|
||||||
|
private double flowerGrowthRate = 0.03;
|
||||||
|
private double shrubGrowthRate = 0.02;
|
||||||
|
private double treeGrowthRate = 0.008;
|
||||||
|
|
||||||
|
// -- Vegetation die-off (flat reduction per tick under bad conditions) --
|
||||||
|
private double grassDieoffRate = 1.00;
|
||||||
|
private double flowerDieoffRate = 0.50;
|
||||||
|
private double shrubDieoffRate = 0.25;
|
||||||
|
private double treeDieoffRate = 0.10;
|
||||||
|
|
||||||
|
// -- Ecological succession --
|
||||||
|
private double baseProgressPerTick = 2.0;
|
||||||
|
private double damagePerBadTick = 5.0;
|
||||||
|
private double damageDecayRate = 0.03;
|
||||||
|
|
||||||
|
// -- Seed dispersal (new ecological corridors feature) --
|
||||||
|
/** Fraction of vegetation pressure emitted as seeds to each neighbour per tick. */
|
||||||
|
private double seedEmissionRate = 0.01;
|
||||||
|
/** Seed multiplier when the target region has 2+ healthy neighbours (corridor effect). */
|
||||||
|
private double corridorBoostMultiplier = 3.5;
|
||||||
|
/** Seeds blocked per unit of pollution score in the target region. */
|
||||||
|
private double seedPollutionBlock = 0.015;
|
||||||
|
|
||||||
|
public EcosystemTuning() {}
|
||||||
|
|
||||||
|
// -- Pollution getters/setters --
|
||||||
|
public double getPollutionDecayRate() { return pollutionDecayRate; }
|
||||||
|
public double getGroundToWaterLeach() { return groundToWaterLeach; }
|
||||||
|
public double getPollutionSpreadRate() { return pollutionSpreadRate; }
|
||||||
|
public double getWindBoost() { return windBoost; }
|
||||||
|
public void setPollutionDecayRate(double v) { pollutionDecayRate = v; }
|
||||||
|
public void setGroundToWaterLeach(double v) { groundToWaterLeach = v; }
|
||||||
|
public void setPollutionSpreadRate(double v) { pollutionSpreadRate = v; }
|
||||||
|
public void setWindBoost(double v) { windBoost = v; }
|
||||||
|
|
||||||
|
// -- Vegetation growth getters/setters --
|
||||||
|
public double getGrassGrowthRate() { return grassGrowthRate; }
|
||||||
|
public double getFlowerGrowthRate() { return flowerGrowthRate; }
|
||||||
|
public double getShrubGrowthRate() { return shrubGrowthRate; }
|
||||||
|
public double getTreeGrowthRate() { return treeGrowthRate; }
|
||||||
|
public void setGrassGrowthRate(double v) { grassGrowthRate = v; }
|
||||||
|
public void setFlowerGrowthRate(double v) { flowerGrowthRate = v; }
|
||||||
|
public void setShrubGrowthRate(double v) { shrubGrowthRate = v; }
|
||||||
|
public void setTreeGrowthRate(double v) { treeGrowthRate = v; }
|
||||||
|
|
||||||
|
// -- Vegetation die-off getters/setters --
|
||||||
|
public double getGrassDieoffRate() { return grassDieoffRate; }
|
||||||
|
public double getFlowerDieoffRate() { return flowerDieoffRate; }
|
||||||
|
public double getShrubDieoffRate() { return shrubDieoffRate; }
|
||||||
|
public double getTreeDieoffRate() { return treeDieoffRate; }
|
||||||
|
public void setGrassDieoffRate(double v) { grassDieoffRate = v; }
|
||||||
|
public void setFlowerDieoffRate(double v) { flowerDieoffRate = v; }
|
||||||
|
public void setShrubDieoffRate(double v) { shrubDieoffRate = v; }
|
||||||
|
public void setTreeDieoffRate(double v) { treeDieoffRate = v; }
|
||||||
|
|
||||||
|
// -- Succession getters/setters --
|
||||||
|
public double getBaseProgressPerTick() { return baseProgressPerTick; }
|
||||||
|
public double getDamagePerBadTick() { return damagePerBadTick; }
|
||||||
|
public double getDamageDecayRate() { return damageDecayRate; }
|
||||||
|
public void setBaseProgressPerTick(double v) { baseProgressPerTick = v; }
|
||||||
|
public void setDamagePerBadTick(double v) { damagePerBadTick = v; }
|
||||||
|
public void setDamageDecayRate(double v) { damageDecayRate = v; }
|
||||||
|
|
||||||
|
// -- Seed dispersal getters/setters --
|
||||||
|
public double getSeedEmissionRate() { return seedEmissionRate; }
|
||||||
|
public double getCorridorBoostMultiplier() { return corridorBoostMultiplier; }
|
||||||
|
public double getSeedPollutionBlock() { return seedPollutionBlock; }
|
||||||
|
public void setSeedEmissionRate(double v) { seedEmissionRate = v; }
|
||||||
|
public void setCorridorBoostMultiplier(double v) { corridorBoostMultiplier = v; }
|
||||||
|
public void setSeedPollutionBlock(double v) { seedPollutionBlock = v; }
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.livingworld.core.services;
|
package com.livingworld.core.services;
|
||||||
|
|
||||||
|
import com.livingworld.config.EcosystemTuning;
|
||||||
import com.livingworld.core.simulation.RegionManager;
|
import com.livingworld.core.simulation.RegionManager;
|
||||||
import com.livingworld.core.simulation.SimulationManager;
|
import com.livingworld.core.simulation.SimulationManager;
|
||||||
import com.livingworld.events.LivingWorldEventBus;
|
import com.livingworld.events.LivingWorldEventBus;
|
||||||
@@ -40,6 +41,9 @@ public final class CoreServices {
|
|||||||
/** ConfigService key — interface already exists. */
|
/** ConfigService key — interface already exists. */
|
||||||
public static final ServiceKey<ConfigService> CONFIG = new ServiceKey<>("config", ConfigService.class);
|
public static final ServiceKey<ConfigService> CONFIG = new ServiceKey<>("config", ConfigService.class);
|
||||||
|
|
||||||
|
/** EcosystemTuning key — holds all tunable simulation constants (loaded from TOML config). */
|
||||||
|
public static final ServiceKey<EcosystemTuning> TUNING = new ServiceKey<>("tuning", EcosystemTuning.class);
|
||||||
|
|
||||||
public static final ServiceKey<RegionManager> REGIONS =
|
public static final ServiceKey<RegionManager> REGIONS =
|
||||||
new ServiceKey<>("regions", RegionManager.class);
|
new ServiceKey<>("regions", RegionManager.class);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.livingworld.modules.pollution;
|
package com.livingworld.modules.pollution;
|
||||||
|
|
||||||
|
import com.livingworld.config.EcosystemTuning;
|
||||||
|
import com.livingworld.core.services.CoreServices;
|
||||||
import com.livingworld.data.serialization.PersistenceReader;
|
import com.livingworld.data.serialization.PersistenceReader;
|
||||||
import com.livingworld.data.serialization.PersistenceWriter;
|
import com.livingworld.data.serialization.PersistenceWriter;
|
||||||
import com.livingworld.events.LivingWorldEvent;
|
import com.livingworld.events.LivingWorldEvent;
|
||||||
@@ -42,11 +44,11 @@ public final class PollutionModule implements SimulationModule {
|
|||||||
|
|
||||||
public static final String MODULE_ID = "pollution";
|
public static final String MODULE_ID = "pollution";
|
||||||
|
|
||||||
private static final double BASE_DECAY_RATE = 0.002;
|
|
||||||
private static final double GROUND_TO_WATER_LEACH = 0.005;
|
|
||||||
private static final double WATER_QUALITY_IMPACT = 0.05;
|
private static final double WATER_QUALITY_IMPACT = 0.05;
|
||||||
private static final double CHANGE_THRESHOLD = 0.01;
|
private static final double CHANGE_THRESHOLD = 0.01;
|
||||||
|
|
||||||
|
private EcosystemTuning tuning = new EcosystemTuning();
|
||||||
|
|
||||||
private static final ModuleMetadata METADATA = new ModuleMetadata(
|
private static final ModuleMetadata METADATA = new ModuleMetadata(
|
||||||
MODULE_ID,
|
MODULE_ID,
|
||||||
"Pollution",
|
"Pollution",
|
||||||
@@ -68,6 +70,7 @@ public final class PollutionModule implements SimulationModule {
|
|||||||
@Override
|
@Override
|
||||||
public void initialize(ModuleContext context) {
|
public void initialize(ModuleContext context) {
|
||||||
if (context == null) throw new IllegalArgumentException("context must not be null");
|
if (context == null) throw new IllegalArgumentException("context must not be null");
|
||||||
|
if (context.hasService(CoreServices.TUNING)) this.tuning = context.getService(CoreServices.TUNING);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -94,10 +97,10 @@ public final class PollutionModule implements SimulationModule {
|
|||||||
double prevWaterQuality = region.getMetrics().getWaterQuality();
|
double prevWaterQuality = region.getMetrics().getWaterQuality();
|
||||||
|
|
||||||
// Natural decay: air decays fastest, water slowest (modulated by resistance).
|
// Natural decay: air decays fastest, water slowest (modulated by resistance).
|
||||||
data.decay(BASE_DECAY_RATE);
|
data.decay(tuning.getPollutionDecayRate());
|
||||||
|
|
||||||
// Ground pollution slowly leaches into water even after decay.
|
// Ground pollution slowly leaches into water even after decay.
|
||||||
double leach = data.getGroundPollution() * GROUND_TO_WATER_LEACH;
|
double leach = data.getGroundPollution() * tuning.getGroundToWaterLeach();
|
||||||
data.addPollution(0.0, 0.0, leach);
|
data.addPollution(0.0, 0.0, leach);
|
||||||
|
|
||||||
// Summary metric: weighted average emphasising waterPollution as most damaging.
|
// Summary metric: weighted average emphasising waterPollution as most damaging.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.livingworld.modules.recovery;
|
package com.livingworld.modules.recovery;
|
||||||
|
|
||||||
|
import com.livingworld.config.EcosystemTuning;
|
||||||
|
import com.livingworld.core.services.CoreServices;
|
||||||
import com.livingworld.data.serialization.PersistenceReader;
|
import com.livingworld.data.serialization.PersistenceReader;
|
||||||
import com.livingworld.data.serialization.PersistenceWriter;
|
import com.livingworld.data.serialization.PersistenceWriter;
|
||||||
import com.livingworld.events.LivingWorldEvent;
|
import com.livingworld.events.LivingWorldEvent;
|
||||||
@@ -41,17 +43,12 @@ public final class RecoveryModule implements SimulationModule {
|
|||||||
|
|
||||||
public static final String MODULE_ID = "recovery";
|
public static final String MODULE_ID = "recovery";
|
||||||
|
|
||||||
/** Base recovery progress per tick (added when conditions are met). */
|
|
||||||
private static final double BASE_PROGRESS_PER_TICK = 2.0;
|
|
||||||
/** Extra recovery progress per point of ecosystemHealth above 50. */
|
/** Extra recovery progress per point of ecosystemHealth above 50. */
|
||||||
private static final double HEALTH_PROGRESS_BONUS = 0.05;
|
private static final double HEALTH_PROGRESS_BONUS = 0.05;
|
||||||
/** Damage accumulated per tick when conditions are badly violated. */
|
|
||||||
private static final double DAMAGE_PER_BAD_TICK = 5.0;
|
|
||||||
/** Damage decays this fraction per tick when conditions are OK.
|
|
||||||
* Kept low so damage sticks around during oscillating conditions. */
|
|
||||||
private static final double DAMAGE_DECAY_RATE = 0.03;
|
|
||||||
private static final double CHANGE_THRESHOLD = 0.01;
|
private static final double CHANGE_THRESHOLD = 0.01;
|
||||||
|
|
||||||
|
private EcosystemTuning tuning = new EcosystemTuning();
|
||||||
|
|
||||||
private static final ModuleMetadata METADATA = new ModuleMetadata(
|
private static final ModuleMetadata METADATA = new ModuleMetadata(
|
||||||
MODULE_ID,
|
MODULE_ID,
|
||||||
"Recovery & Succession",
|
"Recovery & Succession",
|
||||||
@@ -73,6 +70,7 @@ public final class RecoveryModule implements SimulationModule {
|
|||||||
@Override
|
@Override
|
||||||
public void initialize(ModuleContext context) {
|
public void initialize(ModuleContext context) {
|
||||||
if (context == null) throw new IllegalArgumentException("context must not be null");
|
if (context == null) throw new IllegalArgumentException("context must not be null");
|
||||||
|
if (context.hasService(CoreServices.TUNING)) this.tuning = context.getService(CoreServices.TUNING);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -105,24 +103,23 @@ public final class RecoveryModule implements SimulationModule {
|
|||||||
|
|
||||||
if (data.getSuccessionStage().conditionsMetForAdvancement(soil, poll, veg)) {
|
if (data.getSuccessionStage().conditionsMetForAdvancement(soil, poll, veg)) {
|
||||||
// Conditions are good: advance recovery progress.
|
// Conditions are good: advance recovery progress.
|
||||||
double progressGain = BASE_PROGRESS_PER_TICK;
|
double progressGain = tuning.getBaseProgressPerTick();
|
||||||
if (metrics.getEcosystemHealth() > 50.0) {
|
if (metrics.getEcosystemHealth() > 50.0) {
|
||||||
progressGain += (metrics.getEcosystemHealth() - 50.0) * HEALTH_PROGRESS_BONUS;
|
progressGain += (metrics.getEcosystemHealth() - 50.0) * HEALTH_PROGRESS_BONUS;
|
||||||
}
|
}
|
||||||
data.advanceProgress(progressGain, soil, poll, veg);
|
data.advanceProgress(progressGain, soil, poll, veg);
|
||||||
|
|
||||||
// Damage decays passively when conditions are good (bypass accumulateDamage
|
// Damage decays passively when conditions are good. Use 4-arg constructor to
|
||||||
// to avoid triggering regression with a zero-damage call that might fire at 70+).
|
// preserve the dynamic succession cap set by applyDynamicCapUpdate().
|
||||||
// Use 4-arg constructor to preserve the dynamic succession cap.
|
|
||||||
data = new RecoveryRegionData(
|
data = new RecoveryRegionData(
|
||||||
data.getSuccessionStage(),
|
data.getSuccessionStage(),
|
||||||
data.getRecoveryProgress(),
|
data.getRecoveryProgress(),
|
||||||
Math.max(0.0, data.getDamageAccumulation() * (1.0 - DAMAGE_DECAY_RATE)),
|
Math.max(0.0, data.getDamageAccumulation() * (1.0 - tuning.getDamageDecayRate())),
|
||||||
data.getMaxSuccessionStage());
|
data.getMaxSuccessionStage());
|
||||||
|
|
||||||
} else if (data.getSuccessionStage().conditionsMissedForRegression(soil, poll, veg)) {
|
} else if (data.getSuccessionStage().conditionsMissedForRegression(soil, poll, veg)) {
|
||||||
// Conditions are bad: accumulate damage toward regression.
|
// Conditions are bad: accumulate damage toward regression.
|
||||||
data.accumulateDamage(DAMAGE_PER_BAD_TICK, soil, poll, veg);
|
data.accumulateDamage(tuning.getDamagePerBadTick(), soil, poll, veg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recovery pressure reflects distance from mature forest.
|
// Recovery pressure reflects distance from mature forest.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.livingworld.modules.vegetation;
|
package com.livingworld.modules.vegetation;
|
||||||
|
|
||||||
|
import com.livingworld.config.EcosystemTuning;
|
||||||
|
import com.livingworld.core.services.CoreServices;
|
||||||
import com.livingworld.data.serialization.PersistenceReader;
|
import com.livingworld.data.serialization.PersistenceReader;
|
||||||
import com.livingworld.data.serialization.PersistenceWriter;
|
import com.livingworld.data.serialization.PersistenceWriter;
|
||||||
import com.livingworld.events.LivingWorldEvent;
|
import com.livingworld.events.LivingWorldEvent;
|
||||||
@@ -38,36 +40,21 @@ public final class VegetationModule implements SimulationModule {
|
|||||||
|
|
||||||
public static final String MODULE_ID = "vegetation";
|
public static final String MODULE_ID = "vegetation";
|
||||||
|
|
||||||
// --- growth thresholds ---
|
// --- growth thresholds (not tunable — structural to the succession model) ---
|
||||||
private static final double GROWTH_SOIL_THRESHOLD = 35.0;
|
private static final double GROWTH_SOIL_THRESHOLD = 35.0;
|
||||||
private static final double GROWTH_POLLUTION_LIMIT = 40.0;
|
private static final double GROWTH_POLLUTION_LIMIT = 40.0;
|
||||||
private static final double SHRUB_UNLOCK_GRASS = 50.0;
|
private static final double SHRUB_UNLOCK_GRASS = 50.0;
|
||||||
private static final double TREE_UNLOCK_SHRUB = 40.0;
|
private static final double TREE_UNLOCK_SHRUB = 40.0;
|
||||||
|
|
||||||
// --- growth rates per tick ---
|
|
||||||
private static final double GRASS_GROWTH_RATE = 0.06;
|
|
||||||
private static final double FLOWER_GROWTH_RATE = 0.03;
|
|
||||||
private static final double SHRUB_GROWTH_RATE = 0.02;
|
|
||||||
private static final double TREE_GROWTH_RATE = 0.008;
|
|
||||||
|
|
||||||
// --- die-off thresholds ---
|
|
||||||
private static final double DIEOFF_SOIL_THRESHOLD = 20.0;
|
private static final double DIEOFF_SOIL_THRESHOLD = 20.0;
|
||||||
private static final double DIEOFF_POLLUTION_THRESHOLD = 30.0;
|
private static final double DIEOFF_POLLUTION_THRESHOLD = 30.0;
|
||||||
// All living tiers die in bad conditions; trees die slowest.
|
|
||||||
private static final double GRASS_DIEOFF_RATE = 1.00;
|
|
||||||
private static final double FLOWER_DIEOFF_RATE = 0.50;
|
|
||||||
private static final double SHRUB_DIEOFF_RATE = 0.25;
|
|
||||||
private static final double TREE_DIEOFF_RATE = 0.10;
|
|
||||||
private static final double DEAD_ACCUMULATION_RATE = 0.50;
|
private static final double DEAD_ACCUMULATION_RATE = 0.50;
|
||||||
// Trees also die-off when conditions are severe (heavy pollution or near-zero soil).
|
|
||||||
private static final double TREE_SEVERE_POLLUTION = 60.0;
|
private static final double TREE_SEVERE_POLLUTION = 60.0;
|
||||||
private static final double TREE_SEVERE_SOIL = 10.0;
|
private static final double TREE_SEVERE_SOIL = 10.0;
|
||||||
|
|
||||||
// --- decomposition ---
|
|
||||||
private static final double DEAD_DECOMPOSITION_RATE = 0.01;
|
private static final double DEAD_DECOMPOSITION_RATE = 0.01;
|
||||||
|
|
||||||
private static final double CHANGE_THRESHOLD = 0.01;
|
private static final double CHANGE_THRESHOLD = 0.01;
|
||||||
|
|
||||||
|
private EcosystemTuning tuning = new EcosystemTuning();
|
||||||
|
|
||||||
private static final ModuleMetadata METADATA = new ModuleMetadata(
|
private static final ModuleMetadata METADATA = new ModuleMetadata(
|
||||||
MODULE_ID,
|
MODULE_ID,
|
||||||
"Vegetation",
|
"Vegetation",
|
||||||
@@ -89,6 +76,7 @@ public final class VegetationModule implements SimulationModule {
|
|||||||
@Override
|
@Override
|
||||||
public void initialize(ModuleContext context) {
|
public void initialize(ModuleContext context) {
|
||||||
if (context == null) throw new IllegalArgumentException("context must not be null");
|
if (context == null) throw new IllegalArgumentException("context must not be null");
|
||||||
|
if (context.hasService(CoreServices.TUNING)) this.tuning = context.getService(CoreServices.TUNING);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -123,27 +111,27 @@ public final class VegetationModule implements SimulationModule {
|
|||||||
if (goodConditions) {
|
if (goodConditions) {
|
||||||
double soilBonus = metrics.getSoilQuality() - GROWTH_SOIL_THRESHOLD;
|
double soilBonus = metrics.getSoilQuality() - GROWTH_SOIL_THRESHOLD;
|
||||||
|
|
||||||
data.setGrassPressure(data.getGrassPressure() + soilBonus * GRASS_GROWTH_RATE);
|
data.setGrassPressure(data.getGrassPressure() + soilBonus * tuning.getGrassGrowthRate());
|
||||||
data.setFlowerPressure(data.getFlowerPressure() + soilBonus * FLOWER_GROWTH_RATE);
|
data.setFlowerPressure(data.getFlowerPressure() + soilBonus * tuning.getFlowerGrowthRate());
|
||||||
|
|
||||||
// Shrubs grow only once grass is established.
|
// Shrubs grow only once grass is established.
|
||||||
if (data.getGrassPressure() > SHRUB_UNLOCK_GRASS) {
|
if (data.getGrassPressure() > SHRUB_UNLOCK_GRASS) {
|
||||||
data.setShrubPressure(data.getShrubPressure() + soilBonus * SHRUB_GROWTH_RATE);
|
data.setShrubPressure(data.getShrubPressure() + soilBonus * tuning.getShrubGrowthRate());
|
||||||
}
|
}
|
||||||
// Trees grow only once shrubs are established.
|
// Trees grow only once shrubs are established.
|
||||||
if (data.getShrubPressure() > TREE_UNLOCK_SHRUB) {
|
if (data.getShrubPressure() > TREE_UNLOCK_SHRUB) {
|
||||||
data.setTreePressure(data.getTreePressure() + soilBonus * TREE_GROWTH_RATE);
|
data.setTreePressure(data.getTreePressure() + soilBonus * tuning.getTreeGrowthRate());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (badConditions) {
|
if (badConditions) {
|
||||||
data.setGrassPressure(data.getGrassPressure() - GRASS_DIEOFF_RATE);
|
data.setGrassPressure(data.getGrassPressure() - tuning.getGrassDieoffRate());
|
||||||
data.setFlowerPressure(data.getFlowerPressure() - FLOWER_DIEOFF_RATE);
|
data.setFlowerPressure(data.getFlowerPressure() - tuning.getFlowerDieoffRate());
|
||||||
data.setShrubPressure(data.getShrubPressure() - SHRUB_DIEOFF_RATE);
|
data.setShrubPressure(data.getShrubPressure() - tuning.getShrubDieoffRate());
|
||||||
// Trees die only under severe stress to reflect their resilience.
|
// Trees die only under severe stress to reflect their resilience.
|
||||||
if (metrics.getPollutionScore() > TREE_SEVERE_POLLUTION
|
if (metrics.getPollutionScore() > TREE_SEVERE_POLLUTION
|
||||||
|| metrics.getSoilQuality() < TREE_SEVERE_SOIL) {
|
|| metrics.getSoilQuality() < TREE_SEVERE_SOIL) {
|
||||||
data.setTreePressure(data.getTreePressure() - TREE_DIEOFF_RATE);
|
data.setTreePressure(data.getTreePressure() - tuning.getTreeDieoffRate());
|
||||||
}
|
}
|
||||||
data.setDeadVegetation(data.getDeadVegetation() + DEAD_ACCUMULATION_RATE);
|
data.setDeadVegetation(data.getDeadVegetation() + DEAD_ACCUMULATION_RATE);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package com.livingworld.platform.neoforge;
|
||||||
|
|
||||||
|
import com.livingworld.config.EcosystemTuning;
|
||||||
|
import net.neoforged.neoforge.common.ModConfigSpec;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NeoForge server-side TOML configuration for the Living World ecosystem simulation.
|
||||||
|
*
|
||||||
|
* <p>Registered via {@link net.neoforged.fml.ModContainer#registerConfig} with type
|
||||||
|
* {@link net.neoforged.fml.config.ModConfig.Type#SERVER}. The resulting file is written to
|
||||||
|
* {@code saves/<world>/serverconfig/livingworld-server.toml} and can be edited by
|
||||||
|
* server admins to tune all ecosystem rates without recompiling.
|
||||||
|
*
|
||||||
|
* <p>Call {@link #createTuning()} after the server config has loaded (i.e. from
|
||||||
|
* {@link net.neoforged.neoforge.event.server.ServerStartingEvent}) to obtain an
|
||||||
|
* {@link EcosystemTuning} populated with the current config values.
|
||||||
|
*/
|
||||||
|
public final class NeoForgeModConfig {
|
||||||
|
|
||||||
|
public static final ModConfigSpec SPEC;
|
||||||
|
|
||||||
|
// -- pollution --
|
||||||
|
private static final ModConfigSpec.DoubleValue POLLUTION_DECAY_RATE;
|
||||||
|
private static final ModConfigSpec.DoubleValue GROUND_TO_WATER_LEACH;
|
||||||
|
private static final ModConfigSpec.DoubleValue POLLUTION_SPREAD_RATE;
|
||||||
|
private static final ModConfigSpec.DoubleValue WIND_BOOST;
|
||||||
|
|
||||||
|
// -- vegetation growth --
|
||||||
|
private static final ModConfigSpec.DoubleValue GRASS_GROWTH_RATE;
|
||||||
|
private static final ModConfigSpec.DoubleValue FLOWER_GROWTH_RATE;
|
||||||
|
private static final ModConfigSpec.DoubleValue SHRUB_GROWTH_RATE;
|
||||||
|
private static final ModConfigSpec.DoubleValue TREE_GROWTH_RATE;
|
||||||
|
|
||||||
|
// -- vegetation die-off --
|
||||||
|
private static final ModConfigSpec.DoubleValue GRASS_DIEOFF_RATE;
|
||||||
|
private static final ModConfigSpec.DoubleValue FLOWER_DIEOFF_RATE;
|
||||||
|
private static final ModConfigSpec.DoubleValue SHRUB_DIEOFF_RATE;
|
||||||
|
private static final ModConfigSpec.DoubleValue TREE_DIEOFF_RATE;
|
||||||
|
|
||||||
|
// -- recovery succession --
|
||||||
|
private static final ModConfigSpec.DoubleValue BASE_PROGRESS_PER_TICK;
|
||||||
|
private static final ModConfigSpec.DoubleValue DAMAGE_PER_BAD_TICK;
|
||||||
|
private static final ModConfigSpec.DoubleValue DAMAGE_DECAY_RATE;
|
||||||
|
|
||||||
|
// -- seed dispersal --
|
||||||
|
private static final ModConfigSpec.DoubleValue SEED_EMISSION_RATE;
|
||||||
|
private static final ModConfigSpec.DoubleValue CORRIDOR_BOOST_MULTIPLIER;
|
||||||
|
private static final ModConfigSpec.DoubleValue SEED_POLLUTION_BLOCK;
|
||||||
|
|
||||||
|
static {
|
||||||
|
ModConfigSpec.Builder b = new ModConfigSpec.Builder();
|
||||||
|
|
||||||
|
b.comment("Pollution dynamics").push("pollution");
|
||||||
|
POLLUTION_DECAY_RATE = b
|
||||||
|
.comment("Fraction of pollution removed per tick. Lower = stickier pollution. Default: 0.002")
|
||||||
|
.defineInRange("decay_rate", 0.002, 0.0001, 0.5);
|
||||||
|
GROUND_TO_WATER_LEACH = b
|
||||||
|
.comment("Fraction of ground pollution that leaches into water each tick. Default: 0.005")
|
||||||
|
.defineInRange("ground_to_water_leach", 0.005, 0.0, 0.1);
|
||||||
|
POLLUTION_SPREAD_RATE = b
|
||||||
|
.comment("Base rate at which air pollution diffuses to adjacent regions. Default: 0.02")
|
||||||
|
.defineInRange("spread_rate", 0.02, 0.0, 0.5);
|
||||||
|
WIND_BOOST = b
|
||||||
|
.comment("Downwind spread multiplier (0 = no wind bias, 2 = strong directional drift). Default: 0.5")
|
||||||
|
.defineInRange("wind_boost", 0.5, 0.0, 2.0);
|
||||||
|
b.pop();
|
||||||
|
|
||||||
|
b.comment("Vegetation growth rates (per soil-quality unit above threshold per tick)").push("vegetation");
|
||||||
|
GRASS_GROWTH_RATE = b.comment("Grass growth rate. Default: 0.06")
|
||||||
|
.defineInRange("grass_growth", 0.06, 0.001, 2.0);
|
||||||
|
FLOWER_GROWTH_RATE = b.comment("Flower growth rate. Default: 0.03")
|
||||||
|
.defineInRange("flower_growth", 0.03, 0.001, 2.0);
|
||||||
|
SHRUB_GROWTH_RATE = b.comment("Shrub growth rate. Default: 0.02")
|
||||||
|
.defineInRange("shrub_growth", 0.02, 0.001, 2.0);
|
||||||
|
TREE_GROWTH_RATE = b.comment("Tree growth rate. Default: 0.008")
|
||||||
|
.defineInRange("tree_growth", 0.008, 0.001, 2.0);
|
||||||
|
GRASS_DIEOFF_RATE = b.comment("Grass die-off flat reduction per bad tick. Default: 1.0")
|
||||||
|
.defineInRange("grass_dieoff", 1.0, 0.0, 20.0);
|
||||||
|
FLOWER_DIEOFF_RATE = b.comment("Flower die-off per bad tick. Default: 0.5")
|
||||||
|
.defineInRange("flower_dieoff", 0.5, 0.0, 20.0);
|
||||||
|
SHRUB_DIEOFF_RATE = b.comment("Shrub die-off per bad tick. Default: 0.25")
|
||||||
|
.defineInRange("shrub_dieoff", 0.25, 0.0, 20.0);
|
||||||
|
TREE_DIEOFF_RATE = b.comment("Tree die-off per bad tick (only under severe conditions). Default: 0.1")
|
||||||
|
.defineInRange("tree_dieoff", 0.1, 0.0, 20.0);
|
||||||
|
b.pop();
|
||||||
|
|
||||||
|
b.comment("Ecological succession and recovery").push("recovery");
|
||||||
|
BASE_PROGRESS_PER_TICK = b
|
||||||
|
.comment("Recovery progress added per tick under good conditions. Default: 2.0")
|
||||||
|
.defineInRange("base_progress_per_tick", 2.0, 0.1, 50.0);
|
||||||
|
DAMAGE_PER_BAD_TICK = b
|
||||||
|
.comment("Ecological damage accumulated per tick under bad conditions. Default: 5.0")
|
||||||
|
.defineInRange("damage_per_bad_tick", 5.0, 0.1, 100.0);
|
||||||
|
DAMAGE_DECAY_RATE = b
|
||||||
|
.comment("Fraction of accumulated damage that decays per tick when conditions are OK. Default: 0.03")
|
||||||
|
.defineInRange("damage_decay_rate", 0.03, 0.001, 0.5);
|
||||||
|
b.pop();
|
||||||
|
|
||||||
|
b.comment("Seed dispersal and ecological corridors").push("seed_dispersal");
|
||||||
|
SEED_EMISSION_RATE = b
|
||||||
|
.comment("Fraction of vegetation pressure emitted as seeds to each neighbour per tick (requires YOUNG_WOODLAND+). Default: 0.01")
|
||||||
|
.defineInRange("emission_rate", 0.01, 0.0, 0.5);
|
||||||
|
CORRIDOR_BOOST_MULTIPLIER = b
|
||||||
|
.comment("Seed strength multiplier when the target region has 2+ healthy neighbours — the corridor effect. Default: 3.5")
|
||||||
|
.defineInRange("corridor_boost", 3.5, 1.0, 20.0);
|
||||||
|
SEED_POLLUTION_BLOCK = b
|
||||||
|
.comment("Fraction of incoming seeds blocked per unit of pollution score in the target region. Default: 0.015")
|
||||||
|
.defineInRange("pollution_block", 0.015, 0.0, 0.1);
|
||||||
|
b.pop();
|
||||||
|
|
||||||
|
SPEC = b.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private NeoForgeModConfig() {}
|
||||||
|
|
||||||
|
/** Creates an {@link EcosystemTuning} populated from the currently loaded config values. */
|
||||||
|
public static EcosystemTuning createTuning() {
|
||||||
|
EcosystemTuning t = new EcosystemTuning();
|
||||||
|
t.setPollutionDecayRate(POLLUTION_DECAY_RATE.get());
|
||||||
|
t.setGroundToWaterLeach(GROUND_TO_WATER_LEACH.get());
|
||||||
|
t.setPollutionSpreadRate(POLLUTION_SPREAD_RATE.get());
|
||||||
|
t.setWindBoost(WIND_BOOST.get());
|
||||||
|
t.setGrassGrowthRate(GRASS_GROWTH_RATE.get());
|
||||||
|
t.setFlowerGrowthRate(FLOWER_GROWTH_RATE.get());
|
||||||
|
t.setShrubGrowthRate(SHRUB_GROWTH_RATE.get());
|
||||||
|
t.setTreeGrowthRate(TREE_GROWTH_RATE.get());
|
||||||
|
t.setGrassDieoffRate(GRASS_DIEOFF_RATE.get());
|
||||||
|
t.setFlowerDieoffRate(FLOWER_DIEOFF_RATE.get());
|
||||||
|
t.setShrubDieoffRate(SHRUB_DIEOFF_RATE.get());
|
||||||
|
t.setTreeDieoffRate(TREE_DIEOFF_RATE.get());
|
||||||
|
t.setBaseProgressPerTick(BASE_PROGRESS_PER_TICK.get());
|
||||||
|
t.setDamagePerBadTick(DAMAGE_PER_BAD_TICK.get());
|
||||||
|
t.setDamageDecayRate(DAMAGE_DECAY_RATE.get());
|
||||||
|
t.setSeedEmissionRate(SEED_EMISSION_RATE.get());
|
||||||
|
t.setCorridorBoostMultiplier(CORRIDOR_BOOST_MULTIPLIER.get());
|
||||||
|
t.setSeedPollutionBlock(SEED_POLLUTION_BLOCK.get());
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user