Add seasons, player pollution effects, wildfire, fog, water body boost, /lw atmosphere
Seasons (Step 1) - Season enum (SPRING/SUMMER/AUTUMN/WINTER) derived from gameTime / 24000 / 8. - AtmosphereModule applies seasonal rain modifier: spring +10%, summer -15%, winter -25%. Cascades naturally through water → soil → succession. - Bootstrap post-sim pass: spring boosts soil fertility (+0.003/cycle), winter draws moisture out of topsoil (-0.002/cycle). Player pollution effects (Step 2) - Poll > 40: nausea (MobEffects.CONFUSION in 1.21.1); > 60: slowness; > 80: weakness. - Duration 100 ticks, refreshed each PLAYER_CHECK_INTERVAL (1 s). Expires 5 s after the player leaves the polluted region. Wildfire (Step 3) - WILDFIRE added to WorldEffectType. - WorldEffectsModule emits WILDFIRE when droughtRisk > 70 AND thunderLevel > 0.5 with 1% probability per sim cycle. - NeoForgeWorldEffectExecutor places fire blocks on surface grass/leaves/logs; Minecraft fire spreading takes over from there. Fog (Step 4) - When pollutionScore > 40, 1–3 SMOKE particles sent per check interval at player eye-level via ServerLevel.sendParticles(player, ...) — per-player only, not broadcast to everyone. Water body passive boost (Step 5) - On first entry to a region, hasWaterBody() samples 30 surface positions. If ≥5 are water, applyWaterBodyBoost() permanently raises purification capacity +15, water availability +10, and lowers drought risk -10. /lw atmosphere command (Step 6) - Shows: Region (x,z) | Season: Spring | Rain: 72% | Storm: 18% - Wired via Function<CommandSourceStack, String> atmosphereStatus parameter added to LivingWorldCommandRoot.registerDeferred(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,7 +33,11 @@ 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.particles.ParticleTypes;
|
||||
import net.minecraft.network.protocol.game.ClientboundGameEventPacket;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.world.effect.MobEffectInstance;
|
||||
import net.minecraft.world.effect.MobEffects;
|
||||
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||
|
||||
import com.livingworld.bootstrap.LivingWorldBootstrap;
|
||||
@@ -89,6 +93,7 @@ public class LivingWorldMod {
|
||||
private final Map<UUID, RegionCoordinate> playerRegionCache = new HashMap<>();
|
||||
private final Map<UUID, Boolean> playerRainState = new HashMap<>();
|
||||
private final Set<RegionCoordinate> biomeInitialized = new HashSet<>();
|
||||
private final Set<RegionCoordinate> waterBodyInitialized = new HashSet<>();
|
||||
|
||||
public LivingWorldMod(IEventBus eventBus) {
|
||||
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World mod starting...");
|
||||
@@ -113,13 +118,17 @@ public class LivingWorldMod {
|
||||
new NeoForgeWorldEffectExecutor(() -> minecraftServer));
|
||||
bootstrap.setOverworldRaining(
|
||||
() -> minecraftServer != null && minecraftServer.overworld().isRaining());
|
||||
bootstrap.setAbsoluteDaySupplier(
|
||||
() -> minecraftServer != null ? minecraftServer.overworld().getGameTime() / 24000L : 0L);
|
||||
});
|
||||
NeoForge.EVENT_BUS.addListener(ServerStoppingEvent.class, event -> {
|
||||
bootstrap.onServerStopping();
|
||||
bootstrap.setOverworldRaining(null);
|
||||
bootstrap.setAbsoluteDaySupplier(null);
|
||||
playerRegionCache.clear();
|
||||
playerRainState.clear();
|
||||
biomeInitialized.clear();
|
||||
waterBodyInitialized.clear();
|
||||
this.minecraftServer = null;
|
||||
});
|
||||
|
||||
@@ -230,13 +239,21 @@ public class LivingWorldMod {
|
||||
if (!coord.equals(previous)) {
|
||||
bootstrap.notifyPlayerInRegion(coord);
|
||||
|
||||
// Step 6: Biome-aware succession — derive and apply cap once per region.
|
||||
if (!biomeInitialized.contains(coord)
|
||||
&& player.level() instanceof ServerLevel serverLevel) {
|
||||
if (player.level() instanceof ServerLevel serverLevel) {
|
||||
// Biome-aware succession cap — derived once per region.
|
||||
if (!biomeInitialized.contains(coord)) {
|
||||
SuccessionStage cap = deriveBiomeCap(serverLevel, coord);
|
||||
bootstrap.setRegionBiomeCap(coord, cap);
|
||||
biomeInitialized.add(coord);
|
||||
}
|
||||
// Water body passive boost — scanned once per region.
|
||||
if (!waterBodyInitialized.contains(coord)) {
|
||||
if (hasWaterBody(serverLevel, coord)) {
|
||||
bootstrap.applyWaterBodyBoost(coord);
|
||||
}
|
||||
waterBodyInitialized.add(coord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Regional weather — send per-player rain/thunder packets so each region
|
||||
@@ -259,17 +276,62 @@ public class LivingWorldMod {
|
||||
ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, thunderLevel));
|
||||
}
|
||||
|
||||
// Player effects from pollution — applied/refreshed each check interval.
|
||||
Optional<RegionMetrics> metricsOpt = bootstrap.getMetricsAt(dimId, player.getX(), player.getZ());
|
||||
metricsOpt.ifPresent(m -> {
|
||||
double poll = m.getPollutionScore();
|
||||
if (poll > 40) {
|
||||
// CONFUSION = nausea in 1.21.1 (registry name "nausea", Java constant CONFUSION)
|
||||
player.addEffect(new MobEffectInstance(MobEffects.CONFUSION, 100, 0, true, false));
|
||||
}
|
||||
if (poll > 60) {
|
||||
player.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 100, 0, true, false));
|
||||
}
|
||||
if (poll > 80) {
|
||||
player.addEffect(new MobEffectInstance(MobEffects.WEAKNESS, 100, 0, true, false));
|
||||
}
|
||||
// Fog particles — sent only to this player via the targeted sendParticles overload.
|
||||
if (poll > 40 && player.level() instanceof ServerLevel sl) {
|
||||
int fogCount = Math.min(3, (int) ((poll - 40) / 20.0) + 1);
|
||||
for (int f = 0; f < fogCount; f++) {
|
||||
double px = player.getX() + (random.nextDouble() - 0.5) * 6;
|
||||
double py = player.getEyeY() + (random.nextDouble() - 0.5);
|
||||
double pz = player.getZ() + (random.nextDouble() - 0.5) * 6;
|
||||
sl.sendParticles(player, ParticleTypes.SMOKE, false, px, py, pz, 1, 0.4, 0.2, 0.4, 0.01);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Compass HUD — display region health while holding a compass, or debug HUD is on.
|
||||
boolean showHud = player.getMainHandItem().is(Items.COMPASS)
|
||||
|| player.getOffhandItem().is(Items.COMPASS)
|
||||
|| bootstrap.isHudEnabled(player.getUUID());
|
||||
if (showHud) {
|
||||
Optional<RegionMetrics> metricsOpt = bootstrap.getMetricsAt(dimId, player.getX(), player.getZ());
|
||||
metricsOpt.ifPresent(m -> player.displayClientMessage(buildHud(coord, m, atm), true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final int REGION_BLOCKS =
|
||||
LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16;
|
||||
|
||||
/** Scans 30 random surface positions in the region; returns true if ≥5 are water. */
|
||||
private boolean hasWaterBody(ServerLevel level, RegionCoordinate coord) {
|
||||
int baseX = coord.x() * REGION_BLOCKS;
|
||||
int baseZ = coord.z() * REGION_BLOCKS;
|
||||
int waterCount = 0;
|
||||
for (int i = 0; i < 30; i++) {
|
||||
int x = baseX + random.nextInt(REGION_BLOCKS);
|
||||
int z = baseZ + random.nextInt(REGION_BLOCKS);
|
||||
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z) - 1;
|
||||
if (y >= level.getMinBuildHeight()
|
||||
&& level.getFluidState(new BlockPos(x, y, z)).is(FluidTags.WATER)) {
|
||||
waterCount++;
|
||||
}
|
||||
}
|
||||
return waterCount >= 5;
|
||||
}
|
||||
|
||||
/** Step 6: Samples the dominant biome at the region centre and returns a succession ceiling. */
|
||||
private SuccessionStage deriveBiomeCap(ServerLevel level, RegionCoordinate coord) {
|
||||
int cx = coord.x() * LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16 + 64;
|
||||
|
||||
@@ -35,6 +35,7 @@ import com.livingworld.modules.water.WaterModule;
|
||||
import com.livingworld.modules.water.WaterRegionData;
|
||||
import com.livingworld.modules.atmosphere.AtmosphereModule;
|
||||
import com.livingworld.modules.atmosphere.AtmosphereRegionData;
|
||||
import com.livingworld.modules.atmosphere.Season;
|
||||
import com.livingworld.modules.worldeffects.WorldEffectsModule;
|
||||
import com.livingworld.platform.BlockBreakInfo;
|
||||
import com.livingworld.platform.PlatformAdapter;
|
||||
@@ -52,6 +53,7 @@ import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.function.LongSupplier;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
@@ -81,6 +83,7 @@ public final class LivingWorldBootstrap {
|
||||
private WorldEffectsModule worldEffectsModule;
|
||||
private AtmosphereModule atmosphereModule;
|
||||
private BooleanSupplier overworldRaining = () -> false;
|
||||
private LongSupplier absoluteDaySupplier = () -> 0L;
|
||||
private boolean initialized;
|
||||
private boolean serverReady;
|
||||
|
||||
@@ -318,6 +321,14 @@ public final class LivingWorldBootstrap {
|
||||
this.overworldRaining = supplier != null ? supplier : () -> false;
|
||||
}
|
||||
|
||||
public void setAbsoluteDaySupplier(LongSupplier supplier) {
|
||||
this.absoluteDaySupplier = supplier != null ? supplier : () -> 0L;
|
||||
}
|
||||
|
||||
public Season getCurrentSeason() {
|
||||
return Season.fromAbsoluteDay(absoluteDaySupplier.getAsLong());
|
||||
}
|
||||
|
||||
public void onServerTick() {
|
||||
if (!serverReady) {
|
||||
return;
|
||||
@@ -326,6 +337,7 @@ public final class LivingWorldBootstrap {
|
||||
simulationManager.onMinecraftServerTick();
|
||||
if (simulationManager.getSimulationTickCounter() != previousSimulationTick) {
|
||||
spreadPollutionAcrossRegions();
|
||||
applySeasonalEffects();
|
||||
regionManager.saveDirtyRegions();
|
||||
}
|
||||
}
|
||||
@@ -376,6 +388,27 @@ public final class LivingWorldBootstrap {
|
||||
}
|
||||
}
|
||||
|
||||
private void applySeasonalEffects() {
|
||||
Season season = getCurrentSeason();
|
||||
for (Region region : regionManager.getActiveRegions()) {
|
||||
SoilRegionData soil = region.getModuleData()
|
||||
.get(SoilModule.MODULE_ID, SoilRegionData.class)
|
||||
.orElse(null);
|
||||
if (soil == null) continue;
|
||||
switch (season) {
|
||||
case SPRING ->
|
||||
// Snowmelt nutrients give soil a mild fertility boost.
|
||||
soil.setFertility(Math.min(100, soil.getFertility() + 0.003));
|
||||
case WINTER ->
|
||||
// Frost draws moisture out of the topsoil.
|
||||
soil.setMoisture(Math.max(0, soil.getMoisture() - 0.002));
|
||||
default -> { /* SUMMER and AUTUMN have no direct soil effect. */ }
|
||||
}
|
||||
region.getModuleData().put(SoilModule.MODULE_ID, soil);
|
||||
regionManager.markDirty(region);
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the current atmospheric state for a region, if it has been initialised. */
|
||||
public Optional<AtmosphereRegionData> getRegionalWeather(RegionCoordinate coord) {
|
||||
if (!serverReady || coord == null) return Optional.empty();
|
||||
@@ -383,6 +416,40 @@ public final class LivingWorldBootstrap {
|
||||
.flatMap(r -> r.getModuleData().get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class));
|
||||
}
|
||||
|
||||
/** Applies a water quality and purification bonus to regions adjacent to or containing water bodies. */
|
||||
public void applyWaterBodyBoost(RegionCoordinate coord) {
|
||||
if (!serverReady || coord == null) return;
|
||||
regionManager.resolve(coord).ifPresent(region -> {
|
||||
WaterRegionData water = region.getModuleData()
|
||||
.get(WaterModule.MODULE_ID, WaterRegionData.class)
|
||||
.orElse(null);
|
||||
if (water == null) return;
|
||||
water.setPurificationCapacity(Math.min(100, water.getPurificationCapacity() + 15.0));
|
||||
water.setWaterAvailability(Math.min(100, water.getWaterAvailability() + 10.0));
|
||||
water.setDroughtRisk(Math.max(0, water.getDroughtRisk() - 10.0));
|
||||
region.getModuleData().put(WaterModule.MODULE_ID, water);
|
||||
regionManager.markDirty(region);
|
||||
});
|
||||
}
|
||||
|
||||
/** Returns a formatted atmosphere status string for the region at the command source's position. */
|
||||
public String getAtmosphereStatusFor(CommandSourceStack css) {
|
||||
if (!serverReady) return "Server not ready.";
|
||||
String dimId = css.getLevel().dimension().location().toString();
|
||||
var pos = css.getPosition();
|
||||
RegionCoordinate coord = RegionCoordinate.fromBlock(
|
||||
dimId, (int) pos.x, (int) pos.z, LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
|
||||
Season season = getCurrentSeason();
|
||||
return regionManager.resolve(coord)
|
||||
.flatMap(r -> r.getModuleData().get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class))
|
||||
.map(atm -> String.format(
|
||||
"Region (%d,%d) | Season: %s | Rain: %.0f%% | Storm: %.0f%%",
|
||||
coord.x(), coord.z(), season.displayName(),
|
||||
atm.getRainLevel() * 100, atm.getThunderLevel() * 100))
|
||||
.orElse(String.format("Region (%d,%d) — atmosphere not yet computed. Season: %s",
|
||||
coord.x(), coord.z(), season.displayName()));
|
||||
}
|
||||
|
||||
/** Toggles the debug HUD for the given player. Returns true if HUD is now enabled. */
|
||||
public boolean toggleHud(UUID playerId) {
|
||||
if (hudEnabledPlayers.remove(playerId)) return false;
|
||||
@@ -401,7 +468,8 @@ public final class LivingWorldBootstrap {
|
||||
() -> requireService(regionManager, "regionManager"),
|
||||
() -> requireService(moduleRegistry, "moduleRegistry"),
|
||||
() -> requireService(simulationManager, "simulationManager"),
|
||||
this::toggleHud);
|
||||
this::toggleHud,
|
||||
this::getAtmosphereStatusFor);
|
||||
}
|
||||
|
||||
public Path getWorldSaveDirectory() {
|
||||
@@ -661,7 +729,7 @@ public final class LivingWorldBootstrap {
|
||||
registry.register(new EcosystemModule());
|
||||
worldEffectsModule = new WorldEffectsModule();
|
||||
registry.register(worldEffectsModule);
|
||||
atmosphereModule = new AtmosphereModule(() -> overworldRaining.getAsBoolean());
|
||||
atmosphereModule = new AtmosphereModule(() -> overworldRaining.getAsBoolean(), this::getCurrentSeason);
|
||||
registry.register(atmosphereModule);
|
||||
LivingWorldLogger.info(
|
||||
DiagnosticCategory.BOOTSTRAP,
|
||||
|
||||
@@ -38,7 +38,8 @@ public final class LivingWorldCommandRoot {
|
||||
() -> regionManager,
|
||||
() -> moduleRegistry,
|
||||
() -> simulationManager,
|
||||
uuid -> false);
|
||||
uuid -> false,
|
||||
css -> "Atmosphere not available.");
|
||||
}
|
||||
|
||||
public static void registerDeferred(
|
||||
@@ -46,7 +47,8 @@ public final class LivingWorldCommandRoot {
|
||||
Supplier<RegionManager> regionManager,
|
||||
Supplier<ModuleRegistry> moduleRegistry,
|
||||
Supplier<SimulationManager> simulationManager,
|
||||
Function<UUID, Boolean> hudToggle) {
|
||||
Function<UUID, Boolean> hudToggle,
|
||||
Function<CommandSourceStack, String> atmosphereStatus) {
|
||||
if (dispatcher == null) {
|
||||
throw new IllegalArgumentException("dispatcher must not be null");
|
||||
}
|
||||
@@ -104,7 +106,13 @@ public final class LivingWorldCommandRoot {
|
||||
requireService(simulationManager, "simulationManager"),
|
||||
IntegerArgumentType.getInteger(context, "ticks")))))
|
||||
.then(Commands.literal("hud")
|
||||
.executes(context -> toggleHud(context.getSource(), hudToggle))));
|
||||
.executes(context -> toggleHud(context.getSource(), hudToggle)))
|
||||
.then(Commands.literal("atmosphere")
|
||||
.executes(context -> {
|
||||
String info = atmosphereStatus.apply(context.getSource());
|
||||
context.getSource().sendSuccess(() -> Component.literal(info), false);
|
||||
return 1;
|
||||
})));
|
||||
}
|
||||
|
||||
private static int toggleHud(CommandSourceStack source, Function<UUID, Boolean> hudToggle) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.livingworld.regions.Region;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Drives per-region weather from ecosystem state and applies atmospheric feedback
|
||||
@@ -81,9 +82,11 @@ public final class AtmosphereModule implements SimulationModule {
|
||||
false);
|
||||
|
||||
private final BooleanSupplier globalRaining;
|
||||
private final Supplier<Season> currentSeason;
|
||||
|
||||
public AtmosphereModule(BooleanSupplier globalRaining) {
|
||||
public AtmosphereModule(BooleanSupplier globalRaining, Supplier<Season> currentSeason) {
|
||||
this.globalRaining = globalRaining;
|
||||
this.currentSeason = currentSeason;
|
||||
}
|
||||
|
||||
@Override public String getModuleId() { return MODULE_ID; }
|
||||
@@ -134,7 +137,9 @@ public final class AtmosphereModule implements SimulationModule {
|
||||
double pollScore = region.getMetrics().getPollutionScore();
|
||||
boolean mcRaining = globalRaining.getAsBoolean();
|
||||
|
||||
double seasonMod = currentSeason.get().rainModifier();
|
||||
double targetRain = clamp01(ecosystemHealth / 100.0 * MAX_RAIN_FROM_ECO
|
||||
+ seasonMod
|
||||
+ (mcRaining ? GLOBAL_RAIN_BIAS : 0.0));
|
||||
double targetThunder = clamp01(pollScore / 100.0 * 0.8);
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.livingworld.modules.atmosphere;
|
||||
|
||||
/**
|
||||
* The four ecological seasons, derived from absolute Minecraft day count.
|
||||
*
|
||||
* <p>One full year = 32 Minecraft days (8 days per season). Season drives the
|
||||
* rain target modifier in {@link AtmosphereModule} and seasonal soil effects in
|
||||
* the bootstrap post-sim pass.
|
||||
*/
|
||||
public enum Season {
|
||||
|
||||
SPRING(+0.10),
|
||||
SUMMER(-0.15),
|
||||
AUTUMN( 0.0),
|
||||
WINTER(-0.25);
|
||||
|
||||
private static final int SEASON_LENGTH_DAYS = 8;
|
||||
|
||||
/** Additive modifier applied to the atmosphere rain target this season. */
|
||||
private final double rainModifier;
|
||||
|
||||
Season(double rainModifier) {
|
||||
this.rainModifier = rainModifier;
|
||||
}
|
||||
|
||||
public double rainModifier() { return rainModifier; }
|
||||
|
||||
public String displayName() {
|
||||
String n = name();
|
||||
return n.charAt(0) + n.substring(1).toLowerCase();
|
||||
}
|
||||
|
||||
/** Returns the season for the given absolute Minecraft day number (gameTime / 24000). */
|
||||
public static Season fromAbsoluteDay(long absoluteDay) {
|
||||
return values()[(int) ((absoluteDay / SEASON_LENGTH_DAYS) % 4)];
|
||||
}
|
||||
}
|
||||
@@ -39,4 +39,10 @@ public enum WorldEffectType {
|
||||
* left to the platform layer.
|
||||
*/
|
||||
POLLUTION_VISUAL_INDICATOR,
|
||||
|
||||
/**
|
||||
* Drought combined with a storm triggers wildfire ignition — fire is placed
|
||||
* on flammable surface blocks and spreads naturally via Minecraft fire tick.
|
||||
*/
|
||||
WILDFIRE,
|
||||
}
|
||||
|
||||
@@ -9,16 +9,21 @@ import com.livingworld.modules.ModuleUpdateResult;
|
||||
import com.livingworld.modules.RegionUpdateContext;
|
||||
import com.livingworld.modules.ServerContext;
|
||||
import com.livingworld.modules.SimulationModule;
|
||||
import com.livingworld.modules.atmosphere.AtmosphereModule;
|
||||
import com.livingworld.modules.atmosphere.AtmosphereRegionData;
|
||||
import com.livingworld.modules.recovery.RecoveryRegionData;
|
||||
import com.livingworld.modules.recovery.RecoveryModule;
|
||||
import com.livingworld.modules.recovery.SuccessionStage;
|
||||
import com.livingworld.modules.resources.ResourceDepletionModule;
|
||||
import com.livingworld.modules.resources.ResourceRegionData;
|
||||
import com.livingworld.modules.water.WaterModule;
|
||||
import com.livingworld.modules.water.WaterRegionData;
|
||||
import com.livingworld.regions.Region;
|
||||
import com.livingworld.regions.RegionMetrics;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Translates ecosystem simulation state into visible world change requests.
|
||||
@@ -63,6 +68,10 @@ public final class WorldEffectsModule implements SimulationModule {
|
||||
private static final double SAPLING_SLOW_LOGGING_MIN = 50.0;
|
||||
// Smoke particles appear as soon as any meaningful pollution exists.
|
||||
private static final double POLLUTION_INDICATOR_MIN = 10.0;
|
||||
// Wildfire: lightning storm over a drought-stressed region.
|
||||
private static final double WILDFIRE_DROUGHT_MIN = 70.0;
|
||||
private static final double WILDFIRE_THUNDER_MIN = 0.5;
|
||||
private static final double WILDFIRE_CHANCE_PER_CYCLE = 0.01; // 1 % per sim cycle
|
||||
|
||||
private static final ModuleMetadata METADATA = new ModuleMetadata(
|
||||
MODULE_ID,
|
||||
@@ -77,6 +86,7 @@ public final class WorldEffectsModule implements SimulationModule {
|
||||
false);
|
||||
|
||||
private final List<WorldEffectConsumer> consumers = new ArrayList<>();
|
||||
private final Random wildfireRandom = new Random();
|
||||
|
||||
/**
|
||||
* Registers a consumer that will receive effect requests each simulation tick.
|
||||
@@ -178,6 +188,24 @@ public final class WorldEffectsModule implements SimulationModule {
|
||||
emitted = true;
|
||||
}
|
||||
|
||||
// --- Effect 6: wildfire — drought + storm ignites surface vegetation ---
|
||||
AtmosphereRegionData atm = region.getModuleData()
|
||||
.get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class)
|
||||
.orElse(null);
|
||||
WaterRegionData water = region.getModuleData()
|
||||
.get(WaterModule.MODULE_ID, WaterRegionData.class)
|
||||
.orElse(null);
|
||||
if (atm != null && water != null
|
||||
&& water.getDroughtRisk() > WILDFIRE_DROUGHT_MIN
|
||||
&& atm.getThunderLevel() > WILDFIRE_THUNDER_MIN
|
||||
&& wildfireRandom.nextDouble() < WILDFIRE_CHANCE_PER_CYCLE) {
|
||||
double intensity = computeIntensity(atm.getThunderLevel() - WILDFIRE_THUNDER_MIN, 0.5)
|
||||
* computeIntensity(water.getDroughtRisk() - WILDFIRE_DROUGHT_MIN, 30.0);
|
||||
emit(new WorldEffectRequest(
|
||||
WorldEffectType.WILDFIRE, region.getCoordinate(), Math.max(0.1, intensity)));
|
||||
emitted = true;
|
||||
}
|
||||
|
||||
return emitted ? ModuleUpdateResult.changed() : ModuleUpdateResult.noChange();
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
@@ -66,6 +67,8 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
|
||||
case SAPLING_GROWTH_BOOSTED ->
|
||||
placeSaplings(level, baseX, baseZ, request.intensity());
|
||||
case SAPLING_GROWTH_SLOWED -> {} // requires mixin; deferred
|
||||
case WILDFIRE ->
|
||||
igniteVegetation(level, baseX, baseZ, request.intensity());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +168,23 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
|
||||
}
|
||||
}
|
||||
|
||||
private void igniteVegetation(ServerLevel level, int baseX, int baseZ, double intensity) {
|
||||
int attempts = Math.max(1, (int) (intensity * 5));
|
||||
for (int i = 0; i < attempts; i++) {
|
||||
BlockPos pos = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS),
|
||||
baseZ + random.nextInt(REGION_BLOCKS));
|
||||
if (pos == null) continue;
|
||||
var state = level.getBlockState(pos);
|
||||
if (!state.is(Blocks.GRASS_BLOCK) && !state.is(BlockTags.LEAVES)
|
||||
&& !state.is(BlockTags.LOGS)) continue;
|
||||
BlockPos above = pos.above();
|
||||
if (!level.isLoaded(above) || !level.getBlockState(above).isAir()) continue;
|
||||
level.setBlock(above, Blocks.FIRE.defaultBlockState(), Block.UPDATE_ALL);
|
||||
LivingWorldLogger.info(DiagnosticCategory.SIMULATION,
|
||||
"WorldEffect WILDFIRE ignited at " + above);
|
||||
}
|
||||
}
|
||||
|
||||
private void spawnPollutionParticles(
|
||||
ServerLevel level, int baseX, int baseZ, double intensity) {
|
||||
int count = Math.max(1, (int) (intensity * 8));
|
||||
|
||||
Reference in New Issue
Block a user