Complete phase 4 hydrology expansion
This commit is contained in:
@@ -291,7 +291,7 @@ public class LivingWorldMod {
|
|||||||
RegionCoordinate spawnCoord = RegionCoordinate.fromBlock(
|
RegionCoordinate spawnCoord = RegionCoordinate.fromBlock(
|
||||||
dimId, (int) event.getX(), (int) event.getZ(),
|
dimId, (int) event.getX(), (int) event.getZ(),
|
||||||
LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
|
LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
|
||||||
if (bootstrap.isDeadZone(spawnCoord)
|
if (bootstrap.isAquaticSpawnSuppressed(spawnCoord)
|
||||||
&& (event.getEntity() instanceof AbstractFish
|
&& (event.getEntity() instanceof AbstractFish
|
||||||
|| event.getEntity() instanceof Squid
|
|| event.getEntity() instanceof Squid
|
||||||
|| event.getEntity() instanceof Dolphin)) {
|
|| event.getEntity() instanceof Dolphin)) {
|
||||||
@@ -563,7 +563,7 @@ public class LivingWorldMod {
|
|||||||
// -- Pre-compute tidal current for this tick --
|
// -- Pre-compute tidal current for this tick --
|
||||||
ServerLevel overworld = minecraftServer.overworld();
|
ServerLevel overworld = minecraftServer.overworld();
|
||||||
long dayTime = overworld.getDayTime();
|
long dayTime = overworld.getDayTime();
|
||||||
int seaLevel = overworld.getSeaLevel();
|
int seaLevel = bootstrap.getDynamicSeaLevel();
|
||||||
int moonPhase = overworld.getMoonPhase();
|
int moonPhase = overworld.getMoonPhase();
|
||||||
double springNeap = 1.0 + 0.40 * Math.cos(moonPhase * Math.PI / 4.0);
|
double springNeap = 1.0 + 0.40 * Math.cos(moonPhase * Math.PI / 4.0);
|
||||||
// Tidal current magnitude = derivative of tide height (max at mid-tide, zero at high/low)
|
// Tidal current magnitude = derivative of tide height (max at mid-tide, zero at high/low)
|
||||||
@@ -696,7 +696,7 @@ public class LivingWorldMod {
|
|||||||
if (minecraftServer == null) return;
|
if (minecraftServer == null) return;
|
||||||
ServerLevel overworld = minecraftServer.overworld();
|
ServerLevel overworld = minecraftServer.overworld();
|
||||||
long dayTime = overworld.getDayTime();
|
long dayTime = overworld.getDayTime();
|
||||||
int seaLevel = overworld.getSeaLevel();
|
int seaLevel = bootstrap.getDynamicSeaLevel();
|
||||||
|
|
||||||
// Moon phase (0=full, 4=new); full moon = maximum spring tides
|
// Moon phase (0=full, 4=new); full moon = maximum spring tides
|
||||||
int moonPhase = overworld.getMoonPhase();
|
int moonPhase = overworld.getMoonPhase();
|
||||||
|
|||||||
@@ -140,6 +140,11 @@ public final class LivingWorldBootstrap {
|
|||||||
private record GeyserSite(int cycleTick, boolean active) {}
|
private record GeyserSite(int cycleTick, boolean active) {}
|
||||||
private final Map<RegionCoordinate, GeyserSite> geyserSites = new HashMap<>();
|
private final Map<RegionCoordinate, GeyserSite> geyserSites = new HashMap<>();
|
||||||
private int geologicalActivityTick = 0;
|
private int geologicalActivityTick = 0;
|
||||||
|
private final Map<RegionCoordinate, Integer> dryCycles = new HashMap<>();
|
||||||
|
private final Map<RegionCoordinate, Double> previousEffectiveTemperature = new HashMap<>();
|
||||||
|
private final Map<RegionCoordinate, Integer> blizzardHistory = new HashMap<>();
|
||||||
|
private int hydrologyCycleTick = 0;
|
||||||
|
private int dynamicSeaLevel = 62;
|
||||||
|
|
||||||
private PlatformAdapter platformAdapter;
|
private PlatformAdapter platformAdapter;
|
||||||
private Path worldSaveDirectory;
|
private Path worldSaveDirectory;
|
||||||
@@ -273,7 +278,10 @@ public final class LivingWorldBootstrap {
|
|||||||
*/
|
*/
|
||||||
/** Sets the ecosystem tuning loaded from the server config. Must be called before onServerStarting(). */
|
/** Sets the ecosystem tuning loaded from the server config. Must be called before onServerStarting(). */
|
||||||
public void setEcosystemTuning(EcosystemTuning tuning) {
|
public void setEcosystemTuning(EcosystemTuning tuning) {
|
||||||
if (tuning != null) ecosystemTuning = tuning;
|
if (tuning != null) {
|
||||||
|
ecosystemTuning = tuning;
|
||||||
|
dynamicSeaLevel = tuning.getSeaLevel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onServerStopping() {
|
public void onServerStopping() {
|
||||||
@@ -309,6 +317,11 @@ public final class LivingWorldBootstrap {
|
|||||||
volcanicActivityTick = 0;
|
volcanicActivityTick = 0;
|
||||||
geyserSites.clear();
|
geyserSites.clear();
|
||||||
geologicalActivityTick = 0;
|
geologicalActivityTick = 0;
|
||||||
|
dryCycles.clear();
|
||||||
|
previousEffectiveTemperature.clear();
|
||||||
|
blizzardHistory.clear();
|
||||||
|
hydrologyCycleTick = 0;
|
||||||
|
dynamicSeaLevel = ecosystemTuning.getSeaLevel();
|
||||||
simSpeedMultiplier = 1;
|
simSpeedMultiplier = 1;
|
||||||
serverReady = false;
|
serverReady = false;
|
||||||
LivingWorldLogger.info(
|
LivingWorldLogger.info(
|
||||||
@@ -474,6 +487,147 @@ public final class LivingWorldBootstrap {
|
|||||||
if (++geologicalActivityTick % 4 == 0) {
|
if (++geologicalActivityTick % 4 == 0) {
|
||||||
applyGeologicalActivity();
|
applyGeologicalActivity();
|
||||||
}
|
}
|
||||||
|
applyHydrologyExpansion();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyHydrologyExpansion() {
|
||||||
|
Collection<Region> active = regionManager.getActiveRegions();
|
||||||
|
if (active.isEmpty() || worldEffectsModule == null) return;
|
||||||
|
hydrologyCycleTick++;
|
||||||
|
double warming = climateTracker.getWarmingLevel();
|
||||||
|
int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||||
|
Map<RegionCoordinate, Region> byCoord = new HashMap<>();
|
||||||
|
for (Region region : active) byCoord.put(region.getCoordinate(), region);
|
||||||
|
|
||||||
|
double totalRain = 0.0;
|
||||||
|
int rainSamples = 0;
|
||||||
|
for (Region region : active) {
|
||||||
|
RegionCoordinate coord = region.getCoordinate();
|
||||||
|
AtmosphereRegionData atmosphere = region.getModuleData()
|
||||||
|
.get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class).orElse(null);
|
||||||
|
WaterRegionData water = region.getModuleData()
|
||||||
|
.get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null);
|
||||||
|
RecoveryRegionData recovery = region.getModuleData()
|
||||||
|
.get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElse(null);
|
||||||
|
if (atmosphere == null || water == null || recovery == null) continue;
|
||||||
|
totalRain += atmosphere.getRainLevel() * 100.0;
|
||||||
|
rainSamples++;
|
||||||
|
|
||||||
|
double elevation = regionElevations.getOrDefault(coord, 64.0);
|
||||||
|
double effectiveTemperature = regionTemperatures.getOrDefault(coord, 0.8f) + warming * 0.2;
|
||||||
|
double previousTemperature = previousEffectiveTemperature.getOrDefault(
|
||||||
|
coord, effectiveTemperature);
|
||||||
|
previousEffectiveTemperature.put(coord, effectiveTemperature);
|
||||||
|
|
||||||
|
if (previousTemperature < 0.15 && effectiveTemperature >= 0.15) {
|
||||||
|
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||||
|
WorldEffectType.ICE_MELTS, coord, 0.8));
|
||||||
|
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||||
|
WorldEffectType.SNOW_MELTS, coord, 0.8));
|
||||||
|
groundwaterLevel.merge(coord, 12.0, Double::sum);
|
||||||
|
for (int[] offset : offsets) {
|
||||||
|
RegionCoordinate neighbour = new RegionCoordinate(
|
||||||
|
coord.dimensionId(), coord.x() + offset[0], coord.z() + offset[1]);
|
||||||
|
Double neighbourElevation = regionElevations.get(neighbour);
|
||||||
|
if (neighbourElevation != null && neighbourElevation < elevation - 3.0) {
|
||||||
|
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||||
|
WorldEffectType.GROUND_SPRING_EMERGES, neighbour, 0.7));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int dry = atmosphere.getRainLevel() < 0.15
|
||||||
|
? dryCycles.merge(coord, 1, Integer::sum) : 0;
|
||||||
|
if (dry == 0) dryCycles.remove(coord);
|
||||||
|
if (dry >= 20) {
|
||||||
|
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||||
|
WorldEffectType.RIVERBED_DRIES, coord,
|
||||||
|
Math.min(1.0, (dry - 15) / 30.0)));
|
||||||
|
groundwaterLevel.compute(coord,
|
||||||
|
(ignored, level) -> Math.max(0, (level == null ? 0.0 : level) - 2.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean lowSuccession = recovery.getSuccessionStage() == SuccessionStage.BARREN
|
||||||
|
|| recovery.getSuccessionStage() == SuccessionStage.SPARSE_GRASS;
|
||||||
|
double maxSlope = 0.0;
|
||||||
|
RegionCoordinate lowestNeighbour = null;
|
||||||
|
for (int[] offset : offsets) {
|
||||||
|
RegionCoordinate neighbour = new RegionCoordinate(
|
||||||
|
coord.dimensionId(), coord.x() + offset[0], coord.z() + offset[1]);
|
||||||
|
Double neighbourElevation = regionElevations.get(neighbour);
|
||||||
|
if (neighbourElevation != null && elevation - neighbourElevation > maxSlope) {
|
||||||
|
maxSlope = elevation - neighbourElevation;
|
||||||
|
lowestNeighbour = neighbour;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (atmosphere.getRainLevel() > 0.80 && lowSuccession && maxSlope > 10.0
|
||||||
|
&& windRandom.nextDouble() < 0.02
|
||||||
|
&& !isClimateEventActive(coord, ClimateEventType.FLASH_FLOOD)) {
|
||||||
|
ClimateEvent flood = new ClimateEvent(
|
||||||
|
ClimateEventType.FLASH_FLOOD, coord,
|
||||||
|
simulationManager.getSimulationTickCounter(), 0.8);
|
||||||
|
activeClimateEvents.add(flood);
|
||||||
|
pendingEventMessages.add("[LW] Flash flood at region ("
|
||||||
|
+ coord.x() + "," + coord.z() + ").");
|
||||||
|
}
|
||||||
|
|
||||||
|
double flow = riverFlowIntensity.getOrDefault(coord, 0.0);
|
||||||
|
if (flow > 20.0 && lowestNeighbour != null) {
|
||||||
|
if (oceanicRegions.contains(lowestNeighbour)) {
|
||||||
|
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||||
|
WorldEffectType.SEDIMENT_DEPOSIT, lowestNeighbour,
|
||||||
|
Math.min(1.0, flow / 100.0)));
|
||||||
|
pendingElevationResample.add(lowestNeighbour);
|
||||||
|
}
|
||||||
|
if (maxSlope > 10.0) {
|
||||||
|
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||||
|
WorldEffectType.PLUNGE_POOL_DEEPENS, lowestNeighbour,
|
||||||
|
Math.min(1.0, flow / 100.0)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isClimateEventActive(coord, ClimateEventType.BLIZZARD)) {
|
||||||
|
blizzardHistory.merge(coord, 1, Integer::sum);
|
||||||
|
}
|
||||||
|
int snowHistory = blizzardHistory.getOrDefault(coord, 0);
|
||||||
|
if (effectiveTemperature < 0.0 && elevation > 90.0 && snowHistory >= 10
|
||||||
|
&& hydrologyCycleTick % 50 == 0) {
|
||||||
|
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||||
|
WorldEffectType.GLACIER_ADVANCE, coord, 0.7));
|
||||||
|
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||||
|
WorldEffectType.GLACIER_POLISH, coord, 0.7));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isClimateEventActive(coord, ClimateEventType.ACID_RAIN) && flow > 10.0
|
||||||
|
&& lowestNeighbour != null && oceanicRegions.contains(lowestNeighbour)) {
|
||||||
|
Region downstream = byCoord.get(lowestNeighbour);
|
||||||
|
if (downstream != null) {
|
||||||
|
PollutionRegionData downstreamPollution = downstream.getModuleData()
|
||||||
|
.get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
|
||||||
|
if (downstreamPollution != null) {
|
||||||
|
double spike = geothermalRegions.contains(coord) ? 0.2 : 1.0;
|
||||||
|
downstreamPollution.addPollution(0, 0, spike);
|
||||||
|
downstream.getModuleData().put(PollutionModule.MODULE_ID, downstreamPollution);
|
||||||
|
regionManager.markDirty(downstream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hydrologyCycleTick % 500 == 0 && rainSamples > 0) {
|
||||||
|
double averageRain = totalRain / rainSamples;
|
||||||
|
int baseline = ecosystemTuning.getSeaLevel();
|
||||||
|
int oldSeaLevel = dynamicSeaLevel;
|
||||||
|
if (averageRain > 65.0 && dynamicSeaLevel < baseline + 3) dynamicSeaLevel++;
|
||||||
|
if (averageRain < 15.0 && dynamicSeaLevel > baseline - 3) dynamicSeaLevel--;
|
||||||
|
if (oldSeaLevel != dynamicSeaLevel) {
|
||||||
|
pendingEventMessages.add("[LW] Sea level changed to Y=" + dynamicSeaLevel + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDynamicSeaLevel() {
|
||||||
|
return dynamicSeaLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyGeologicalActivity() {
|
private void applyGeologicalActivity() {
|
||||||
@@ -983,6 +1137,15 @@ public final class LivingWorldBootstrap {
|
|||||||
return coord != null && deadZoneRegions.contains(coord);
|
return coord != null && deadZoneRegions.contains(coord);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isAquaticSpawnSuppressed(RegionCoordinate coord) {
|
||||||
|
if (coord == null || deadZoneRegions.contains(coord)) return coord != null;
|
||||||
|
return regionManager != null && regionManager.resolve(coord)
|
||||||
|
.flatMap(region -> region.getModuleData()
|
||||||
|
.get(PollutionModule.MODULE_ID, PollutionRegionData.class))
|
||||||
|
.map(pollution -> pollution.getWaterPollution() > 50.0)
|
||||||
|
.orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns whether a region is known to be oceanic. */
|
/** Returns whether a region is known to be oceanic. */
|
||||||
public boolean isOceanicRegion(RegionCoordinate coord) {
|
public boolean isOceanicRegion(RegionCoordinate coord) {
|
||||||
return coord != null && oceanicRegions.contains(coord);
|
return coord != null && oceanicRegions.contains(coord);
|
||||||
@@ -1795,6 +1958,13 @@ public final class LivingWorldBootstrap {
|
|||||||
}
|
}
|
||||||
if (ev.getTicksActive() >= 2) shouldResolve = true;
|
if (ev.getTicksActive() >= 2) shouldResolve = true;
|
||||||
}
|
}
|
||||||
|
case FLASH_FLOOD -> {
|
||||||
|
if (worldEffectsModule != null) {
|
||||||
|
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||||
|
WorldEffectType.FLASH_FLOOD, coord, ev.getSeverity()));
|
||||||
|
}
|
||||||
|
if (ev.getTicksActive() >= 5) shouldResolve = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ public enum ClimateEventType {
|
|||||||
SANDSTORM("Sandstorm", "Dry high winds stripping plants and depositing sand"),
|
SANDSTORM("Sandstorm", "Dry high winds stripping plants and depositing sand"),
|
||||||
LIGHTNING_STORM("Lightning Storm", "Dry thunderstorm producing isolated ignition strikes"),
|
LIGHTNING_STORM("Lightning Storm", "Dry thunderstorm producing isolated ignition strikes"),
|
||||||
EARTHQUAKE("Earthquake", "Seismic rupture opening cracks and unstable slopes"),
|
EARTHQUAKE("Earthquake", "Seismic rupture opening cracks and unstable slopes"),
|
||||||
SINKHOLE("Sinkhole", "Groundwater-driven collapse into a shallow cave");
|
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 displayName;
|
||||||
private final String description;
|
private final String description;
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ public final class EcosystemTuning {
|
|||||||
private double corridorBoostMultiplier = 3.5;
|
private double corridorBoostMultiplier = 3.5;
|
||||||
/** Seeds blocked per unit of pollution score in the target region. */
|
/** Seeds blocked per unit of pollution score in the target region. */
|
||||||
private double seedPollutionBlock = 0.015;
|
private double seedPollutionBlock = 0.015;
|
||||||
|
private int seaLevel = 62;
|
||||||
|
|
||||||
public EcosystemTuning() {}
|
public EcosystemTuning() {}
|
||||||
|
|
||||||
@@ -91,4 +92,7 @@ public final class EcosystemTuning {
|
|||||||
public void setSeedEmissionRate(double v) { seedEmissionRate = v; }
|
public void setSeedEmissionRate(double v) { seedEmissionRate = v; }
|
||||||
public void setCorridorBoostMultiplier(double v) { corridorBoostMultiplier = v; }
|
public void setCorridorBoostMultiplier(double v) { corridorBoostMultiplier = v; }
|
||||||
public void setSeedPollutionBlock(double v) { seedPollutionBlock = 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)); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,4 +166,25 @@ public enum WorldEffectType {
|
|||||||
|
|
||||||
/** A cooling volcanic roof collapses to reveal a lava-adjacent cavity. */
|
/** A cooling volcanic roof collapses to reveal a lava-adjacent cavity. */
|
||||||
LAVA_TUBE_COLLAPSE,
|
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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ public final class NeoForgeModConfig {
|
|||||||
private static final ModConfigSpec.DoubleValue SEED_EMISSION_RATE;
|
private static final ModConfigSpec.DoubleValue SEED_EMISSION_RATE;
|
||||||
private static final ModConfigSpec.DoubleValue CORRIDOR_BOOST_MULTIPLIER;
|
private static final ModConfigSpec.DoubleValue CORRIDOR_BOOST_MULTIPLIER;
|
||||||
private static final ModConfigSpec.DoubleValue SEED_POLLUTION_BLOCK;
|
private static final ModConfigSpec.DoubleValue SEED_POLLUTION_BLOCK;
|
||||||
|
private static final ModConfigSpec.IntValue SEA_LEVEL;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
ModConfigSpec.Builder b = new ModConfigSpec.Builder();
|
ModConfigSpec.Builder b = new ModConfigSpec.Builder();
|
||||||
@@ -65,6 +66,12 @@ public final class NeoForgeModConfig {
|
|||||||
.defineInRange("wind_boost", 0.5, 0.0, 2.0);
|
.defineInRange("wind_boost", 0.5, 0.0, 2.0);
|
||||||
b.pop();
|
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");
|
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")
|
GRASS_GROWTH_RATE = b.comment("Grass growth rate. Default: 0.06")
|
||||||
.defineInRange("grass_growth", 0.06, 0.001, 2.0);
|
.defineInRange("grass_growth", 0.06, 0.001, 2.0);
|
||||||
@@ -134,6 +141,7 @@ public final class NeoForgeModConfig {
|
|||||||
t.setSeedEmissionRate(SEED_EMISSION_RATE.get());
|
t.setSeedEmissionRate(SEED_EMISSION_RATE.get());
|
||||||
t.setCorridorBoostMultiplier(CORRIDOR_BOOST_MULTIPLIER.get());
|
t.setCorridorBoostMultiplier(CORRIDOR_BOOST_MULTIPLIER.get());
|
||||||
t.setSeedPollutionBlock(SEED_POLLUTION_BLOCK.get());
|
t.setSeedPollutionBlock(SEED_POLLUTION_BLOCK.get());
|
||||||
|
t.setSeaLevel(SEA_LEVEL.get());
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
|
|||||||
private final Random random = new Random();
|
private final Random random = new Random();
|
||||||
private record TimedWater(BlockPos pos, long expiresAt) {}
|
private record TimedWater(BlockPos pos, long expiresAt) {}
|
||||||
private final Map<ResourceKey<Level>, List<TimedWater>> geyserWater = new HashMap<>();
|
private final Map<ResourceKey<Level>, List<TimedWater>> geyserWater = new HashMap<>();
|
||||||
|
private final Map<ResourceKey<Level>, List<TimedWater>> floodWater = new HashMap<>();
|
||||||
|
|
||||||
public NeoForgeWorldEffectExecutor(Supplier<MinecraftServer> serverSupplier) {
|
public NeoForgeWorldEffectExecutor(Supplier<MinecraftServer> serverSupplier) {
|
||||||
this.serverSupplier = serverSupplier;
|
this.serverSupplier = serverSupplier;
|
||||||
@@ -65,6 +66,7 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
|
|||||||
int baseX = request.region().x() * REGION_BLOCKS;
|
int baseX = request.region().x() * REGION_BLOCKS;
|
||||||
int baseZ = request.region().z() * REGION_BLOCKS;
|
int baseZ = request.region().z() * REGION_BLOCKS;
|
||||||
cleanExpiredGeyserWater(level, dimensionKey);
|
cleanExpiredGeyserWater(level, dimensionKey);
|
||||||
|
cleanExpiredFloodWater(level, dimensionKey);
|
||||||
|
|
||||||
switch (request.type()) {
|
switch (request.type()) {
|
||||||
case GRASS_DEGRADES_TO_DIRT ->
|
case GRASS_DEGRADES_TO_DIRT ->
|
||||||
@@ -134,6 +136,20 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
|
|||||||
landslide(level, baseX, baseZ, request.intensity());
|
landslide(level, baseX, baseZ, request.intensity());
|
||||||
case LAVA_TUBE_COLLAPSE ->
|
case LAVA_TUBE_COLLAPSE ->
|
||||||
collapseLavaTube(level, baseX, baseZ, request.intensity());
|
collapseLavaTube(level, baseX, baseZ, request.intensity());
|
||||||
|
case FLASH_FLOOD ->
|
||||||
|
flashFlood(level, dimensionKey, baseX, baseZ, request.intensity());
|
||||||
|
case ICE_MELTS ->
|
||||||
|
meltIce(level, baseX, baseZ, request.intensity());
|
||||||
|
case RIVERBED_DRIES ->
|
||||||
|
dryRiverbed(level, baseX, baseZ, request.intensity());
|
||||||
|
case SEDIMENT_DEPOSIT ->
|
||||||
|
depositSediment(level, baseX, baseZ, request.intensity());
|
||||||
|
case GLACIER_ADVANCE ->
|
||||||
|
advanceGlacier(level, baseX, baseZ, request.intensity());
|
||||||
|
case GLACIER_POLISH ->
|
||||||
|
polishGlacierRock(level, baseX, baseZ, request.intensity());
|
||||||
|
case PLUNGE_POOL_DEEPENS ->
|
||||||
|
deepenPlungePool(level, baseX, baseZ, request.intensity());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -991,6 +1007,146 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
|
|||||||
|| state.is(Blocks.BASALT);
|
|| state.is(Blocks.BASALT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void cleanExpiredFloodWater(ServerLevel level, ResourceKey<Level> dimensionKey) {
|
||||||
|
List<TimedWater> entries = floodWater.get(dimensionKey);
|
||||||
|
if (entries == null) return;
|
||||||
|
long now = level.getGameTime();
|
||||||
|
entries.removeIf(entry -> {
|
||||||
|
if (entry.expiresAt() > now) return false;
|
||||||
|
if (level.isLoaded(entry.pos()) && level.getBlockState(entry.pos()).is(Blocks.WATER)) {
|
||||||
|
level.setBlock(entry.pos(), Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (entries.isEmpty()) floodWater.remove(dimensionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flashFlood(ServerLevel level, ResourceKey<Level> dimensionKey,
|
||||||
|
int baseX, int baseZ, double intensity) {
|
||||||
|
if (random.nextDouble() > 0.35) return;
|
||||||
|
List<TimedWater> entries = floodWater.computeIfAbsent(dimensionKey, ignored -> new ArrayList<>());
|
||||||
|
BlockPos lowest = null;
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
BlockPos sample = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS),
|
||||||
|
baseZ + random.nextInt(REGION_BLOCKS));
|
||||||
|
if (sample != null && (lowest == null || sample.getY() < lowest.getY())) lowest = sample;
|
||||||
|
}
|
||||||
|
if (lowest == null) return;
|
||||||
|
int writes = 0;
|
||||||
|
for (Direction direction : Direction.Plane.HORIZONTAL) {
|
||||||
|
BlockPos pos = lowest.relative(direction).above();
|
||||||
|
if (writes >= 5) break;
|
||||||
|
if (level.isLoaded(pos) && level.getBlockState(pos).isAir()) {
|
||||||
|
level.setBlock(pos, Blocks.WATER.defaultBlockState(), Block.UPDATE_ALL);
|
||||||
|
entries.add(new TimedWater(pos, level.getGameTime() + 6000));
|
||||||
|
writes++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void meltIce(ServerLevel level, int baseX, int baseZ, double intensity) {
|
||||||
|
for (int i = 0; i < Math.max(2, (int) (intensity * 8)); i++) {
|
||||||
|
int x = baseX + random.nextInt(REGION_BLOCKS);
|
||||||
|
int z = baseZ + random.nextInt(REGION_BLOCKS);
|
||||||
|
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z) - 1;
|
||||||
|
BlockPos pos = new BlockPos(x, y, z);
|
||||||
|
if (level.isLoaded(pos) && level.getBlockState(pos).is(Blocks.ICE)) {
|
||||||
|
level.setBlock(pos, Blocks.WATER.defaultBlockState(), Block.UPDATE_ALL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dryRiverbed(ServerLevel level, int baseX, int baseZ, double intensity) {
|
||||||
|
if (random.nextDouble() > 0.25) return;
|
||||||
|
for (int i = 0; i < Math.max(2, (int) (intensity * 8)); i++) {
|
||||||
|
int x = baseX + random.nextInt(REGION_BLOCKS);
|
||||||
|
int z = baseZ + random.nextInt(REGION_BLOCKS);
|
||||||
|
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z) - 1;
|
||||||
|
BlockPos pos = new BlockPos(x, y, z);
|
||||||
|
if (!level.isLoaded(pos) || !level.getBlockState(pos).is(Blocks.WATER)
|
||||||
|
|| !level.getFluidState(pos).isSource()) continue;
|
||||||
|
BlockPos below = pos.below();
|
||||||
|
var belowState = level.getBlockState(below);
|
||||||
|
if (belowState.is(Blocks.SAND) || belowState.is(Blocks.DIRT)
|
||||||
|
|| belowState.is(Blocks.GRAVEL) || belowState.is(Blocks.STONE)) {
|
||||||
|
level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
|
||||||
|
level.setBlock(below, random.nextBoolean() ? Blocks.COARSE_DIRT.defaultBlockState()
|
||||||
|
: Blocks.GRAVEL.defaultBlockState(), Block.UPDATE_ALL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void depositSediment(ServerLevel level, int baseX, int baseZ, double intensity) {
|
||||||
|
if (random.nextDouble() > 0.25) return;
|
||||||
|
for (int i = 0; i < Math.max(1, (int) (intensity * 5)); i++) {
|
||||||
|
int x = baseX + random.nextInt(REGION_BLOCKS);
|
||||||
|
int z = baseZ + random.nextInt(REGION_BLOCKS);
|
||||||
|
int y = level.getHeight(Heightmap.Types.OCEAN_FLOOR, x, z);
|
||||||
|
if (y >= level.getSeaLevel()) continue;
|
||||||
|
BlockPos pos = new BlockPos(x, y, z);
|
||||||
|
if (level.isLoaded(pos) && level.getFluidState(pos).is(FluidTags.WATER)) {
|
||||||
|
level.setBlock(pos, random.nextBoolean() ? Blocks.SAND.defaultBlockState()
|
||||||
|
: Blocks.GRAVEL.defaultBlockState(), Block.UPDATE_ALL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void advanceGlacier(ServerLevel level, int baseX, int baseZ, double intensity) {
|
||||||
|
BlockPos snow = null;
|
||||||
|
BlockPos low = null;
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
BlockPos sample = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS),
|
||||||
|
baseZ + random.nextInt(REGION_BLOCKS));
|
||||||
|
if (sample == null) continue;
|
||||||
|
if ((level.getBlockState(sample).is(Blocks.SNOW_BLOCK)
|
||||||
|
|| level.getBlockState(sample).is(Blocks.ICE))
|
||||||
|
&& (snow == null || sample.getY() < snow.getY())) snow = sample;
|
||||||
|
if (low == null || sample.getY() < low.getY()) low = sample;
|
||||||
|
}
|
||||||
|
if (snow == null || low == null) return;
|
||||||
|
int dx = Integer.compare(low.getX(), snow.getX());
|
||||||
|
int dz = Integer.compare(low.getZ(), snow.getZ());
|
||||||
|
for (Direction direction : Direction.Plane.HORIZONTAL) {
|
||||||
|
BlockPos source = snow.relative(direction);
|
||||||
|
var sourceState = level.getBlockState(source);
|
||||||
|
if (!sourceState.is(Blocks.GRAVEL) && !sourceState.is(Blocks.STONE)) continue;
|
||||||
|
BlockPos target = source.offset(dx, 0, dz);
|
||||||
|
if (level.isLoaded(target) && level.getBlockState(target).isAir()) {
|
||||||
|
level.setBlock(source, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
|
||||||
|
level.setBlock(target, sourceState, Block.UPDATE_ALL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void polishGlacierRock(ServerLevel level, int baseX, int baseZ, double intensity) {
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
BlockPos pos = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS),
|
||||||
|
baseZ + random.nextInt(REGION_BLOCKS));
|
||||||
|
if (pos != null && (level.getBlockState(pos).is(Blocks.STONE)
|
||||||
|
|| level.getBlockState(pos).is(Blocks.ANDESITE))) {
|
||||||
|
level.setBlock(pos, Blocks.POLISHED_ANDESITE.defaultBlockState(), Block.UPDATE_ALL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deepenPlungePool(ServerLevel level, int baseX, int baseZ, double intensity) {
|
||||||
|
if (random.nextDouble() > 0.25) return;
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
int x = baseX + random.nextInt(REGION_BLOCKS);
|
||||||
|
int z = baseZ + random.nextInt(REGION_BLOCKS);
|
||||||
|
int surfaceY = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z) - 1;
|
||||||
|
BlockPos water = new BlockPos(x, surfaceY, z);
|
||||||
|
if (!level.isLoaded(water) || !level.getFluidState(water).is(FluidTags.WATER)) continue;
|
||||||
|
BlockPos base = water.below();
|
||||||
|
var state = level.getBlockState(base);
|
||||||
|
if (state.is(Blocks.STONE) || state.is(Blocks.GRAVEL) || state.is(Blocks.SAND)) {
|
||||||
|
level.setBlock(base, Blocks.WATER.defaultBlockState(), Block.UPDATE_ALL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private BlockPos surfaceAt(ServerLevel level, int x, int z) {
|
private BlockPos surfaceAt(ServerLevel level, int x, int z) {
|
||||||
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1;
|
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1;
|
||||||
if (y < level.getMinBuildHeight()) {
|
if (y < level.getMinBuildHeight()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user