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 |
|
||||
| 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 |
|
||||
| **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 |
|
||||
|
||||
### 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
|
||||
- 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)
|
||||
- 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
|
||||
|
||||
### 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)
|
||||
|
||||
- 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 stats` | Simulation profiler statistics |
|
||||
| `/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 recover` | One-command demo: restore current region to mature forest |
|
||||
|
||||
@@ -291,13 +333,16 @@ LivingWorldMod (NeoForge event wiring)
|
||||
applySeasonalEffects() [soil/water/veg seasonal]
|
||||
applyClimateWarmingEffects() [drought pressure from CO₂]
|
||||
applyWaterRunoff() [elevation-based; river erosion]
|
||||
applyGroundwaterAndSprings() [aquifer recharge; valley springs]
|
||||
applyDynamicCapUpdate() [raise/lower succession ceiling]
|
||||
applySeedDispersal() [corridor-boosted recolonisation]
|
||||
recordHealthTrend() [↑↓→ trend tracking]
|
||||
applyClimateEvents() [drought/wildfire/flood events]
|
||||
applyVolcanicActivity() [volcano lifecycle; lava/ash/new land]
|
||||
|
||||
LivingWorldMod platform hooks:
|
||||
updateTidalEffects() [block-level tidal simulation]
|
||||
applyRiverCurrents() [player/entity velocity in flowing water]
|
||||
initializeRegionFromBiome() [biome-aware starting values]
|
||||
checkPlayerRegions() [HUD, effects, ambient sounds]
|
||||
scanAndRecordFurnaceActivity() [pollution from lit furnaces]
|
||||
@@ -320,7 +365,8 @@ Requires NeoForge 21.1.172 / Minecraft 1.21.1.
|
||||
## Planned / In Progress
|
||||
|
||||
- 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 and accumulated seed rain across restarts
|
||||
- Persistence for river flow intensity, groundwater level, and volcanic state across restarts
|
||||
- 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 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 Random random = new Random();
|
||||
@@ -104,6 +108,7 @@ public class LivingWorldMod {
|
||||
private int playerCheckTick = 0;
|
||||
private int tideTick = 0;
|
||||
private double lastTideLevel = 0.0;
|
||||
private int riverCurrentTick = 0;
|
||||
private final Map<UUID, RegionCoordinate> playerRegionCache = new HashMap<>();
|
||||
private final Map<UUID, Boolean> playerRainState = new HashMap<>();
|
||||
private final Set<RegionCoordinate> biomeInitialized = new HashSet<>();
|
||||
@@ -157,6 +162,7 @@ public class LivingWorldMod {
|
||||
regionSoundLastTick.clear();
|
||||
tideTick = 0;
|
||||
lastTideLevel = 0.0;
|
||||
riverCurrentTick = 0;
|
||||
this.minecraftServer = null;
|
||||
});
|
||||
|
||||
@@ -172,6 +178,9 @@ public class LivingWorldMod {
|
||||
tideTick = 0;
|
||||
updateTidalEffects();
|
||||
}
|
||||
if (++riverCurrentTick % RIVER_CURRENT_INTERVAL == 0) {
|
||||
applyRiverCurrents();
|
||||
}
|
||||
// Broadcast any climate event messages queued by the bootstrap
|
||||
java.util.List<String> eventMsgs = bootstrap.pollClimateEventMessages();
|
||||
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.
|
||||
* Each sound fires at low volume and random pitch so it blends naturally.
|
||||
@@ -397,8 +436,25 @@ public class LivingWorldMod {
|
||||
*/
|
||||
private boolean tryPlayRegionAmbience(
|
||||
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()
|
||||
&& health > 50.0 && random.nextInt(10) == 0) {
|
||||
// Rustling leaves — healthy forest ambience.
|
||||
|
||||
@@ -83,6 +83,15 @@ public final class LivingWorldBootstrap {
|
||||
private static final double RIVER_CARVE_THRESHOLD = 80.0;
|
||||
/** Climate event checks run every N post-sim cycles to avoid per-tick overhead. */
|
||||
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 final Random windRandom = new Random();
|
||||
@@ -108,6 +117,15 @@ public final class LivingWorldBootstrap {
|
||||
private final List<String> pendingEventMessages = new ArrayList<>();
|
||||
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 Path worldSaveDirectory;
|
||||
private ServiceRegistry services;
|
||||
@@ -259,6 +277,11 @@ public final class LivingWorldBootstrap {
|
||||
activeClimateEvents.clear();
|
||||
pendingEventMessages.clear();
|
||||
climateEventTick = 0;
|
||||
groundwaterLevel.clear();
|
||||
volcanoPhase.clear();
|
||||
volcanoTicks.clear();
|
||||
volcanicRegions.clear();
|
||||
volcanicActivityTick = 0;
|
||||
simSpeedMultiplier = 1;
|
||||
serverReady = false;
|
||||
LivingWorldLogger.info(
|
||||
@@ -410,12 +433,16 @@ public final class LivingWorldBootstrap {
|
||||
climateTracker.update(regionManager.getActiveRegions());
|
||||
applyClimateWarmingEffects();
|
||||
applyWaterRunoff();
|
||||
applyGroundwaterAndSprings();
|
||||
applyDynamicCapUpdate();
|
||||
applySeedDispersal();
|
||||
recordHealthTrend();
|
||||
if (++climateEventTick % CLIMATE_CHECK_INTERVAL == 0) {
|
||||
applyClimateEvents();
|
||||
}
|
||||
if (++volcanicActivityTick % VOLCANO_TICK_INTERVAL == 0) {
|
||||
applyVolcanicActivity();
|
||||
}
|
||||
}
|
||||
|
||||
/** 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
|
||||
* {@link #applyWaterRunoff()} can model water flowing downhill between neighbours.
|
||||
@@ -1228,7 +1521,26 @@ public final class LivingWorldBootstrap {
|
||||
this::getAtmosphereStatusFor,
|
||||
this::getClimateStatusFor,
|
||||
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() {
|
||||
|
||||
@@ -4,7 +4,8 @@ public enum ClimateEventType {
|
||||
|
||||
DROUGHT("Drought", "Prolonged dry spell spreading across adjacent 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 description;
|
||||
|
||||
@@ -44,7 +44,9 @@ public final class LivingWorldCommandRoot {
|
||||
css -> "Atmosphere not available.",
|
||||
css -> "Climate not available.",
|
||||
n -> n,
|
||||
() -> "Wind not available.");
|
||||
() -> "Wind not available.",
|
||||
css -> "Events not available.",
|
||||
() -> "No volcanic regions.");
|
||||
}
|
||||
|
||||
public static void registerDeferred(
|
||||
@@ -56,7 +58,9 @@ public final class LivingWorldCommandRoot {
|
||||
Function<CommandSourceStack, String> atmosphereStatus,
|
||||
Function<CommandSourceStack, String> climateStatus,
|
||||
IntUnaryOperator speedSetter,
|
||||
Supplier<String> windStatus) {
|
||||
Supplier<String> windStatus,
|
||||
Function<CommandSourceStack, String> eventsStatus,
|
||||
Supplier<String> volcanoStatus) {
|
||||
if (dispatcher == null) {
|
||||
throw new IllegalArgumentException("dispatcher must not be null");
|
||||
}
|
||||
@@ -154,7 +158,19 @@ public final class LivingWorldCommandRoot {
|
||||
})))
|
||||
.then(LivingWorldMapCommand.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) {
|
||||
|
||||
@@ -67,4 +67,39 @@ public enum WorldEffectType {
|
||||
* through the region, gradually carving a visible riverbed over many cycles.
|
||||
*/
|
||||
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.world.level.Level;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
@@ -76,6 +77,16 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
|
||||
formWaterPool(level, baseX, baseZ, request.intensity());
|
||||
case RIVER_CARVE ->
|
||||
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) {
|
||||
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1;
|
||||
if (y < level.getMinBuildHeight()) {
|
||||
|
||||
Reference in New Issue
Block a user