diff --git a/src/main/java/com/livingworld/LivingWorldMod.java b/src/main/java/com/livingworld/LivingWorldMod.java index d808ab9..0148014 100644 --- a/src/main/java/com/livingworld/LivingWorldMod.java +++ b/src/main/java/com/livingworld/LivingWorldMod.java @@ -1,6 +1,8 @@ package com.livingworld; +import net.neoforged.fml.ModContainer; import net.neoforged.fml.common.Mod; +import net.neoforged.fml.config.ModConfig; import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; import net.neoforged.bus.api.IEventBus; 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.levelgen.Heightmap; import net.minecraft.world.level.storage.LevelResource; +import net.minecraft.core.Holder; import net.minecraft.core.particles.ParticleTypes; 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.world.effect.MobEffectInstance; import net.minecraft.world.effect.MobEffects; @@ -46,6 +53,7 @@ import com.livingworld.core.LivingWorldConstants; import com.livingworld.debug.DiagnosticCategory; import com.livingworld.debug.LivingWorldLogger; import com.livingworld.modules.recovery.SuccessionStage; +import com.livingworld.platform.neoforge.NeoForgeModConfig; import com.livingworld.platform.neoforge.NeoForgePlatformAdapter; import com.livingworld.platform.neoforge.NeoForgeWorldEffectExecutor; import com.livingworld.regions.Region; @@ -98,10 +106,15 @@ public class LivingWorldMod { /** Stores playerCheckTick value when a region was last water-body-scanned. */ private final Map waterBodyLastScan = new HashMap<>(); private final Set elevationInitialized = new HashSet<>(); + /** Stores playerCheckTick when a region last played an ambient sound (per-region throttle). */ + private final Map regionSoundLastTick = new HashMap<>(); - public LivingWorldMod(IEventBus eventBus) { + public LivingWorldMod(IEventBus eventBus, ModContainer modContainer) { 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(); NeoForgePlatformAdapter platformAdapter = new NeoForgePlatformAdapter( bootstrap::getWorldSaveDirectory, @@ -113,8 +126,11 @@ public class LivingWorldMod { eventBus.addListener(FMLCommonSetupEvent.class, event -> bootstrap.onCommonSetup()); NeoForge.EVENT_BUS.addListener( ServerStartingEvent.class, - event -> bootstrap.onServerStarting( - event.getServer().getWorldPath(LevelResource.ROOT))); + event -> { + // 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 -> { this.minecraftServer = event.getServer(); bootstrap.onServerStarted(); @@ -134,6 +150,7 @@ public class LivingWorldMod { biomeInitialized.clear(); waterBodyLastScan.clear(); elevationInitialized.clear(); + regionSoundLastTick.clear(); this.minecraftServer = null; }); @@ -334,9 +351,62 @@ public class LivingWorldMod { if (showHud) { 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 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 = LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16; diff --git a/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java b/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java index f076419..22ddbee 100644 --- a/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java +++ b/src/main/java/com/livingworld/bootstrap/LivingWorldBootstrap.java @@ -2,6 +2,7 @@ package com.livingworld.bootstrap; import com.livingworld.commands.LivingWorldCommandRoot; import com.livingworld.config.DefaultConfigService; +import com.livingworld.config.EcosystemTuning; import com.livingworld.config.SimulationConfig; import com.livingworld.core.LivingWorldConstants; import com.livingworld.core.services.CoreServices; @@ -69,11 +70,18 @@ import net.minecraft.commands.CommandSourceStack; */ 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 double windAngle = 0.0; 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 accumulatedSeedRain = new HashMap<>(); private final Set hudEnabledPlayers = new HashSet<>(); private PlatformAdapter platformAdapter; @@ -206,6 +214,11 @@ public final class LivingWorldBootstrap { /** * 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() { if (!serverReady) { return; @@ -215,6 +228,7 @@ public final class LivingWorldBootstrap { climateTracker.save(worldSaveDirectory.resolve("living_world/global_climate.dat")); hudEnabledPlayers.clear(); regionElevations.clear(); + accumulatedSeedRain.clear(); simSpeedMultiplier = 1; serverReady = false; LivingWorldLogger.info( @@ -367,6 +381,7 @@ public final class LivingWorldBootstrap { applyClimateWarmingEffects(); applyWaterRunoff(); applyDynamicCapUpdate(); + applySeedDispersal(); } /** Sets the simulation speed multiplier (1 = real-time, max 100). */ @@ -379,8 +394,6 @@ public final class LivingWorldBootstrap { public int getSimSpeedMultiplier() { return simSpeedMultiplier; } - private static final double POLLUTION_SPREAD_RATE = 0.02; - private void spreadPollutionAcrossRegions() { Collection active = regionManager.getActiveRegions(); if (active.size() < 2) return; @@ -413,7 +426,8 @@ public final class LivingWorldBootstrap { // Wind alignment: positive = downwind (faster spread), negative = upwind (slower). double offsetAngle = Math.atan2(off[1], off[0]); double alignment = Math.cos(windAngle - offsetAngle); - double rate = POLLUTION_SPREAD_RATE * (1.0 + alignment * WIND_BOOST); + double rate = ecosystemTuning.getPollutionSpreadRate() + * (1.0 + alignment * ecosystemTuning.getWindBoost()); double transfer = diff * rate; data.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.setDroughtRisk(Math.max(0, nWater.getDroughtRisk() - runoff * 0.5)); 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); } region.getModuleData().put(WaterModule.MODULE_ID, myWater); @@ -578,6 +606,130 @@ public final class LivingWorldBootstrap { 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). + * + *

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 active = regionManager.getActiveRegions(); + if (active.size() < 2) return; + + Map byCoord = new HashMap<>(); + for (Region r : active) byCoord.put(r.getCoordinate(), r); + + // First pass: collect seed emissions from healthy source regions. + Map 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 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. */ public Optional getRegionalWeather(RegionCoordinate coord) { if (!serverReady || coord == null) return Optional.empty(); @@ -662,7 +814,8 @@ public final class LivingWorldBootstrap { this::toggleHud, this::getAtmosphereStatusFor, this::getClimateStatusFor, - this::setSimSpeedMultiplier); + this::setSimSpeedMultiplier, + this::getWindInfo); } public Path getWorldSaveDirectory() { @@ -712,6 +865,7 @@ public final class LivingWorldBootstrap { services = new ServiceRegistry(); services.register(CoreServices.CONFIG, configService); + services.register(CoreServices.TUNING, ecosystemTuning); services.register(CoreServices.REGIONS, regionManager); services.register(CoreServices.SIMULATION, simulationManager); services.register(CoreServices.PERSISTENCE, persistenceService); diff --git a/src/main/java/com/livingworld/commands/LivingWorldCommandRoot.java b/src/main/java/com/livingworld/commands/LivingWorldCommandRoot.java index 7467854..9746e7c 100644 --- a/src/main/java/com/livingworld/commands/LivingWorldCommandRoot.java +++ b/src/main/java/com/livingworld/commands/LivingWorldCommandRoot.java @@ -43,7 +43,8 @@ public final class LivingWorldCommandRoot { uuid -> false, css -> "Atmosphere not available.", css -> "Climate not available.", - n -> n); + n -> n, + () -> "Wind not available."); } public static void registerDeferred( @@ -54,7 +55,8 @@ public final class LivingWorldCommandRoot { Function hudToggle, Function atmosphereStatus, Function climateStatus, - IntUnaryOperator speedSetter) { + IntUnaryOperator speedSetter, + Supplier windStatus) { if (dispatcher == null) { throw new IllegalArgumentException("dispatcher must not be null"); } @@ -92,7 +94,8 @@ public final class LivingWorldCommandRoot { .executes(context -> ForceUpdateCommand.executeAtSelf( context.getSource(), requireService(regionManager, "regionManager"), - requireService(simulationManager, "simulationManager"))))) + requireService(simulationManager, "simulationManager")))) + .then(RegionBordersCommand.build())) .then(Commands.literal("modules") .then(Commands.literal("list") .executes(context -> listModules( @@ -125,6 +128,12 @@ public final class LivingWorldCommandRoot { context.getSource().sendSuccess(() -> Component.literal(info), false); 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.argument("multiplier", IntegerArgumentType.integer(1, 100)) .executes(context -> { diff --git a/src/main/java/com/livingworld/commands/RegionBordersCommand.java b/src/main/java/com/livingworld/commands/RegionBordersCommand.java new file mode 100644 index 0000000..cf3cf6e --- /dev/null +++ b/src/main/java/com/livingworld/commands/RegionBordersCommand.java @@ -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]}. + * + *

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.

+ */ +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 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) + } +} diff --git a/src/main/java/com/livingworld/config/EcosystemTuning.java b/src/main/java/com/livingworld/config/EcosystemTuning.java new file mode 100644 index 0000000..5650c1f --- /dev/null +++ b/src/main/java/com/livingworld/config/EcosystemTuning.java @@ -0,0 +1,94 @@ +package com.livingworld.config; + +/** + * Tuning constants for the Living World ecosystem simulation. + * + *

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()}. + * + *

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; } +} diff --git a/src/main/java/com/livingworld/core/services/CoreServices.java b/src/main/java/com/livingworld/core/services/CoreServices.java index d2796fe..3537ade 100644 --- a/src/main/java/com/livingworld/core/services/CoreServices.java +++ b/src/main/java/com/livingworld/core/services/CoreServices.java @@ -1,5 +1,6 @@ package com.livingworld.core.services; +import com.livingworld.config.EcosystemTuning; import com.livingworld.core.simulation.RegionManager; import com.livingworld.core.simulation.SimulationManager; import com.livingworld.events.LivingWorldEventBus; @@ -40,6 +41,9 @@ public final class CoreServices { /** ConfigService key — interface already exists. */ public static final ServiceKey CONFIG = new ServiceKey<>("config", ConfigService.class); + /** EcosystemTuning key — holds all tunable simulation constants (loaded from TOML config). */ + public static final ServiceKey TUNING = new ServiceKey<>("tuning", EcosystemTuning.class); + public static final ServiceKey REGIONS = new ServiceKey<>("regions", RegionManager.class); diff --git a/src/main/java/com/livingworld/modules/pollution/PollutionModule.java b/src/main/java/com/livingworld/modules/pollution/PollutionModule.java index 72f14a7..acd7c4d 100644 --- a/src/main/java/com/livingworld/modules/pollution/PollutionModule.java +++ b/src/main/java/com/livingworld/modules/pollution/PollutionModule.java @@ -1,5 +1,7 @@ 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.PersistenceWriter; import com.livingworld.events.LivingWorldEvent; @@ -42,10 +44,10 @@ public final class PollutionModule implements SimulationModule { 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 CHANGE_THRESHOLD = 0.01; + private static final double WATER_QUALITY_IMPACT = 0.05; + private static final double CHANGE_THRESHOLD = 0.01; + + private EcosystemTuning tuning = new EcosystemTuning(); private static final ModuleMetadata METADATA = new ModuleMetadata( MODULE_ID, @@ -68,6 +70,7 @@ public final class PollutionModule implements SimulationModule { @Override public void initialize(ModuleContext context) { if (context == null) throw new IllegalArgumentException("context must not be null"); + if (context.hasService(CoreServices.TUNING)) this.tuning = context.getService(CoreServices.TUNING); } @Override @@ -94,10 +97,10 @@ public final class PollutionModule implements SimulationModule { double prevWaterQuality = region.getMetrics().getWaterQuality(); // 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. - double leach = data.getGroundPollution() * GROUND_TO_WATER_LEACH; + double leach = data.getGroundPollution() * tuning.getGroundToWaterLeach(); data.addPollution(0.0, 0.0, leach); // Summary metric: weighted average emphasising waterPollution as most damaging. diff --git a/src/main/java/com/livingworld/modules/recovery/RecoveryModule.java b/src/main/java/com/livingworld/modules/recovery/RecoveryModule.java index f595f65..bec39ec 100644 --- a/src/main/java/com/livingworld/modules/recovery/RecoveryModule.java +++ b/src/main/java/com/livingworld/modules/recovery/RecoveryModule.java @@ -1,5 +1,7 @@ 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.PersistenceWriter; import com.livingworld.events.LivingWorldEvent; @@ -41,16 +43,11 @@ public final class RecoveryModule implements SimulationModule { 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. */ - 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 HEALTH_PROGRESS_BONUS = 0.05; + private static final double CHANGE_THRESHOLD = 0.01; + + private EcosystemTuning tuning = new EcosystemTuning(); private static final ModuleMetadata METADATA = new ModuleMetadata( MODULE_ID, @@ -73,6 +70,7 @@ public final class RecoveryModule implements SimulationModule { @Override public void initialize(ModuleContext context) { if (context == null) throw new IllegalArgumentException("context must not be null"); + if (context.hasService(CoreServices.TUNING)) this.tuning = context.getService(CoreServices.TUNING); } @Override @@ -105,24 +103,23 @@ public final class RecoveryModule implements SimulationModule { if (data.getSuccessionStage().conditionsMetForAdvancement(soil, poll, veg)) { // Conditions are good: advance recovery progress. - double progressGain = BASE_PROGRESS_PER_TICK; + double progressGain = tuning.getBaseProgressPerTick(); if (metrics.getEcosystemHealth() > 50.0) { progressGain += (metrics.getEcosystemHealth() - 50.0) * HEALTH_PROGRESS_BONUS; } data.advanceProgress(progressGain, soil, poll, veg); - // Damage decays passively when conditions are good (bypass accumulateDamage - // to avoid triggering regression with a zero-damage call that might fire at 70+). - // Use 4-arg constructor to preserve the dynamic succession cap. + // Damage decays passively when conditions are good. Use 4-arg constructor to + // preserve the dynamic succession cap set by applyDynamicCapUpdate(). data = new RecoveryRegionData( data.getSuccessionStage(), 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()); } else if (data.getSuccessionStage().conditionsMissedForRegression(soil, poll, veg)) { // 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. diff --git a/src/main/java/com/livingworld/modules/vegetation/VegetationModule.java b/src/main/java/com/livingworld/modules/vegetation/VegetationModule.java index 90f6b35..6c95273 100644 --- a/src/main/java/com/livingworld/modules/vegetation/VegetationModule.java +++ b/src/main/java/com/livingworld/modules/vegetation/VegetationModule.java @@ -1,5 +1,7 @@ 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.PersistenceWriter; import com.livingworld.events.LivingWorldEvent; @@ -38,35 +40,20 @@ public final class VegetationModule implements SimulationModule { public static final String MODULE_ID = "vegetation"; - // --- growth thresholds --- - private static final double GROWTH_SOIL_THRESHOLD = 35.0; - private static final double GROWTH_POLLUTION_LIMIT = 40.0; - private static final double SHRUB_UNLOCK_GRASS = 50.0; - private static final double TREE_UNLOCK_SHRUB = 40.0; + // --- growth thresholds (not tunable — structural to the succession model) --- + private static final double GROWTH_SOIL_THRESHOLD = 35.0; + private static final double GROWTH_POLLUTION_LIMIT = 40.0; + private static final double SHRUB_UNLOCK_GRASS = 50.0; + private static final double TREE_UNLOCK_SHRUB = 40.0; + private static final double DIEOFF_SOIL_THRESHOLD = 20.0; + private static final double DIEOFF_POLLUTION_THRESHOLD = 30.0; + private static final double DEAD_ACCUMULATION_RATE = 0.50; + private static final double TREE_SEVERE_POLLUTION = 60.0; + private static final double TREE_SEVERE_SOIL = 10.0; + private static final double DEAD_DECOMPOSITION_RATE = 0.01; + private static final double CHANGE_THRESHOLD = 0.01; - // --- 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_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; - // 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_SOIL = 10.0; - - // --- decomposition --- - private static final double DEAD_DECOMPOSITION_RATE = 0.01; - - private static final double CHANGE_THRESHOLD = 0.01; + private EcosystemTuning tuning = new EcosystemTuning(); private static final ModuleMetadata METADATA = new ModuleMetadata( MODULE_ID, @@ -89,6 +76,7 @@ public final class VegetationModule implements SimulationModule { @Override public void initialize(ModuleContext context) { if (context == null) throw new IllegalArgumentException("context must not be null"); + if (context.hasService(CoreServices.TUNING)) this.tuning = context.getService(CoreServices.TUNING); } @Override @@ -123,27 +111,27 @@ public final class VegetationModule implements SimulationModule { if (goodConditions) { double soilBonus = metrics.getSoilQuality() - GROWTH_SOIL_THRESHOLD; - data.setGrassPressure(data.getGrassPressure() + soilBonus * GRASS_GROWTH_RATE); - data.setFlowerPressure(data.getFlowerPressure() + soilBonus * FLOWER_GROWTH_RATE); + data.setGrassPressure(data.getGrassPressure() + soilBonus * tuning.getGrassGrowthRate()); + data.setFlowerPressure(data.getFlowerPressure() + soilBonus * tuning.getFlowerGrowthRate()); // Shrubs grow only once grass is established. 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. if (data.getShrubPressure() > TREE_UNLOCK_SHRUB) { - data.setTreePressure(data.getTreePressure() + soilBonus * TREE_GROWTH_RATE); + data.setTreePressure(data.getTreePressure() + soilBonus * tuning.getTreeGrowthRate()); } } if (badConditions) { - data.setGrassPressure(data.getGrassPressure() - GRASS_DIEOFF_RATE); - data.setFlowerPressure(data.getFlowerPressure() - FLOWER_DIEOFF_RATE); - data.setShrubPressure(data.getShrubPressure() - SHRUB_DIEOFF_RATE); + data.setGrassPressure(data.getGrassPressure() - tuning.getGrassDieoffRate()); + data.setFlowerPressure(data.getFlowerPressure() - tuning.getFlowerDieoffRate()); + data.setShrubPressure(data.getShrubPressure() - tuning.getShrubDieoffRate()); // Trees die only under severe stress to reflect their resilience. if (metrics.getPollutionScore() > TREE_SEVERE_POLLUTION || 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); } diff --git a/src/main/java/com/livingworld/platform/neoforge/NeoForgeModConfig.java b/src/main/java/com/livingworld/platform/neoforge/NeoForgeModConfig.java new file mode 100644 index 0000000..8310518 --- /dev/null +++ b/src/main/java/com/livingworld/platform/neoforge/NeoForgeModConfig.java @@ -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. + * + *

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//serverconfig/livingworld-server.toml} and can be edited by + * server admins to tune all ecosystem rates without recompiling. + * + *

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; + } +}