Complete phase 10 dimensional extensions
This commit is contained in:
@@ -9,6 +9,7 @@ import net.neoforged.neoforge.common.NeoForge;
|
||||
import net.neoforged.neoforge.event.entity.living.FinalizeSpawnEvent;
|
||||
import net.neoforged.neoforge.event.entity.living.LivingDeathEvent;
|
||||
import net.neoforged.neoforge.event.entity.EntityLeaveLevelEvent;
|
||||
import net.neoforged.neoforge.event.entity.EntityTravelToDimensionEvent;
|
||||
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.neoforged.neoforge.event.level.BlockEvent;
|
||||
import net.neoforged.neoforge.event.level.ChunkEvent;
|
||||
@@ -38,6 +39,7 @@ import net.minecraft.world.entity.monster.Monster;
|
||||
import net.minecraft.world.entity.MobSpawnType;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
|
||||
@@ -163,7 +165,8 @@ public class LivingWorldMod {
|
||||
bootstrap.getWorldEffectsModule().registerConsumer(
|
||||
new NeoForgeWorldEffectExecutor(
|
||||
() -> minecraftServer,
|
||||
coord -> !bootstrap.isPollinatorCollapse(coord)));
|
||||
coord -> !bootstrap.isPollinatorCollapse(coord),
|
||||
bootstrap::notifyAncientRuinsExposed));
|
||||
bootstrap.setOverworldRaining(
|
||||
() -> minecraftServer != null && minecraftServer.overworld().isRaining());
|
||||
bootstrap.setAbsoluteDaySupplier(
|
||||
@@ -369,6 +372,16 @@ public class LivingWorldMod {
|
||||
RegionCoordinate coord = trackedPassiveMobs.remove(event.getEntity().getUUID());
|
||||
if (coord != null) bootstrap.recordPassiveMobDeparture(coord);
|
||||
});
|
||||
NeoForge.EVENT_BUS.addListener(EntityTravelToDimensionEvent.class, event -> {
|
||||
if (!bootstrap.isServerReady() || !(event.getEntity().level() instanceof ServerLevel level)) return;
|
||||
boolean toNether = event.getDimension() == Level.NETHER;
|
||||
boolean toEnd = event.getDimension() == Level.END;
|
||||
if (!toNether && !toEnd) return;
|
||||
BlockPos pos = event.getEntity().blockPosition();
|
||||
bootstrap.registerPortalTravel(
|
||||
level.dimension().location().toString(),
|
||||
pos.getX(), pos.getZ(), toNether);
|
||||
});
|
||||
|
||||
// Step 4: Agriculture — bone meal boosts soil fertility.
|
||||
NeoForge.EVENT_BUS.addListener(PlayerInteractEvent.RightClickBlock.class, event -> {
|
||||
@@ -754,6 +767,11 @@ public class LivingWorldMod {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (bootstrap.isNetherBleedActive(coord) && random.nextInt(6) == 0) {
|
||||
playAmbientSound(player, Holder.direct(SoundEvents.FIRE_AMBIENT),
|
||||
0.18f, 0.75f + random.nextFloat() * 0.25f);
|
||||
return true;
|
||||
}
|
||||
|
||||
float pitch = 0.75f + random.nextFloat() * 0.5f;
|
||||
if (stage != null && stage.ordinal() <= SuccessionStage.SPARSE_GRASS.ordinal()
|
||||
|
||||
@@ -165,6 +165,15 @@ public final class LivingWorldBootstrap {
|
||||
private final Set<RegionCoordinate> exhaustedFarmlandRegions = new HashSet<>();
|
||||
private final Map<RegionCoordinate, Integer> farmingInactiveCycles = new HashMap<>();
|
||||
private int undergroundCycleTick = 0;
|
||||
private enum PortalInfluence { NETHER, END }
|
||||
private record PortalSite(String dimensionId, int blockX, int blockZ, PortalInfluence influence) {}
|
||||
private final Set<PortalSite> portalSites = new HashSet<>();
|
||||
private final Map<RegionCoordinate, Integer> netherExposureCycles = new HashMap<>();
|
||||
private final Map<RegionCoordinate, Integer> endExposureCycles = new HashMap<>();
|
||||
private final Set<RegionCoordinate> netherBleedRegions = new HashSet<>();
|
||||
private final Set<RegionCoordinate> endCorruptionRegions = new HashSet<>();
|
||||
private final Map<RegionCoordinate, Integer> erosionExposureCycles = new HashMap<>();
|
||||
private final Set<RegionCoordinate> exposedRuinsRegions = new HashSet<>();
|
||||
|
||||
private PlatformAdapter platformAdapter;
|
||||
private Path worldSaveDirectory;
|
||||
@@ -362,6 +371,13 @@ public final class LivingWorldBootstrap {
|
||||
exhaustedFarmlandRegions.clear();
|
||||
farmingInactiveCycles.clear();
|
||||
undergroundCycleTick = 0;
|
||||
portalSites.clear();
|
||||
netherExposureCycles.clear();
|
||||
endExposureCycles.clear();
|
||||
netherBleedRegions.clear();
|
||||
endCorruptionRegions.clear();
|
||||
erosionExposureCycles.clear();
|
||||
exposedRuinsRegions.clear();
|
||||
simSpeedMultiplier = 1;
|
||||
serverReady = false;
|
||||
LivingWorldLogger.info(
|
||||
@@ -536,6 +552,89 @@ public final class LivingWorldBootstrap {
|
||||
applyLongCycleEffects();
|
||||
applyPlayerFeedbackLoops();
|
||||
applyUndergroundSystems();
|
||||
applyFantasticalExtensions();
|
||||
}
|
||||
|
||||
public void registerPortalTravel(
|
||||
String dimensionId, int blockX, int blockZ, boolean netherPortal) {
|
||||
if (dimensionId == null) return;
|
||||
portalSites.add(new PortalSite(
|
||||
dimensionId, blockX, blockZ,
|
||||
netherPortal ? PortalInfluence.NETHER : PortalInfluence.END));
|
||||
}
|
||||
|
||||
public boolean isNetherBleedActive(RegionCoordinate coord) {
|
||||
return coord != null && netherBleedRegions.contains(coord);
|
||||
}
|
||||
|
||||
public void notifyAncientRuinsExposed(RegionCoordinate coord) {
|
||||
if (coord != null && exposedRuinsRegions.add(coord)) {
|
||||
pendingEventMessages.add("[LW] Ancient ruins uncovered at ("
|
||||
+ coord.x() + "," + coord.z() + ")!");
|
||||
}
|
||||
}
|
||||
|
||||
private void applyFantasticalExtensions() {
|
||||
if (worldEffectsModule == null) return;
|
||||
for (Region region : regionManager.getActiveRegions()) {
|
||||
RegionCoordinate coord = region.getCoordinate();
|
||||
boolean nearNether = false;
|
||||
boolean nearEnd = false;
|
||||
int minX = coord.x() * LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16;
|
||||
int minZ = coord.z() * LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16;
|
||||
int maxX = minX + LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16 - 1;
|
||||
int maxZ = minZ + LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16 - 1;
|
||||
for (PortalSite site : portalSites) {
|
||||
if (!site.dimensionId().equals(coord.dimensionId())) continue;
|
||||
int dx = site.blockX() < minX ? minX - site.blockX()
|
||||
: site.blockX() > maxX ? site.blockX() - maxX : 0;
|
||||
int dz = site.blockZ() < minZ ? minZ - site.blockZ()
|
||||
: site.blockZ() > maxZ ? site.blockZ() - maxZ : 0;
|
||||
if (Math.max(dx, dz) > 48) continue;
|
||||
nearNether |= site.influence() == PortalInfluence.NETHER;
|
||||
nearEnd |= site.influence() == PortalInfluence.END;
|
||||
}
|
||||
|
||||
if (nearNether) {
|
||||
int cycles = netherExposureCycles.merge(coord, 1, Integer::sum);
|
||||
if (cycles >= 50) {
|
||||
netherBleedRegions.add(coord);
|
||||
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||
WorldEffectType.NETHER_BLEED, coord,
|
||||
Math.min(1.0, cycles / 150.0)));
|
||||
}
|
||||
}
|
||||
if (nearEnd) {
|
||||
int cycles = endExposureCycles.merge(coord, 1, Integer::sum);
|
||||
if (cycles >= 50) {
|
||||
endCorruptionRegions.add(coord);
|
||||
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||
WorldEffectType.END_CORRUPTION, coord,
|
||||
Math.min(1.0, cycles / 150.0)));
|
||||
}
|
||||
}
|
||||
|
||||
double flow = riverFlowIntensity.getOrDefault(coord, 0.0);
|
||||
boolean majorQuake = false;
|
||||
for (ClimateEvent event : activeClimateEvents) {
|
||||
if (event.getType() == ClimateEventType.EARTHQUAKE
|
||||
&& event.getSeverity() >= 0.8
|
||||
&& event.getAffectedRegions().contains(coord)) {
|
||||
majorQuake = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
int erosionCycles = flow > RIVER_CARVE_THRESHOLD
|
||||
? erosionExposureCycles.merge(coord, 1, Integer::sum) : 0;
|
||||
if (flow <= RIVER_CARVE_THRESHOLD) erosionExposureCycles.remove(coord);
|
||||
if ((erosionCycles >= 20 || majorQuake)
|
||||
&& !exposedRuinsRegions.contains(coord)
|
||||
&& windRandom.nextDouble() < 0.05) {
|
||||
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||
WorldEffectType.RUINS_EXPOSED, coord,
|
||||
majorQuake ? 1.0 : Math.min(1.0, flow / 160.0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyUndergroundSystems() {
|
||||
|
||||
@@ -220,4 +220,13 @@ public enum WorldEffectType {
|
||||
|
||||
/** 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,
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Consumer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.particles.ParticleTypes;
|
||||
@@ -45,19 +46,29 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
|
||||
private final Supplier<MinecraftServer> serverSupplier;
|
||||
private final Random random = new Random();
|
||||
private final Predicate<com.livingworld.regions.RegionCoordinate> flowersAllowed;
|
||||
private final Consumer<com.livingworld.regions.RegionCoordinate> ruinsExposedCallback;
|
||||
private record TimedWater(BlockPos pos, long expiresAt) {}
|
||||
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) {
|
||||
this(serverSupplier, ignored -> true);
|
||||
this(serverSupplier, ignored -> true, ignored -> {});
|
||||
}
|
||||
|
||||
public NeoForgeWorldEffectExecutor(
|
||||
Supplier<MinecraftServer> serverSupplier,
|
||||
Predicate<com.livingworld.regions.RegionCoordinate> flowersAllowed) {
|
||||
this(serverSupplier, flowersAllowed, ignored -> {});
|
||||
}
|
||||
|
||||
public NeoForgeWorldEffectExecutor(
|
||||
Supplier<MinecraftServer> serverSupplier,
|
||||
Predicate<com.livingworld.regions.RegionCoordinate> flowersAllowed,
|
||||
Consumer<com.livingworld.regions.RegionCoordinate> ruinsExposedCallback) {
|
||||
this.serverSupplier = serverSupplier;
|
||||
this.flowersAllowed = flowersAllowed != null ? flowersAllowed : ignored -> true;
|
||||
this.ruinsExposedCallback = ruinsExposedCallback != null
|
||||
? ruinsExposedCallback : ignored -> {};
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -183,6 +194,12 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
|
||||
shiftMineralVein(level, baseX, baseZ, request.intensity());
|
||||
case STALACTITE_GROWS ->
|
||||
growStalactite(level, baseX, baseZ, request.intensity());
|
||||
case NETHER_BLEED ->
|
||||
netherBleed(level, baseX, baseZ, request.intensity());
|
||||
case END_CORRUPTION ->
|
||||
endCorruption(level, baseX, baseZ, request.intensity());
|
||||
case RUINS_EXPOSED ->
|
||||
exposeRuins(level, request.region(), baseX, baseZ, request.intensity());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1397,6 +1414,93 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
|
||||
}
|
||||
}
|
||||
|
||||
private void netherBleed(ServerLevel level, int baseX, int baseZ, double intensity) {
|
||||
if (random.nextDouble() > 0.20) return;
|
||||
for (int i = 0; i < Math.max(2, (int) (intensity * 8)); i++) {
|
||||
BlockPos pos = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS),
|
||||
baseZ + random.nextInt(REGION_BLOCKS));
|
||||
if (pos == null) continue;
|
||||
var state = level.getBlockState(pos);
|
||||
if (state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.DIRT)
|
||||
|| state.is(Blocks.STONE) || state.is(Blocks.GRAVEL)) {
|
||||
Block replacement = random.nextInt(5) == 0 ? Blocks.SOUL_SAND : Blocks.NETHERRACK;
|
||||
level.setBlock(pos, replacement.defaultBlockState(), Block.UPDATE_ALL);
|
||||
BlockPos above = pos.above();
|
||||
if (replacement == Blocks.NETHERRACK && level.getBlockState(above).isAir()
|
||||
&& random.nextDouble() < 0.35) {
|
||||
level.setBlock(above, random.nextBoolean()
|
||||
? Blocks.CRIMSON_FUNGUS.defaultBlockState()
|
||||
: Blocks.WARPED_FUNGUS.defaultBlockState(), Block.UPDATE_ALL);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void endCorruption(ServerLevel level, int baseX, int baseZ, double intensity) {
|
||||
if (random.nextDouble() > 0.20) return;
|
||||
for (int i = 0; i < Math.max(2, (int) (intensity * 8)); i++) {
|
||||
BlockPos pos = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS),
|
||||
baseZ + random.nextInt(REGION_BLOCKS));
|
||||
if (pos == null) continue;
|
||||
var state = level.getBlockState(pos);
|
||||
if (state.is(Blocks.STONE) || state.is(Blocks.GRASS_BLOCK)
|
||||
|| state.is(Blocks.DIRT)) {
|
||||
level.setBlock(pos, Blocks.END_STONE.defaultBlockState(), Block.UPDATE_ALL);
|
||||
BlockPos above = pos.above();
|
||||
if (level.getBlockState(above).isAir()) {
|
||||
if (random.nextDouble() < 0.20) {
|
||||
level.setBlock(above, Blocks.CHORUS_FLOWER.defaultBlockState(), Block.UPDATE_ALL);
|
||||
} else if (random.nextDouble() < 0.12) {
|
||||
level.setBlock(above, Blocks.OBSIDIAN.defaultBlockState(), Block.UPDATE_ALL);
|
||||
}
|
||||
}
|
||||
} else if (state.is(Blocks.OBSIDIAN) && intensity > 0.7
|
||||
&& level.getBlockState(pos.above()).isAir()) {
|
||||
level.setBlock(pos.above(), Blocks.OBSIDIAN.defaultBlockState(), Block.UPDATE_ALL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void exposeRuins(ServerLevel level,
|
||||
com.livingworld.regions.RegionCoordinate region,
|
||||
int baseX, int baseZ, double intensity) {
|
||||
for (int attempt = 0; attempt < 20; attempt++) {
|
||||
int x = baseX + random.nextInt(REGION_BLOCKS);
|
||||
int z = baseZ + random.nextInt(REGION_BLOCKS);
|
||||
int surfaceY = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1;
|
||||
for (int depth = 2; depth <= 10; depth++) {
|
||||
BlockPos structure = new BlockPos(x, surfaceY - depth, z);
|
||||
if (!level.isLoaded(structure) || !isAncientStructure(level.getBlockState(structure))) {
|
||||
continue;
|
||||
}
|
||||
int writes = 0;
|
||||
for (int y = surfaceY; y > structure.getY() && writes < 9; y--) {
|
||||
BlockPos cover = new BlockPos(x, y, z);
|
||||
if (isNaturalTerrain(level.getBlockState(cover))) {
|
||||
level.setBlock(cover, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
|
||||
writes++;
|
||||
}
|
||||
}
|
||||
BlockPos marker = structure.above();
|
||||
if (writes < 10 && level.isLoaded(marker) && level.getBlockState(marker).isAir()) {
|
||||
level.setBlock(marker, Blocks.LANTERN.defaultBlockState(), Block.UPDATE_ALL);
|
||||
}
|
||||
ruinsExposedCallback.accept(region);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAncientStructure(net.minecraft.world.level.block.state.BlockState state) {
|
||||
return state.is(Blocks.MOSSY_COBBLESTONE)
|
||||
|| state.is(Blocks.CHISELED_STONE_BRICKS)
|
||||
|| state.is(Blocks.CRACKED_STONE_BRICKS)
|
||||
|| state.is(Blocks.MOSSY_STONE_BRICKS)
|
||||
|| state.is(Blocks.CHISELED_DEEPSLATE)
|
||||
|| state.is(Blocks.DEEPSLATE_TILES)
|
||||
|| state.is(Blocks.CRACKED_DEEPSLATE_TILES);
|
||||
}
|
||||
|
||||
private BlockPos surfaceAt(ServerLevel level, int x, int z) {
|
||||
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1;
|
||||
if (y < level.getMinBuildHeight()) {
|
||||
|
||||
Reference in New Issue
Block a user