Add water physics, river currents, volcanic system with lava & new land
Groundwater aquifer (valley springs): - Per-region subsurface water table recharges from rainfall + lateral seepage from uphill neighbours; valley floors (≥2 higher neighbours) develop springs - GROUND_SPRING_EMERGES places a permanent water source block at the lowest natural terrain point; vanilla fluid physics carries it downhill from there - Active springs feed riverFlowIntensity for erosion even without rain River current physics (real player forces): - Every 4 ticks, players in flowing water receive a velocity push via FluidState.getFlow() — the exact direction Minecraft's own rendering uses - Force scales with fluid level: level 7 (near source) = strong; level 1 = gentle Hydraulic erosion: - HYDRAULIC_EROSION fires whenever flow intensity is significant, rain-independent - Flowing water adjacent to stone/gravel/sand/dirt progressively softens it: stone→gravel→sand→air; river valleys widen over time Volcano lifecycle (DORMANT→BUILDING→ERUPTING→COOLING→FERTILE→DORMANT): - High-elevation regions (avg surface Y ≥ 85) registered as potentially volcanic - LAVA_FLOW places source blocks at summit; vanilla physics flows them downhill - ASH_DEPOSIT covers surrounding terrain with tuff/gravel; kills surface plants; ash clouds spread to adjacent regions with air pollution spike - COBBLESTONE_FORMS solidifies lava: lava+water→cobblestone, lava+air→basalt — creates permanent new terrain that did not exist before the eruption - FERTILE phase: soil fertility +30, contamination −20, rapid pollution decay - Volcanic ambience: fire crackling during eruption, deep rumble while building New commands: /lw events (active climate events), /lw volcanoes (lifecycle states) New ClimateEventType: VOLCANIC_ERUPTION New WorldEffectTypes: GROUND_SPRING_EMERGES, HYDRAULIC_EROSION, LAVA_FLOW, ASH_DEPOSIT, COBBLESTONE_FORMS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,11 @@ All effects are real block changes players can see:
|
|||||||
| Wildfire | Drought > 70, thunder > 0.5 | Fire placed on surface vegetation |
|
| Wildfire | Drought > 70, thunder > 0.5 | Fire placed on surface vegetation |
|
||||||
| Water pools form | Rain + barren/sparse terrain | Water source blocks in surface depressions |
|
| Water pools form | Rain + barren/sparse terrain | Water source blocks in surface depressions |
|
||||||
| **River carve** | High accumulated water runoff | Water channels carved through natural terrain; gradually deepens |
|
| **River carve** | High accumulated water runoff | Water channels carved through natural terrain; gradually deepens |
|
||||||
|
| **Ground spring** | Saturated aquifer in valley floor | Permanent water source placed; river flows downhill naturally |
|
||||||
|
| **Hydraulic erosion** | Flowing water adjacent to rock | Stone → gravel → sand → air; valley walls slowly widen |
|
||||||
|
| **Lava flow** | Volcanic eruption | Lava source blocks placed at summit; flows and reshapes terrain |
|
||||||
|
| **Ash deposit** | Active eruption or nearby blast | Grass → tuff, soil → gravel; surface plants killed over wide area |
|
||||||
|
| **Cobblestone forms** | Lava cooling | Lava + water → cobblestone; lava in air → basalt; new permanent land |
|
||||||
| Pollution particles | Pollution > 10 | Smoke particles in degraded regions |
|
| Pollution particles | Pollution > 10 | Smoke particles in degraded regions |
|
||||||
|
|
||||||
### Player Feedback — **Complete**
|
### Player Feedback — **Complete**
|
||||||
@@ -126,13 +131,48 @@ Automatically triggered when regional conditions reach critical thresholds. All
|
|||||||
- Visible in the compass HUD next to the region coordinates
|
- Visible in the compass HUD next to the region coordinates
|
||||||
- Full trend detail in `/lw atmosphere`
|
- Full trend detail in `/lw atmosphere`
|
||||||
|
|
||||||
### River Erosion — **Complete**
|
### Groundwater & Springs — **Complete**
|
||||||
|
|
||||||
|
- Every region tracks a subsurface aquifer level that recharges slowly from rainfall
|
||||||
|
- Underground seepage: high-elevation neighbours drain laterally into the aquifer of lower regions (water flows underground even between sim cycles)
|
||||||
|
- When the aquifer is saturated in a **valley floor** (≥2 higher neighbours), a `GROUND_SPRING_EMERGES` effect fires — a water source block is placed at the lowest natural terrain point
|
||||||
|
- The spring flows downhill under Minecraft's own fluid physics, forming a permanent river without further simulation involvement
|
||||||
|
- Active springs contribute to river erosion intensity even without rain
|
||||||
|
|
||||||
|
### River Erosion & Hydraulic Erosion — **Complete**
|
||||||
|
|
||||||
- Accumulated water runoff intensity tracked per region (flow from higher → lower elevation)
|
- Accumulated water runoff intensity tracked per region (flow from higher → lower elevation)
|
||||||
- When flow intensity crosses the threshold (80 units), a `RIVER_CARVE` world effect fires
|
- When flow intensity crosses the threshold (80 units), a `RIVER_CARVE` world effect fires
|
||||||
- Block effect: water source placed on natural terrain (sand/dirt/gravel/grass); at high intensity the block below is also removed, deepening the channel
|
- Block effect: water source placed on natural terrain; at high intensity the block below is removed (deepening channel)
|
||||||
|
- **Hydraulic erosion** fires independently of rainfall whenever significant flow intensity is present: flowing water (level 1–7) adjacent to soft rock softens it — stone → gravel → sand → air — progressively widening river valleys
|
||||||
- Rivers form gradually over many sim cycles — permanent terrain changes
|
- Rivers form gradually over many sim cycles — permanent terrain changes
|
||||||
|
|
||||||
|
### River Current Physics — **Complete** (real player forces)
|
||||||
|
|
||||||
|
- Every 4 ticks, all players standing in **flowing** water (level 1–7) receive a directional velocity push
|
||||||
|
- Force direction: `FluidState.getFlow()` — the exact vector Minecraft already computes for its own flow rendering; currents always point the same way the water visually flows
|
||||||
|
- Force magnitude: scales with flow level — level 7 (one block from a spring, fastest) pushes hard; level 1 (distant, slow lowland river) barely nudges
|
||||||
|
- Mountain rivers are genuinely dangerous to wade against; lowland rivers are gentle enough to cross
|
||||||
|
- Boats are naturally carried by vanilla water physics; swimming players and mobs now feel the additional push
|
||||||
|
|
||||||
|
### Volcanic System — **Complete** (new land formation)
|
||||||
|
|
||||||
|
Four-phase lifecycle per high-elevation region (average surface Y ≥ 85):
|
||||||
|
|
||||||
|
| Phase | What Happens |
|
||||||
|
|-------|-------------|
|
||||||
|
| **DORMANT** | Normal state; rare stochastic chance to awaken (~0.2% per check) |
|
||||||
|
| **BUILDING** | Seismic venting — mild pollution spike; player broadcast warns of imminent eruption |
|
||||||
|
| **ERUPTING** | `LAVA_FLOW` places source blocks at the summit; `ASH_DEPOSIT` covers surrounding terrain; ash clouds spread to adjacent regions; vegetation dies in the eruption zone; air pollution spikes; lasts ~40 sim cycles |
|
||||||
|
| **COOLING** | `COBBLESTONE_FORMS` converts lava adjacent to water → cobblestone; lava in air → basalt; permanent new terrain created where none existed before |
|
||||||
|
| **FERTILE** | Volcanic minerals boost soil fertility +30, contamination −20; pollution decays rapidly; succession cap can rise; then returns DORMANT |
|
||||||
|
|
||||||
|
Visible effects:
|
||||||
|
- Lava flows downhill under vanilla physics, permanently reshaping terrain
|
||||||
|
- Ash converts grass → tuff, loose soil → gravel; kills surface plants across a wide area
|
||||||
|
- New cobblestone and basalt islands form where lava met water
|
||||||
|
- Players in active eruption zones hear fire crackle ambience; building-phase rumble in BUILDING regions
|
||||||
|
|
||||||
### Tidal Simulation — **Complete** (physical blocks)
|
### Tidal Simulation — **Complete** (physical blocks)
|
||||||
|
|
||||||
- Two tidal cycles per Minecraft day (24 000 ticks) using a sine wave
|
- Two tidal cycles per Minecraft day (24 000 ticks) using a sine wave
|
||||||
@@ -210,6 +250,8 @@ All commands require permission level 2.
|
|||||||
| `/lw simulate <ticks>` | Force N simulation cycles on all active regions |
|
| `/lw simulate <ticks>` | Force N simulation cycles on all active regions |
|
||||||
| `/lw stats` | Simulation profiler statistics |
|
| `/lw stats` | Simulation profiler statistics |
|
||||||
| `/lw modules list` | List all registered modules and their enabled state |
|
| `/lw modules list` | List all registered modules and their enabled state |
|
||||||
|
| `/lw events` | List all active climate events (drought/wildfire/flood/eruption) with epicentre, severity, affected regions |
|
||||||
|
| `/lw volcanoes` | List all tracked volcanic regions and their current lifecycle phase |
|
||||||
| `/lw demo degrade` | One-command demo: degrade current region to barren |
|
| `/lw demo degrade` | One-command demo: degrade current region to barren |
|
||||||
| `/lw demo recover` | One-command demo: restore current region to mature forest |
|
| `/lw demo recover` | One-command demo: restore current region to mature forest |
|
||||||
|
|
||||||
@@ -291,13 +333,16 @@ LivingWorldMod (NeoForge event wiring)
|
|||||||
applySeasonalEffects() [soil/water/veg seasonal]
|
applySeasonalEffects() [soil/water/veg seasonal]
|
||||||
applyClimateWarmingEffects() [drought pressure from CO₂]
|
applyClimateWarmingEffects() [drought pressure from CO₂]
|
||||||
applyWaterRunoff() [elevation-based; river erosion]
|
applyWaterRunoff() [elevation-based; river erosion]
|
||||||
|
applyGroundwaterAndSprings() [aquifer recharge; valley springs]
|
||||||
applyDynamicCapUpdate() [raise/lower succession ceiling]
|
applyDynamicCapUpdate() [raise/lower succession ceiling]
|
||||||
applySeedDispersal() [corridor-boosted recolonisation]
|
applySeedDispersal() [corridor-boosted recolonisation]
|
||||||
recordHealthTrend() [↑↓→ trend tracking]
|
recordHealthTrend() [↑↓→ trend tracking]
|
||||||
applyClimateEvents() [drought/wildfire/flood events]
|
applyClimateEvents() [drought/wildfire/flood events]
|
||||||
|
applyVolcanicActivity() [volcano lifecycle; lava/ash/new land]
|
||||||
|
|
||||||
LivingWorldMod platform hooks:
|
LivingWorldMod platform hooks:
|
||||||
updateTidalEffects() [block-level tidal simulation]
|
updateTidalEffects() [block-level tidal simulation]
|
||||||
|
applyRiverCurrents() [player/entity velocity in flowing water]
|
||||||
initializeRegionFromBiome() [biome-aware starting values]
|
initializeRegionFromBiome() [biome-aware starting values]
|
||||||
checkPlayerRegions() [HUD, effects, ambient sounds]
|
checkPlayerRegions() [HUD, effects, ambient sounds]
|
||||||
scanAndRecordFurnaceActivity() [pollution from lit furnaces]
|
scanAndRecordFurnaceActivity() [pollution from lit furnaces]
|
||||||
@@ -320,7 +365,8 @@ Requires NeoForge 21.1.172 / Minecraft 1.21.1.
|
|||||||
## Planned / In Progress
|
## Planned / In Progress
|
||||||
|
|
||||||
- Full worldgen integration (custom biome distribution, terrain shaping beyond post-load modification)
|
- Full worldgen integration (custom biome distribution, terrain shaping beyond post-load modification)
|
||||||
- `/lw events` command to list active climate events and their affected regions
|
- Persistence for river flow intensity, groundwater level, and volcanic state across restarts
|
||||||
- Persistence for river flow intensity and accumulated seed rain across restarts
|
|
||||||
- Multiplayer region ownership / notification system
|
- Multiplayer region ownership / notification system
|
||||||
- More succession stages (wetland, alpine, volcanic)
|
- More succession stages (wetland, alpine, volcanic basalt plain)
|
||||||
|
- Snowmelt rivers: winter snow accumulation releases as meltwater in spring
|
||||||
|
- Player damage in active eruption zones (lava proximity heat)
|
||||||
|
|||||||
@@ -96,6 +96,10 @@ public class LivingWorldMod {
|
|||||||
private static final double HOSTILE_SUPPRESS_HEALTH = 60.0;
|
private static final double HOSTILE_SUPPRESS_HEALTH = 60.0;
|
||||||
|
|
||||||
private static final int TIDE_CHECK_INTERVAL = 1200; // ~1 real minute
|
private static final int TIDE_CHECK_INTERVAL = 1200; // ~1 real minute
|
||||||
|
/** Apply river current forces to players every N ticks. */
|
||||||
|
private static final int RIVER_CURRENT_INTERVAL = 4;
|
||||||
|
/** Additional horizontal push (m/tick²) per unit of normalised flow speed. */
|
||||||
|
private static final double CURRENT_STRENGTH = 0.045;
|
||||||
|
|
||||||
private final LivingWorldBootstrap bootstrap;
|
private final LivingWorldBootstrap bootstrap;
|
||||||
private final Random random = new Random();
|
private final Random random = new Random();
|
||||||
@@ -104,6 +108,7 @@ public class LivingWorldMod {
|
|||||||
private int playerCheckTick = 0;
|
private int playerCheckTick = 0;
|
||||||
private int tideTick = 0;
|
private int tideTick = 0;
|
||||||
private double lastTideLevel = 0.0;
|
private double lastTideLevel = 0.0;
|
||||||
|
private int riverCurrentTick = 0;
|
||||||
private final Map<UUID, RegionCoordinate> playerRegionCache = new HashMap<>();
|
private final Map<UUID, RegionCoordinate> playerRegionCache = new HashMap<>();
|
||||||
private final Map<UUID, Boolean> playerRainState = new HashMap<>();
|
private final Map<UUID, Boolean> playerRainState = new HashMap<>();
|
||||||
private final Set<RegionCoordinate> biomeInitialized = new HashSet<>();
|
private final Set<RegionCoordinate> biomeInitialized = new HashSet<>();
|
||||||
@@ -157,6 +162,7 @@ public class LivingWorldMod {
|
|||||||
regionSoundLastTick.clear();
|
regionSoundLastTick.clear();
|
||||||
tideTick = 0;
|
tideTick = 0;
|
||||||
lastTideLevel = 0.0;
|
lastTideLevel = 0.0;
|
||||||
|
riverCurrentTick = 0;
|
||||||
this.minecraftServer = null;
|
this.minecraftServer = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -172,6 +178,9 @@ public class LivingWorldMod {
|
|||||||
tideTick = 0;
|
tideTick = 0;
|
||||||
updateTidalEffects();
|
updateTidalEffects();
|
||||||
}
|
}
|
||||||
|
if (++riverCurrentTick % RIVER_CURRENT_INTERVAL == 0) {
|
||||||
|
applyRiverCurrents();
|
||||||
|
}
|
||||||
// Broadcast any climate event messages queued by the bootstrap
|
// Broadcast any climate event messages queued by the bootstrap
|
||||||
java.util.List<String> eventMsgs = bootstrap.pollClimateEventMessages();
|
java.util.List<String> eventMsgs = bootstrap.pollClimateEventMessages();
|
||||||
if (!eventMsgs.isEmpty()) {
|
if (!eventMsgs.isEmpty()) {
|
||||||
@@ -390,6 +399,36 @@ public class LivingWorldMod {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies realistic river current forces to players and non-player entities standing
|
||||||
|
* in flowing water. Uses {@link net.minecraft.world.level.material.FluidState#getFlow}
|
||||||
|
* — the same directional vector Minecraft computes internally — so currents always
|
||||||
|
* follow the actual water flow path, not a simulation approximation.
|
||||||
|
* Mountain rivers (level 7, close to source) push hard; lowland channels (level 1-2)
|
||||||
|
* are gentle enough to wade through. Heavy armour players notice the difference.
|
||||||
|
*/
|
||||||
|
private void applyRiverCurrents() {
|
||||||
|
if (minecraftServer == null) return;
|
||||||
|
for (ServerPlayer player : minecraftServer.getPlayerList().getPlayers()) {
|
||||||
|
if (!(player.level() instanceof ServerLevel level)) continue;
|
||||||
|
BlockPos pos = player.blockPosition();
|
||||||
|
var fluid = level.getFluidState(pos);
|
||||||
|
// Only flowing water — source blocks carry no directional current
|
||||||
|
if (!fluid.is(FluidTags.WATER) || fluid.isSource()) continue;
|
||||||
|
net.minecraft.world.phys.Vec3 flowVec = fluid.getFlow(level, pos);
|
||||||
|
if (flowVec.lengthSqr() < 0.0001) continue;
|
||||||
|
// Fluid amount: 8 = source, decreases with distance.
|
||||||
|
// Amount 7 = one block from source (fastest); amount 1 = farthest (slowest).
|
||||||
|
double flowStrength = fluid.getAmount() / 8.0;
|
||||||
|
double force = CURRENT_STRENGTH * flowStrength;
|
||||||
|
var movement = player.getDeltaMovement();
|
||||||
|
player.setDeltaMovement(
|
||||||
|
movement.x + flowVec.x * force,
|
||||||
|
movement.y,
|
||||||
|
movement.z + flowVec.z * force);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays a single ambient sound appropriate for the region's ecological state.
|
* 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.
|
* Each sound fires at low volume and random pitch so it blends naturally.
|
||||||
@@ -397,8 +436,25 @@ public class LivingWorldMod {
|
|||||||
*/
|
*/
|
||||||
private boolean tryPlayRegionAmbience(
|
private boolean tryPlayRegionAmbience(
|
||||||
ServerPlayer player, SuccessionStage stage, double health, double pollution) {
|
ServerPlayer player, SuccessionStage stage, double health, double pollution) {
|
||||||
float pitch = 0.75f + random.nextFloat() * 0.5f;
|
String dimId = player.level().dimension().location().toString();
|
||||||
|
int regionX = (int) Math.floor(player.getX() / (LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16.0));
|
||||||
|
int regionZ = (int) Math.floor(player.getZ() / (LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16.0));
|
||||||
|
RegionCoordinate coord = new RegionCoordinate(dimId, regionX, regionZ);
|
||||||
|
|
||||||
|
// Volcanic ambience — highest priority when eruption is active
|
||||||
|
String volcanoPhase = bootstrap.getVolcanoPhaseAt(coord);
|
||||||
|
if (volcanoPhase != null) {
|
||||||
|
if ("ERUPTING".equals(volcanoPhase) && random.nextInt(4) == 0) {
|
||||||
|
playAmbientSound(player, Holder.direct(SoundEvents.FIRE_AMBIENT), 0.55f, 0.8f + random.nextFloat() * 0.4f);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ("BUILDING".equals(volcanoPhase) && random.nextInt(20) == 0) {
|
||||||
|
playAmbientSound(player, SoundEvents.AMBIENT_BASALT_DELTAS_MOOD, 0.25f, 0.7f + random.nextFloat() * 0.2f);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float pitch = 0.75f + random.nextFloat() * 0.5f;
|
||||||
if (stage != null && stage.ordinal() >= SuccessionStage.YOUNG_WOODLAND.ordinal()
|
if (stage != null && stage.ordinal() >= SuccessionStage.YOUNG_WOODLAND.ordinal()
|
||||||
&& health > 50.0 && random.nextInt(10) == 0) {
|
&& health > 50.0 && random.nextInt(10) == 0) {
|
||||||
// Rustling leaves — healthy forest ambience.
|
// Rustling leaves — healthy forest ambience.
|
||||||
|
|||||||
@@ -83,6 +83,15 @@ public final class LivingWorldBootstrap {
|
|||||||
private static final double RIVER_CARVE_THRESHOLD = 80.0;
|
private static final double RIVER_CARVE_THRESHOLD = 80.0;
|
||||||
/** Climate event checks run every N post-sim cycles to avoid per-tick overhead. */
|
/** Climate event checks run every N post-sim cycles to avoid per-tick overhead. */
|
||||||
private static final int CLIMATE_CHECK_INTERVAL = 8;
|
private static final int CLIMATE_CHECK_INTERVAL = 8;
|
||||||
|
/** Groundwater level required for a valley spring to emerge. */
|
||||||
|
private static final double GROUNDWATER_THRESHOLD = 50.0;
|
||||||
|
/** Average surface elevation (Y) above which a region is treated as potentially volcanic. */
|
||||||
|
private static final double VOLCANIC_ELEVATION = 85.0;
|
||||||
|
/** Volcano state machine advances every N post-sim cycles. */
|
||||||
|
private static final int VOLCANO_TICK_INTERVAL = 16;
|
||||||
|
|
||||||
|
/** Lifecycle states for a volcanic region. */
|
||||||
|
private enum VolcanoPhase { DORMANT, BUILDING, ERUPTING, COOLING, FERTILE }
|
||||||
|
|
||||||
private double windAngle = 0.0;
|
private double windAngle = 0.0;
|
||||||
private final Random windRandom = new Random();
|
private final Random windRandom = new Random();
|
||||||
@@ -108,6 +117,15 @@ public final class LivingWorldBootstrap {
|
|||||||
private final List<String> pendingEventMessages = new ArrayList<>();
|
private final List<String> pendingEventMessages = new ArrayList<>();
|
||||||
private int climateEventTick = 0;
|
private int climateEventTick = 0;
|
||||||
|
|
||||||
|
// --- Groundwater aquifer (transient) ---
|
||||||
|
private final Map<RegionCoordinate, Double> groundwaterLevel = new HashMap<>();
|
||||||
|
|
||||||
|
// --- Volcano lifecycle (transient — re-derives from elevation on restart) ---
|
||||||
|
private final Map<RegionCoordinate, VolcanoPhase> volcanoPhase = new HashMap<>();
|
||||||
|
private final Map<RegionCoordinate, Integer> volcanoTicks = new HashMap<>();
|
||||||
|
private final Set<RegionCoordinate> volcanicRegions = new HashSet<>();
|
||||||
|
private int volcanicActivityTick = 0;
|
||||||
|
|
||||||
private PlatformAdapter platformAdapter;
|
private PlatformAdapter platformAdapter;
|
||||||
private Path worldSaveDirectory;
|
private Path worldSaveDirectory;
|
||||||
private ServiceRegistry services;
|
private ServiceRegistry services;
|
||||||
@@ -259,6 +277,11 @@ public final class LivingWorldBootstrap {
|
|||||||
activeClimateEvents.clear();
|
activeClimateEvents.clear();
|
||||||
pendingEventMessages.clear();
|
pendingEventMessages.clear();
|
||||||
climateEventTick = 0;
|
climateEventTick = 0;
|
||||||
|
groundwaterLevel.clear();
|
||||||
|
volcanoPhase.clear();
|
||||||
|
volcanoTicks.clear();
|
||||||
|
volcanicRegions.clear();
|
||||||
|
volcanicActivityTick = 0;
|
||||||
simSpeedMultiplier = 1;
|
simSpeedMultiplier = 1;
|
||||||
serverReady = false;
|
serverReady = false;
|
||||||
LivingWorldLogger.info(
|
LivingWorldLogger.info(
|
||||||
@@ -410,12 +433,16 @@ public final class LivingWorldBootstrap {
|
|||||||
climateTracker.update(regionManager.getActiveRegions());
|
climateTracker.update(regionManager.getActiveRegions());
|
||||||
applyClimateWarmingEffects();
|
applyClimateWarmingEffects();
|
||||||
applyWaterRunoff();
|
applyWaterRunoff();
|
||||||
|
applyGroundwaterAndSprings();
|
||||||
applyDynamicCapUpdate();
|
applyDynamicCapUpdate();
|
||||||
applySeedDispersal();
|
applySeedDispersal();
|
||||||
recordHealthTrend();
|
recordHealthTrend();
|
||||||
if (++climateEventTick % CLIMATE_CHECK_INTERVAL == 0) {
|
if (++climateEventTick % CLIMATE_CHECK_INTERVAL == 0) {
|
||||||
applyClimateEvents();
|
applyClimateEvents();
|
||||||
}
|
}
|
||||||
|
if (++volcanicActivityTick % VOLCANO_TICK_INTERVAL == 0) {
|
||||||
|
applyVolcanicActivity();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the simulation speed multiplier (1 = real-time, max 100). */
|
/** Sets the simulation speed multiplier (1 = real-time, max 100). */
|
||||||
@@ -547,6 +574,272 @@ public final class LivingWorldBootstrap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks subsurface groundwater per region: recharges from rainfall and lateral seepage
|
||||||
|
* from uphill neighbours. When the aquifer level is high in a valley (lower elevation
|
||||||
|
* than ≥2 neighbours), a GROUND_SPRING_EMERGES effect is queued — placing a permanent
|
||||||
|
* water source that flows naturally downhill under Minecraft's own fluid physics.
|
||||||
|
*/
|
||||||
|
private void applyGroundwaterAndSprings() {
|
||||||
|
Collection<Region> active = regionManager.getActiveRegions();
|
||||||
|
if (active.isEmpty()) return;
|
||||||
|
int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||||
|
|
||||||
|
for (Region region : active) {
|
||||||
|
RegionCoordinate coord = region.getCoordinate();
|
||||||
|
AtmosphereRegionData atm = region.getModuleData()
|
||||||
|
.get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class).orElse(null);
|
||||||
|
Double myElev = regionElevations.get(coord);
|
||||||
|
|
||||||
|
// Recharge from rainfall
|
||||||
|
if (atm != null && atm.getRainLevel() > 0.1) {
|
||||||
|
groundwaterLevel.merge(coord, atm.getRainLevel() * 0.008, Double::sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lateral underground seepage from higher neighbours
|
||||||
|
if (myElev != null) {
|
||||||
|
for (int[] off : offsets) {
|
||||||
|
RegionCoordinate nCoord = new RegionCoordinate(
|
||||||
|
coord.dimensionId(), coord.x() + off[0], coord.z() + off[1]);
|
||||||
|
Double nElev = regionElevations.get(nCoord);
|
||||||
|
if (nElev != null && nElev > myElev + 3) {
|
||||||
|
double seepage = 0.002 * Math.min((nElev - myElev) / 50.0, 1.0);
|
||||||
|
groundwaterLevel.merge(coord, seepage, Double::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slow evaporation / natural discharge
|
||||||
|
double level = Math.max(0, groundwaterLevel.getOrDefault(coord, 0.0) - 0.001);
|
||||||
|
groundwaterLevel.put(coord, level);
|
||||||
|
|
||||||
|
// Spring emergence: valley floor with saturated aquifer
|
||||||
|
if (level >= GROUNDWATER_THRESHOLD && myElev != null) {
|
||||||
|
int higherNeighbours = 0;
|
||||||
|
for (int[] off : offsets) {
|
||||||
|
RegionCoordinate nCoord = new RegionCoordinate(
|
||||||
|
coord.dimensionId(), coord.x() + off[0], coord.z() + off[1]);
|
||||||
|
Double nElev = regionElevations.get(nCoord);
|
||||||
|
if (nElev != null && nElev > myElev + 5) higherNeighbours++;
|
||||||
|
}
|
||||||
|
if (higherNeighbours >= 2 && worldEffectsModule != null) {
|
||||||
|
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||||
|
WorldEffectType.GROUND_SPRING_EMERGES, coord,
|
||||||
|
Math.min(1.0, (level - GROUNDWATER_THRESHOLD) / 50.0 + 0.3)));
|
||||||
|
// Discharge: spring reduces aquifer level
|
||||||
|
groundwaterLevel.put(coord, level - 15.0);
|
||||||
|
WaterRegionData water = region.getModuleData()
|
||||||
|
.get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null);
|
||||||
|
if (water != null) {
|
||||||
|
water.setWaterAvailability(Math.min(100, water.getWaterAvailability() + 5.0));
|
||||||
|
water.setDroughtRisk(Math.max(0, water.getDroughtRisk() - 3.0));
|
||||||
|
region.getModuleData().put(WaterModule.MODULE_ID, water);
|
||||||
|
}
|
||||||
|
// Active spring contributes to river erosion even without rain
|
||||||
|
riverFlowIntensity.merge(coord, 10.0, Double::sum);
|
||||||
|
regionManager.markDirty(region);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hydraulic erosion fires when the region has meaningful flow intensity
|
||||||
|
// from any source (rainfall runoff OR active spring), independent of rain
|
||||||
|
double flowHere = riverFlowIntensity.getOrDefault(coord, 0.0);
|
||||||
|
if (flowHere > 20.0 && worldEffectsModule != null) {
|
||||||
|
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||||
|
WorldEffectType.HYDRAULIC_EROSION, coord,
|
||||||
|
Math.min(1.0, flowHere / (RIVER_CARVE_THRESHOLD * 2))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advances the volcanic lifecycle for all high-elevation regions:
|
||||||
|
* DORMANT → BUILDING → ERUPTING → COOLING → FERTILE → DORMANT.
|
||||||
|
* Each phase applies physical world effects and ecosystem data changes.
|
||||||
|
*/
|
||||||
|
private void applyVolcanicActivity() {
|
||||||
|
Collection<Region> active = regionManager.getActiveRegions();
|
||||||
|
if (active.isEmpty()) return;
|
||||||
|
Map<RegionCoordinate, Region> byCoord = new HashMap<>();
|
||||||
|
for (Region r : active) byCoord.put(r.getCoordinate(), r);
|
||||||
|
int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||||
|
|
||||||
|
for (Region region : active) {
|
||||||
|
RegionCoordinate coord = region.getCoordinate();
|
||||||
|
Double elev = regionElevations.get(coord);
|
||||||
|
if (elev == null) continue;
|
||||||
|
|
||||||
|
// Register high-elevation regions as potentially volcanic on first encounter
|
||||||
|
if (!volcanoPhase.containsKey(coord)) {
|
||||||
|
if (elev < VOLCANIC_ELEVATION) continue;
|
||||||
|
volcanicRegions.add(coord);
|
||||||
|
volcanoPhase.put(coord, VolcanoPhase.DORMANT);
|
||||||
|
volcanoTicks.put(coord, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
VolcanoPhase phase = volcanoPhase.get(coord);
|
||||||
|
int ticks = volcanoTicks.getOrDefault(coord, 0);
|
||||||
|
volcanoTicks.put(coord, ticks + 1);
|
||||||
|
|
||||||
|
switch (phase) {
|
||||||
|
case DORMANT -> {
|
||||||
|
// Rare stochastic chance to awaken (~0.2% per check)
|
||||||
|
if (windRandom.nextDouble() < 0.002) {
|
||||||
|
volcanoPhase.put(coord, VolcanoPhase.BUILDING);
|
||||||
|
volcanoTicks.put(coord, 0);
|
||||||
|
pendingEventMessages.add("[LW] 🌋 Seismic activity at mountain region ("
|
||||||
|
+ coord.x() + "," + coord.z() + ") — a volcano may be awakening...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case BUILDING -> {
|
||||||
|
// Venting gases: mild pollution increase; 30 cycles before eruption
|
||||||
|
PollutionRegionData poll = region.getModuleData()
|
||||||
|
.get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
|
||||||
|
if (poll != null) {
|
||||||
|
poll.addPollution(0.3, 0.1, 0);
|
||||||
|
region.getModuleData().put(PollutionModule.MODULE_ID, poll);
|
||||||
|
regionManager.markDirty(region);
|
||||||
|
}
|
||||||
|
if (ticks >= 30) {
|
||||||
|
volcanoPhase.put(coord, VolcanoPhase.ERUPTING);
|
||||||
|
volcanoTicks.put(coord, 0);
|
||||||
|
pendingEventMessages.add("[LW] 🌋 VOLCANIC ERUPTION at region ("
|
||||||
|
+ coord.x() + "," + coord.z() + ")! Lava and ash spreading now!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ERUPTING -> {
|
||||||
|
// Lava flows and ash — physical block changes
|
||||||
|
if (worldEffectsModule != null) {
|
||||||
|
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||||
|
WorldEffectType.LAVA_FLOW, coord,
|
||||||
|
Math.min(1.0, 0.4 + (ticks / 20.0) * 0.6)));
|
||||||
|
if (windRandom.nextDouble() < 0.40) {
|
||||||
|
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||||
|
WorldEffectType.ASH_DEPOSIT, coord, 0.7));
|
||||||
|
}
|
||||||
|
if (windRandom.nextDouble() < 0.25) {
|
||||||
|
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||||
|
WorldEffectType.VEGETATION_DIES, coord, 0.9));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ash cloud spreads to adjacent regions
|
||||||
|
for (int[] off : offsets) {
|
||||||
|
RegionCoordinate nCoord = new RegionCoordinate(
|
||||||
|
coord.dimensionId(), coord.x() + off[0], coord.z() + off[1]);
|
||||||
|
Region nb = byCoord.get(nCoord);
|
||||||
|
if (nb == null) continue;
|
||||||
|
if (worldEffectsModule != null && windRandom.nextDouble() < 0.3) {
|
||||||
|
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||||
|
WorldEffectType.ASH_DEPOSIT, nCoord, 0.3));
|
||||||
|
}
|
||||||
|
PollutionRegionData nbPoll = nb.getModuleData()
|
||||||
|
.get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
|
||||||
|
if (nbPoll != null) {
|
||||||
|
nbPoll.addPollution(0.8, 0.2, 0);
|
||||||
|
nb.getModuleData().put(PollutionModule.MODULE_ID, nbPoll);
|
||||||
|
regionManager.markDirty(nb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Eruption zone: spike air pollution, drain vegetation
|
||||||
|
PollutionRegionData poll = region.getModuleData()
|
||||||
|
.get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
|
||||||
|
if (poll != null) {
|
||||||
|
poll.addPollution(2.0, 1.0, 0.5);
|
||||||
|
region.getModuleData().put(PollutionModule.MODULE_ID, poll);
|
||||||
|
}
|
||||||
|
VegetationRegionData veg = region.getModuleData()
|
||||||
|
.get(VegetationModule.MODULE_ID, VegetationRegionData.class).orElse(null);
|
||||||
|
if (veg != null) {
|
||||||
|
veg.setGrassPressure(Math.max(0, veg.getGrassPressure() - 5));
|
||||||
|
veg.setTreePressure(Math.max(0, veg.getTreePressure() - 3));
|
||||||
|
region.getModuleData().put(VegetationModule.MODULE_ID, veg);
|
||||||
|
}
|
||||||
|
regionManager.markDirty(region);
|
||||||
|
// Eruption lasts ~40 cycles
|
||||||
|
if (ticks >= 40) {
|
||||||
|
volcanoPhase.put(coord, VolcanoPhase.COOLING);
|
||||||
|
volcanoTicks.put(coord, 0);
|
||||||
|
pendingEventMessages.add("[LW] 🌋 Eruption subsiding at ("
|
||||||
|
+ coord.x() + "," + coord.z() + ") — lava cooling and solidifying...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case COOLING -> {
|
||||||
|
// Lava solidifies — new land forms from cooling rock
|
||||||
|
if (worldEffectsModule != null && windRandom.nextDouble() < 0.5) {
|
||||||
|
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||||
|
WorldEffectType.COBBLESTONE_FORMS, coord, 0.6));
|
||||||
|
}
|
||||||
|
if (ticks >= 30) {
|
||||||
|
volcanoPhase.put(coord, VolcanoPhase.FERTILE);
|
||||||
|
volcanoTicks.put(coord, 0);
|
||||||
|
pendingEventMessages.add("[LW] 🌱 Volcanic soil enriching region ("
|
||||||
|
+ coord.x() + "," + coord.z() + ") — fertile basalt plain emerging!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case FERTILE -> {
|
||||||
|
// One-time volcanic mineral fertility boost in the first 10 cycles
|
||||||
|
if (ticks < 10) {
|
||||||
|
SoilRegionData soil = region.getModuleData()
|
||||||
|
.get(SoilModule.MODULE_ID, SoilRegionData.class).orElse(null);
|
||||||
|
if (soil != null) {
|
||||||
|
soil.setFertility(Math.min(100, soil.getFertility() + 30));
|
||||||
|
soil.setContamination(Math.max(0, soil.getContamination() - 20));
|
||||||
|
region.getModuleData().put(SoilModule.MODULE_ID, soil);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Pollution clears naturally as fresh minerals neutralise it
|
||||||
|
if (ticks < 20) {
|
||||||
|
PollutionRegionData poll = region.getModuleData()
|
||||||
|
.get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
|
||||||
|
if (poll != null) {
|
||||||
|
poll.addPollution(-1.0, -0.5, -0.3);
|
||||||
|
region.getModuleData().put(PollutionModule.MODULE_ID, poll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
regionManager.markDirty(region);
|
||||||
|
if (ticks >= 50) {
|
||||||
|
volcanoPhase.put(coord, VolcanoPhase.DORMANT);
|
||||||
|
volcanoTicks.put(coord, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the current volcano phase name for the given region, or null if not volcanic. */
|
||||||
|
public String getVolcanoPhaseAt(RegionCoordinate coord) {
|
||||||
|
if (coord == null) return null;
|
||||||
|
VolcanoPhase phase = volcanoPhase.get(coord);
|
||||||
|
return phase != null ? phase.name() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a formatted string listing all tracked volcanic regions and their current phase.
|
||||||
|
* Used by {@code /lw volcanoes}.
|
||||||
|
*/
|
||||||
|
public String getVolcanoStatusString() {
|
||||||
|
if (volcanicRegions.isEmpty()) return "[LW] No volcanic regions detected yet (requires high-elevation terrain).";
|
||||||
|
StringBuilder sb = new StringBuilder("[LW] Volcanic regions: ").append(volcanicRegions.size()).append("\n");
|
||||||
|
for (RegionCoordinate coord : volcanicRegions) {
|
||||||
|
VolcanoPhase phase = volcanoPhase.getOrDefault(coord, VolcanoPhase.DORMANT);
|
||||||
|
int ticks = volcanoTicks.getOrDefault(coord, 0);
|
||||||
|
Double elev = regionElevations.get(coord);
|
||||||
|
sb.append(" (").append(coord.x()).append(",").append(coord.z()).append(") ")
|
||||||
|
.append(phase.name())
|
||||||
|
.append(" — ticks: ").append(ticks);
|
||||||
|
if (elev != null) sb.append(", elev: ").append(String.format("%.0f", elev));
|
||||||
|
sb.append("\n");
|
||||||
|
}
|
||||||
|
return sb.toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a snapshot of all active climate events for the {@code /lw events} command.
|
||||||
|
*/
|
||||||
|
public List<ClimateEvent> getActiveClimateEventSnapshot() {
|
||||||
|
return List.copyOf(activeClimateEvents);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the sampled average surface elevation for a region so that
|
* Stores the sampled average surface elevation for a region so that
|
||||||
* {@link #applyWaterRunoff()} can model water flowing downhill between neighbours.
|
* {@link #applyWaterRunoff()} can model water flowing downhill between neighbours.
|
||||||
@@ -1228,7 +1521,26 @@ public final class LivingWorldBootstrap {
|
|||||||
this::getAtmosphereStatusFor,
|
this::getAtmosphereStatusFor,
|
||||||
this::getClimateStatusFor,
|
this::getClimateStatusFor,
|
||||||
this::setSimSpeedMultiplier,
|
this::setSimSpeedMultiplier,
|
||||||
this::getWindInfo);
|
this::getWindInfo,
|
||||||
|
css -> getEventsStatusFor(),
|
||||||
|
this::getVolcanoStatusString);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a formatted string of all active climate events for {@code /lw events}. */
|
||||||
|
public String getEventsStatusFor() {
|
||||||
|
List<ClimateEvent> events = getActiveClimateEventSnapshot();
|
||||||
|
if (events.isEmpty()) return "[LW] No active climate events.";
|
||||||
|
StringBuilder sb = new StringBuilder("[LW] Active Climate Events: ")
|
||||||
|
.append(events.size()).append("\n");
|
||||||
|
for (ClimateEvent ev : events) {
|
||||||
|
sb.append(" ").append(ev.getType().displayName())
|
||||||
|
.append(" at (").append(ev.getEpicenter().x()).append(",").append(ev.getEpicenter().z()).append(")")
|
||||||
|
.append(" | ticks: ").append(ev.getTicksActive())
|
||||||
|
.append(" | severity: ").append(String.format("%.0f%%", ev.getSeverity() * 100))
|
||||||
|
.append(" | regions: ").append(ev.getAffectedRegions().size())
|
||||||
|
.append("\n");
|
||||||
|
}
|
||||||
|
return sb.toString().trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Path getWorldSaveDirectory() {
|
public Path getWorldSaveDirectory() {
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ public enum ClimateEventType {
|
|||||||
|
|
||||||
DROUGHT("Drought", "Prolonged dry spell spreading across adjacent regions"),
|
DROUGHT("Drought", "Prolonged dry spell spreading across adjacent regions"),
|
||||||
WILDFIRE("Wildfire", "Fire spreading through drought-stressed forest regions"),
|
WILDFIRE("Wildfire", "Fire spreading through drought-stressed forest regions"),
|
||||||
FLOOD("Flood", "Heavy rainfall overwhelming low-lying terrain");
|
FLOOD("Flood", "Heavy rainfall overwhelming low-lying terrain"),
|
||||||
|
VOLCANIC_ERUPTION("Volcanic Eruption", "Lava flows and ash clouds from an active volcano");
|
||||||
|
|
||||||
private final String displayName;
|
private final String displayName;
|
||||||
private final String description;
|
private final String description;
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ public final class LivingWorldCommandRoot {
|
|||||||
css -> "Atmosphere not available.",
|
css -> "Atmosphere not available.",
|
||||||
css -> "Climate not available.",
|
css -> "Climate not available.",
|
||||||
n -> n,
|
n -> n,
|
||||||
() -> "Wind not available.");
|
() -> "Wind not available.",
|
||||||
|
css -> "Events not available.",
|
||||||
|
() -> "No volcanic regions.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void registerDeferred(
|
public static void registerDeferred(
|
||||||
@@ -56,7 +58,9 @@ public final class LivingWorldCommandRoot {
|
|||||||
Function<CommandSourceStack, String> atmosphereStatus,
|
Function<CommandSourceStack, String> atmosphereStatus,
|
||||||
Function<CommandSourceStack, String> climateStatus,
|
Function<CommandSourceStack, String> climateStatus,
|
||||||
IntUnaryOperator speedSetter,
|
IntUnaryOperator speedSetter,
|
||||||
Supplier<String> windStatus) {
|
Supplier<String> windStatus,
|
||||||
|
Function<CommandSourceStack, String> eventsStatus,
|
||||||
|
Supplier<String> volcanoStatus) {
|
||||||
if (dispatcher == null) {
|
if (dispatcher == null) {
|
||||||
throw new IllegalArgumentException("dispatcher must not be null");
|
throw new IllegalArgumentException("dispatcher must not be null");
|
||||||
}
|
}
|
||||||
@@ -154,7 +158,19 @@ public final class LivingWorldCommandRoot {
|
|||||||
})))
|
})))
|
||||||
.then(LivingWorldMapCommand.build(regionManager))
|
.then(LivingWorldMapCommand.build(regionManager))
|
||||||
.then(RegionSetCommand.build(regionManager))
|
.then(RegionSetCommand.build(regionManager))
|
||||||
.then(EcoDemoCommand.build(regionManager)));
|
.then(EcoDemoCommand.build(regionManager))
|
||||||
|
.then(Commands.literal("events")
|
||||||
|
.executes(context -> {
|
||||||
|
String info = eventsStatus.apply(context.getSource());
|
||||||
|
context.getSource().sendSuccess(() -> Component.literal(info), false);
|
||||||
|
return 1;
|
||||||
|
}))
|
||||||
|
.then(Commands.literal("volcanoes")
|
||||||
|
.executes(context -> {
|
||||||
|
String info = volcanoStatus.get();
|
||||||
|
context.getSource().sendSuccess(() -> Component.literal(info), false);
|
||||||
|
return 1;
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int toggleHud(CommandSourceStack source, Function<UUID, Boolean> hudToggle) {
|
private static int toggleHud(CommandSourceStack source, Function<UUID, Boolean> hudToggle) {
|
||||||
|
|||||||
@@ -67,4 +67,39 @@ public enum WorldEffectType {
|
|||||||
* through the region, gradually carving a visible riverbed over many cycles.
|
* through the region, gradually carving a visible riverbed over many cycles.
|
||||||
*/
|
*/
|
||||||
RIVER_CARVE,
|
RIVER_CARVE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A groundwater spring emerges at the valley floor: a water source block is placed
|
||||||
|
* at the lowest natural terrain point in the region. The water then flows downhill
|
||||||
|
* under Minecraft's own fluid physics, forming a permanent river without relying on rain.
|
||||||
|
*/
|
||||||
|
GROUND_SPRING_EMERGES,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flowing water adjacent to soft terrain gradually softens and removes it: stone
|
||||||
|
* becomes gravel, gravel becomes sand, sand and dirt are removed. Widens river
|
||||||
|
* valleys and carves gorges independently of rainfall.
|
||||||
|
*/
|
||||||
|
HYDRAULIC_EROSION,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active volcanic eruption: lava source blocks are placed at the summit and on
|
||||||
|
* natural rock surfaces. Lava flows downhill under Minecraft physics, permanently
|
||||||
|
* reshaping the terrain around the volcano.
|
||||||
|
*/
|
||||||
|
LAVA_FLOW,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Volcanic ash clouds deposit a grey layer over the surrounding landscape: grass
|
||||||
|
* and soil surfaces are covered with tuff and gravel, surface plants are killed.
|
||||||
|
* Spreads outward from the eruption epicentre.
|
||||||
|
*/
|
||||||
|
ASH_DEPOSIT,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cooling lava solidifies into new land: lava adjacent to water converts to
|
||||||
|
* cobblestone, lava exposed to air converts to basalt. Creates permanent new
|
||||||
|
* terrain that was not there before the eruption.
|
||||||
|
*/
|
||||||
|
COBBLESTONE_FORMS,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import net.minecraft.server.MinecraftServer;
|
|||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
import net.minecraft.tags.BlockTags;
|
import net.minecraft.tags.BlockTags;
|
||||||
|
import net.minecraft.tags.FluidTags;
|
||||||
import net.minecraft.world.level.block.Block;
|
import net.minecraft.world.level.block.Block;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
import net.minecraft.world.level.levelgen.Heightmap;
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
@@ -76,6 +77,16 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
|
|||||||
formWaterPool(level, baseX, baseZ, request.intensity());
|
formWaterPool(level, baseX, baseZ, request.intensity());
|
||||||
case RIVER_CARVE ->
|
case RIVER_CARVE ->
|
||||||
carveRiverChannel(level, baseX, baseZ, request.intensity());
|
carveRiverChannel(level, baseX, baseZ, request.intensity());
|
||||||
|
case GROUND_SPRING_EMERGES ->
|
||||||
|
groundSpringEmerges(level, baseX, baseZ, request.intensity());
|
||||||
|
case HYDRAULIC_EROSION ->
|
||||||
|
hydraulicErosion(level, baseX, baseZ, request.intensity());
|
||||||
|
case LAVA_FLOW ->
|
||||||
|
lavaFlow(level, baseX, baseZ, request.intensity());
|
||||||
|
case ASH_DEPOSIT ->
|
||||||
|
ashDeposit(level, baseX, baseZ, request.intensity());
|
||||||
|
case COBBLESTONE_FORMS ->
|
||||||
|
cobblestoneFromLava(level, baseX, baseZ, request.intensity());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,6 +355,168 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Places a permanent water source at the lowest natural terrain point found in the
|
||||||
|
* region. The source then flows downhill under vanilla water physics, forming a river
|
||||||
|
* without any further simulation involvement.
|
||||||
|
*/
|
||||||
|
private void groundSpringEmerges(ServerLevel level, int baseX, int baseZ, double intensity) {
|
||||||
|
if (random.nextDouble() > 0.30) return;
|
||||||
|
int samples = 20;
|
||||||
|
BlockPos lowestPos = null;
|
||||||
|
for (int i = 0; i < samples; i++) {
|
||||||
|
BlockPos candidate = surfaceAt(level,
|
||||||
|
baseX + random.nextInt(REGION_BLOCKS),
|
||||||
|
baseZ + random.nextInt(REGION_BLOCKS));
|
||||||
|
if (candidate != null && (lowestPos == null || candidate.getY() < lowestPos.getY())) {
|
||||||
|
lowestPos = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lowestPos == null) return;
|
||||||
|
var surface = level.getBlockState(lowestPos);
|
||||||
|
if (!surface.is(Blocks.DIRT) && !surface.is(Blocks.GRASS_BLOCK)
|
||||||
|
&& !surface.is(Blocks.SAND) && !surface.is(Blocks.GRAVEL)
|
||||||
|
&& !surface.is(Blocks.COARSE_DIRT)) return;
|
||||||
|
BlockPos springPos = lowestPos.above();
|
||||||
|
if (!level.isLoaded(springPos)) return;
|
||||||
|
if (!level.getBlockState(springPos).isAir()) return;
|
||||||
|
level.setBlock(springPos, Blocks.WATER.defaultBlockState(), Block.UPDATE_ALL);
|
||||||
|
LivingWorldLogger.info(DiagnosticCategory.SIMULATION, "WorldEffect GROUND_SPRING_EMERGES at " + springPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flowing water (level 1–7) adjacent to soft rock gradually softens the terrain:
|
||||||
|
* stone → gravel → sand, sand/dirt → air. Widens river valleys over time without
|
||||||
|
* requiring rainfall; only fires ~15% of the time per call.
|
||||||
|
*/
|
||||||
|
private void hydraulicErosion(ServerLevel level, int baseX, int baseZ, double intensity) {
|
||||||
|
if (random.nextDouble() > 0.15) return;
|
||||||
|
int attempts = Math.max(1, (int) (intensity * 6));
|
||||||
|
for (int i = 0; i < attempts; i++) {
|
||||||
|
int x = baseX + random.nextInt(REGION_BLOCKS);
|
||||||
|
int z = baseZ + random.nextInt(REGION_BLOCKS);
|
||||||
|
int surfaceY = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z) - 1;
|
||||||
|
if (surfaceY < level.getMinBuildHeight()) continue;
|
||||||
|
// Scan downward for flowing water
|
||||||
|
for (int y = surfaceY; y > surfaceY - 6; y--) {
|
||||||
|
BlockPos pos = new BlockPos(x, y, z);
|
||||||
|
if (!level.isLoaded(pos)) break;
|
||||||
|
var fluidState = level.getFluidState(pos);
|
||||||
|
if (!fluidState.is(FluidTags.WATER) || fluidState.isSource()) continue;
|
||||||
|
// Found flowing water — soften adjacent horizontal blocks
|
||||||
|
for (Direction dir : Direction.Plane.HORIZONTAL) {
|
||||||
|
BlockPos adj = pos.relative(dir);
|
||||||
|
if (!level.isLoaded(adj)) continue;
|
||||||
|
var adjState = level.getBlockState(adj);
|
||||||
|
if (adjState.is(Blocks.STONE) || adjState.is(Blocks.DEEPSLATE)) {
|
||||||
|
if (intensity > 0.65) level.setBlock(adj, Blocks.GRAVEL.defaultBlockState(), Block.UPDATE_ALL);
|
||||||
|
} else if (adjState.is(Blocks.GRAVEL) && intensity > 0.45) {
|
||||||
|
level.setBlock(adj, Blocks.SAND.defaultBlockState(), Block.UPDATE_ALL);
|
||||||
|
} else if ((adjState.is(Blocks.SAND) || adjState.is(Blocks.DIRT)
|
||||||
|
|| adjState.is(Blocks.COARSE_DIRT)) && intensity > 0.75) {
|
||||||
|
level.setBlock(adj, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active volcanic eruption: places lava source blocks at random high-surface positions.
|
||||||
|
* Lava flows downhill under vanilla physics, permanently reshaping terrain.
|
||||||
|
* Fires ~20% of the time per call so flows build gradually.
|
||||||
|
*/
|
||||||
|
private void lavaFlow(ServerLevel level, int baseX, int baseZ, double intensity) {
|
||||||
|
if (random.nextDouble() > 0.20) return;
|
||||||
|
int attempts = Math.max(1, (int) (intensity * 3));
|
||||||
|
for (int i = 0; i < attempts; 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()) continue;
|
||||||
|
BlockPos pos = new BlockPos(x, y, z);
|
||||||
|
if (!level.isLoaded(pos)) continue;
|
||||||
|
var state = level.getBlockState(pos);
|
||||||
|
boolean naturalRock = state.is(Blocks.STONE) || state.is(Blocks.DEEPSLATE)
|
||||||
|
|| state.is(Blocks.ANDESITE) || state.is(Blocks.DIORITE)
|
||||||
|
|| state.is(Blocks.GRANITE) || state.is(Blocks.GRAVEL)
|
||||||
|
|| state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.DIRT)
|
||||||
|
|| state.is(Blocks.TUFF);
|
||||||
|
if (!naturalRock) continue;
|
||||||
|
BlockPos above = pos.above();
|
||||||
|
if (!level.isLoaded(above) || !level.getBlockState(above).isAir()) continue;
|
||||||
|
level.setBlock(above, Blocks.LAVA.defaultBlockState(), Block.UPDATE_ALL);
|
||||||
|
LivingWorldLogger.info(DiagnosticCategory.SIMULATION, "WorldEffect LAVA_FLOW at " + above);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deposits volcanic ash: grass → tuff, loose soil → gravel; surface plants are killed.
|
||||||
|
* Fires ~25% of the time; applied over wide areas during eruptions.
|
||||||
|
*/
|
||||||
|
private void ashDeposit(ServerLevel level, int baseX, int baseZ, double intensity) {
|
||||||
|
if (random.nextDouble() > 0.25) return;
|
||||||
|
int attempts = Math.max(1, (int) (intensity * BLOCK_ATTEMPTS));
|
||||||
|
for (int i = 0; i < attempts; i++) {
|
||||||
|
BlockPos pos = surfaceAt(level,
|
||||||
|
baseX + random.nextInt(REGION_BLOCKS),
|
||||||
|
baseZ + random.nextInt(REGION_BLOCKS));
|
||||||
|
if (pos == null) continue;
|
||||||
|
var state = level.getBlockState(pos);
|
||||||
|
if (state.is(Blocks.GRASS_BLOCK)) {
|
||||||
|
level.setBlock(pos, Blocks.TUFF.defaultBlockState(), Block.UPDATE_ALL);
|
||||||
|
} else if (state.is(Blocks.DIRT) || state.is(Blocks.COARSE_DIRT)) {
|
||||||
|
level.setBlock(pos, Blocks.GRAVEL.defaultBlockState(), Block.UPDATE_ALL);
|
||||||
|
}
|
||||||
|
BlockPos above = pos.above();
|
||||||
|
if (!level.isLoaded(above)) continue;
|
||||||
|
var aboveState = level.getBlockState(above);
|
||||||
|
if (aboveState.is(Blocks.SHORT_GRASS) || aboveState.is(Blocks.TALL_GRASS)
|
||||||
|
|| aboveState.is(Blocks.FERN) || aboveState.is(BlockTags.SAPLINGS)
|
||||||
|
|| aboveState.is(Blocks.DANDELION) || aboveState.is(Blocks.POPPY)) {
|
||||||
|
level.setBlock(above, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Solidifies cooling lava into new permanent land: lava adjacent to water → cobblestone;
|
||||||
|
* lava exposed to air → basalt. This creates terrain that did not exist before the eruption.
|
||||||
|
* Fires ~35% of the time per call.
|
||||||
|
*/
|
||||||
|
private void cobblestoneFromLava(ServerLevel level, int baseX, int baseZ, double intensity) {
|
||||||
|
if (random.nextDouble() > 0.35) return;
|
||||||
|
int attempts = Math.max(2, (int) (intensity * 8));
|
||||||
|
for (int i = 0; i < attempts; i++) {
|
||||||
|
int x = baseX + random.nextInt(REGION_BLOCKS);
|
||||||
|
int z = baseZ + random.nextInt(REGION_BLOCKS);
|
||||||
|
int surfY = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z);
|
||||||
|
for (int y = surfY + 2; y >= Math.max(level.getMinBuildHeight(), surfY - 4); y--) {
|
||||||
|
BlockPos pos = new BlockPos(x, y, z);
|
||||||
|
if (!level.isLoaded(pos)) continue;
|
||||||
|
if (!level.getBlockState(pos).is(Blocks.LAVA)) continue;
|
||||||
|
boolean adjacentWater = false;
|
||||||
|
for (Direction dir : new Direction[]{
|
||||||
|
Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST, Direction.UP}) {
|
||||||
|
BlockPos adj = pos.relative(dir);
|
||||||
|
if (level.isLoaded(adj) && level.getFluidState(adj).is(FluidTags.WATER)) {
|
||||||
|
adjacentWater = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (adjacentWater) {
|
||||||
|
level.setBlock(pos, Blocks.COBBLESTONE.defaultBlockState(), Block.UPDATE_ALL);
|
||||||
|
LivingWorldLogger.info(DiagnosticCategory.SIMULATION, "WorldEffect COBBLESTONE_FORMS at " + pos);
|
||||||
|
} else if (random.nextDouble() < 0.40) {
|
||||||
|
level.setBlock(pos, Blocks.BASALT.defaultBlockState(), Block.UPDATE_ALL);
|
||||||
|
LivingWorldLogger.info(DiagnosticCategory.SIMULATION, "WorldEffect COBBLESTONE_FORMS basalt at " + pos);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private BlockPos surfaceAt(ServerLevel level, int x, int z) {
|
private BlockPos surfaceAt(ServerLevel level, int x, int z) {
|
||||||
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1;
|
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1;
|
||||||
if (y < level.getMinBuildHeight()) {
|
if (y < level.getMinBuildHeight()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user