Compare commits
26 Commits
67a1e07b82
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0799d4d4c9 | |||
| d7f8e3ea36 | |||
| d9158bc8e9 | |||
| 48643b6ef6 | |||
| eaf5db723b | |||
| 98f97bfbc4 | |||
| 2304266544 | |||
| 38ee028651 | |||
| 5331d5b207 | |||
| f8995392eb | |||
| 7802e4b603 | |||
| 54a6be91de | |||
| fde7264815 | |||
| 113741abd6 | |||
| b68390983c | |||
| 72d94b9f28 | |||
| 30e680b650 | |||
| 725ac6b33f | |||
| 1727c3a406 | |||
| 66dc2f336a | |||
| b17a8990b0 | |||
| 0f2a265c61 | |||
| 32e413cc9f | |||
| 178d50883e | |||
| c82a2afc4f | |||
| 61abff52dc |
@@ -0,0 +1,372 @@
|
||||
# Living World
|
||||
|
||||
A NeoForge 1.21.1 ecosystem simulation mod that turns every region of your world into a living, breathing environment. Pollution, soil quality, water cycles, vegetation succession, climate events, tidal systems, and river erosion interact in a fully simulated pipeline — all visible as real block changes and player effects in-game.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The world is divided into **regions** (configurable chunk-grid cells, default 16×16 chunks). Each region is independently simulated through a 9-stage pipeline every tick cycle. Everything from a single furnace firing to a multi-region wildfire can alter the ecological trajectory of the land you stand on.
|
||||
|
||||
---
|
||||
|
||||
## Simulation Pipeline
|
||||
|
||||
Modules run in this fixed order each cycle so each stage reads fully updated data from the stage before it:
|
||||
|
||||
| # | Module | Role |
|
||||
|---|--------|------|
|
||||
| 1 | **Pollution** | Decays air/ground/water pollution; leaches ground → water |
|
||||
| 2 | **Soil** | Updates fertility, moisture, contamination, compaction, erosion |
|
||||
| 3 | **Water** | Tracks availability, purification, drought/flood risk |
|
||||
| 4 | **Vegetation** | Succession: grass → flowers → shrubs → trees; die-off under stress |
|
||||
| 5 | **Resource Depletion** | Tracks mining, logging, farming pressure |
|
||||
| 6 | **Recovery** | Manages ecological succession stage (BARREN → MATURE_FOREST) |
|
||||
| 7 | **Ecosystem** | Synthesises overall health, stress, resilience |
|
||||
| 8 | **World Effects** | Translates simulation state into visible block changes |
|
||||
| 9 | **Atmosphere** | Per-region weather (rain/thunder/acid rain) driven by health |
|
||||
|
||||
---
|
||||
|
||||
## Feature Status
|
||||
|
||||
### Core Ecosystem — **Complete**
|
||||
|
||||
- **Pollution system** — air, ground, and water pollution tracked independently; natural decay rates; tree canopy scrubs air pollution; ground leaches into water
|
||||
- **Soil dynamics** — fertility, moisture, contamination, compaction, erosion; spring snowmelt and autumn leaf decomposition cycle
|
||||
- **Water simulation** — availability, drought risk, flood risk, purification capacity; runoff flows downhill between regions carrying pollution downstream
|
||||
- **Vegetation succession** — grass → flowers → shrubs → trees; each tier requires the tier below; severe stress kills trees before shrubs before grass
|
||||
- **Recovery & succession** — six ecological stages (BARREN → SPARSE_GRASS → GRASSLAND → SCRUBLAND → YOUNG_WOODLAND → MATURE_FOREST); progress/damage system; biome-derived succession ceiling
|
||||
- **Resource depletion** — logging, mining, farming each drain their respective region metrics
|
||||
- **Ecosystem health** — synthesised score (0–100) based on all module outputs; feeds spawn suppression, player effects, ambient sounds
|
||||
|
||||
### Visible World Effects — **Complete**
|
||||
|
||||
All effects are real block changes players can see:
|
||||
|
||||
| Effect | Trigger | What You See |
|
||||
|--------|---------|--------------|
|
||||
| Grass degrades to dirt | Pollution > 15, soil < 75 | Grass → dirt → coarse dirt |
|
||||
| Vegetation spreads | Veg pressure > 60, soil > 50 | Dirt → grass, flowers, short grass |
|
||||
| Vegetation dies | Soil < 20 OR pollution > 30 | Leaves and upper trunks removed; stumps left |
|
||||
| Saplings boosted | Stage ≥ YOUNG_WOODLAND | Oak/birch/spruce/dark-oak/cherry saplings placed |
|
||||
| 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**
|
||||
|
||||
- **Compass HUD** — hold a compass (or `/lw hud`) to see the action-bar HUD:
|
||||
`[LW] (0,0)↑ Eco:72 Poll:3.2 Soil:61 Wat:58 Rain:42% Seeds:1.8`
|
||||
- Trend arrow (↑↓→) shows whether the region is recovering or declining
|
||||
- Seed rain displayed when active
|
||||
- Colour-coded (green/yellow/red) for each metric
|
||||
- **Pollution effects** — nausea > 40, slowness > 60, weakness > 80 pollution; smoke fog particles
|
||||
- **Mob spawn feedback** — passive mobs suppressed in regions below 30 health; hostile mobs suppressed above 60 health
|
||||
- **Regional weather** — per-player rain/thunder packets matching region atmosphere; storm sends lightning visuals
|
||||
- **Ambient sounds** — forest: rustling azalea leaves; barren: shifting sand; heavy pollution: basalt deltas mood
|
||||
- **Climate event broadcasts** — drought, wildfire, flood events announced to all players
|
||||
|
||||
### Seasons — **Complete**
|
||||
|
||||
32-day year (8 days per season). Seasons affect all simulation layers:
|
||||
|
||||
| Season | Soil | Water | Vegetation | Drought |
|
||||
|--------|------|-------|------------|---------|
|
||||
| **Spring** | Fertility +, moisture ++ | Availability ++ | Grass/flower surge | Eases |
|
||||
| **Summer** | Moisture — | Availability — (evaporation) | Mild growth (if moist) | Worsens |
|
||||
| **Autumn** | Fertility + (decomposition) | — | Senescence, dead litter | Neutral |
|
||||
| **Winter** | Moisture — (frost) | Availability — (freeze) | Die-back, shrubs retreat | Creeps up |
|
||||
|
||||
### Ecological Corridors & Seed Dispersal — **Complete**
|
||||
|
||||
- Healthy regions (YOUNG_WOODLAND+) emit seed rain each cycle to adjacent regions
|
||||
- **Corridor boost**: ≥2 healthy neighbours → 3.5× seed effectiveness
|
||||
- Pollution in target region blocks germination (proportional)
|
||||
- Sufficient accumulated seed rain pioneers the succession cap one stage ahead of physical conditions
|
||||
- Seed rain accumulation visible in HUD and `/lw atmosphere`
|
||||
|
||||
### Pollution Spreading — **Complete**
|
||||
|
||||
- Air pollution spreads to adjacent regions each cycle based on wind direction
|
||||
- Wind drifts realistically (±0.05 rad per cycle); `/lw wind` shows direction and spread rate
|
||||
- Downwind spread is boosted; upwind transfer is reduced
|
||||
- Water runoff also carries ground pollution downstream (15% of runoff × ground pollution)
|
||||
|
||||
### Climate Events — **Complete** (multi-region)
|
||||
|
||||
Automatically triggered when regional conditions reach critical thresholds. All have visible in-world effects.
|
||||
|
||||
#### Drought
|
||||
- **Trigger**: water availability < 20, drought risk > 65, with 2+ dry neighbours
|
||||
- **Effect**: further drains water availability and raises drought risk across affected regions; spreads to adjacent dry regions every 5 cycles
|
||||
- **Resolution**: epicentre water availability recovers above 45
|
||||
|
||||
#### Wildfire
|
||||
- **Trigger**: YOUNG_WOODLAND+ region, drought risk > 65, no rain, air pollution > 25 (stochastic)
|
||||
- **Effect**: `VEGETATION_DIES` and `WILDFIRE` block effects (actual fire!) on affected regions; air pollution spike; spreads to adjacent drought-stressed forest
|
||||
- **Resolution**: rain level > 0.4 in epicentre
|
||||
|
||||
#### Flood
|
||||
- **Trigger**: water availability > 92, rain > 0.72, with a lower-elevation downstream neighbour
|
||||
- **Effect**: `WATER_POOL_FORMS` in downstream regions; soil contamination from silt
|
||||
- **Resolution**: water availability drops below 70 or rain eases
|
||||
|
||||
### Desertification Permanence — **Complete**
|
||||
|
||||
- Regions with sustained heavy damage (damage accumulation > 65, health < 20) have their succession cap **lowered** by one stage
|
||||
- This means a desertified region can no longer recover to forest on its own — player intervention (removing pollution, adding water, bone meal, replanting) is required
|
||||
- Players are notified via chat when desertification lowers a cap
|
||||
|
||||
### Region Trend Tracking — **Complete**
|
||||
|
||||
- Last 5 health samples per region stored each post-sim cycle
|
||||
- Trend arrow (↑↓→) computed from recent vs older average
|
||||
- Visible in the compass HUD next to the region coordinates
|
||||
- Full trend detail in `/lw atmosphere`
|
||||
|
||||
### 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; 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
|
||||
- Every 1 200 ticks (~1 real minute):
|
||||
- **Coastal regions** (elevation ≤ sea level + 6) receive water-availability adjustments
|
||||
- **Physical block changes**: on rising tide, water sources are placed on natural shoreline blocks (sand/gravel/stone) at sea level; on falling tide, those sources are removed
|
||||
- Result: visible waterline that rises and falls on beaches and ocean shores
|
||||
|
||||
### Biome-Aware Initialisation — **Complete**
|
||||
|
||||
- When a player first enters a region, the biome temperature and downfall (wetness) are read
|
||||
- Soil fertility, soil moisture, water availability, and drought risk are initialised to biome-appropriate starting values
|
||||
- Deserts start arid and nutrient-poor; jungles/swamps start moist and fertile; mountains start cool and dry
|
||||
- Biome-derived succession ceiling set simultaneously (desert = SPARSE_GRASS cap; forest = MATURE_FOREST cap)
|
||||
|
||||
### Config File — **Complete**
|
||||
|
||||
Server-side TOML: `world/serverconfig/livingworld-server.toml`
|
||||
|
||||
Tunable at runtime without recompiling:
|
||||
|
||||
```toml
|
||||
[pollution]
|
||||
decay_rate = 0.002
|
||||
ground_to_water_leach = 0.0005
|
||||
spread_rate = 0.02
|
||||
wind_boost = 0.5
|
||||
|
||||
[vegetation]
|
||||
grass_growth_rate = 0.06
|
||||
flower_growth_rate = 0.04
|
||||
shrub_growth_rate = 0.03
|
||||
tree_growth_rate = 0.02
|
||||
# ... dieoff rates
|
||||
|
||||
[recovery]
|
||||
base_progress_per_tick = 2.0
|
||||
damage_per_bad_tick = 1.5
|
||||
damage_decay_rate = 0.05
|
||||
|
||||
[seed_dispersal]
|
||||
seed_emission_rate = 0.01
|
||||
corridor_boost_multiplier = 3.5
|
||||
seed_pollution_block = 0.015
|
||||
```
|
||||
|
||||
### Persistence — **Complete**
|
||||
|
||||
All 8 data-bearing modules use a Properties-based codec system. Region data survives server restart correctly. Global climate tracker (carbon ppm, warming level, biodiversity index) saved separately to `living_world/global_climate.dat`.
|
||||
|
||||
### Region Border Visualiser — **Complete**
|
||||
|
||||
`/lw region borders` — sends cyan dust particles along all 4 edges of the region at surface height. Optional `[regionX regionZ]` args to visualise a specific region.
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
All commands require permission level 2.
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/lw status` | Show active regions, enabled modules, simulation tick |
|
||||
| `/lw region info [x z]` | Full diagnostics for current or specified region |
|
||||
| `/lw region borders [rx rz]` | Visualise region boundary with particles |
|
||||
| `/lw region force-update` | Force an immediate simulation cycle on current region |
|
||||
| `/lw region set <stat> <value>` | Manually override a region metric (debug) |
|
||||
| `/lw map [radius]` | ASCII coloured region map (1–7 radius, default 3) |
|
||||
| `/lw atmosphere` | Region atmosphere: season, rain, storm, succession, trend, seeds |
|
||||
| `/lw climate` | Global climate: CO₂ ppm, warming °C, biodiversity, rain penalty |
|
||||
| `/lw wind` | Wind direction, angle, spread rate, speed multiplier |
|
||||
| `/lw hud` | Toggle persistent HUD without holding compass |
|
||||
| `/lw speed <1–100>` | Accelerate simulation (100x = very fast testing) |
|
||||
| `/lw speed reset` | Return to real-time speed (1x) |
|
||||
| `/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 |
|
||||
|
||||
### /lw map Output Example
|
||||
|
||||
```
|
||||
[LW] Map 7×7 around (0,0) | [X]=you, ↑N ↓S ←W →E
|
||||
F F W W s · ·
|
||||
F F W s G G ·
|
||||
F W [X] G G g ·
|
||||
W G G G g B ·
|
||||
s G g B B · ·
|
||||
· · · · · · ·
|
||||
· · · · · · ·
|
||||
F=Forest W=Woodland s=Scrub G=Grass g=Sparse B=Barren ·=Unknown
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Compass HUD Reference
|
||||
|
||||
Hold a compass (or `/lw hud`) to see the action-bar HUD:
|
||||
|
||||
```
|
||||
[LW] (0,0)↑ Eco:72 Poll:3.2 Soil:61 Wat:58 Rain:42% Storm:8% Seeds:1.8
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
|-------|---------|
|
||||
| `(0,0)` | Region coordinates |
|
||||
| `↑↓→` | Ecosystem health trend (recovering / declining / stable) |
|
||||
| `Eco` | Ecosystem health 0–100 (green >60, yellow >30, red ≤30) |
|
||||
| `Poll` | Pollution score 0–100 (green <15, yellow <40, red ≥40) |
|
||||
| `Soil` | Soil quality 0–100 |
|
||||
| `Wat` | Water quality 0–100 |
|
||||
| `Rain` | Regional rain level % |
|
||||
| `Storm` | Thunder/storm level % (shown only when >15%) |
|
||||
| `Seeds` | Accumulated seed rain (shown only when active) |
|
||||
|
||||
---
|
||||
|
||||
## Succession Stages
|
||||
|
||||
| Stage | Symbol | Conditions Required |
|
||||
|-------|--------|---------------------|
|
||||
| BARREN | `B` | — |
|
||||
| SPARSE_GRASS | `g` | Soil ≥ 10, pollution ≤ 80, veg ≥ 10 |
|
||||
| GRASSLAND | `G` | Soil ≥ 25, pollution ≤ 70, veg ≥ 20 |
|
||||
| SCRUBLAND | `s` | Soil ≥ 40, pollution ≤ 60, veg ≥ 35 |
|
||||
| YOUNG_WOODLAND | `W` | Soil ≥ 50, pollution ≤ 50, veg ≥ 45 |
|
||||
| MATURE_FOREST | `F` | Soil ≥ 60, pollution ≤ 40, veg ≥ 55 |
|
||||
|
||||
Biome caps prevent a desert from reaching MATURE_FOREST naturally. Sustained damage (damage accumulation > 65, health < 20) can lower the cap permanently — **desertification**. Player intervention is required to restore the potential.
|
||||
|
||||
---
|
||||
|
||||
## Player Effects from Pollution
|
||||
|
||||
| Pollution Level | Effect |
|
||||
|----------------|--------|
|
||||
| > 40 | Nausea (Confusion) + smoke fog particles |
|
||||
| > 60 | Slowness |
|
||||
| > 80 | Weakness |
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
LivingWorldMod (NeoForge event wiring)
|
||||
└─ LivingWorldBootstrap (lifecycle, post-sim hooks)
|
||||
├─ SimulationManager → SimulationScheduler → RegionUpdateJob
|
||||
├─ RegionManager → RegionStorage → FileRegionPersistenceService
|
||||
├─ ModuleRegistry (9 modules, pipeline order)
|
||||
├─ GlobalClimateTracker (CO₂, warming, biodiversity)
|
||||
├─ EcosystemTuning (config-loaded tuning constants)
|
||||
└─ Post-sim hooks:
|
||||
spreadPollutionAcrossRegions() [wind-driven]
|
||||
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]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
./gradlew build
|
||||
```
|
||||
|
||||
Output: `build/libs/living_world-*.jar`
|
||||
|
||||
Requires NeoForge 21.1.172 / Minecraft 1.21.1.
|
||||
|
||||
---
|
||||
|
||||
## Planned / In Progress
|
||||
|
||||
- Full worldgen integration (custom biome distribution, terrain shaping beyond post-load modification)
|
||||
- Persistence for river flow intensity, groundwater level, and volcanic state across restarts
|
||||
- Multiplayer region ownership / notification system
|
||||
- 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)
|
||||
+391
@@ -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 20–50 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; 1–3 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 3–5 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 (1–3) = surface cracks only; High (4–5) = fissures + landslide trigger
|
||||
- **Effect:** Low magnitude — remove 3–5 random surface blocks in a line (creates crack pattern); replace with air + expose layer beneath. High magnitude — carve a fissure 2 blocks wide, 8–15 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 5–15 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 Predator–Prey 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 5–10 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 5–20 blocks below surface for ore blocks adjacent to stone; convert surrounding stone → basalt/tuff (burying the ore); simultaneously, scan shallow depth (5–10 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 (1–2 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 |
|
||||
@@ -45,4 +45,7 @@ dependencies {
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
scanForTestClasses = false
|
||||
include '**/*Test.class'
|
||||
include '**/*Test$*.class'
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
package com.livingworld.climate;
|
||||
|
||||
import com.livingworld.regions.RegionCoordinate;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* An active multi-region climate event (drought, wildfire, or flood).
|
||||
* Events spread to adjacent regions over time and resolve when conditions improve.
|
||||
*/
|
||||
public final class ClimateEvent {
|
||||
|
||||
private final ClimateEventType type;
|
||||
private final RegionCoordinate epicenter;
|
||||
private final long startTick;
|
||||
private double severity;
|
||||
private final Set<RegionCoordinate> affectedRegions = new HashSet<>();
|
||||
private boolean resolved;
|
||||
private int ticksActive;
|
||||
|
||||
public ClimateEvent(ClimateEventType type, RegionCoordinate epicenter, long startTick, double severity) {
|
||||
this.type = type;
|
||||
this.epicenter = epicenter;
|
||||
this.startTick = startTick;
|
||||
this.severity = severity;
|
||||
this.resolved = false;
|
||||
this.ticksActive = 0;
|
||||
this.affectedRegions.add(epicenter);
|
||||
}
|
||||
|
||||
public ClimateEventType getType() { return type; }
|
||||
public RegionCoordinate getEpicenter() { return epicenter; }
|
||||
public long getStartTick() { return startTick; }
|
||||
public double getSeverity() { return severity; }
|
||||
public Set<RegionCoordinate> getAffectedRegions() { return affectedRegions; }
|
||||
public boolean isResolved() { return resolved; }
|
||||
public int getTicksActive() { return ticksActive; }
|
||||
|
||||
public void setSeverity(double severity) { this.severity = Math.max(0, Math.min(1, severity)); }
|
||||
public void resolve() { this.resolved = true; }
|
||||
public void addAffectedRegion(RegionCoordinate coord) { affectedRegions.add(coord); }
|
||||
public void incrementTick() { ticksActive++; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return type.displayName() + " at " + epicenter + " (tick " + ticksActive
|
||||
+ ", severity=" + String.format("%.2f", severity)
|
||||
+ ", regions=" + affectedRegions.size() + ")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.livingworld.climate;
|
||||
|
||||
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"),
|
||||
ALGAE_BLOOM("Algae Bloom", "Nutrient pollution causing a bloom and aquatic dead zone"),
|
||||
ACID_RAIN("Acid Rain", "Polluted rainfall damaging soil, stone and vegetation"),
|
||||
BLIZZARD("Blizzard", "Wind-driven snow accumulation and freezing water"),
|
||||
SANDSTORM("Sandstorm", "Dry high winds stripping plants and depositing sand"),
|
||||
LIGHTNING_STORM("Lightning Storm", "Dry thunderstorm producing isolated ignition strikes"),
|
||||
EARTHQUAKE("Earthquake", "Seismic rupture opening cracks and unstable slopes"),
|
||||
SINKHOLE("Sinkhole", "Groundwater-driven collapse into a shallow cave"),
|
||||
FLASH_FLOOD("Flash Flood", "Rapid runoff surging across barren steep terrain");
|
||||
|
||||
private final String displayName;
|
||||
private final String description;
|
||||
|
||||
ClimateEventType(String displayName, String description) {
|
||||
this.displayName = displayName;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String displayName() { return displayName; }
|
||||
public String description() { return description; }
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package com.livingworld.climate;
|
||||
|
||||
import com.livingworld.debug.DiagnosticCategory;
|
||||
import com.livingworld.debug.LivingWorldLogger;
|
||||
import com.livingworld.modules.vegetation.VegetationModule;
|
||||
import com.livingworld.modules.vegetation.VegetationRegionData;
|
||||
import com.livingworld.regions.Region;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* Tracks planetary-scale atmospheric carbon and its derived climate effects.
|
||||
*
|
||||
* <h3>Carbon cycle</h3>
|
||||
* Each simulation cycle, the tracker aggregates:
|
||||
* <ul>
|
||||
* <li>Emissions — proportional to total air pollution across all active regions.
|
||||
* <li>Sink — proportional to total tree canopy pressure across all active regions.
|
||||
* </ul>
|
||||
* The net delta is applied to the running {@code carbonPpm} value which drifts
|
||||
* slowly — meaningful climate change requires sustained industrial activity or
|
||||
* sustained reforestation to reverse.
|
||||
*
|
||||
* <h3>Derived effects</h3>
|
||||
* <ul>
|
||||
* <li>{@link #getWarmingLevel()} — 0.0 (pre-industrial) to 1.0 (catastrophic, +5°C).
|
||||
* <li>{@link #getRainPenalty()} — subtracted from every region's atmospheric rain target.
|
||||
* <li>{@link #getBiodiversityIndex()} — average ecosystem health across all active regions.
|
||||
* </ul>
|
||||
*
|
||||
* <h3>Persistence</h3>
|
||||
* Carbon state is saved to {@code living_world/global_climate.dat} on server stop and
|
||||
* restored on server start, so climate change accumulates across play sessions.
|
||||
*/
|
||||
public final class GlobalClimateTracker {
|
||||
|
||||
// Pre-industrial CO₂ baseline (ppm).
|
||||
private static final double CARBON_BASELINE = 280.0;
|
||||
// Level at which warming reaches +5°C equivalent (arbitrary but feels right for gameplay).
|
||||
private static final double CARBON_CRITICAL = 800.0;
|
||||
|
||||
// Per sim cycle: each unit of total regional air-pollution score → ppm added.
|
||||
private static final double EMISSION_RATE = 0.05;
|
||||
// Per sim cycle: each unit of total tree pressure → ppm removed.
|
||||
private static final double SINK_RATE = 0.002;
|
||||
|
||||
// Maximum rain-target reduction at full warming (applied globally per region).
|
||||
private static final double MAX_RAIN_PENALTY = 0.15;
|
||||
|
||||
private double carbonPpm = CARBON_BASELINE;
|
||||
private double warmingLevel = 0.0; // 0–1
|
||||
private double biodiversityIndex = 60.0; // 0–100
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Simulation update
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Updates the global climate state from the current state of all active regions.
|
||||
* Must be called once per simulation cycle after modules have run.
|
||||
*/
|
||||
public void update(Collection<Region> regions) {
|
||||
if (regions.isEmpty()) return;
|
||||
|
||||
double totalPollution = 0.0;
|
||||
double totalTreePressure = 0.0;
|
||||
double totalHealth = 0.0;
|
||||
int count = 0;
|
||||
|
||||
for (Region region : regions) {
|
||||
totalPollution += region.getMetrics().getPollutionScore();
|
||||
VegetationRegionData veg = region.getModuleData()
|
||||
.get(VegetationModule.MODULE_ID, VegetationRegionData.class)
|
||||
.orElse(null);
|
||||
if (veg != null) totalTreePressure += veg.getTreePressure();
|
||||
totalHealth += region.getMetrics().getEcosystemHealth();
|
||||
count++;
|
||||
}
|
||||
|
||||
double emissions = totalPollution * EMISSION_RATE;
|
||||
double sink = totalTreePressure * SINK_RATE;
|
||||
carbonPpm = Math.min(CARBON_CRITICAL, Math.max(CARBON_BASELINE, carbonPpm + emissions - sink));
|
||||
warmingLevel = (carbonPpm - CARBON_BASELINE) / (CARBON_CRITICAL - CARBON_BASELINE);
|
||||
biodiversityIndex = totalHealth / count;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Accessors
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Raw atmospheric carbon in ppm (280 = pre-industrial baseline, 800 = critical). */
|
||||
public double getCarbonPpm() { return carbonPpm; }
|
||||
|
||||
/** Warming level 0.0–1.0 (0 = baseline, 1 = +5°C equivalent). */
|
||||
public double getWarmingLevel() { return warmingLevel; }
|
||||
|
||||
/** Temperature anomaly in degrees Celsius equivalent (0–5°C). */
|
||||
public double getTemperatureCelsius(){ return warmingLevel * 5.0; }
|
||||
|
||||
/** Average ecosystem health across all active regions (0–100). */
|
||||
public double getBiodiversityIndex() { return biodiversityIndex; }
|
||||
|
||||
/**
|
||||
* Rain-target penalty applied globally in AtmosphereModule.
|
||||
* Increases as warming rises; max {@value MAX_RAIN_PENALTY} at full warming.
|
||||
*/
|
||||
public double getRainPenalty() { return warmingLevel * MAX_RAIN_PENALTY; }
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Persistence
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
public void save(Path path) {
|
||||
try {
|
||||
Files.createDirectories(path.getParent());
|
||||
Properties props = new Properties();
|
||||
props.setProperty("carbonPpm", String.valueOf(carbonPpm));
|
||||
try (OutputStream out = Files.newOutputStream(path)) {
|
||||
props.store(out, "Living World global climate state — do not edit manually");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LivingWorldLogger.warn(DiagnosticCategory.BOOTSTRAP,
|
||||
"Failed to save global climate state: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void load(Path path) {
|
||||
if (!Files.exists(path)) return;
|
||||
try {
|
||||
Properties props = new Properties();
|
||||
try (InputStream in = Files.newInputStream(path)) {
|
||||
props.load(in);
|
||||
}
|
||||
carbonPpm = Double.parseDouble(props.getProperty("carbonPpm",
|
||||
String.valueOf(CARBON_BASELINE)));
|
||||
warmingLevel = (carbonPpm - CARBON_BASELINE) / (CARBON_CRITICAL - CARBON_BASELINE);
|
||||
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP,
|
||||
String.format("Loaded global climate: %.1f ppm, warming +%.2f°C",
|
||||
carbonPpm, getTemperatureCelsius()));
|
||||
} catch (IOException | NumberFormatException e) {
|
||||
LivingWorldLogger.warn(DiagnosticCategory.BOOTSTRAP,
|
||||
"Failed to load global climate state — resetting to baseline: " + e.getMessage());
|
||||
carbonPpm = CARBON_BASELINE;
|
||||
warmingLevel = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package com.livingworld.commands;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import com.livingworld.core.LivingWorldConstants;
|
||||
import com.livingworld.modules.atmosphere.AtmosphereModule;
|
||||
import com.livingworld.modules.atmosphere.AtmosphereRegionData;
|
||||
import com.livingworld.modules.pollution.PollutionModule;
|
||||
import com.livingworld.modules.pollution.PollutionRegionData;
|
||||
import com.livingworld.modules.recovery.RecoveryModule;
|
||||
import com.livingworld.modules.recovery.RecoveryRegionData;
|
||||
import com.livingworld.modules.recovery.SuccessionStage;
|
||||
import com.livingworld.modules.soil.SoilModule;
|
||||
import com.livingworld.modules.soil.SoilRegionData;
|
||||
import com.livingworld.modules.vegetation.VegetationModule;
|
||||
import com.livingworld.modules.vegetation.VegetationRegionData;
|
||||
import com.livingworld.modules.water.WaterModule;
|
||||
import com.livingworld.modules.water.WaterRegionData;
|
||||
import com.livingworld.regions.Region;
|
||||
import com.livingworld.regions.RegionCoordinate;
|
||||
import com.livingworld.regions.RegionManager;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/**
|
||||
* One-command demo presets for watching ecosystem transformation.
|
||||
*
|
||||
* <pre>
|
||||
* /lw demo degrade — forces MATURE_FOREST then applies severe drought + dead soil
|
||||
* so the region visibly regresses toward BARREN.
|
||||
* Recommended speed: /lw speed 20
|
||||
*
|
||||
* /lw demo recover — resets soil and water to lush conditions so the region
|
||||
* advances from wherever it is back toward MATURE_FOREST.
|
||||
* Recommended speed: /lw speed 50
|
||||
* </pre>
|
||||
*/
|
||||
public final class EcoDemoCommand {
|
||||
|
||||
private EcoDemoCommand() {}
|
||||
|
||||
public static LiteralArgumentBuilder<CommandSourceStack> build(Supplier<RegionManager> rm) {
|
||||
return Commands.literal("demo")
|
||||
.then(Commands.literal("degrade")
|
||||
.executes(ctx -> runDegrade(ctx.getSource(), rm.get())))
|
||||
.then(Commands.literal("recover")
|
||||
.executes(ctx -> runRecover(ctx.getSource(), rm.get())));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Presets
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private static int runDegrade(CommandSourceStack src, RegionManager rm) {
|
||||
return applyPreset(src, rm, "DEGRADE", region -> {
|
||||
// Force starting point: peak forest.
|
||||
region.getModuleData().put(RecoveryModule.MODULE_ID,
|
||||
new RecoveryRegionData(SuccessionStage.MATURE_FOREST, 0.0, 0.0,
|
||||
SuccessionStage.MATURE_FOREST));
|
||||
|
||||
// Lush vegetation so it visibly dies rather than starting at zero.
|
||||
region.getModuleData().put(VegetationModule.MODULE_ID,
|
||||
new VegetationRegionData(100.0, 80.0, 80.0, 70.0, 0.0));
|
||||
|
||||
// Dead soil: near-zero fertility, high erosion and contamination.
|
||||
// soilQuality ≈ 5 − 8×0.4 − 8×0.3 = 5 − 3.2 − 2.4 = −0.6 → 0
|
||||
// That floor triggers badConditions in VegetationModule and
|
||||
// conditionsMissedForRegression in RecoveryModule every cycle.
|
||||
SoilRegionData soil = SoilRegionData.defaults();
|
||||
soil.setFertility(5.0);
|
||||
soil.setMoisture(5.0);
|
||||
soil.setContamination(8.0);
|
||||
soil.setErosion(8.0);
|
||||
region.getModuleData().put(SoilModule.MODULE_ID, soil);
|
||||
|
||||
// Severe drought — water availability near zero.
|
||||
WaterRegionData water = WaterRegionData.defaults();
|
||||
water.setWaterAvailability(5.0);
|
||||
water.setDroughtRisk(90.0);
|
||||
water.setPurificationCapacity(5.0);
|
||||
region.getModuleData().put(WaterModule.MODULE_ID, water);
|
||||
|
||||
// Clear pollution so it doesn't confuse the read-out (soil drives the decay here).
|
||||
region.getModuleData().put(PollutionModule.MODULE_ID, PollutionRegionData.defaults());
|
||||
|
||||
// Dry sky: low rain so per-player weather reflects the drought.
|
||||
AtmosphereRegionData atm = new AtmosphereRegionData(0.05, 0.0);
|
||||
region.getModuleData().put(AtmosphereModule.MODULE_ID, atm);
|
||||
}, "Stage forced to MATURE_FOREST | Soil destroyed | Drought=90 | Use /lw speed 20 to watch");
|
||||
}
|
||||
|
||||
private static int runRecover(CommandSourceStack src, RegionManager rm) {
|
||||
return applyPreset(src, rm, "RECOVER", region -> {
|
||||
// Raise cap — region must be allowed to reach MATURE_FOREST again.
|
||||
RecoveryRegionData rec = region.getModuleData()
|
||||
.get(RecoveryModule.MODULE_ID, RecoveryRegionData.class)
|
||||
.orElseGet(RecoveryRegionData::defaults);
|
||||
rec.setMaxSuccessionStage(SuccessionStage.MATURE_FOREST);
|
||||
region.getModuleData().put(RecoveryModule.MODULE_ID, rec);
|
||||
|
||||
// Rich, clean soil — contamination and erosion both zeroed.
|
||||
SoilRegionData soil = SoilRegionData.defaults();
|
||||
soil.setFertility(85.0);
|
||||
soil.setMoisture(75.0);
|
||||
soil.setContamination(0.0);
|
||||
soil.setErosion(0.0);
|
||||
region.getModuleData().put(SoilModule.MODULE_ID, soil);
|
||||
|
||||
// Plentiful water, no drought.
|
||||
WaterRegionData water = WaterRegionData.defaults();
|
||||
water.setWaterAvailability(85.0);
|
||||
water.setDroughtRisk(5.0);
|
||||
water.setPurificationCapacity(80.0);
|
||||
region.getModuleData().put(WaterModule.MODULE_ID, water);
|
||||
|
||||
// Clean air.
|
||||
region.getModuleData().put(PollutionModule.MODULE_ID, PollutionRegionData.defaults());
|
||||
|
||||
// Good rain signal in the atmosphere.
|
||||
AtmosphereRegionData atm = new AtmosphereRegionData(0.75, 0.0);
|
||||
region.getModuleData().put(AtmosphereModule.MODULE_ID, atm);
|
||||
|
||||
// Seed minimal grass so vegetation succession has something to start from.
|
||||
// (Shrubs and trees grow naturally from here.)
|
||||
VegetationRegionData veg = region.getModuleData()
|
||||
.get(VegetationModule.MODULE_ID, VegetationRegionData.class)
|
||||
.orElseGet(VegetationRegionData::defaults);
|
||||
veg.setGrassPressure(Math.max(veg.getGrassPressure(), 5.0));
|
||||
veg.setDeadVegetation(0.0);
|
||||
region.getModuleData().put(VegetationModule.MODULE_ID, veg);
|
||||
}, "Cap raised to MATURE_FOREST | Soil+water restored | Use /lw speed 50 to watch");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private static int applyPreset(CommandSourceStack src, RegionManager rm,
|
||||
String label, RegionMutation mutation, String hint) {
|
||||
if (rm == null) { src.sendFailure(Component.literal("Region manager not ready.")); return 0; }
|
||||
ServerPlayer player;
|
||||
try { player = src.getPlayerOrException(); }
|
||||
catch (CommandSyntaxException e) {
|
||||
src.sendFailure(Component.literal("/lw demo must be run by a player."));
|
||||
return 0;
|
||||
}
|
||||
|
||||
String dimId = player.level().dimension().location().toString();
|
||||
int rx = (int) Math.floor(player.getX() / (LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16.0));
|
||||
int rz = (int) Math.floor(player.getZ() / (LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16.0));
|
||||
RegionCoordinate coord = new RegionCoordinate(dimId, rx, rz);
|
||||
|
||||
Region region = rm.getOrCreateRegion(coord);
|
||||
mutation.apply(region);
|
||||
rm.markDirty(region);
|
||||
|
||||
src.sendSuccess(() -> Component.literal(
|
||||
"[LW] Demo:" + label + " applied to (" + rx + "," + rz + ") — " + hint), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface RegionMutation { void apply(Region region); }
|
||||
}
|
||||
@@ -2,12 +2,14 @@ package com.livingworld.commands;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.IntUnaryOperator;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.arguments.DoubleArgumentType;
|
||||
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
@@ -38,7 +40,13 @@ public final class LivingWorldCommandRoot {
|
||||
() -> regionManager,
|
||||
() -> moduleRegistry,
|
||||
() -> simulationManager,
|
||||
uuid -> false);
|
||||
uuid -> false,
|
||||
css -> "Atmosphere not available.",
|
||||
css -> "Climate not available.",
|
||||
n -> n,
|
||||
() -> "Wind not available.",
|
||||
css -> "Events not available.",
|
||||
() -> "No volcanic regions.");
|
||||
}
|
||||
|
||||
public static void registerDeferred(
|
||||
@@ -46,7 +54,13 @@ public final class LivingWorldCommandRoot {
|
||||
Supplier<RegionManager> regionManager,
|
||||
Supplier<ModuleRegistry> moduleRegistry,
|
||||
Supplier<SimulationManager> simulationManager,
|
||||
Function<UUID, Boolean> hudToggle) {
|
||||
Function<UUID, Boolean> hudToggle,
|
||||
Function<CommandSourceStack, String> atmosphereStatus,
|
||||
Function<CommandSourceStack, String> climateStatus,
|
||||
IntUnaryOperator speedSetter,
|
||||
Supplier<String> windStatus,
|
||||
Function<CommandSourceStack, String> eventsStatus,
|
||||
Supplier<String> volcanoStatus) {
|
||||
if (dispatcher == null) {
|
||||
throw new IllegalArgumentException("dispatcher must not be null");
|
||||
}
|
||||
@@ -84,7 +98,8 @@ public final class LivingWorldCommandRoot {
|
||||
.executes(context -> ForceUpdateCommand.executeAtSelf(
|
||||
context.getSource(),
|
||||
requireService(regionManager, "regionManager"),
|
||||
requireService(simulationManager, "simulationManager")))))
|
||||
requireService(simulationManager, "simulationManager"))))
|
||||
.then(RegionBordersCommand.build()))
|
||||
.then(Commands.literal("modules")
|
||||
.then(Commands.literal("list")
|
||||
.executes(context -> listModules(
|
||||
@@ -104,7 +119,58 @@ public final class LivingWorldCommandRoot {
|
||||
requireService(simulationManager, "simulationManager"),
|
||||
IntegerArgumentType.getInteger(context, "ticks")))))
|
||||
.then(Commands.literal("hud")
|
||||
.executes(context -> toggleHud(context.getSource(), hudToggle))));
|
||||
.executes(context -> toggleHud(context.getSource(), hudToggle)))
|
||||
.then(Commands.literal("atmosphere")
|
||||
.executes(context -> {
|
||||
String info = atmosphereStatus.apply(context.getSource());
|
||||
context.getSource().sendSuccess(() -> Component.literal(info), false);
|
||||
return 1;
|
||||
}))
|
||||
.then(Commands.literal("climate")
|
||||
.executes(context -> {
|
||||
String info = climateStatus.apply(context.getSource());
|
||||
context.getSource().sendSuccess(() -> Component.literal(info), false);
|
||||
return 1;
|
||||
}))
|
||||
.then(Commands.literal("wind")
|
||||
.executes(context -> {
|
||||
String info = windStatus.get();
|
||||
context.getSource().sendSuccess(() -> Component.literal(info), false);
|
||||
return 1;
|
||||
}))
|
||||
.then(Commands.literal("speed")
|
||||
.then(Commands.argument("multiplier", IntegerArgumentType.integer(1, 100))
|
||||
.executes(context -> {
|
||||
int n = IntegerArgumentType.getInteger(context, "multiplier");
|
||||
int actual = speedSetter.applyAsInt(n);
|
||||
context.getSource().sendSuccess(
|
||||
() -> Component.literal("[LW] Simulation speed: " + actual + "x"),
|
||||
false);
|
||||
return actual;
|
||||
}))
|
||||
.then(Commands.literal("reset")
|
||||
.executes(context -> {
|
||||
speedSetter.applyAsInt(1);
|
||||
context.getSource().sendSuccess(
|
||||
() -> Component.literal("[LW] Simulation speed reset to 1x"),
|
||||
false);
|
||||
return 1;
|
||||
})))
|
||||
.then(LivingWorldMapCommand.build(regionManager))
|
||||
.then(RegionSetCommand.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) {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.livingworld.commands;
|
||||
|
||||
import com.livingworld.core.LivingWorldConstants;
|
||||
import com.livingworld.modules.recovery.RecoveryModule;
|
||||
import com.livingworld.modules.recovery.RecoveryRegionData;
|
||||
import com.livingworld.modules.recovery.SuccessionStage;
|
||||
import com.livingworld.regions.RegionCoordinate;
|
||||
import com.livingworld.regions.RegionManager;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import java.util.function.Supplier;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.MutableComponent;
|
||||
|
||||
/**
|
||||
* Renders an ASCII coloured grid of nearby regions showing their succession stage.
|
||||
* Usage: {@code /lw map} (7×7 grid) or {@code /lw map <radius>} (1–7, giving 3×3 to 15×15).
|
||||
*/
|
||||
public final class LivingWorldMapCommand {
|
||||
|
||||
private static final int DEFAULT_RADIUS = 3;
|
||||
|
||||
private LivingWorldMapCommand() {}
|
||||
|
||||
public static LiteralArgumentBuilder<CommandSourceStack> build(Supplier<RegionManager> regionManager) {
|
||||
return Commands.literal("map")
|
||||
.executes(ctx -> executeMap(ctx.getSource(), regionManager, DEFAULT_RADIUS))
|
||||
.then(Commands.argument("radius", IntegerArgumentType.integer(1, 7))
|
||||
.executes(ctx -> executeMap(ctx.getSource(), regionManager,
|
||||
IntegerArgumentType.getInteger(ctx, "radius"))));
|
||||
}
|
||||
|
||||
private static int executeMap(
|
||||
CommandSourceStack source, Supplier<RegionManager> rmSupplier, int radius) {
|
||||
RegionManager rm = rmSupplier.get();
|
||||
if (rm == null) {
|
||||
source.sendFailure(Component.literal("Region manager not ready."));
|
||||
return 0;
|
||||
}
|
||||
|
||||
String dimId = source.getLevel().dimension().location().toString();
|
||||
var pos = source.getPosition();
|
||||
int prx = (int) Math.floor(pos.x / (LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16.0));
|
||||
int prz = (int) Math.floor(pos.z / (LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16.0));
|
||||
int size = radius * 2 + 1;
|
||||
|
||||
source.sendSuccess(() -> Component.literal(
|
||||
"[LW] Map " + size + "×" + size
|
||||
+ " around (" + prx + "," + prz + ") | [X]=you, ↑N ↓S ←W →E")
|
||||
.withStyle(ChatFormatting.GOLD), false);
|
||||
|
||||
// dz goes from -radius (north) to +radius (south) visually top-to-bottom
|
||||
for (int dz = -radius; dz <= radius; dz++) {
|
||||
MutableComponent row = Component.empty();
|
||||
for (int dx = -radius; dx <= radius; dx++) {
|
||||
RegionCoordinate coord = new RegionCoordinate(dimId, prx + dx, prz + dz);
|
||||
row.append(renderCell(rm, coord, dx == 0 && dz == 0));
|
||||
}
|
||||
final MutableComponent finalRow = row;
|
||||
source.sendSuccess(() -> finalRow, false);
|
||||
}
|
||||
|
||||
source.sendSuccess(() -> Component.literal(
|
||||
"§2F§r=Forest §aW§r=Woodland §es§r=Scrub §fG§r=Grass §6g§r=Sparse §cB§r=Barren §8·§r=Unknown")
|
||||
.withStyle(ChatFormatting.DARK_GRAY), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static MutableComponent renderCell(RegionManager rm, RegionCoordinate coord, boolean isPlayer) {
|
||||
if (isPlayer) {
|
||||
return Component.literal("[X]").withStyle(ChatFormatting.AQUA, ChatFormatting.BOLD);
|
||||
}
|
||||
SuccessionStage stage = rm.resolve(coord)
|
||||
.flatMap(r -> r.getModuleData().get(RecoveryModule.MODULE_ID, RecoveryRegionData.class))
|
||||
.map(RecoveryRegionData::getSuccessionStage)
|
||||
.orElse(null);
|
||||
|
||||
if (stage == null) {
|
||||
return Component.literal(" · ").withStyle(ChatFormatting.DARK_GRAY);
|
||||
}
|
||||
return switch (stage) {
|
||||
case MATURE_FOREST -> Component.literal(" F ").withStyle(ChatFormatting.DARK_GREEN);
|
||||
case YOUNG_WOODLAND -> Component.literal(" W ").withStyle(ChatFormatting.GREEN);
|
||||
case SCRUBLAND -> Component.literal(" s ").withStyle(ChatFormatting.YELLOW);
|
||||
case GRASSLAND -> Component.literal(" G ").withStyle(ChatFormatting.WHITE);
|
||||
case SPARSE_GRASS -> Component.literal(" g ").withStyle(ChatFormatting.GOLD);
|
||||
case BARREN -> Component.literal(" B ").withStyle(ChatFormatting.RED);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.livingworld.commands;
|
||||
|
||||
import com.livingworld.core.LivingWorldConstants;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.core.particles.DustParticleOptions;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
/**
|
||||
* Implements {@code /lw region borders [regionX regionZ]}.
|
||||
*
|
||||
* <p>Draws temporary cyan dust-particle lines along all four edges of the
|
||||
* specified region (or the player's current region if no coordinates are given).
|
||||
* Each edge is sampled at the world surface so borders follow terrain.</p>
|
||||
*/
|
||||
public final class RegionBordersCommand {
|
||||
|
||||
private static final int REGION_BLOCKS = LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16;
|
||||
private static final int EDGE_SAMPLES = 32; // particles per edge (~4 blocks apart on 128-block edge)
|
||||
|
||||
// Cyan-teal colour for region borders.
|
||||
private static final DustParticleOptions BORDER_PARTICLE =
|
||||
new DustParticleOptions(new Vector3f(0.0f, 0.95f, 0.85f), 1.5f);
|
||||
|
||||
private RegionBordersCommand() {}
|
||||
|
||||
public static LiteralArgumentBuilder<CommandSourceStack> build() {
|
||||
return Commands.literal("borders")
|
||||
.executes(ctx -> execute(ctx.getSource(), null, null))
|
||||
.then(Commands.argument("regionX", IntegerArgumentType.integer())
|
||||
.then(Commands.argument("regionZ", IntegerArgumentType.integer())
|
||||
.executes(ctx -> execute(
|
||||
ctx.getSource(),
|
||||
IntegerArgumentType.getInteger(ctx, "regionX"),
|
||||
IntegerArgumentType.getInteger(ctx, "regionZ")))));
|
||||
}
|
||||
|
||||
private static int execute(CommandSourceStack source, Integer regionX, Integer regionZ) {
|
||||
ServerPlayer player;
|
||||
try {
|
||||
player = source.getPlayerOrException();
|
||||
} catch (CommandSyntaxException e) {
|
||||
source.sendFailure(Component.literal("/lw region borders requires a player"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int rx = regionX != null ? regionX
|
||||
: (int) Math.floor(player.getX() / REGION_BLOCKS);
|
||||
int rz = regionZ != null ? regionZ
|
||||
: (int) Math.floor(player.getZ() / REGION_BLOCKS);
|
||||
|
||||
ServerLevel level = player.serverLevel();
|
||||
int baseX = rx * REGION_BLOCKS;
|
||||
int baseZ = rz * REGION_BLOCKS;
|
||||
|
||||
for (int i = 0; i <= EDGE_SAMPLES; i++) {
|
||||
int offset = (int) ((double) i / EDGE_SAMPLES * REGION_BLOCKS);
|
||||
// North edge: z = baseZ, vary x
|
||||
spawnEdgeParticles(level, baseX + offset, baseZ);
|
||||
// South edge: z = baseZ + REGION_BLOCKS, vary x
|
||||
spawnEdgeParticles(level, baseX + offset, baseZ + REGION_BLOCKS);
|
||||
// West edge: x = baseX, vary z
|
||||
spawnEdgeParticles(level, baseX, baseZ + offset);
|
||||
// East edge: x = baseX + REGION_BLOCKS, vary z
|
||||
spawnEdgeParticles(level, baseX + REGION_BLOCKS, baseZ + offset);
|
||||
}
|
||||
|
||||
source.sendSuccess(() -> Component.literal(String.format(
|
||||
"[LW] Region (%d,%d) borders — blocks X[%d..%d] Z[%d..%d]",
|
||||
rx, rz, baseX, baseX + REGION_BLOCKS - 1, baseZ, baseZ + REGION_BLOCKS - 1)),
|
||||
false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static void spawnEdgeParticles(ServerLevel level, int x, int z) {
|
||||
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z) + 1;
|
||||
// Use count=3 so the vertical column is visible even in high terrain.
|
||||
level.sendParticles(BORDER_PARTICLE,
|
||||
x + 0.5, y + 0.5, z + 0.5,
|
||||
3, // count
|
||||
0.0, 0.8, 0.0, // spread x/y/z
|
||||
0.0); // speed (0 = stationary)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package com.livingworld.commands;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import com.livingworld.core.LivingWorldConstants;
|
||||
import com.livingworld.modules.atmosphere.AtmosphereModule;
|
||||
import com.livingworld.modules.atmosphere.AtmosphereRegionData;
|
||||
import com.livingworld.modules.pollution.PollutionModule;
|
||||
import com.livingworld.modules.pollution.PollutionRegionData;
|
||||
import com.livingworld.modules.recovery.RecoveryModule;
|
||||
import com.livingworld.modules.recovery.RecoveryRegionData;
|
||||
import com.livingworld.modules.recovery.SuccessionStage;
|
||||
import com.livingworld.modules.soil.SoilModule;
|
||||
import com.livingworld.modules.soil.SoilRegionData;
|
||||
import com.livingworld.modules.water.WaterModule;
|
||||
import com.livingworld.modules.water.WaterRegionData;
|
||||
import com.livingworld.regions.Region;
|
||||
import com.livingworld.regions.RegionCoordinate;
|
||||
import com.livingworld.regions.RegionManager;
|
||||
import com.mojang.brigadier.arguments.DoubleArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.SuggestionProvider;
|
||||
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.SharedSuggestionProvider;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/**
|
||||
* Debug command branch: {@code /lw set <property> <value>}.
|
||||
*
|
||||
* <p>Directly mutates simulation data in the player's current region so that
|
||||
* ecosystem transformation systems (succession, water runoff, pool formation)
|
||||
* can be tested without waiting for natural accumulation.
|
||||
*
|
||||
* <h3>Sub-commands</h3>
|
||||
* <pre>
|
||||
* /lw set soil fertility <0-100>
|
||||
* /lw set soil moisture <0-100>
|
||||
* /lw set water availability <0-100>
|
||||
* /lw set water drought <0-100>
|
||||
* /lw set pollution <0-100> (sets all three pollution layers proportionally)
|
||||
* /lw set stage <STAGE_NAME> (forces succession stage; resets progress)
|
||||
* /lw set cap <STAGE_NAME> (forces succession ceiling)
|
||||
* /lw set rain <0.0-1.0> (overrides regional rain level for one sim cycle)
|
||||
* </pre>
|
||||
*/
|
||||
public final class RegionSetCommand {
|
||||
|
||||
private static final SuggestionProvider<CommandSourceStack> STAGE_SUGGESTIONS =
|
||||
(ctx, builder) -> SharedSuggestionProvider.suggest(
|
||||
Arrays.stream(SuccessionStage.values()).map(Enum::name), builder);
|
||||
|
||||
private RegionSetCommand() {}
|
||||
|
||||
public static LiteralArgumentBuilder<CommandSourceStack> build(Supplier<RegionManager> rm) {
|
||||
return Commands.literal("set")
|
||||
// --- soil ---
|
||||
.then(Commands.literal("soil")
|
||||
.then(Commands.literal("fertility")
|
||||
.then(Commands.argument("value", DoubleArgumentType.doubleArg(0, 100))
|
||||
.executes(ctx -> setSoilFertility(ctx.getSource(), rm.get(),
|
||||
DoubleArgumentType.getDouble(ctx, "value")))))
|
||||
.then(Commands.literal("moisture")
|
||||
.then(Commands.argument("value", DoubleArgumentType.doubleArg(0, 100))
|
||||
.executes(ctx -> setSoilMoisture(ctx.getSource(), rm.get(),
|
||||
DoubleArgumentType.getDouble(ctx, "value"))))))
|
||||
// --- water ---
|
||||
.then(Commands.literal("water")
|
||||
.then(Commands.literal("availability")
|
||||
.then(Commands.argument("value", DoubleArgumentType.doubleArg(0, 100))
|
||||
.executes(ctx -> setWaterAvailability(ctx.getSource(), rm.get(),
|
||||
DoubleArgumentType.getDouble(ctx, "value")))))
|
||||
.then(Commands.literal("drought")
|
||||
.then(Commands.argument("value", DoubleArgumentType.doubleArg(0, 100))
|
||||
.executes(ctx -> setWaterDrought(ctx.getSource(), rm.get(),
|
||||
DoubleArgumentType.getDouble(ctx, "value"))))))
|
||||
// --- pollution ---
|
||||
.then(Commands.literal("pollution")
|
||||
.then(Commands.argument("value", DoubleArgumentType.doubleArg(0, 100))
|
||||
.executes(ctx -> setPollution(ctx.getSource(), rm.get(),
|
||||
DoubleArgumentType.getDouble(ctx, "value")))))
|
||||
// --- succession stage ---
|
||||
.then(Commands.literal("stage")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.suggests(STAGE_SUGGESTIONS)
|
||||
.executes(ctx -> setStage(ctx.getSource(), rm.get(),
|
||||
StringArgumentType.getString(ctx, "name")))))
|
||||
// --- succession cap ---
|
||||
.then(Commands.literal("cap")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.suggests(STAGE_SUGGESTIONS)
|
||||
.executes(ctx -> setCap(ctx.getSource(), rm.get(),
|
||||
StringArgumentType.getString(ctx, "name")))))
|
||||
// --- regional rain level ---
|
||||
.then(Commands.literal("rain")
|
||||
.then(Commands.argument("value", DoubleArgumentType.doubleArg(0, 1))
|
||||
.executes(ctx -> setRain(ctx.getSource(), rm.get(),
|
||||
DoubleArgumentType.getDouble(ctx, "value")))));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Executors
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private static int setSoilFertility(CommandSourceStack src, RegionManager rm, double value) {
|
||||
return modifyRegion(src, rm, "soil.fertility=" + value, region -> {
|
||||
SoilRegionData soil = getOrDefault(region, SoilModule.MODULE_ID, SoilRegionData.class,
|
||||
SoilRegionData.defaults());
|
||||
soil.setFertility(value);
|
||||
region.getModuleData().put(SoilModule.MODULE_ID, soil);
|
||||
});
|
||||
}
|
||||
|
||||
private static int setSoilMoisture(CommandSourceStack src, RegionManager rm, double value) {
|
||||
return modifyRegion(src, rm, "soil.moisture=" + value, region -> {
|
||||
SoilRegionData soil = getOrDefault(region, SoilModule.MODULE_ID, SoilRegionData.class,
|
||||
SoilRegionData.defaults());
|
||||
soil.setMoisture(value);
|
||||
region.getModuleData().put(SoilModule.MODULE_ID, soil);
|
||||
});
|
||||
}
|
||||
|
||||
private static int setWaterAvailability(CommandSourceStack src, RegionManager rm, double value) {
|
||||
return modifyRegion(src, rm, "water.availability=" + value, region -> {
|
||||
WaterRegionData water = getOrDefault(region, WaterModule.MODULE_ID, WaterRegionData.class,
|
||||
WaterRegionData.defaults());
|
||||
water.setWaterAvailability(value);
|
||||
region.getModuleData().put(WaterModule.MODULE_ID, water);
|
||||
});
|
||||
}
|
||||
|
||||
private static int setWaterDrought(CommandSourceStack src, RegionManager rm, double value) {
|
||||
return modifyRegion(src, rm, "water.drought=" + value, region -> {
|
||||
WaterRegionData water = getOrDefault(region, WaterModule.MODULE_ID, WaterRegionData.class,
|
||||
WaterRegionData.defaults());
|
||||
water.setDroughtRisk(value);
|
||||
region.getModuleData().put(WaterModule.MODULE_ID, water);
|
||||
});
|
||||
}
|
||||
|
||||
private static int setPollution(CommandSourceStack src, RegionManager rm, double value) {
|
||||
return modifyRegion(src, rm, "pollution=" + value, region -> {
|
||||
PollutionRegionData existing = getOrDefault(region, PollutionModule.MODULE_ID,
|
||||
PollutionRegionData.class, PollutionRegionData.defaults());
|
||||
region.getModuleData().put(PollutionModule.MODULE_ID,
|
||||
new PollutionRegionData(value, value * 0.5, value * 0.3,
|
||||
existing.getDecayResistance()));
|
||||
});
|
||||
}
|
||||
|
||||
private static int setStage(CommandSourceStack src, RegionManager rm, String stageName) {
|
||||
SuccessionStage stage = parseStage(src, stageName);
|
||||
if (stage == null) return 0;
|
||||
return modifyRegion(src, rm, "stage=" + stage.name(), region -> {
|
||||
RecoveryRegionData rec = getOrDefault(region, RecoveryModule.MODULE_ID,
|
||||
RecoveryRegionData.class, RecoveryRegionData.defaults());
|
||||
// Construct a fresh data object at the desired stage, preserving cap.
|
||||
SuccessionStage cap = rec.getMaxSuccessionStage();
|
||||
if (stage.ordinal() > cap.ordinal()) {
|
||||
// Auto-raise cap if forcing to a higher stage.
|
||||
cap = stage;
|
||||
}
|
||||
region.getModuleData().put(RecoveryModule.MODULE_ID,
|
||||
new RecoveryRegionData(stage, 0.0, 0.0, cap));
|
||||
});
|
||||
}
|
||||
|
||||
private static int setCap(CommandSourceStack src, RegionManager rm, String stageName) {
|
||||
SuccessionStage stage = parseStage(src, stageName);
|
||||
if (stage == null) return 0;
|
||||
return modifyRegion(src, rm, "cap=" + stage.name(), region -> {
|
||||
RecoveryRegionData rec = getOrDefault(region, RecoveryModule.MODULE_ID,
|
||||
RecoveryRegionData.class, RecoveryRegionData.defaults());
|
||||
rec.setMaxSuccessionStage(stage);
|
||||
region.getModuleData().put(RecoveryModule.MODULE_ID, rec);
|
||||
});
|
||||
}
|
||||
|
||||
private static int setRain(CommandSourceStack src, RegionManager rm, double value) {
|
||||
return modifyRegion(src, rm, "rain=" + value, region -> {
|
||||
AtmosphereRegionData atm = getOrDefault(region, AtmosphereModule.MODULE_ID,
|
||||
AtmosphereRegionData.class, AtmosphereRegionData.defaults());
|
||||
atm.setRainLevel(value);
|
||||
region.getModuleData().put(AtmosphereModule.MODULE_ID, atm);
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Shared helpers
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Resolves the player's region, applies {@code mutation}, marks dirty, replies. */
|
||||
private static int modifyRegion(CommandSourceStack src, RegionManager rm,
|
||||
String change, RegionMutation mutation) {
|
||||
if (rm == null) {
|
||||
src.sendFailure(Component.literal("Region manager not ready."));
|
||||
return 0;
|
||||
}
|
||||
ServerPlayer player;
|
||||
try {
|
||||
player = src.getPlayerOrException();
|
||||
} catch (CommandSyntaxException e) {
|
||||
src.sendFailure(Component.literal("/lw set must be run by a player."));
|
||||
return 0;
|
||||
}
|
||||
|
||||
String dimId = player.level().dimension().location().toString();
|
||||
int rx = (int) Math.floor(player.getX() / (LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16.0));
|
||||
int rz = (int) Math.floor(player.getZ() / (LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16.0));
|
||||
RegionCoordinate coord = new RegionCoordinate(dimId, rx, rz);
|
||||
|
||||
Region region = rm.getOrCreateRegion(coord);
|
||||
mutation.apply(region);
|
||||
rm.markDirty(region);
|
||||
|
||||
src.sendSuccess(() -> Component.literal(
|
||||
String.format("[LW] Set %s in region (%d,%d)", change, rx, rz)), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static <T> T getOrDefault(Region region, String moduleId, Class<T> type, T fallback) {
|
||||
return region.getModuleData().get(moduleId, type).orElse(fallback);
|
||||
}
|
||||
|
||||
private static SuccessionStage parseStage(CommandSourceStack src, String name) {
|
||||
try {
|
||||
return SuccessionStage.valueOf(name.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
src.sendFailure(Component.literal(
|
||||
"Unknown stage '" + name + "'. Valid: BARREN, SPARSE_GRASS, GRASSLAND,"
|
||||
+ " SCRUBLAND, YOUNG_WOODLAND, MATURE_FOREST"));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface RegionMutation {
|
||||
void apply(Region region);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.livingworld.config;
|
||||
|
||||
/**
|
||||
* Tuning constants for the Living World ecosystem simulation.
|
||||
*
|
||||
* <p>Populated from {@code config/livingworld-server.toml} at server startup via
|
||||
* {@link com.livingworld.platform.neoforge.NeoForgeModConfig}. Modules access this
|
||||
* via {@link com.livingworld.core.services.CoreServices#TUNING} from the service
|
||||
* registry during {@code initialize()}.
|
||||
*
|
||||
* <p>Default values match the hardcoded constants they replaced, so a missing
|
||||
* config file produces identical simulation behaviour to earlier versions.
|
||||
*/
|
||||
public final class EcosystemTuning {
|
||||
|
||||
// -- Pollution --
|
||||
private double pollutionDecayRate = 0.002;
|
||||
private double groundToWaterLeach = 0.005;
|
||||
private double pollutionSpreadRate = 0.02;
|
||||
private double windBoost = 0.5;
|
||||
|
||||
// -- Vegetation growth (per soil-quality unit above threshold per tick) --
|
||||
private double grassGrowthRate = 0.06;
|
||||
private double flowerGrowthRate = 0.03;
|
||||
private double shrubGrowthRate = 0.02;
|
||||
private double treeGrowthRate = 0.008;
|
||||
|
||||
// -- Vegetation die-off (flat reduction per tick under bad conditions) --
|
||||
private double grassDieoffRate = 1.00;
|
||||
private double flowerDieoffRate = 0.50;
|
||||
private double shrubDieoffRate = 0.25;
|
||||
private double treeDieoffRate = 0.10;
|
||||
|
||||
// -- Ecological succession --
|
||||
private double baseProgressPerTick = 2.0;
|
||||
private double damagePerBadTick = 5.0;
|
||||
private double damageDecayRate = 0.03;
|
||||
|
||||
// -- Seed dispersal (new ecological corridors feature) --
|
||||
/** Fraction of vegetation pressure emitted as seeds to each neighbour per tick. */
|
||||
private double seedEmissionRate = 0.01;
|
||||
/** Seed multiplier when the target region has 2+ healthy neighbours (corridor effect). */
|
||||
private double corridorBoostMultiplier = 3.5;
|
||||
/** Seeds blocked per unit of pollution score in the target region. */
|
||||
private double seedPollutionBlock = 0.015;
|
||||
private int seaLevel = 62;
|
||||
|
||||
public EcosystemTuning() {}
|
||||
|
||||
// -- Pollution getters/setters --
|
||||
public double getPollutionDecayRate() { return pollutionDecayRate; }
|
||||
public double getGroundToWaterLeach() { return groundToWaterLeach; }
|
||||
public double getPollutionSpreadRate() { return pollutionSpreadRate; }
|
||||
public double getWindBoost() { return windBoost; }
|
||||
public void setPollutionDecayRate(double v) { pollutionDecayRate = v; }
|
||||
public void setGroundToWaterLeach(double v) { groundToWaterLeach = v; }
|
||||
public void setPollutionSpreadRate(double v) { pollutionSpreadRate = v; }
|
||||
public void setWindBoost(double v) { windBoost = v; }
|
||||
|
||||
// -- Vegetation growth getters/setters --
|
||||
public double getGrassGrowthRate() { return grassGrowthRate; }
|
||||
public double getFlowerGrowthRate() { return flowerGrowthRate; }
|
||||
public double getShrubGrowthRate() { return shrubGrowthRate; }
|
||||
public double getTreeGrowthRate() { return treeGrowthRate; }
|
||||
public void setGrassGrowthRate(double v) { grassGrowthRate = v; }
|
||||
public void setFlowerGrowthRate(double v) { flowerGrowthRate = v; }
|
||||
public void setShrubGrowthRate(double v) { shrubGrowthRate = v; }
|
||||
public void setTreeGrowthRate(double v) { treeGrowthRate = v; }
|
||||
|
||||
// -- Vegetation die-off getters/setters --
|
||||
public double getGrassDieoffRate() { return grassDieoffRate; }
|
||||
public double getFlowerDieoffRate() { return flowerDieoffRate; }
|
||||
public double getShrubDieoffRate() { return shrubDieoffRate; }
|
||||
public double getTreeDieoffRate() { return treeDieoffRate; }
|
||||
public void setGrassDieoffRate(double v) { grassDieoffRate = v; }
|
||||
public void setFlowerDieoffRate(double v) { flowerDieoffRate = v; }
|
||||
public void setShrubDieoffRate(double v) { shrubDieoffRate = v; }
|
||||
public void setTreeDieoffRate(double v) { treeDieoffRate = v; }
|
||||
|
||||
// -- Succession getters/setters --
|
||||
public double getBaseProgressPerTick() { return baseProgressPerTick; }
|
||||
public double getDamagePerBadTick() { return damagePerBadTick; }
|
||||
public double getDamageDecayRate() { return damageDecayRate; }
|
||||
public void setBaseProgressPerTick(double v) { baseProgressPerTick = v; }
|
||||
public void setDamagePerBadTick(double v) { damagePerBadTick = v; }
|
||||
public void setDamageDecayRate(double v) { damageDecayRate = v; }
|
||||
|
||||
// -- Seed dispersal getters/setters --
|
||||
public double getSeedEmissionRate() { return seedEmissionRate; }
|
||||
public double getCorridorBoostMultiplier() { return corridorBoostMultiplier; }
|
||||
public double getSeedPollutionBlock() { return seedPollutionBlock; }
|
||||
public void setSeedEmissionRate(double v) { seedEmissionRate = v; }
|
||||
public void setCorridorBoostMultiplier(double v) { corridorBoostMultiplier = v; }
|
||||
public void setSeedPollutionBlock(double v) { seedPollutionBlock = v; }
|
||||
|
||||
public int getSeaLevel() { return seaLevel; }
|
||||
public void setSeaLevel(int seaLevel) { this.seaLevel = Math.max(1, Math.min(320, seaLevel)); }
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.livingworld.core.services;
|
||||
|
||||
import com.livingworld.config.EcosystemTuning;
|
||||
import com.livingworld.core.simulation.RegionManager;
|
||||
import com.livingworld.core.simulation.SimulationManager;
|
||||
import com.livingworld.events.LivingWorldEventBus;
|
||||
@@ -40,6 +41,9 @@ public final class CoreServices {
|
||||
/** ConfigService key — interface already exists. */
|
||||
public static final ServiceKey<ConfigService> CONFIG = new ServiceKey<>("config", ConfigService.class);
|
||||
|
||||
/** EcosystemTuning key — holds all tunable simulation constants (loaded from TOML config). */
|
||||
public static final ServiceKey<EcosystemTuning> TUNING = new ServiceKey<>("tuning", EcosystemTuning.class);
|
||||
|
||||
public static final ServiceKey<RegionManager> REGIONS =
|
||||
new ServiceKey<>("regions", RegionManager.class);
|
||||
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package com.livingworld.modules.atmosphere;
|
||||
|
||||
import com.livingworld.data.serialization.PersistenceReader;
|
||||
import com.livingworld.data.serialization.PersistenceWriter;
|
||||
import com.livingworld.events.LivingWorldEvent;
|
||||
import com.livingworld.modules.ModuleContext;
|
||||
import com.livingworld.modules.ModuleMetadata;
|
||||
import com.livingworld.modules.ModuleUpdateResult;
|
||||
import com.livingworld.modules.RegionUpdateContext;
|
||||
import com.livingworld.modules.ServerContext;
|
||||
import com.livingworld.modules.SimulationModule;
|
||||
import com.livingworld.modules.pollution.PollutionModule;
|
||||
import com.livingworld.modules.pollution.PollutionRegionData;
|
||||
import com.livingworld.modules.soil.SoilModule;
|
||||
import com.livingworld.modules.soil.SoilRegionData;
|
||||
import com.livingworld.modules.vegetation.VegetationModule;
|
||||
import com.livingworld.modules.vegetation.VegetationRegionData;
|
||||
import com.livingworld.modules.water.WaterModule;
|
||||
import com.livingworld.modules.water.WaterRegionData;
|
||||
import com.livingworld.regions.Region;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.DoubleSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Drives per-region weather from ecosystem state and applies atmospheric feedback
|
||||
* to other simulation layers.
|
||||
*
|
||||
* <h3>Pipeline position</h3>
|
||||
* Runs last (after WorldEffects) so ecosystem health and pollution scores are final
|
||||
* before weather targets are computed.
|
||||
*
|
||||
* <h3>Per-cycle rules</h3>
|
||||
* <ol>
|
||||
* <li>Tree canopy scrubs air pollution: {@code airPollution -= treePressure × TREE_SCRUB_RATE}.
|
||||
* <li>Target rain level is derived from ecosystem health (healthy = wet, degraded = drought).
|
||||
* Global Minecraft rain adds a small bias so natural weather still matters.
|
||||
* <li>Target thunder level is derived from pollution (polluted = stormy).
|
||||
* <li>Rain and thunder smoothly interpolate toward their targets each cycle.
|
||||
* <li>Rain fills water availability; drought raises drought risk.
|
||||
* <li>High thunder + high pollution triggers acid rain (drains soil fertility).
|
||||
* </ol>
|
||||
*/
|
||||
public final class AtmosphereModule implements SimulationModule {
|
||||
|
||||
public static final String MODULE_ID = "atmosphere";
|
||||
|
||||
// Tree scrub: treePressure (0-100) × rate removed from air pollution per cycle.
|
||||
private static final double TREE_SCRUB_RATE = 0.003;
|
||||
|
||||
// Rain target ceiling when ecosystem is pristine (no global rain).
|
||||
private static final double MAX_RAIN_FROM_ECO = 0.85;
|
||||
// Additive bias when Minecraft's overworld is actually raining.
|
||||
private static final double GLOBAL_RAIN_BIAS = 0.15;
|
||||
|
||||
// Smoothing rates — weather changes slowly, not instantly.
|
||||
private static final double RAIN_SMOOTHING = 0.05;
|
||||
private static final double THUNDER_SMOOTHING = 0.03;
|
||||
|
||||
// Rain < this → drought conditions worsen each cycle.
|
||||
private static final double DROUGHT_THRESHOLD = 0.2;
|
||||
private static final double RAIN_MOISTURE_GAIN = 0.3;
|
||||
private static final double RAIN_DROUGHT_RELIEF = 0.15;
|
||||
private static final double DRY_DROUGHT_INCREASE = 0.05;
|
||||
|
||||
// Acid rain fires when BOTH thunder and pollution exceed these thresholds.
|
||||
private static final double ACID_THUNDER_THRESHOLD = 0.4;
|
||||
private static final double ACID_POLL_THRESHOLD = 20.0;
|
||||
private static final double ACID_FERTILITY_DRAIN = 0.0005;
|
||||
|
||||
private static final ModuleMetadata METADATA = new ModuleMetadata(
|
||||
MODULE_ID,
|
||||
"Atmosphere",
|
||||
"1.0.0",
|
||||
"Per-region weather driven by ecosystem health and pollution. Trees scrub air pollution.",
|
||||
"9",
|
||||
List.of(),
|
||||
List.of(),
|
||||
true,
|
||||
true,
|
||||
false);
|
||||
|
||||
private final BooleanSupplier globalRaining;
|
||||
private final Supplier<Season> currentSeason;
|
||||
private final DoubleSupplier globalWarmingPenalty;
|
||||
|
||||
public AtmosphereModule(BooleanSupplier globalRaining, Supplier<Season> currentSeason,
|
||||
DoubleSupplier globalWarmingPenalty) {
|
||||
this.globalRaining = globalRaining;
|
||||
this.currentSeason = currentSeason;
|
||||
this.globalWarmingPenalty = globalWarmingPenalty;
|
||||
}
|
||||
|
||||
@Override public String getModuleId() { return MODULE_ID; }
|
||||
@Override public ModuleMetadata getMetadata() { return METADATA; }
|
||||
@Override public void initialize(ModuleContext ctx) {}
|
||||
@Override public void onServerStarted(ServerContext ctx) {}
|
||||
@Override public void onLivingWorldEvent(LivingWorldEvent event) {}
|
||||
@Override public void saveModuleData(PersistenceWriter w) {}
|
||||
@Override public void loadModuleData(PersistenceReader r) {}
|
||||
@Override public void shutdown() {}
|
||||
|
||||
@Override
|
||||
public void createDefaultRegionData(Region region) {
|
||||
if (!region.getModuleData().contains(MODULE_ID)) {
|
||||
region.getModuleData().put(MODULE_ID, AtmosphereRegionData.defaults());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModuleUpdateResult updateRegion(RegionUpdateContext context) {
|
||||
Region region = context.getRegion();
|
||||
|
||||
AtmosphereRegionData data = region.getModuleData()
|
||||
.get(MODULE_ID, AtmosphereRegionData.class)
|
||||
.orElseGet(AtmosphereRegionData::defaults);
|
||||
|
||||
// 1. Vegetation scrubs air pollution.
|
||||
VegetationRegionData veg = region.getModuleData()
|
||||
.get(VegetationModule.MODULE_ID, VegetationRegionData.class)
|
||||
.orElse(null);
|
||||
PollutionRegionData pollution = region.getModuleData()
|
||||
.get(PollutionModule.MODULE_ID, PollutionRegionData.class)
|
||||
.orElse(null);
|
||||
|
||||
if (veg != null && pollution != null) {
|
||||
double scrub = veg.getTreePressure() * TREE_SCRUB_RATE;
|
||||
pollution.addPollution(-scrub, 0.0, 0.0);
|
||||
region.getModuleData().put(PollutionModule.MODULE_ID, pollution);
|
||||
// Recompute pollution score now that trees have scrubbed the air.
|
||||
double pollScore = pollution.getAirPollution() * 0.40
|
||||
+ pollution.getGroundPollution() * 0.35
|
||||
+ pollution.getWaterPollution() * 0.25;
|
||||
region.getMetrics().setPollutionScore(pollScore);
|
||||
}
|
||||
|
||||
// 2. Compute targets.
|
||||
double ecosystemHealth = region.getMetrics().getEcosystemHealth();
|
||||
double pollScore = region.getMetrics().getPollutionScore();
|
||||
boolean mcRaining = globalRaining.getAsBoolean();
|
||||
|
||||
double seasonMod = currentSeason.get().rainModifier();
|
||||
double targetRain = clamp01(ecosystemHealth / 100.0 * MAX_RAIN_FROM_ECO
|
||||
+ seasonMod
|
||||
- globalWarmingPenalty.getAsDouble()
|
||||
+ (mcRaining ? GLOBAL_RAIN_BIAS : 0.0));
|
||||
double targetThunder = clamp01(pollScore / 100.0 * 0.8);
|
||||
|
||||
// 3. Smooth toward targets.
|
||||
double rain = data.getRainLevel() + (targetRain - data.getRainLevel()) * RAIN_SMOOTHING;
|
||||
double thunder = data.getThunderLevel() + (targetThunder - data.getThunderLevel()) * THUNDER_SMOOTHING;
|
||||
data.setRainLevel(rain);
|
||||
data.setThunderLevel(thunder);
|
||||
|
||||
// 4. Apply rain effects to water availability and drought risk.
|
||||
WaterRegionData water = region.getModuleData()
|
||||
.get(WaterModule.MODULE_ID, WaterRegionData.class)
|
||||
.orElse(null);
|
||||
if (water != null) {
|
||||
water.setWaterAvailability(
|
||||
Math.min(100, water.getWaterAvailability() + rain * RAIN_MOISTURE_GAIN));
|
||||
if (rain < DROUGHT_THRESHOLD) {
|
||||
water.setDroughtRisk(
|
||||
Math.min(100, water.getDroughtRisk() + DRY_DROUGHT_INCREASE * (1.0 - rain)));
|
||||
} else {
|
||||
water.setDroughtRisk(
|
||||
Math.max(0, water.getDroughtRisk() - RAIN_DROUGHT_RELIEF * rain));
|
||||
}
|
||||
|
||||
// 5. Acid rain: stormy + polluted atmosphere corrodes soil and water.
|
||||
if (thunder > ACID_THUNDER_THRESHOLD && pollScore > ACID_POLL_THRESHOLD) {
|
||||
double acidStrength = thunder * (pollScore - ACID_POLL_THRESHOLD) * ACID_FERTILITY_DRAIN;
|
||||
SoilRegionData soil = region.getModuleData()
|
||||
.get(SoilModule.MODULE_ID, SoilRegionData.class)
|
||||
.orElse(null);
|
||||
if (soil != null) {
|
||||
soil.setFertility(Math.max(0, soil.getFertility() - acidStrength));
|
||||
soil.setContamination(Math.min(100, soil.getContamination() + acidStrength * 0.5));
|
||||
region.getModuleData().put(SoilModule.MODULE_ID, soil);
|
||||
}
|
||||
water.setWaterAvailability(
|
||||
Math.max(0, water.getWaterAvailability() - acidStrength * 0.3));
|
||||
}
|
||||
region.getModuleData().put(WaterModule.MODULE_ID, water);
|
||||
}
|
||||
|
||||
region.getModuleData().put(MODULE_ID, data);
|
||||
return ModuleUpdateResult.changed();
|
||||
}
|
||||
|
||||
private static double clamp01(double v) { return Math.min(1.0, Math.max(0.0, v)); }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.livingworld.modules.atmosphere;
|
||||
|
||||
/**
|
||||
* Per-region atmospheric state tracked by {@link AtmosphereModule}.
|
||||
*
|
||||
* <p>Both values smoothly interpolate toward ecosystem-derived targets each
|
||||
* simulation cycle and are used to drive per-player weather packets.
|
||||
*
|
||||
* <ul>
|
||||
* <li><b>rainLevel</b> – 0.0 = drought, 1.0 = heavy rainfall
|
||||
* <li><b>thunderLevel</b> – 0.0 = calm, 1.0 = heavy storm (acid rain above 0.4)
|
||||
* </ul>
|
||||
*/
|
||||
public final class AtmosphereRegionData {
|
||||
|
||||
private double rainLevel;
|
||||
private double thunderLevel;
|
||||
|
||||
public AtmosphereRegionData(double rainLevel, double thunderLevel) {
|
||||
this.rainLevel = clamp(rainLevel);
|
||||
this.thunderLevel = clamp(thunderLevel);
|
||||
}
|
||||
|
||||
public static AtmosphereRegionData defaults() {
|
||||
return new AtmosphereRegionData(0.4, 0.0);
|
||||
}
|
||||
|
||||
public double getRainLevel() { return rainLevel; }
|
||||
public double getThunderLevel() { return thunderLevel; }
|
||||
|
||||
public void setRainLevel(double v) { this.rainLevel = clamp(v); }
|
||||
public void setThunderLevel(double v) { this.thunderLevel = clamp(v); }
|
||||
|
||||
public AtmosphereRegionData copy() {
|
||||
return new AtmosphereRegionData(rainLevel, thunderLevel);
|
||||
}
|
||||
|
||||
private static double clamp(double v) { return Math.min(1.0, Math.max(0.0, v)); }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AtmosphereRegionData{rain=" + rainLevel + ", thunder=" + thunderLevel + "}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.livingworld.modules.atmosphere;
|
||||
|
||||
/**
|
||||
* The four ecological seasons, derived from absolute Minecraft day count.
|
||||
*
|
||||
* <p>One full year = 32 Minecraft days (8 days per season). Season drives the
|
||||
* rain target modifier in {@link AtmosphereModule} and seasonal soil effects in
|
||||
* the bootstrap post-sim pass.
|
||||
*/
|
||||
public enum Season {
|
||||
|
||||
// rain temp drought vegGrowth
|
||||
SPRING( +0.10, +0.02, -0.05, +0.04),
|
||||
SUMMER( -0.15, +0.05, +0.03, +0.02),
|
||||
AUTUMN( 0.0, -0.01, 0.00, -0.02),
|
||||
WINTER( -0.25, -0.05, +0.02, -0.06);
|
||||
|
||||
private static final int SEASON_LENGTH_DAYS = 8;
|
||||
|
||||
/** Additive modifier applied to the atmosphere rain target this season. */
|
||||
private final double rainModifier;
|
||||
/** Per-cycle soil-temperature effect (positive = warming, negative = cooling). */
|
||||
private final double temperatureMod;
|
||||
/** Per-cycle drought risk delta (positive = drought worsens, negative = eases). */
|
||||
private final double droughtMod;
|
||||
/** Per-cycle vegetation growth delta (positive = growth, negative = senescence). */
|
||||
private final double vegGrowthMod;
|
||||
|
||||
Season(double rainModifier, double temperatureMod, double droughtMod, double vegGrowthMod) {
|
||||
this.rainModifier = rainModifier;
|
||||
this.temperatureMod = temperatureMod;
|
||||
this.droughtMod = droughtMod;
|
||||
this.vegGrowthMod = vegGrowthMod;
|
||||
}
|
||||
|
||||
public double rainModifier() { return rainModifier; }
|
||||
public double temperatureMod() { return temperatureMod; }
|
||||
public double droughtMod() { return droughtMod; }
|
||||
public double vegGrowthMod() { return vegGrowthMod; }
|
||||
|
||||
public String displayName() {
|
||||
String n = name();
|
||||
return n.charAt(0) + n.substring(1).toLowerCase();
|
||||
}
|
||||
|
||||
/** Returns the season for the given absolute Minecraft day number (gameTime / 24000). */
|
||||
public static Season fromAbsoluteDay(long absoluteDay) {
|
||||
return values()[(int) ((absoluteDay / SEASON_LENGTH_DAYS) % 4)];
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.livingworld.modules.pollution;
|
||||
|
||||
import com.livingworld.config.EcosystemTuning;
|
||||
import com.livingworld.core.services.CoreServices;
|
||||
import com.livingworld.data.serialization.PersistenceReader;
|
||||
import com.livingworld.data.serialization.PersistenceWriter;
|
||||
import com.livingworld.events.LivingWorldEvent;
|
||||
@@ -42,11 +44,11 @@ public final class PollutionModule implements SimulationModule {
|
||||
|
||||
public static final String MODULE_ID = "pollution";
|
||||
|
||||
private static final double BASE_DECAY_RATE = 0.008;
|
||||
private static final double GROUND_TO_WATER_LEACH = 0.005;
|
||||
private static final double WATER_QUALITY_IMPACT = 0.05;
|
||||
private static final double CHANGE_THRESHOLD = 0.01;
|
||||
|
||||
private EcosystemTuning tuning = new EcosystemTuning();
|
||||
|
||||
private static final ModuleMetadata METADATA = new ModuleMetadata(
|
||||
MODULE_ID,
|
||||
"Pollution",
|
||||
@@ -68,6 +70,7 @@ public final class PollutionModule implements SimulationModule {
|
||||
@Override
|
||||
public void initialize(ModuleContext context) {
|
||||
if (context == null) throw new IllegalArgumentException("context must not be null");
|
||||
if (context.hasService(CoreServices.TUNING)) this.tuning = context.getService(CoreServices.TUNING);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -94,10 +97,10 @@ public final class PollutionModule implements SimulationModule {
|
||||
double prevWaterQuality = region.getMetrics().getWaterQuality();
|
||||
|
||||
// Natural decay: air decays fastest, water slowest (modulated by resistance).
|
||||
data.decay(BASE_DECAY_RATE);
|
||||
data.decay(tuning.getPollutionDecayRate());
|
||||
|
||||
// Ground pollution slowly leaches into water even after decay.
|
||||
double leach = data.getGroundPollution() * GROUND_TO_WATER_LEACH;
|
||||
double leach = data.getGroundPollution() * tuning.getGroundToWaterLeach();
|
||||
data.addPollution(0.0, 0.0, leach);
|
||||
|
||||
// Summary metric: weighted average emphasising waterPollution as most damaging.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.livingworld.modules.recovery;
|
||||
|
||||
import com.livingworld.config.EcosystemTuning;
|
||||
import com.livingworld.core.services.CoreServices;
|
||||
import com.livingworld.data.serialization.PersistenceReader;
|
||||
import com.livingworld.data.serialization.PersistenceWriter;
|
||||
import com.livingworld.events.LivingWorldEvent;
|
||||
@@ -41,16 +43,12 @@ public final class RecoveryModule implements SimulationModule {
|
||||
|
||||
public static final String MODULE_ID = "recovery";
|
||||
|
||||
/** Base recovery progress per tick (added when conditions are met). */
|
||||
private static final double BASE_PROGRESS_PER_TICK = 0.5;
|
||||
/** Extra recovery progress per point of ecosystemHealth above 50. */
|
||||
private static final double HEALTH_PROGRESS_BONUS = 0.02;
|
||||
/** Damage accumulated per tick when conditions are badly violated. */
|
||||
private static final double DAMAGE_PER_BAD_TICK = 3.0;
|
||||
/** Damage decays this fraction per tick when conditions are OK. */
|
||||
private static final double DAMAGE_DECAY_RATE = 0.05;
|
||||
private static final double HEALTH_PROGRESS_BONUS = 0.05;
|
||||
private static final double CHANGE_THRESHOLD = 0.01;
|
||||
|
||||
private EcosystemTuning tuning = new EcosystemTuning();
|
||||
|
||||
private static final ModuleMetadata METADATA = new ModuleMetadata(
|
||||
MODULE_ID,
|
||||
"Recovery & Succession",
|
||||
@@ -72,6 +70,7 @@ public final class RecoveryModule implements SimulationModule {
|
||||
@Override
|
||||
public void initialize(ModuleContext context) {
|
||||
if (context == null) throw new IllegalArgumentException("context must not be null");
|
||||
if (context.hasService(CoreServices.TUNING)) this.tuning = context.getService(CoreServices.TUNING);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -104,22 +103,23 @@ public final class RecoveryModule implements SimulationModule {
|
||||
|
||||
if (data.getSuccessionStage().conditionsMetForAdvancement(soil, poll, veg)) {
|
||||
// Conditions are good: advance recovery progress.
|
||||
double progressGain = BASE_PROGRESS_PER_TICK;
|
||||
double progressGain = tuning.getBaseProgressPerTick();
|
||||
if (metrics.getEcosystemHealth() > 50.0) {
|
||||
progressGain += (metrics.getEcosystemHealth() - 50.0) * HEALTH_PROGRESS_BONUS;
|
||||
}
|
||||
data.advanceProgress(progressGain, soil, poll, veg);
|
||||
|
||||
// Damage decays passively when conditions are good (bypass accumulateDamage
|
||||
// to avoid triggering regression with a zero-damage call that might fire at 70+).
|
||||
// Damage decays passively when conditions are good. Use 4-arg constructor to
|
||||
// preserve the dynamic succession cap set by applyDynamicCapUpdate().
|
||||
data = new RecoveryRegionData(
|
||||
data.getSuccessionStage(),
|
||||
data.getRecoveryProgress(),
|
||||
Math.max(0.0, data.getDamageAccumulation() * (1.0 - DAMAGE_DECAY_RATE)));
|
||||
Math.max(0.0, data.getDamageAccumulation() * (1.0 - tuning.getDamageDecayRate())),
|
||||
data.getMaxSuccessionStage());
|
||||
|
||||
} else if (data.getSuccessionStage().conditionsMissedForRegression(soil, poll, veg)) {
|
||||
// Conditions are bad: accumulate damage toward regression.
|
||||
data.accumulateDamage(DAMAGE_PER_BAD_TICK, soil, poll, veg);
|
||||
data.accumulateDamage(tuning.getDamagePerBadTick(), soil, poll, veg);
|
||||
}
|
||||
|
||||
// Recovery pressure reflects distance from mature forest.
|
||||
|
||||
@@ -72,6 +72,23 @@ public final class RecoveryRegionData {
|
||||
}
|
||||
}
|
||||
|
||||
/** Advances one restoration stage without bypassing the region's biome cap. */
|
||||
public void boostOneStage() {
|
||||
if (successionStage.hasNext()
|
||||
&& successionStage.next().ordinal() <= maxSuccessionStage.ordinal()) {
|
||||
successionStage = successionStage.next();
|
||||
recoveryProgress = 0.0;
|
||||
damageAccumulation = Math.max(0.0, damageAccumulation - 20.0);
|
||||
}
|
||||
}
|
||||
|
||||
/** Permanently raises the ecological ceiling by one stage. */
|
||||
public void raiseCapOneStage() {
|
||||
if (maxSuccessionStage.hasNext()) {
|
||||
maxSuccessionStage = maxSuccessionStage.next();
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Mutation
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.livingworld.modules.vegetation;
|
||||
|
||||
import com.livingworld.config.EcosystemTuning;
|
||||
import com.livingworld.core.services.CoreServices;
|
||||
import com.livingworld.data.serialization.PersistenceReader;
|
||||
import com.livingworld.data.serialization.PersistenceWriter;
|
||||
import com.livingworld.events.LivingWorldEvent;
|
||||
@@ -38,29 +40,21 @@ public final class VegetationModule implements SimulationModule {
|
||||
|
||||
public static final String MODULE_ID = "vegetation";
|
||||
|
||||
// --- growth thresholds ---
|
||||
// --- growth thresholds (not tunable — structural to the succession model) ---
|
||||
private static final double GROWTH_SOIL_THRESHOLD = 35.0;
|
||||
private static final double GROWTH_POLLUTION_LIMIT = 40.0;
|
||||
private static final double SHRUB_UNLOCK_GRASS = 50.0;
|
||||
private static final double TREE_UNLOCK_SHRUB = 40.0;
|
||||
|
||||
// --- growth rates per tick ---
|
||||
private static final double GRASS_GROWTH_RATE = 0.015;
|
||||
private static final double FLOWER_GROWTH_RATE = 0.008;
|
||||
private static final double SHRUB_GROWTH_RATE = 0.005;
|
||||
private static final double TREE_GROWTH_RATE = 0.002;
|
||||
|
||||
// --- die-off thresholds ---
|
||||
private static final double DIEOFF_SOIL_THRESHOLD = 20.0;
|
||||
private static final double DIEOFF_POLLUTION_THRESHOLD = 30.0;
|
||||
private static final double GRASS_DIEOFF_RATE = 0.30;
|
||||
private static final double DEAD_ACCUMULATION_RATE = 0.20;
|
||||
|
||||
// --- decomposition ---
|
||||
private static final double DEAD_ACCUMULATION_RATE = 0.50;
|
||||
private static final double TREE_SEVERE_POLLUTION = 60.0;
|
||||
private static final double TREE_SEVERE_SOIL = 10.0;
|
||||
private static final double DEAD_DECOMPOSITION_RATE = 0.01;
|
||||
|
||||
private static final double CHANGE_THRESHOLD = 0.01;
|
||||
|
||||
private EcosystemTuning tuning = new EcosystemTuning();
|
||||
|
||||
private static final ModuleMetadata METADATA = new ModuleMetadata(
|
||||
MODULE_ID,
|
||||
"Vegetation",
|
||||
@@ -82,6 +76,7 @@ public final class VegetationModule implements SimulationModule {
|
||||
@Override
|
||||
public void initialize(ModuleContext context) {
|
||||
if (context == null) throw new IllegalArgumentException("context must not be null");
|
||||
if (context.hasService(CoreServices.TUNING)) this.tuning = context.getService(CoreServices.TUNING);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -116,21 +111,28 @@ public final class VegetationModule implements SimulationModule {
|
||||
if (goodConditions) {
|
||||
double soilBonus = metrics.getSoilQuality() - GROWTH_SOIL_THRESHOLD;
|
||||
|
||||
data.setGrassPressure(data.getGrassPressure() + soilBonus * GRASS_GROWTH_RATE);
|
||||
data.setFlowerPressure(data.getFlowerPressure() + soilBonus * FLOWER_GROWTH_RATE);
|
||||
data.setGrassPressure(data.getGrassPressure() + soilBonus * tuning.getGrassGrowthRate());
|
||||
data.setFlowerPressure(data.getFlowerPressure() + soilBonus * tuning.getFlowerGrowthRate());
|
||||
|
||||
// Shrubs grow only once grass is established.
|
||||
if (data.getGrassPressure() > SHRUB_UNLOCK_GRASS) {
|
||||
data.setShrubPressure(data.getShrubPressure() + soilBonus * SHRUB_GROWTH_RATE);
|
||||
data.setShrubPressure(data.getShrubPressure() + soilBonus * tuning.getShrubGrowthRate());
|
||||
}
|
||||
// Trees grow only once shrubs are established.
|
||||
if (data.getShrubPressure() > TREE_UNLOCK_SHRUB) {
|
||||
data.setTreePressure(data.getTreePressure() + soilBonus * TREE_GROWTH_RATE);
|
||||
data.setTreePressure(data.getTreePressure() + soilBonus * tuning.getTreeGrowthRate());
|
||||
}
|
||||
}
|
||||
|
||||
if (badConditions) {
|
||||
data.setGrassPressure(data.getGrassPressure() - GRASS_DIEOFF_RATE);
|
||||
data.setGrassPressure(data.getGrassPressure() - tuning.getGrassDieoffRate());
|
||||
data.setFlowerPressure(data.getFlowerPressure() - tuning.getFlowerDieoffRate());
|
||||
data.setShrubPressure(data.getShrubPressure() - tuning.getShrubDieoffRate());
|
||||
// Trees die only under severe stress to reflect their resilience.
|
||||
if (metrics.getPollutionScore() > TREE_SEVERE_POLLUTION
|
||||
|| metrics.getSoilQuality() < TREE_SEVERE_SOIL) {
|
||||
data.setTreePressure(data.getTreePressure() - tuning.getTreeDieoffRate());
|
||||
}
|
||||
data.setDeadVegetation(data.getDeadVegetation() + DEAD_ACCUMULATION_RATE);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,4 +39,194 @@ public enum WorldEffectType {
|
||||
* left to the platform layer.
|
||||
*/
|
||||
POLLUTION_VISUAL_INDICATOR,
|
||||
|
||||
/**
|
||||
* Drought combined with a storm triggers wildfire ignition — fire is placed
|
||||
* on flammable surface blocks and spreads naturally via Minecraft fire tick.
|
||||
*/
|
||||
WILDFIRE,
|
||||
|
||||
/**
|
||||
* Sustained bad conditions (low soil quality or high pollution) cause living
|
||||
* vegetation to die visibly: leaves are removed, upper log sections stripped to
|
||||
* stumps, and surface plants replaced with dead material.
|
||||
*/
|
||||
VEGETATION_DIES,
|
||||
|
||||
/**
|
||||
* Heavy regional rain on low-succession (barren / sparse-grass) terrain causes
|
||||
* water to pool in low-lying spots. The platform adapter places water source blocks
|
||||
* in surface depressions. Once present, the water body scan detects them and feeds
|
||||
* back a hydration boost to the region, enabling succession toward fertile land.
|
||||
*/
|
||||
WATER_POOL_FORMS,
|
||||
|
||||
/**
|
||||
* Sustained high-intensity water runoff erodes the river channel: the platform
|
||||
* adapter replaces natural surface blocks with water along the low-lying path
|
||||
* 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,
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
||||
/** Polluted rainfall weathers exposed rock and kills surface vegetation. */
|
||||
ACID_RAIN_DAMAGE,
|
||||
|
||||
/** Blizzard snowfall builds bounded snow layers on exposed terrain. */
|
||||
SNOW_ACCUMULATION,
|
||||
|
||||
/** Blizzard conditions freeze exposed standing water. */
|
||||
WATER_FREEZES,
|
||||
|
||||
/** Warm conditions remove accumulated seasonal snow one layer at a time. */
|
||||
SNOW_MELTS,
|
||||
|
||||
/** Dry high winds deposit sand and strip fragile surface plants. */
|
||||
SAND_DEPOSIT,
|
||||
|
||||
/** A registered geothermal site ejects a temporary column of water. */
|
||||
GEYSER_ERUPT,
|
||||
|
||||
/** Repeated geyser eruptions build a calcite and tuff travertine ring. */
|
||||
TRAVERTINE_DEPOSIT,
|
||||
|
||||
/** Low-magnitude seismic activity removes a short line of surface blocks. */
|
||||
GROUND_CRACK,
|
||||
|
||||
/** High-magnitude seismic activity opens a narrow, deep fissure. */
|
||||
FISSURE_OPENS,
|
||||
|
||||
/** Saturated ground collapses into a detected shallow cave. */
|
||||
SINKHOLE_COLLAPSES,
|
||||
|
||||
/** Unstable steep terrain transfers loose surface material downhill. */
|
||||
LANDSLIDE,
|
||||
|
||||
/** A cooling volcanic roof collapses to reveal a lava-adjacent cavity. */
|
||||
LAVA_TUBE_COLLAPSE,
|
||||
|
||||
/** A temporary surge places water along a low terrain path. */
|
||||
FLASH_FLOOD,
|
||||
|
||||
/** Seasonal thaw converts exposed ice back into water. */
|
||||
ICE_MELTS,
|
||||
|
||||
/** Sustained drought drains exposed river sources and hardens the bed. */
|
||||
RIVERBED_DRIES,
|
||||
|
||||
/** River sediment accumulates below sea level at an ocean mouth. */
|
||||
SEDIMENT_DEPOSIT,
|
||||
|
||||
/** A glacier toe pushes loose rock downhill. */
|
||||
GLACIER_ADVANCE,
|
||||
|
||||
/** Moving ice leaves polished andesite behind. */
|
||||
GLACIER_POLISH,
|
||||
|
||||
/** Falling river water deepens its impact basin. */
|
||||
PLUNGE_POOL_DEEPENS,
|
||||
|
||||
/** Waterlogged wetland soil slowly develops a persistent peat profile. */
|
||||
PEAT_FORMS,
|
||||
|
||||
/** Deciduous leaves enter a temporary autumn colour stage. */
|
||||
LEAVES_CHANGE_COLOUR,
|
||||
|
||||
/** Coloured deciduous leaves fall after the seasonal display. */
|
||||
LEAVES_FALL,
|
||||
|
||||
/** Exposed drought-baked dirt develops an impermeable clay crust. */
|
||||
SOIL_CRUSTS,
|
||||
|
||||
/** Heavy rain breaks clay crust back through gravel toward soil. */
|
||||
SOIL_CRUST_BREAKS,
|
||||
|
||||
/** Warming frozen subsoil collapses into wet mud and meltwater. */
|
||||
PERMAFROST_THAWS,
|
||||
|
||||
/** Deforested runoff replaces river sand with coarse gravel silt. */
|
||||
RIVER_SILTS,
|
||||
|
||||
/** Exhausted farmland fails into coarse dirt and loses its crop. */
|
||||
CROPLAND_EXHAUSTS,
|
||||
|
||||
/** Saturated groundwater enters a natural cave opening. */
|
||||
CAVE_FLOODS,
|
||||
|
||||
/** Volcanic mineralisation buries old ore and exposes a new shallow vein. */
|
||||
VEIN_SHIFTS,
|
||||
|
||||
/** Wet dripstone cave ceilings grow a pointed stalactite segment. */
|
||||
STALACTITE_GROWS,
|
||||
|
||||
/** Long-lived Nether portal influence converts nearby surface terrain. */
|
||||
NETHER_BLEED,
|
||||
|
||||
/** End portal influence introduces end stone, chorus growth and obsidian. */
|
||||
END_CORRUPTION,
|
||||
|
||||
/** Erosion or earthquakes uncover a recognized buried structure. */
|
||||
RUINS_EXPOSED,
|
||||
}
|
||||
|
||||
@@ -9,16 +9,21 @@ import com.livingworld.modules.ModuleUpdateResult;
|
||||
import com.livingworld.modules.RegionUpdateContext;
|
||||
import com.livingworld.modules.ServerContext;
|
||||
import com.livingworld.modules.SimulationModule;
|
||||
import com.livingworld.modules.atmosphere.AtmosphereModule;
|
||||
import com.livingworld.modules.atmosphere.AtmosphereRegionData;
|
||||
import com.livingworld.modules.recovery.RecoveryRegionData;
|
||||
import com.livingworld.modules.recovery.RecoveryModule;
|
||||
import com.livingworld.modules.recovery.SuccessionStage;
|
||||
import com.livingworld.modules.resources.ResourceDepletionModule;
|
||||
import com.livingworld.modules.resources.ResourceRegionData;
|
||||
import com.livingworld.modules.water.WaterModule;
|
||||
import com.livingworld.modules.water.WaterRegionData;
|
||||
import com.livingworld.regions.Region;
|
||||
import com.livingworld.regions.RegionMetrics;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Translates ecosystem simulation state into visible world change requests.
|
||||
@@ -58,11 +63,18 @@ public final class WorldEffectsModule implements SimulationModule {
|
||||
// Soil defaults to 60 so the gate is set at 75 to avoid blocking early effects.
|
||||
private static final double GRASS_DEGRADE_POLLUTION_MIN = 15.0;
|
||||
private static final double GRASS_DEGRADE_SOIL_MAX = 75.0;
|
||||
// Vegetation die-off: mirrors VegetationModule's bad-conditions thresholds.
|
||||
private static final double DIEOFF_SOIL_MAX = 20.0;
|
||||
private static final double DIEOFF_POLLUTION_MIN = 30.0;
|
||||
private static final double VEG_SPREAD_VEG_MIN = 60.0;
|
||||
private static final double VEG_SPREAD_SOIL_MIN = 50.0;
|
||||
private static final double SAPLING_SLOW_LOGGING_MIN = 50.0;
|
||||
// Smoke particles appear as soon as any meaningful pollution exists.
|
||||
private static final double POLLUTION_INDICATOR_MIN = 10.0;
|
||||
// Wildfire: lightning storm over a drought-stressed region.
|
||||
private static final double WILDFIRE_DROUGHT_MIN = 70.0;
|
||||
private static final double WILDFIRE_THUNDER_MIN = 0.5;
|
||||
private static final double WILDFIRE_CHANCE_PER_CYCLE = 0.01; // 1 % per sim cycle
|
||||
|
||||
private static final ModuleMetadata METADATA = new ModuleMetadata(
|
||||
MODULE_ID,
|
||||
@@ -77,6 +89,7 @@ public final class WorldEffectsModule implements SimulationModule {
|
||||
false);
|
||||
|
||||
private final List<WorldEffectConsumer> consumers = new ArrayList<>();
|
||||
private final Random wildfireRandom = new Random();
|
||||
|
||||
/**
|
||||
* Registers a consumer that will receive effect requests each simulation tick.
|
||||
@@ -93,6 +106,14 @@ public final class WorldEffectsModule implements SimulationModule {
|
||||
return Collections.unmodifiableList(consumers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly emits an effect request to all registered consumers from outside the
|
||||
* normal module update cycle (e.g. from the bootstrap post-sim hooks).
|
||||
*/
|
||||
public void queueEffect(WorldEffectRequest request) {
|
||||
emit(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModuleId() { return MODULE_ID; }
|
||||
|
||||
@@ -178,6 +199,55 @@ public final class WorldEffectsModule implements SimulationModule {
|
||||
emitted = true;
|
||||
}
|
||||
|
||||
// --- Effect 6: wildfire — drought + storm ignites surface vegetation ---
|
||||
AtmosphereRegionData atm = region.getModuleData()
|
||||
.get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class)
|
||||
.orElse(null);
|
||||
WaterRegionData water = region.getModuleData()
|
||||
.get(WaterModule.MODULE_ID, WaterRegionData.class)
|
||||
.orElse(null);
|
||||
if (atm != null && water != null
|
||||
&& water.getDroughtRisk() > WILDFIRE_DROUGHT_MIN
|
||||
&& atm.getThunderLevel() > WILDFIRE_THUNDER_MIN
|
||||
&& wildfireRandom.nextDouble() < WILDFIRE_CHANCE_PER_CYCLE) {
|
||||
double intensity = computeIntensity(atm.getThunderLevel() - WILDFIRE_THUNDER_MIN, 0.5)
|
||||
* computeIntensity(water.getDroughtRisk() - WILDFIRE_DROUGHT_MIN, 30.0);
|
||||
emit(new WorldEffectRequest(
|
||||
WorldEffectType.WILDFIRE, region.getCoordinate(), Math.max(0.1, intensity)));
|
||||
emitted = true;
|
||||
}
|
||||
|
||||
// --- Effect 7: vegetation dies under bad soil or heavy pollution ---
|
||||
// Mirrors the bad-conditions gate in VegetationModule so block changes
|
||||
// stay in sync with the data-layer die-off.
|
||||
boolean badSoil = m.getSoilQuality() < DIEOFF_SOIL_MAX;
|
||||
boolean badPollution = m.getPollutionScore() > DIEOFF_POLLUTION_MIN;
|
||||
if (badSoil || badPollution) {
|
||||
double intensitySoil = badSoil
|
||||
? computeIntensity(DIEOFF_SOIL_MAX - m.getSoilQuality(), DIEOFF_SOIL_MAX)
|
||||
: 0.0;
|
||||
double intensityPoll = badPollution
|
||||
? computeIntensity(m.getPollutionScore() - DIEOFF_POLLUTION_MIN, 70.0)
|
||||
: 0.0;
|
||||
double dieoffIntensity = Math.max(intensitySoil, intensityPoll);
|
||||
emit(new WorldEffectRequest(
|
||||
WorldEffectType.VEGETATION_DIES, region.getCoordinate(),
|
||||
Math.max(0.1, dieoffIntensity)));
|
||||
emitted = true;
|
||||
}
|
||||
|
||||
// --- Effect 8: rain pools in arid low-succession terrain ---
|
||||
// Heavy regional rain on barren/sparse-grass land cannot drain into vegetation;
|
||||
// water collects in depressions, forming puddles visible as actual water blocks.
|
||||
if (recovery != null && atm != null
|
||||
&& recovery.getSuccessionStage().ordinal() <= SuccessionStage.SPARSE_GRASS.ordinal()
|
||||
&& atm.getRainLevel() > 0.3) {
|
||||
double intensity = computeIntensity(atm.getRainLevel() - 0.3, 0.5);
|
||||
emit(new WorldEffectRequest(
|
||||
WorldEffectType.WATER_POOL_FORMS, region.getCoordinate(), Math.max(0.1, intensity)));
|
||||
emitted = true;
|
||||
}
|
||||
|
||||
return emitted ? ModuleUpdateResult.changed() : ModuleUpdateResult.noChange();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package com.livingworld.platform.neoforge;
|
||||
|
||||
import com.livingworld.config.EcosystemTuning;
|
||||
import net.neoforged.neoforge.common.ModConfigSpec;
|
||||
|
||||
/**
|
||||
* NeoForge server-side TOML configuration for the Living World ecosystem simulation.
|
||||
*
|
||||
* <p>Registered via {@link net.neoforged.fml.ModContainer#registerConfig} with type
|
||||
* {@link net.neoforged.fml.config.ModConfig.Type#SERVER}. The resulting file is written to
|
||||
* {@code saves/<world>/serverconfig/livingworld-server.toml} and can be edited by
|
||||
* server admins to tune all ecosystem rates without recompiling.
|
||||
*
|
||||
* <p>Call {@link #createTuning()} after the server config has loaded (i.e. from
|
||||
* {@link net.neoforged.neoforge.event.server.ServerStartingEvent}) to obtain an
|
||||
* {@link EcosystemTuning} populated with the current config values.
|
||||
*/
|
||||
public final class NeoForgeModConfig {
|
||||
|
||||
public static final ModConfigSpec SPEC;
|
||||
|
||||
// -- pollution --
|
||||
private static final ModConfigSpec.DoubleValue POLLUTION_DECAY_RATE;
|
||||
private static final ModConfigSpec.DoubleValue GROUND_TO_WATER_LEACH;
|
||||
private static final ModConfigSpec.DoubleValue POLLUTION_SPREAD_RATE;
|
||||
private static final ModConfigSpec.DoubleValue WIND_BOOST;
|
||||
|
||||
// -- vegetation growth --
|
||||
private static final ModConfigSpec.DoubleValue GRASS_GROWTH_RATE;
|
||||
private static final ModConfigSpec.DoubleValue FLOWER_GROWTH_RATE;
|
||||
private static final ModConfigSpec.DoubleValue SHRUB_GROWTH_RATE;
|
||||
private static final ModConfigSpec.DoubleValue TREE_GROWTH_RATE;
|
||||
|
||||
// -- vegetation die-off --
|
||||
private static final ModConfigSpec.DoubleValue GRASS_DIEOFF_RATE;
|
||||
private static final ModConfigSpec.DoubleValue FLOWER_DIEOFF_RATE;
|
||||
private static final ModConfigSpec.DoubleValue SHRUB_DIEOFF_RATE;
|
||||
private static final ModConfigSpec.DoubleValue TREE_DIEOFF_RATE;
|
||||
|
||||
// -- recovery succession --
|
||||
private static final ModConfigSpec.DoubleValue BASE_PROGRESS_PER_TICK;
|
||||
private static final ModConfigSpec.DoubleValue DAMAGE_PER_BAD_TICK;
|
||||
private static final ModConfigSpec.DoubleValue DAMAGE_DECAY_RATE;
|
||||
|
||||
// -- seed dispersal --
|
||||
private static final ModConfigSpec.DoubleValue SEED_EMISSION_RATE;
|
||||
private static final ModConfigSpec.DoubleValue CORRIDOR_BOOST_MULTIPLIER;
|
||||
private static final ModConfigSpec.DoubleValue SEED_POLLUTION_BLOCK;
|
||||
private static final ModConfigSpec.IntValue SEA_LEVEL;
|
||||
|
||||
static {
|
||||
ModConfigSpec.Builder b = new ModConfigSpec.Builder();
|
||||
|
||||
b.comment("Pollution dynamics").push("pollution");
|
||||
POLLUTION_DECAY_RATE = b
|
||||
.comment("Fraction of pollution removed per tick. Lower = stickier pollution. Default: 0.002")
|
||||
.defineInRange("decay_rate", 0.002, 0.0001, 0.5);
|
||||
GROUND_TO_WATER_LEACH = b
|
||||
.comment("Fraction of ground pollution that leaches into water each tick. Default: 0.005")
|
||||
.defineInRange("ground_to_water_leach", 0.005, 0.0, 0.1);
|
||||
POLLUTION_SPREAD_RATE = b
|
||||
.comment("Base rate at which air pollution diffuses to adjacent regions. Default: 0.02")
|
||||
.defineInRange("spread_rate", 0.02, 0.0, 0.5);
|
||||
WIND_BOOST = b
|
||||
.comment("Downwind spread multiplier (0 = no wind bias, 2 = strong directional drift). Default: 0.5")
|
||||
.defineInRange("wind_boost", 0.5, 0.0, 2.0);
|
||||
b.pop();
|
||||
|
||||
b.comment("Long-timescale hydrology").push("hydrology");
|
||||
SEA_LEVEL = b
|
||||
.comment("Baseline simulated sea level. Long climate cycles may drift this by +/-3 blocks.")
|
||||
.defineInRange("sea_level", 62, 1, 320);
|
||||
b.pop();
|
||||
|
||||
b.comment("Vegetation growth rates (per soil-quality unit above threshold per tick)").push("vegetation");
|
||||
GRASS_GROWTH_RATE = b.comment("Grass growth rate. Default: 0.06")
|
||||
.defineInRange("grass_growth", 0.06, 0.001, 2.0);
|
||||
FLOWER_GROWTH_RATE = b.comment("Flower growth rate. Default: 0.03")
|
||||
.defineInRange("flower_growth", 0.03, 0.001, 2.0);
|
||||
SHRUB_GROWTH_RATE = b.comment("Shrub growth rate. Default: 0.02")
|
||||
.defineInRange("shrub_growth", 0.02, 0.001, 2.0);
|
||||
TREE_GROWTH_RATE = b.comment("Tree growth rate. Default: 0.008")
|
||||
.defineInRange("tree_growth", 0.008, 0.001, 2.0);
|
||||
GRASS_DIEOFF_RATE = b.comment("Grass die-off flat reduction per bad tick. Default: 1.0")
|
||||
.defineInRange("grass_dieoff", 1.0, 0.0, 20.0);
|
||||
FLOWER_DIEOFF_RATE = b.comment("Flower die-off per bad tick. Default: 0.5")
|
||||
.defineInRange("flower_dieoff", 0.5, 0.0, 20.0);
|
||||
SHRUB_DIEOFF_RATE = b.comment("Shrub die-off per bad tick. Default: 0.25")
|
||||
.defineInRange("shrub_dieoff", 0.25, 0.0, 20.0);
|
||||
TREE_DIEOFF_RATE = b.comment("Tree die-off per bad tick (only under severe conditions). Default: 0.1")
|
||||
.defineInRange("tree_dieoff", 0.1, 0.0, 20.0);
|
||||
b.pop();
|
||||
|
||||
b.comment("Ecological succession and recovery").push("recovery");
|
||||
BASE_PROGRESS_PER_TICK = b
|
||||
.comment("Recovery progress added per tick under good conditions. Default: 2.0")
|
||||
.defineInRange("base_progress_per_tick", 2.0, 0.1, 50.0);
|
||||
DAMAGE_PER_BAD_TICK = b
|
||||
.comment("Ecological damage accumulated per tick under bad conditions. Default: 5.0")
|
||||
.defineInRange("damage_per_bad_tick", 5.0, 0.1, 100.0);
|
||||
DAMAGE_DECAY_RATE = b
|
||||
.comment("Fraction of accumulated damage that decays per tick when conditions are OK. Default: 0.03")
|
||||
.defineInRange("damage_decay_rate", 0.03, 0.001, 0.5);
|
||||
b.pop();
|
||||
|
||||
b.comment("Seed dispersal and ecological corridors").push("seed_dispersal");
|
||||
SEED_EMISSION_RATE = b
|
||||
.comment("Fraction of vegetation pressure emitted as seeds to each neighbour per tick (requires YOUNG_WOODLAND+). Default: 0.01")
|
||||
.defineInRange("emission_rate", 0.01, 0.0, 0.5);
|
||||
CORRIDOR_BOOST_MULTIPLIER = b
|
||||
.comment("Seed strength multiplier when the target region has 2+ healthy neighbours — the corridor effect. Default: 3.5")
|
||||
.defineInRange("corridor_boost", 3.5, 1.0, 20.0);
|
||||
SEED_POLLUTION_BLOCK = b
|
||||
.comment("Fraction of incoming seeds blocked per unit of pollution score in the target region. Default: 0.015")
|
||||
.defineInRange("pollution_block", 0.015, 0.0, 0.1);
|
||||
b.pop();
|
||||
|
||||
SPEC = b.build();
|
||||
}
|
||||
|
||||
private NeoForgeModConfig() {}
|
||||
|
||||
/** Creates an {@link EcosystemTuning} populated from the currently loaded config values. */
|
||||
public static EcosystemTuning createTuning() {
|
||||
EcosystemTuning t = new EcosystemTuning();
|
||||
t.setPollutionDecayRate(POLLUTION_DECAY_RATE.get());
|
||||
t.setGroundToWaterLeach(GROUND_TO_WATER_LEACH.get());
|
||||
t.setPollutionSpreadRate(POLLUTION_SPREAD_RATE.get());
|
||||
t.setWindBoost(WIND_BOOST.get());
|
||||
t.setGrassGrowthRate(GRASS_GROWTH_RATE.get());
|
||||
t.setFlowerGrowthRate(FLOWER_GROWTH_RATE.get());
|
||||
t.setShrubGrowthRate(SHRUB_GROWTH_RATE.get());
|
||||
t.setTreeGrowthRate(TREE_GROWTH_RATE.get());
|
||||
t.setGrassDieoffRate(GRASS_DIEOFF_RATE.get());
|
||||
t.setFlowerDieoffRate(FLOWER_DIEOFF_RATE.get());
|
||||
t.setShrubDieoffRate(SHRUB_DIEOFF_RATE.get());
|
||||
t.setTreeDieoffRate(TREE_DIEOFF_RATE.get());
|
||||
t.setBaseProgressPerTick(BASE_PROGRESS_PER_TICK.get());
|
||||
t.setDamagePerBadTick(DAMAGE_PER_BAD_TICK.get());
|
||||
t.setDamageDecayRate(DAMAGE_DECAY_RATE.get());
|
||||
t.setSeedEmissionRate(SEED_EMISSION_RATE.get());
|
||||
t.setCorridorBoostMultiplier(CORRIDOR_BOOST_MULTIPLIER.get());
|
||||
t.setSeedPollutionBlock(SEED_POLLUTION_BLOCK.get());
|
||||
t.setSeaLevel(SEA_LEVEL.get());
|
||||
return t;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user