Complete phase 10 dimensional extensions

This commit is contained in:
George
2026-06-11 18:31:50 +01:00
parent d9158bc8e9
commit d7f8e3ea36
4 changed files with 232 additions and 2 deletions
@@ -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.FinalizeSpawnEvent;
import net.neoforged.neoforge.event.entity.living.LivingDeathEvent; import net.neoforged.neoforge.event.entity.living.LivingDeathEvent;
import net.neoforged.neoforge.event.entity.EntityLeaveLevelEvent; 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.entity.player.PlayerInteractEvent;
import net.neoforged.neoforge.event.level.BlockEvent; import net.neoforged.neoforge.event.level.BlockEvent;
import net.neoforged.neoforge.event.level.ChunkEvent; 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.MobSpawnType;
import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.Items; 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.Biome;
import net.minecraft.world.level.biome.Biomes; import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity; import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
@@ -163,7 +165,8 @@ public class LivingWorldMod {
bootstrap.getWorldEffectsModule().registerConsumer( bootstrap.getWorldEffectsModule().registerConsumer(
new NeoForgeWorldEffectExecutor( new NeoForgeWorldEffectExecutor(
() -> minecraftServer, () -> minecraftServer,
coord -> !bootstrap.isPollinatorCollapse(coord))); coord -> !bootstrap.isPollinatorCollapse(coord),
bootstrap::notifyAncientRuinsExposed));
bootstrap.setOverworldRaining( bootstrap.setOverworldRaining(
() -> minecraftServer != null && minecraftServer.overworld().isRaining()); () -> minecraftServer != null && minecraftServer.overworld().isRaining());
bootstrap.setAbsoluteDaySupplier( bootstrap.setAbsoluteDaySupplier(
@@ -369,6 +372,16 @@ public class LivingWorldMod {
RegionCoordinate coord = trackedPassiveMobs.remove(event.getEntity().getUUID()); RegionCoordinate coord = trackedPassiveMobs.remove(event.getEntity().getUUID());
if (coord != null) bootstrap.recordPassiveMobDeparture(coord); 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. // Step 4: Agriculture — bone meal boosts soil fertility.
NeoForge.EVENT_BUS.addListener(PlayerInteractEvent.RightClickBlock.class, event -> { NeoForge.EVENT_BUS.addListener(PlayerInteractEvent.RightClickBlock.class, event -> {
@@ -754,6 +767,11 @@ public class LivingWorldMod {
return true; 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; float pitch = 0.75f + random.nextFloat() * 0.5f;
if (stage != null && stage.ordinal() <= SuccessionStage.SPARSE_GRASS.ordinal() 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 Set<RegionCoordinate> exhaustedFarmlandRegions = new HashSet<>();
private final Map<RegionCoordinate, Integer> farmingInactiveCycles = new HashMap<>(); private final Map<RegionCoordinate, Integer> farmingInactiveCycles = new HashMap<>();
private int undergroundCycleTick = 0; 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 PlatformAdapter platformAdapter;
private Path worldSaveDirectory; private Path worldSaveDirectory;
@@ -362,6 +371,13 @@ public final class LivingWorldBootstrap {
exhaustedFarmlandRegions.clear(); exhaustedFarmlandRegions.clear();
farmingInactiveCycles.clear(); farmingInactiveCycles.clear();
undergroundCycleTick = 0; undergroundCycleTick = 0;
portalSites.clear();
netherExposureCycles.clear();
endExposureCycles.clear();
netherBleedRegions.clear();
endCorruptionRegions.clear();
erosionExposureCycles.clear();
exposedRuinsRegions.clear();
simSpeedMultiplier = 1; simSpeedMultiplier = 1;
serverReady = false; serverReady = false;
LivingWorldLogger.info( LivingWorldLogger.info(
@@ -536,6 +552,89 @@ public final class LivingWorldBootstrap {
applyLongCycleEffects(); applyLongCycleEffects();
applyPlayerFeedbackLoops(); applyPlayerFeedbackLoops();
applyUndergroundSystems(); 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() { private void applyUndergroundSystems() {
@@ -220,4 +220,13 @@ public enum WorldEffectType {
/** Wet dripstone cave ceilings grow a pointed stalactite segment. */ /** Wet dripstone cave ceilings grow a pointed stalactite segment. */
STALACTITE_GROWS, 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.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.Consumer;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.core.particles.ParticleTypes; import net.minecraft.core.particles.ParticleTypes;
@@ -45,19 +46,29 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
private final Supplier<MinecraftServer> serverSupplier; private final Supplier<MinecraftServer> serverSupplier;
private final Random random = new Random(); private final Random random = new Random();
private final Predicate<com.livingworld.regions.RegionCoordinate> flowersAllowed; private final Predicate<com.livingworld.regions.RegionCoordinate> flowersAllowed;
private final Consumer<com.livingworld.regions.RegionCoordinate> ruinsExposedCallback;
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<>(); private final Map<ResourceKey<Level>, List<TimedWater>> floodWater = new HashMap<>();
public NeoForgeWorldEffectExecutor(Supplier<MinecraftServer> serverSupplier) { public NeoForgeWorldEffectExecutor(Supplier<MinecraftServer> serverSupplier) {
this(serverSupplier, ignored -> true); this(serverSupplier, ignored -> true, ignored -> {});
} }
public NeoForgeWorldEffectExecutor( public NeoForgeWorldEffectExecutor(
Supplier<MinecraftServer> serverSupplier, Supplier<MinecraftServer> serverSupplier,
Predicate<com.livingworld.regions.RegionCoordinate> flowersAllowed) { 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.serverSupplier = serverSupplier;
this.flowersAllowed = flowersAllowed != null ? flowersAllowed : ignored -> true; this.flowersAllowed = flowersAllowed != null ? flowersAllowed : ignored -> true;
this.ruinsExposedCallback = ruinsExposedCallback != null
? ruinsExposedCallback : ignored -> {};
} }
@Override @Override
@@ -183,6 +194,12 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
shiftMineralVein(level, baseX, baseZ, request.intensity()); shiftMineralVein(level, baseX, baseZ, request.intensity());
case STALACTITE_GROWS -> case STALACTITE_GROWS ->
growStalactite(level, baseX, baseZ, request.intensity()); 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) { 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()) {