Complete phase 1 ocean ecosystem

This commit is contained in:
George
2026-06-11 18:01:13 +01:00
parent 54a6be91de
commit 7802e4b603
6 changed files with 887 additions and 55 deletions
+391
View File
@@ -0,0 +1,391 @@
# Living World — Big Plan
A complete roadmap of every natural phenomenon to implement. Organised into phases by theme and dependency order. Each phase builds on the systems already in place without requiring the previous phase to be 100% complete.
---
## Current State (implemented)
- 9-module ecosystem pipeline: Pollution → Soil → Water → Vegetation → ResourceDepletion → Recovery → Ecosystem → WorldEffects → Atmosphere
- Succession stages: BARREN → SPARSE_GRASS → GRASSLAND → SCRUBLAND → YOUNG_WOODLAND → MATURE_FOREST
- Tides with moon-phase amplitude (spring/neap cycle)
- River currents + hydraulic erosion
- Groundwater aquifer + springs
- Mountain volcanoes (lava flow, ash deposit, fertile phase)
- Ocean/submarine volcanoes (seafloor buildup → island formation)
- World-wide simulation via ChunkEvent.Load (all loaded chunks, not just player-adjacent)
- `/lw` command suite: status, region, atmosphere, climate, wind, speed, events, volcanoes
---
## Phase 1 — Ocean Ecosystem
*Completes the ocean as a living system. Builds directly on submarine volcanism already implemented.*
### 1.1 Coral Reefs
- **Trigger:** Clean ocean region (pollution < 20) with shallow floor (OCEAN_FLOOR heightmap y > 48)
- **Growth:** Place coral blocks and coral fans on seafloor over many cycles; species diversity scales with ecosystem health
- **Bleaching:** Pollution 2050 turns coral to dead coral; pollution > 50 removes coral entirely
- **New WorldEffectType:** `CORAL_GROWTH`, `CORAL_BLEACHING`
- **Feedback:** Healthy reef boosts hydration and water quality scores for the region
### 1.2 Kelp Forests
- **Trigger:** Clean, cold ocean region (temp < 0.5, pollution < 30)
- **Growth:** Place kelp column from OCEAN_FLOOR upward, max 8 blocks high; grows one block per fertile cycle
- **Death:** High pollution converts kelp to dead bush / removes it
- **New WorldEffectType:** `KELP_GROWTH`, `KELP_DIE`
### 1.3 Algae Blooms & Dead Zones
- **Trigger:** Ocean pollution > 60 sustained for 10+ cycles
- **Bloom:** Green-tinted water (place green stained glass? or particles), kelp overgrowth forced at high rate
- **Die-off:** After bloom peaks, mass die-off deposits dirt/gravel on seafloor, spikes pollution further
- **Dead zone:** Suppress all aquatic mob spawning (Cod, Salmon, Squid, Glow Squid, Dolphin) while active
- **New WorldEffectType:** `ALGAE_BLOOM`, `DEAD_ZONE`
- **New ClimateEventType:** `ALGAE_BLOOM`
### 1.4 Bioluminescence
- **Trigger:** Clean deep ocean region (pollution < 15, depth > 32 below sea level)
- **Effect:** Glow squid particle bursts at night only (check `dayTime > 12000`)
- **No new WorldEffectType needed** — purely particle, handled in LivingWorldMod tick
### 1.5 Hydrothermal Vent Fields
- **Trigger:** Oceanic volcanic regions in COOLING or FERTILE phase
- **Effect:** Permanent seafloor feature: magma blocks and soul sand form around vent opening; continuous bubble/smoke particles; small pollution spike kept constant
- **New WorldEffectType:** `HYDROTHERMAL_VENT`
---
## Phase 2 — Atmospheric Events
*Weather phenomena driven by the existing Atmosphere module. Most are purely additive — new WorldEffectTypes using existing block change infrastructure.*
### 2.1 Acid Rain
- **Trigger:** Regional air pollution > 70 during a rain event
- **Effect:** Stone/cobblestone on exposed surface → gravel; grass → dirt; surface plants die; soil contamination +5 per cycle during event
- **Duration:** Lasts as long as raining + pollution remains high
- **New WorldEffectType:** `ACID_RAIN_DAMAGE`
- **New ClimateEventType:** `ACID_RAIN`
### 2.2 Blizzards
- **Trigger:** Cold biome (temp < 0.15), high precipitation, wind speed above threshold
- **Effect:** Snow layers accumulate on exposed surfaces each cycle (up to depth 4); standing water freezes to ice; mobs and players get slowness-equivalent push-back from wind
- **Melt:** When temperature rises above 0 in same region, snow layers reduce one per cycle
- **New WorldEffectType:** `SNOW_ACCUMULATION`, `WATER_FREEZES`
- **New ClimateEventType:** `BLIZZARD`
### 2.3 Sandstorms
- **Trigger:** Desert biome, low humidity, high wind
- **Effect:** Sand deposits on exposed surfaces (place sand one block above lowest surrounding blocks); surface plants stripped; minor soil contamination (airborne particles)
- **New WorldEffectType:** `SAND_DEPOSIT`
- **New ClimateEventType:** `SANDSTORM`
### 2.4 Lightning Strikes
- **Trigger:** Storm climate event, dry region (humidity < 30)
- **Effect:** Single ignition point (one fire block) placed on a flammable surface block; spreads naturally via Minecraft fire tick — distinct from the existing area-wide WILDFIRE
- **Implementation:** Use `ServerLevel.lightning()` or place fire directly; 13 strikes per storm cycle
- **New ClimateEventType:** `LIGHTNING_STORM` (already partially covered by WILDFIRE; make it a separate trigger)
### 2.5 Fog Banks
- **Trigger:** Coastal or wetland biome, humidity > 70, calm wind (low speed)
- **Effect:** Dense particle fog at ground level (CLOUD particles in a 64×64 area); no block changes; purely visual + ambience
- **Implementation:** LivingWorldMod tick, no new WorldEffectType needed
### 2.6 Heat Shimmer
- **Trigger:** Desert biome OR pollution > 60, daytime only
- **Effect:** Flame particles at very low count (0.01 speed) near ground; no block changes; visual indicator of heat stress
- **Implementation:** LivingWorldMod tick, no new WorldEffectType needed
### 2.7 Auroras
- **Trigger:** Cold/polar biome, clear night (no rain, time > 13000), atmosphere pollution < 10
- **Effect:** Coloured enchantment-glyph or end rod particles high in the sky (y > 180); purely cosmetic
- **Implementation:** LivingWorldMod tick, no new WorldEffectType needed
---
## Phase 3 — Geological Activity
*Ground-rupturing events. Require careful throttling — these permanently reshape terrain.*
### 3.1 Geysers
- **Trigger:** Volcanic-adjacent region OR high geothermal elevation OR dripstone-cave biome below
- **Registration:** Like volcanoes, geyser sites are registered on first encounter and persist
- **Cycle:** 5-minute dormant → 30-second active → 5-minute dormant
- **Effect (active):** Column of water source blocks shoots upward (place 35 blocks of water at geyser X,Z at increasing Y each tick, then remove after 2 seconds); BUBBLE_COLUMN particles; travertine ring (calcite/tuff) grows 1 block wider each active cycle
- **New WorldEffectType:** `GEYSER_ERUPT`, `TRAVERTINE_DEPOSIT`
- **New field in bootstrap:** `Map<RegionCoordinate, GeyserSite>` (location + cooldown)
### 3.2 Earthquakes
- **Trigger:** Volcanic region in BUILDING/ERUPTING phase (3× frequency), or any high-elevation region (1× base)
- **Magnitude:** Low (13) = surface cracks only; High (45) = fissures + landslide trigger
- **Effect:** Low magnitude — remove 35 random surface blocks in a line (creates crack pattern); replace with air + expose layer beneath. High magnitude — carve a fissure 2 blocks wide, 815 blocks deep in a random direction from region centre
- **Player feedback:** Screen shake analogue — apply velocity impulse to nearby players/mobs
- **New WorldEffectType:** `GROUND_CRACK`, `FISSURE_OPENS`
- **New ClimateEventType:** `EARTHQUAKE`
### 3.3 Sinkholes
- **Trigger:** Sustained high groundwater (> 80) + shallow cave system below surface (detected via downward scan for air pockets)
- **Effect:** Surface collapses — remove a 3×3 to 5×5 area of blocks at surface, creating a sudden pit 515 blocks deep; water may flood the pit if groundwater is at max
- **Rarity:** Very rare, ~0.05% chance per cycle when conditions met
- **New WorldEffectType:** `SINKHOLE_COLLAPSES`
- **New ClimateEventType:** `SINKHOLE`
### 3.4 Landslides
- **Trigger:** Steep slope (elevation delta > 15 between adjacent regions) + high precipitation OR earthquake
- **Effect:** Gravel/sand/dirt on steep surface "falls" — remove top 2 blocks of highest surface point in the region, place them at the lowest adjacent surface point (gravity simulation)
- **New WorldEffectType:** `LANDSLIDE`
### 3.5 Lava Tubes
- **Trigger:** Volcanic region COOLING phase, stochastic (~0.1% per cycle)
- **Effect:** Scan downward from surface to find a lava-adjacent air pocket; remove the ceiling block, creating a visible opening to the lava below
- **New WorldEffectType:** `LAVA_TUBE_COLLAPSE`
---
## Phase 4 — Hydrology Expansion
*Extends the existing river/groundwater/tidal systems with longer-timescale water dynamics.*
### 4.1 Flash Floods
- **Trigger:** High precipitation (> 80) on barren/low-succession region (BARREN or SPARSE_GRASS) with high slope
- **Effect:** Temporary surge of water source blocks placed along lowest terrain path through region; blocks removed after 5 minutes (track flood blocks in a `Map<RegionCoordinate, List<BlockPos>>`)
- **New WorldEffectType:** `FLASH_FLOOD`
- **New ClimateEventType:** `FLASH_FLOOD`
### 4.2 Spring Melt
- **Trigger:** Cold region that transitions from freezing to thawing (temperature crosses 0.15 threshold)
- **Effect:** Ice blocks → water, snow layers removed top-down one per cycle; groundwater spikes; briefly triggers GROUND_SPRING_EMERGES in adjacent lower-elevation regions
- **New WorldEffectType:** `ICE_MELTS`, `SNOW_MELTS`
### 4.3 Drought Cycles
- **Trigger:** Atmosphere module humidity < 15 sustained for 20+ cycles
- **Effect:** River water sources removed (RIVER_CARVE in reverse); exposed riverbeds turn to coarse dirt / cracked stone (using existing GRASS_DEGRADES chain); groundwater level drops 2 per cycle
- **New WorldEffectType:** `RIVERBED_DRIES`
- **New ClimateEventType:** `DROUGHT` (probably already exists — extend it)
### 4.4 Sedimentation & Delta Formation
- **Trigger:** River-carrying region (high riverFlowIntensity) meets an ocean-adjacent region
- **Effect:** Each cycle, place sand or gravel blocks at the mouth of the river just below sea level; over dozens of cycles these accumulate into a visible delta fan
- **Elevation resample:** Queue `pendingElevationResample` after delta grows to trigger island-check logic
- **New WorldEffectType:** `SEDIMENT_DEPOSIT`
### 4.5 Sea Level Change
- **Mechanism:** New `seaLevel` config value (default 62) that can drift ±3 blocks over very long timeframes
- **Trigger:** Global sustained rainfall (average precipitation across all active regions > 65) raises sea level by 1 block every 500 cycles; sustained drought lowers it
- **Effect:** Use existing `applyTideBlocks` logic as the block-change driver, but shift the baseline permanently
- **Broadcast:** Server message when sea level changes by 1 block
### 4.6 Glaciers
- **Trigger:** Cold high-elevation region (temp < 0, elev > 90) with sustained blizzard history
- **Effect:** Glacier "toe" (lowest point of snowfield) pushes gravel/stone blocks one position downhill per 50 cycles; leaves a trail of polished andesite (glacier-polished rock) behind
- **New WorldEffectType:** `GLACIER_ADVANCE`, `GLACIER_POLISH`
### 4.7 pH Cascade
- **Trigger:** Acid rain event in region with river flowing to ocean
- **Effect:** Ocean pollution in downstream region spikes; if dripstone cave biome detected below region, neutralisation bonus applied (limestone buffer)
- **Salmon/fish spawning** in affected regions suppressed while pH is acidic (pollution > 50)
- **No new WorldEffectType** — uses existing pollution data
### 4.8 Waterfalls Carving Plunge Pools
- **Trigger:** River-carve region with large elevation drop to adjacent region (delta > 10)
- **Effect:** At the drop point, scan the base of the waterfall for a small depression; deepen it one block per cycle (remove stone/gravel at base, replace with water)
- **New WorldEffectType:** `PLUNGE_POOL_DEEPENS`
---
## Phase 5 — Ecology
*Biological systems — mobs responding to ecosystem state. Most are spawn suppression/boost mechanics using existing NeoForge spawn events.*
### 5.1 Migration Patterns
- **Mechanism:** Track `Map<RegionCoordinate, Integer> passiveMobPressure` — incremented each time a passive mob spawns in a region, decremented each time one is killed or despawns
- **Effect:** High-vegetation, low-pollution regions attract passive mobs (boost spawn rate 50%); barren/polluted regions suppress them (same logic as existing health-based suppression, but add directional bias toward healthy neighbours)
- **Visible result:** Herds naturally congregate in meadows and forests; wastelands are empty of animals
### 5.2 PredatorPrey Pressure
- **Mechanism:** If passive mob pressure in a region is high, hostile mob spawn rate is boosted (prey attracts predators); if passive mob pressure is very low, hostile mobs also suppress (no prey = predators leave)
- **Uses existing** FinalizeSpawnEvent hook — add predator-prey calculation alongside ecosystem health check
### 5.3 Local Extinction
- **Mechanism:** Track `Set<RegionCoordinate> extinctRegions`; a region enters this set if passive mob pressure = 0 for 100+ consecutive cycles
- **Effect:** No passive mob spawning allowed until succession reaches YOUNG_WOODLAND or higher
- **Recovery:** On reaching threshold, remove from set and broadcast "Wildlife returning to region (x,z)"
### 5.4 Pollinator Collapse
- **Trigger:** Pollution > 50 in a region that contains bee nests or has flowering vegetation
- **Effect:** Bee spawning suppressed; flower spread (VEGETATION_SPREADS placing flowers) disabled; crop growth rate feedback reduces fertility gain by 50%
- **Recovery:** Pollution drops below 20 → bee spawning resumes, flower spread re-enables
### 5.5 Rewilding
- **Trigger:** Player plants 10+ saplings in a BARREN or SPARSE_GRASS region within a single game day
- **Effect:** Bootstrap detects this via a new sapling-placement counter; if threshold met, grant a one-time succession boost (+2 stages over next 20 cycles) and rapid pollution decay
- **Broadcast:** "Rewilding effort detected in region (x,z) — ecosystem recovering!"
### 5.6 Peat Bogs
- **Trigger:** Wetland/swamp biome, waterlogged for 30+ consecutive cycles, low succession pressure
- **Effect:** Grass/dirt on waterlogged surface slowly converts to mud blocks; over time mud → packed mud → a distinct "peat" layer (use brown terracotta or existing mud); permanently lowers succession cap for the region (bogs stay boggy)
- **New WorldEffectType:** `PEAT_FORMS`
---
## Phase 6 — Seasons & Long Cycles
*Multi-cycle temporal patterns that give the world a rhythm players notice over days of play.*
### 6.1 Leaf Colour Change
- **Trigger:** Temperature crossing cold threshold (< 0.2) after a period of warmth; occurs once per seasonal cycle
- **Effect:** Oak and birch leaves on surface trees swap to orange/brown terracotta for 510 cycles, then leaves are removed (leaf fall); particles simulate leaf scatter
- **New WorldEffectType:** `LEAVES_CHANGE_COLOUR`, `LEAVES_FALL`
### 6.2 Soil Crust
- **Trigger:** Drought cycle + bare exposed dirt (no plant cover) for 15+ consecutive cycles
- **Effect:** Exposed dirt/coarse dirt → terracotta (baked clay); water no longer pools on it (hydration score penalised); crack visual (particles)
- **Breaking:** Heavy rain event removes terracotta → reverts to gravel first, then dirt over time
- **New WorldEffectType:** `SOIL_CRUSTS`
### 6.3 Permafrost Thaw
- **Trigger:** Cold region (temp < 0) that experiences sustained temperature rise (climate warming event or neighbouring pollution heat)
- **Effect:** Subsurface stone/gravel → mud; surface depression forms; groundwater spikes (permafrost melt adds water); region permanently shifts to higher succession cap
- **New WorldEffectType:** `PERMAFROST_THAWS`
---
## Phase 7 — Player Feedback Loops
*Systems that make player actions have visible, lasting consequences on the world.*
### 7.1 Deforestation Cascade
- **Mechanism:** When tree pressure drops below 5 for 10+ consecutive cycles (logging pressure), trigger cascade:
- Soil erosion rate ×2 (faster contamination rise)
- River silt (gravel replaces sand blocks in river channel)
- Flash flood risk doubles (lower threshold for trigger)
- Succession cap locked at SPARSE_GRASS until tree pressure recovers
- **Broadcast:** "Deforestation detected at (x,z) — soil erosion accelerating"
### 7.2 Crop Exhaustion
- **Trigger:** Farmland blocks detected in region (scan during water body scan cycle) with soil fertility < 20 AND contamination > 40
- **Effect:** Farmland → coarse dirt over time; crop growth suppressed; broadcast warning to nearby players
- **Recovery:** Player uses bone meal OR region is left fallow (no farming activity for 50 cycles) → fertility slowly recovers
### 7.3 Soundscape Degradation
- **Mechanism:** Existing `tryPlayRegionAmbience()` is already in place — extend it:
- MATURE_FOREST + low pollution → full bird/insect ambience (BIRD_AMBIENT or custom sound)
- GRASSLAND → lighter ambience
- BARREN + high pollution → dead silence (suppress all ambient sounds)
- Transition is gradual — blend based on health score
---
## Phase 8 — Underground Systems
*Block changes below the surface — rare but dramatic.*
### 8.1 Cave Flooding
- **Trigger:** Groundwater level > 80 AND a natural cave opening (air block) detected within 10 blocks below surface
- **Effect:** Place water source at the cave opening; water spreads naturally via Minecraft physics into the cave system
- **New WorldEffectType:** `CAVE_FLOODS`
### 8.2 Mineral Vein Shifting
- **Trigger:** Volcanic FERTILE phase
- **Effect:** Scan 520 blocks below surface for ore blocks adjacent to stone; convert surrounding stone → basalt/tuff (burying the ore); simultaneously, scan shallow depth (510 blocks) for stone with no adjacent ore and convert some to raw gold block or copper ore (new vein exposure)
- **New WorldEffectType:** `VEIN_SHIFTS`
### 8.3 Stalactite Growth
- **Trigger:** Wet cave biome (dripstone caves) in a high-groundwater region
- **Effect:** Scan cave ceilings (air block with solid block above); place one pointed dripstone block hanging downward per many cycles; extremely slow — one block per 100 cycles
- **New WorldEffectType:** `STALACTITE_GROWS`
---
## Phase 9 — Sound & Ambience Polish
*No block changes — purely audio-visual reinforcement of existing systems.*
### 9.1 Eruption Rumble
- Play `BLAZE_AMBIENT` or `BASALT_DELTAS_MOOD` looped for players within 3 regions of a BUILDING or ERUPTING volcano — already partially implemented; extend range and vary pitch by phase
### 9.2 Storm Pressure Drop
- When a ClimateEventType storm is incoming (12 cycles before trigger), play `WEATHER_RAIN` at low volume as a warning cue
### 9.3 Ocean Current Drift
- Items dropped in ocean source blocks get a velocity nudge matching the existing tidal current direction; uses the `ItemEntity` tick event
- Boats and floating players already affected by current system; extend to dropped item entities
### 9.4 Soundscape Biome Blending
- Extend soundscape degradation (7.3) to blend between multiple ambient tracks rather than binary on/off; use a weighted selection based on health + succession stage
---
## Phase 10 — Fantastical / Minecraft-Specific Extensions
*Not real-world but fits Minecraft's universe and makes each dimension feel alive.*
### 10.1 Nether Bleed
- **Trigger:** Region within 3 chunks of an active Nether portal for 50+ cycles
- **Effect:** Netherrack patches appear on surface; crimson fungi / warped fungi spread; soul sand appears in depressions; ambient fire sound increases
- **New WorldEffectType:** `NETHER_BLEED`
### 10.2 End Corruption
- **Trigger:** Region within 3 chunks of an End portal frame
- **Effect:** Chorus plant shoots appear on high ground; end stone patches replace stone on surface; obsidian pillars slowly grow (place one block per many cycles)
- **New WorldEffectType:** `END_CORRUPTION`
### 10.3 Ancient Site Exposure
- **Trigger:** Sustained hydraulic erosion (HYDRAULIC_EROSION WorldEffect, high intensity) OR earthquake (magnitude 4+) in a region with buried structure blocks detected below surface (scan for mossy cobblestone, chiselled stone bricks, deepslate tiles within 10 blocks of surface)
- **Effect:** Remove the surface blocks above the buried structure to expose it; place torch/lantern inside to signal the discovery; broadcast "Ancient ruins uncovered at (x,z)!"
- **New WorldEffectType:** `RUINS_EXPOSED`
---
## Implementation Notes
### Adding a new WorldEffectType
1. Add enum constant + Javadoc to `WorldEffectType.java`
2. Add switch case in `NeoForgeWorldEffectExecutor.consume()`
3. Write the private handler method
### Adding a new ClimateEventType
1. Add enum constant to `ClimateEventType.java`
2. Add trigger logic in `LivingWorldBootstrap` (usually in `runPostSimHooks()`)
3. Add to `getEventsStatusFor()` display if needed
### Adding a new per-region data field
- Store in `LivingWorldBootstrap` as `Map<RegionCoordinate, T>`
- Clear in `onServerStopping()`
- Expose via public getter if needed by `LivingWorldMod`
### Throttling rules
- Block-placing effects: always gate with `random.nextDouble() > threshold` to prevent performance spikes
- Scan loops: cap at `REGION_BLOCKS` samples, never unbounded
- Elevation resamples: use `pendingElevationResample` pattern — don't re-read heightmaps on every tick
### Performance budget
- Each new phase should add no more than ~0.1ms per tick to the simulation cycle
- Particle effects: max 5 per region per tick
- Block writes: max 10 per region per WorldEffectRequest execution
---
## Rough Priority / Effort Matrix
| Feature | Player Impact | Effort | Dependency |
|---|---|---|---|
| Coral reefs | High | Low | Phase 1 |
| Kelp forests | Medium | Low | Phase 1 |
| Algae blooms | High | Medium | Phase 1 |
| Acid rain | High | Low | Phase 2 |
| Blizzards | High | Medium | Phase 2 |
| Geysers | High | Medium | Phase 3 |
| Earthquakes | High | Medium | Phase 3 |
| Sinkholes | High | Low | Phase 3 |
| Landslides | Medium | Low | Phase 3 |
| Flash floods | High | Medium | Phase 4 |
| Drought cycles | High | Low | Phase 4 |
| Migration patterns | Very High | Medium | Phase 5 |
| Pollinator collapse | High | Low | Phase 5 |
| Deforestation cascade | Very High | Low | Phase 7 |
| Leaf colour change | High | Low | Phase 6 |
| Sea level change | Very High | High | Phase 4 |
| Delta formation | High | Medium | Phase 4 |
| Cave flooding | High | Low | Phase 8 |
| Nether bleed | Medium | Low | Phase 10 |
| Ancient site exposure | High | Medium | Phase 10 |
@@ -27,6 +27,9 @@ import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.BiomeTags;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.entity.animal.Animal;
import net.minecraft.world.entity.animal.AbstractFish;
import net.minecraft.world.entity.animal.Dolphin;
import net.minecraft.world.entity.animal.Squid;
import net.minecraft.world.entity.monster.Monster;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.biome.Biome;
@@ -209,6 +212,10 @@ public class LivingWorldMod {
bootstrap.initializeRegionBiomeValues(coord, temp, downfall);
bootstrap.setRegionBiomeCap(coord, deriveBiomeCap(level, coord));
biomeInitialized.add(coord);
// Mark ocean biome regions so the volcanic system can host submarine eruptions
if (biome.is(BiomeTags.IS_OCEAN) || biome.is(BiomeTags.IS_DEEP_OCEAN)) {
bootstrap.markOceanicRegion(coord);
}
}
});
NeoForge.EVENT_BUS.addListener(ServerStoppingEvent.class, event -> {
@@ -242,6 +249,12 @@ public class LivingWorldMod {
if (++riverCurrentTick % RIVER_CURRENT_INTERVAL == 0) {
applyRiverCurrents();
}
// Re-sample elevation for regions whose terrain has changed (e.g. island growth)
java.util.Set<com.livingworld.regions.RegionCoordinate> resampleSet =
bootstrap.pollPendingElevationResample();
if (!resampleSet.isEmpty()) {
elevationInitialized.removeAll(resampleSet);
}
// Broadcast any climate event messages queued by the bootstrap
java.util.List<String> eventMsgs = bootstrap.pollClimateEventMessages();
if (!eventMsgs.isEmpty()) {
@@ -263,6 +276,16 @@ public class LivingWorldMod {
bootstrap.getMetricsAt(dimId, event.getX(), event.getZ());
if (metricsOpt.isEmpty()) return;
double health = metricsOpt.get().getEcosystemHealth();
RegionCoordinate spawnCoord = RegionCoordinate.fromBlock(
dimId, (int) event.getX(), (int) event.getZ(),
LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
if (bootstrap.isDeadZone(spawnCoord)
&& (event.getEntity() instanceof AbstractFish
|| event.getEntity() instanceof Squid
|| event.getEntity() instanceof Dolphin)) {
event.setSpawnCancelled(true);
return;
}
if (event.getEntity() instanceof Animal) {
if (health < PASSIVE_SUPPRESS_HEALTH) {
double chance = (PASSIVE_SUPPRESS_HEALTH - health) / PASSIVE_SUPPRESS_HEALTH * 0.7;
@@ -456,6 +479,15 @@ public class LivingWorldMod {
regionSoundLastTick.put(coord, playerCheckTick);
}
}
if (bootstrap.isOceanicRegion(coord)
&& ambLevel.getDayTime() % 24000L > 12000L
&& metricsOpt.map(RegionMetrics::getPollutionScore).orElse(100.0) < 15.0
&& bootstrap.getRegionElevation(coord) != null
&& bootstrap.getRegionElevation(coord) < ambLevel.getSeaLevel() - 32) {
ambLevel.sendParticles(player, ParticleTypes.GLOW, false,
player.getX(), player.getY() + 1.0, player.getZ(),
4, 5.0, 2.0, 5.0, 0.01);
}
}
}
}
@@ -121,9 +121,17 @@ public final class LivingWorldBootstrap {
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 Map<RegionCoordinate, VolcanoPhase> volcanoPhase = new HashMap<>();
private final Map<RegionCoordinate, Integer> volcanoTicks = new HashMap<>();
private final Set<RegionCoordinate> volcanicRegions = new HashSet<>();
/** Ocean-floor regions; can develop submarine volcanoes regardless of surface elevation. */
private final Set<RegionCoordinate> oceanicRegions = new HashSet<>();
private final Map<RegionCoordinate, Float> regionTemperatures = new HashMap<>();
private final Map<RegionCoordinate, Integer> oceanPollutionCycles = new HashMap<>();
private final Set<RegionCoordinate> deadZoneRegions = new HashSet<>();
private final Set<RegionCoordinate> ventedRegions = new HashSet<>();
/** Regions whose terrain has changed significantly and need an elevation re-sample. */
private final Set<RegionCoordinate> pendingElevationResample = new HashSet<>();
private int volcanicActivityTick = 0;
private PlatformAdapter platformAdapter;
@@ -281,6 +289,12 @@ public final class LivingWorldBootstrap {
volcanoPhase.clear();
volcanoTicks.clear();
volcanicRegions.clear();
oceanicRegions.clear();
regionTemperatures.clear();
oceanPollutionCycles.clear();
deadZoneRegions.clear();
ventedRegions.clear();
pendingElevationResample.clear();
volcanicActivityTick = 0;
simSpeedMultiplier = 1;
serverReady = false;
@@ -434,6 +448,7 @@ public final class LivingWorldBootstrap {
applyClimateWarmingEffects();
applyWaterRunoff();
applyGroundwaterAndSprings();
applyOceanEcosystems();
applyDynamicCapUpdate();
applySeedDispersal();
recordHealthTrend();
@@ -669,9 +684,10 @@ public final class LivingWorldBootstrap {
Double elev = regionElevations.get(coord);
if (elev == null) continue;
// Register high-elevation regions as potentially volcanic on first encounter
// Register high-elevation or oceanic regions as potentially volcanic on first encounter
boolean isOceanic = oceanicRegions.contains(coord);
if (!volcanoPhase.containsKey(coord)) {
if (elev < VOLCANIC_ELEVATION) continue;
if (elev < VOLCANIC_ELEVATION && !isOceanic) continue;
volcanicRegions.add(coord);
volcanoPhase.put(coord, VolcanoPhase.DORMANT);
volcanoTicks.put(coord, 0);
@@ -687,8 +703,9 @@ public final class LivingWorldBootstrap {
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...");
String typeLabel = isOceanic ? "ocean region" : "mountain region";
pendingEventMessages.add("[LW] Seismic activity at " + typeLabel + " ("
+ coord.x() + "," + coord.z() + ") — a submarine volcano may be awakening...");
}
}
case BUILDING -> {
@@ -703,63 +720,88 @@ public final class LivingWorldBootstrap {
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!");
String eruptMsg = isOceanic
? "[LW] SUBMARINE ERUPTION at ocean region (" + coord.x() + "," + coord.z()
+ ")! Magma building from the seafloor!"
: "[LW] VOLCANIC ERUPTION at region (" + coord.x() + "," + coord.z()
+ ")! Lava and ash spreading now!";
pendingEventMessages.add(eruptMsg);
}
}
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) {
if (isOceanic) {
// Submarine eruption: build seafloor column upward through water
if (worldEffectsModule != null) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.ASH_DEPOSIT, coord, 0.7));
WorldEffectType.OCEAN_VOLCANIC_BUILDUP, coord,
Math.min(1.0, 0.5 + (ticks / 30.0) * 0.5)));
if (windRandom.nextDouble() < 0.30) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.COBBLESTONE_FORMS, coord, 0.5));
}
}
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()
// Hydrothermal venting: spike water pollution
PollutionRegionData poll = region.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);
if (poll != null) {
poll.addPollution(1.5, 0.5, 0.8);
region.getModuleData().put(PollutionModule.MODULE_ID, poll);
}
regionManager.markDirty(region);
} else {
// Surface eruption: lava flows and ash cloud
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 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 ("
pendingEventMessages.add("[LW] Eruption subsiding at ("
+ coord.x() + "," + coord.z() + ") — lava cooling and solidifying...");
}
}
@@ -769,11 +811,21 @@ public final class LivingWorldBootstrap {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.COBBLESTONE_FORMS, coord, 0.6));
}
// Queue elevation re-sample so the platform layer can detect island growth
pendingElevationResample.add(coord);
if (isOceanic && ventedRegions.add(coord) && worldEffectsModule != null) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.HYDROTHERMAL_VENT, coord, 1.0));
}
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!");
String coolingMsg = isOceanic
? "[LW] Submarine eruption cooling at (" + coord.x() + "," + coord.z()
+ ") — basalt seamount solidifying beneath the waves..."
: "[LW] Volcanic soil enriching region (" + coord.x() + "," + coord.z()
+ ") — fertile basalt plain emerging!";
pendingEventMessages.add(coolingMsg);
}
}
case FERTILE -> {
@@ -797,6 +849,12 @@ public final class LivingWorldBootstrap {
}
}
regionManager.markDirty(region);
// Check if an oceanic volcano has broken the surface and formed an island
if (isOceanic && elev != null && elev > 62.0) {
oceanicRegions.remove(coord);
pendingEventMessages.add("[LW] NEW ISLAND FORMED at (" + coord.x() + "," + coord.z()
+ ")! A volcanic seamount has risen above sea level!");
}
if (ticks >= 50) {
volcanoPhase.put(coord, VolcanoPhase.DORMANT);
volcanoTicks.put(coord, 0);
@@ -813,18 +871,135 @@ public final class LivingWorldBootstrap {
return phase != null ? phase.name() : null;
}
/** Marks a region as oceanic so it can host submarine volcanoes. */
public void markOceanicRegion(RegionCoordinate coord) {
if (coord != null) oceanicRegions.add(coord);
}
/** Returns whether aquatic spawning should be blocked by an active dead zone. */
public boolean isDeadZone(RegionCoordinate coord) {
return coord != null && deadZoneRegions.contains(coord);
}
/** Returns whether a region is known to be oceanic. */
public boolean isOceanicRegion(RegionCoordinate coord) {
return coord != null && oceanicRegions.contains(coord);
}
/**
* Advances reefs, kelp forests, blooms and dead zones. All physical work is queued
* through the bounded world-effect executor.
*/
private void applyOceanEcosystems() {
if (worldEffectsModule == null) return;
long simTick = simulationManager.getSimulationTickCounter();
for (Region region : regionManager.getActiveRegions()) {
RegionCoordinate coord = region.getCoordinate();
if (!oceanicRegions.contains(coord)) continue;
PollutionRegionData pollution = region.getModuleData()
.get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
WaterRegionData water = region.getModuleData()
.get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null);
if (pollution == null || water == null) continue;
double waterPollution = pollution.getWaterPollution();
double elevation = regionElevations.getOrDefault(coord, 63.0);
float temperature = regionTemperatures.getOrDefault(coord, 0.8f);
if (waterPollution > 50.0) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.CORAL_BLEACHING, coord, 1.0));
} else if (waterPollution >= 20.0 && elevation > 48.0) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.CORAL_BLEACHING, coord, waterPollution / 50.0));
}
if (waterPollution > 60.0) {
int pollutedCycles = oceanPollutionCycles.merge(coord, 1, Integer::sum);
if (pollutedCycles == 10) {
activeClimateEvents.add(new ClimateEvent(
ClimateEventType.ALGAE_BLOOM, coord, simTick,
Math.min(1.0, (waterPollution - 50.0) / 50.0)));
pendingEventMessages.add("[LW] Algae bloom forming in ocean region ("
+ coord.x() + "," + coord.z() + ").");
}
if (pollutedCycles >= 10 && pollutedCycles < 20) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.ALGAE_BLOOM, coord, Math.min(1.0, waterPollution / 100.0)));
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.KELP_GROWTH, coord, 0.9));
} else if (pollutedCycles >= 20) {
if (deadZoneRegions.add(coord)) {
pendingEventMessages.add("[LW] Aquatic dead zone established at ("
+ coord.x() + "," + coord.z() + ").");
}
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.DEAD_ZONE, coord, Math.min(1.0, waterPollution / 100.0)));
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.KELP_DIE, coord, 1.0));
pollution.addPollution(0, 0.15, 0.35);
}
} else {
oceanPollutionCycles.remove(coord);
if (waterPollution < 35.0) deadZoneRegions.remove(coord);
if (elevation > 48.0) {
if (waterPollution < 20.0) {
double health = region.getMetrics().getEcosystemHealth();
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.CORAL_GROWTH, coord,
Math.max(0.2, Math.min(1.0, health / 100.0))));
water.setWaterAvailability(Math.min(100, water.getWaterAvailability() + 0.03));
water.setPurificationCapacity(Math.min(100, water.getPurificationCapacity() + 0.05));
}
}
if (temperature < 0.5f && waterPollution < 30.0) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.KELP_GROWTH, coord, 0.55));
} else if (waterPollution >= 30.0) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.KELP_DIE, coord, waterPollution / 100.0));
}
}
if (ventedRegions.contains(coord)) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.HYDROTHERMAL_VENT, coord, 0.25));
pollution.addPollution(0, 0, 0.02);
}
region.getModuleData().put(PollutionModule.MODULE_ID, pollution);
region.getModuleData().put(WaterModule.MODULE_ID, water);
regionManager.markDirty(region);
}
}
/**
* Returns and clears the set of regions whose terrain has changed enough (e.g. volcanic
* island growth) to warrant an elevation re-sample by the platform layer.
*/
public Set<RegionCoordinate> pollPendingElevationResample() {
if (pendingElevationResample.isEmpty()) return Set.of();
Set<RegionCoordinate> copy = new HashSet<>(pendingElevationResample);
pendingElevationResample.clear();
return copy;
}
/**
* 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).";
if (volcanicRegions.isEmpty()) return "[LW] No volcanic regions detected yet.";
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);
boolean oceanic = oceanicRegions.contains(coord);
sb.append(" (").append(coord.x()).append(",").append(coord.z()).append(") ")
.append(oceanic ? "[OCEANIC] " : "[MOUNTAIN] ")
.append(phase.name())
.append(" — ticks: ").append(ticks);
if (elev != null) sb.append(", elev: ").append(String.format("%.0f", elev));
@@ -1332,6 +1507,16 @@ public final class LivingWorldBootstrap {
shouldResolve = true;
}
}
case ALGAE_BLOOM -> {
PollutionRegionData pollution = region.getModuleData()
.get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
if (pollution == null || pollution.getWaterPollution() < 35.0) {
shouldResolve = true;
} else if (worldEffectsModule != null && ev.getTicksActive() < 20) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.ALGAE_BLOOM, coord, ev.getSeverity()));
}
}
}
}
@@ -1382,6 +1567,7 @@ public final class LivingWorldBootstrap {
*/
public void initializeRegionBiomeValues(RegionCoordinate coord, float temp, float downfall) {
if (!serverReady || coord == null) return;
regionTemperatures.put(coord, temp);
regionManager.resolve(coord).ifPresent(region -> {
SoilRegionData soil = region.getModuleData()
.get(SoilModule.MODULE_ID, SoilRegionData.class).orElse(null);
@@ -5,7 +5,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"),
VOLCANIC_ERUPTION("Volcanic Eruption", "Lava flows and ash clouds from an active volcano");
VOLCANIC_ERUPTION("Volcanic Eruption", "Lava flows and ash clouds from an active volcano"),
ALGAE_BLOOM("Algae Bloom", "Nutrient pollution causing a bloom and aquatic dead zone");
private final String displayName;
private final String description;
@@ -102,4 +102,32 @@ public enum WorldEffectType {
* terrain that was not there before the eruption.
*/
COBBLESTONE_FORMS,
/**
* Submarine volcanic eruption builds new land upward from the seafloor: basalt and
* cobblestone are stacked on the ocean floor, gradually rising through the water column.
* Over many eruption cycles the seamount breaks the surface and forms a new island.
*/
OCEAN_VOLCANIC_BUILDUP,
/** Healthy shallow ocean terrain develops living coral blocks and fans. */
CORAL_GROWTH,
/** Polluted reefs bleach into dead coral or disappear at extreme pollution. */
CORAL_BLEACHING,
/** Cold, clean ocean terrain grows kelp columns from the seafloor. */
KELP_GROWTH,
/** Polluted ocean terrain loses kelp and other fragile aquatic plants. */
KELP_DIE,
/** Nutrient pollution creates a visible algae bloom near the ocean surface. */
ALGAE_BLOOM,
/** A collapsed bloom leaves a sedimented, oxygen-poor dead zone. */
DEAD_ZONE,
/** Volcanic seafloor activity creates a permanent bubbling vent field. */
HYDROTHERMAL_VENT,
}
@@ -87,6 +87,22 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
ashDeposit(level, baseX, baseZ, request.intensity());
case COBBLESTONE_FORMS ->
cobblestoneFromLava(level, baseX, baseZ, request.intensity());
case OCEAN_VOLCANIC_BUILDUP ->
oceanVolcanicBuildup(level, baseX, baseZ, request.intensity());
case CORAL_GROWTH ->
growCoral(level, baseX, baseZ, request.intensity());
case CORAL_BLEACHING ->
bleachCoral(level, baseX, baseZ, request.intensity());
case KELP_GROWTH ->
growKelp(level, baseX, baseZ, request.intensity());
case KELP_DIE ->
killKelp(level, baseX, baseZ, request.intensity());
case ALGAE_BLOOM ->
algaeBloom(level, baseX, baseZ, request.intensity());
case DEAD_ZONE ->
deadZone(level, baseX, baseZ, request.intensity());
case HYDROTHERMAL_VENT ->
hydrothermalVent(level, baseX, baseZ, request.intensity());
}
}
@@ -517,6 +533,184 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
}
}
/**
* Builds a submarine volcano upward from the ocean floor. Each call attempts to stack
* one layer of basalt or cobblestone on top of the current seafloor column. Fires ~40%
* of the time so the island grows gradually over dozens of eruption cycles.
*
* <p>Uses OCEAN_FLOOR heightmap to find the top solid non-water block. Places one block
* directly above it. Once the column reaches sea level the water above drains naturally
* and lava starts behaving as a surface eruption.</p>
*/
private void oceanVolcanicBuildup(ServerLevel level, int baseX, int baseZ, double intensity) {
if (random.nextDouble() > 0.40) return;
int attempts = Math.max(2, (int) (intensity * 6));
for (int i = 0; i < attempts; i++) {
int x = baseX + random.nextInt(REGION_BLOCKS);
int z = baseZ + random.nextInt(REGION_BLOCKS);
// OCEAN_FLOOR gives the top solid non-water block
int floorY = level.getHeight(Heightmap.Types.OCEAN_FLOOR, x, z) - 1;
if (floorY < level.getMinBuildHeight()) continue;
BlockPos floorPos = new BlockPos(x, floorY, z);
if (!level.isLoaded(floorPos)) continue;
var floorState = level.getBlockState(floorPos);
// Only build on natural submarine terrain
boolean naturalSeafloor = floorState.is(Blocks.SAND) || floorState.is(Blocks.GRAVEL)
|| floorState.is(Blocks.STONE) || floorState.is(Blocks.DEEPSLATE)
|| floorState.is(Blocks.COBBLESTONE) || floorState.is(Blocks.BASALT)
|| floorState.is(Blocks.TUFF) || floorState.is(Blocks.ANDESITE)
|| floorState.is(Blocks.DIORITE) || floorState.is(Blocks.GRANITE);
if (!naturalSeafloor) continue;
BlockPos buildPos = floorPos.above();
if (!level.isLoaded(buildPos)) continue;
var buildState = level.getBlockState(buildPos);
// Only build into water or air — never overwrite solid blocks
boolean buildable = buildState.is(Blocks.WATER) || buildState.isAir();
if (!buildable) continue;
// Alternate between basalt (main volcanic rock) and cobblestone (lava+water contact)
Block material = random.nextDouble() < 0.65 ? Blocks.BASALT : Blocks.COBBLESTONE;
level.setBlock(buildPos, material.defaultBlockState(), Block.UPDATE_ALL);
LivingWorldLogger.info(DiagnosticCategory.SIMULATION,
"WorldEffect OCEAN_VOLCANIC_BUILDUP placed " + material + " at " + buildPos
+ " (floorY=" + floorY + ")");
}
}
private void growCoral(ServerLevel level, int baseX, int baseZ, double intensity) {
if (random.nextDouble() > 0.18) return;
Block[] corals = {Blocks.TUBE_CORAL_BLOCK, Blocks.BRAIN_CORAL_BLOCK,
Blocks.BUBBLE_CORAL_BLOCK, Blocks.FIRE_CORAL_BLOCK, Blocks.HORN_CORAL_BLOCK};
Block[] fans = {Blocks.TUBE_CORAL_FAN, Blocks.BRAIN_CORAL_FAN,
Blocks.BUBBLE_CORAL_FAN, Blocks.FIRE_CORAL_FAN, Blocks.HORN_CORAL_FAN};
int attempts = Math.max(1, (int) (intensity * 5));
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.OCEAN_FLOOR, x, z) - 1;
BlockPos floor = new BlockPos(x, y, z);
BlockPos above = floor.above();
if (!level.isLoaded(above) || !level.getFluidState(above).is(FluidTags.WATER)) continue;
var floorState = level.getBlockState(floor);
if (!floorState.is(Blocks.SAND) && !floorState.is(Blocks.GRAVEL)
&& !floorState.is(Blocks.STONE) && !floorState.is(BlockTags.CORAL_BLOCKS)) continue;
int species = random.nextInt(corals.length);
level.setBlock(floor, corals[species].defaultBlockState(), Block.UPDATE_ALL);
if (random.nextDouble() < intensity * 0.5) {
level.setBlock(above, fans[species].defaultBlockState(), Block.UPDATE_ALL);
}
}
}
private void bleachCoral(ServerLevel level, int baseX, int baseZ, double intensity) {
if (random.nextDouble() > 0.25) 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 y = level.getHeight(Heightmap.Types.OCEAN_FLOOR, x, z) - 1;
BlockPos pos = new BlockPos(x, y, z);
if (!level.isLoaded(pos)) continue;
var state = level.getBlockState(pos);
Block replacement = state.is(Blocks.TUBE_CORAL_BLOCK) ? Blocks.DEAD_TUBE_CORAL_BLOCK
: state.is(Blocks.BRAIN_CORAL_BLOCK) ? Blocks.DEAD_BRAIN_CORAL_BLOCK
: state.is(Blocks.BUBBLE_CORAL_BLOCK) ? Blocks.DEAD_BUBBLE_CORAL_BLOCK
: state.is(Blocks.FIRE_CORAL_BLOCK) ? Blocks.DEAD_FIRE_CORAL_BLOCK
: state.is(Blocks.HORN_CORAL_BLOCK) ? Blocks.DEAD_HORN_CORAL_BLOCK : null;
if (replacement != null) {
level.setBlock(pos, intensity >= 0.95 ? Blocks.GRAVEL.defaultBlockState()
: replacement.defaultBlockState(), Block.UPDATE_ALL);
}
BlockPos above = pos.above();
if (intensity >= 0.95 && level.isLoaded(above)
&& level.getBlockState(above).is(BlockTags.CORALS)) {
level.setBlock(above, Blocks.WATER.defaultBlockState(), Block.UPDATE_ALL);
}
}
}
private void growKelp(ServerLevel level, int baseX, int baseZ, double intensity) {
if (random.nextDouble() > 0.20) return;
int attempts = Math.max(1, (int) (intensity * 5));
for (int i = 0; i < attempts; i++) {
int x = baseX + random.nextInt(REGION_BLOCKS);
int z = baseZ + random.nextInt(REGION_BLOCKS);
int floorY = level.getHeight(Heightmap.Types.OCEAN_FLOOR, x, z) - 1;
BlockPos cursor = new BlockPos(x, floorY + 1, z);
for (int h = 0; h < 8 && level.isLoaded(cursor); h++, cursor = cursor.above()) {
var state = level.getBlockState(cursor);
if (state.is(Blocks.KELP_PLANT)) continue;
if (state.is(Blocks.KELP)) {
BlockPos above = cursor.above();
if (level.isLoaded(above) && level.getFluidState(above).is(FluidTags.WATER)) {
level.setBlock(cursor, Blocks.KELP_PLANT.defaultBlockState(), Block.UPDATE_ALL);
level.setBlock(above, Blocks.KELP.defaultBlockState(), Block.UPDATE_ALL);
}
} else if (level.getFluidState(cursor).is(FluidTags.WATER)) {
level.setBlock(cursor, Blocks.KELP.defaultBlockState(), Block.UPDATE_ALL);
}
break;
}
}
}
private void killKelp(ServerLevel level, int baseX, int baseZ, double intensity) {
if (random.nextDouble() > 0.30) return;
for (int i = 0; i < Math.max(2, (int) (intensity * 8)); i++) {
int x = baseX + random.nextInt(REGION_BLOCKS);
int z = baseZ + random.nextInt(REGION_BLOCKS);
int floorY = level.getHeight(Heightmap.Types.OCEAN_FLOOR, x, z);
for (int h = 0; h < 8; h++) {
BlockPos pos = new BlockPos(x, floorY + h, z);
if (!level.isLoaded(pos)) break;
if (level.getBlockState(pos).is(Blocks.KELP)
|| level.getBlockState(pos).is(Blocks.KELP_PLANT)) {
level.setBlock(pos, Blocks.WATER.defaultBlockState(), Block.UPDATE_ALL);
}
}
}
}
private void algaeBloom(ServerLevel level, int baseX, int baseZ, double intensity) {
int count = Math.max(1, Math.min(5, (int) Math.ceil(intensity * 5)));
for (int i = 0; i < count; i++) {
double x = baseX + random.nextDouble() * REGION_BLOCKS;
double z = baseZ + random.nextDouble() * REGION_BLOCKS;
int y = level.getSeaLevel() - random.nextInt(3);
level.sendParticles(ParticleTypes.HAPPY_VILLAGER, x, y, z, 2, 1.5, 0.4, 1.5, 0.01);
}
}
private void deadZone(ServerLevel level, int baseX, int baseZ, double intensity) {
if (random.nextDouble() > 0.15) return;
for (int i = 0; i < Math.max(1, (int) (intensity * 5)); i++) {
int x = baseX + random.nextInt(REGION_BLOCKS);
int z = baseZ + random.nextInt(REGION_BLOCKS);
int y = level.getHeight(Heightmap.Types.OCEAN_FLOOR, x, z) - 1;
BlockPos pos = new BlockPos(x, y, z);
if (!level.isLoaded(pos)) continue;
var state = level.getBlockState(pos);
if (state.is(Blocks.SAND) || state.is(Blocks.GRAVEL)) {
level.setBlock(pos, random.nextBoolean() ? Blocks.DIRT.defaultBlockState()
: Blocks.GRAVEL.defaultBlockState(), Block.UPDATE_ALL);
}
}
}
private void hydrothermalVent(ServerLevel level, int baseX, int baseZ, double intensity) {
if (random.nextDouble() > 0.35) return;
for (int i = 0; i < 4; i++) {
int x = baseX + random.nextInt(REGION_BLOCKS);
int z = baseZ + random.nextInt(REGION_BLOCKS);
int y = level.getHeight(Heightmap.Types.OCEAN_FLOOR, x, z) - 1;
BlockPos floor = new BlockPos(x, y, z);
if (!level.isLoaded(floor.above()) || !level.getFluidState(floor.above()).is(FluidTags.WATER)) continue;
level.setBlock(floor, random.nextBoolean() ? Blocks.MAGMA_BLOCK.defaultBlockState()
: Blocks.SOUL_SAND.defaultBlockState(), Block.UPDATE_ALL);
level.sendParticles(ParticleTypes.BUBBLE_COLUMN_UP, x + 0.5, y + 1.5, z + 0.5,
5, 0.2, 1.0, 0.2, 0.03);
}
}
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()) {