Complete phase 7 player feedback loops
This commit is contained in:
@@ -45,4 +45,7 @@ dependencies {
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
scanForTestClasses = false
|
||||
include '**/*Test.class'
|
||||
include '**/*Test$*.class'
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import net.neoforged.neoforge.event.entity.EntityLeaveLevelEvent;
|
||||
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.neoforged.neoforge.event.level.BlockEvent;
|
||||
import net.neoforged.neoforge.event.level.ChunkEvent;
|
||||
import net.neoforged.neoforge.event.level.block.CropGrowEvent;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.neoforged.neoforge.event.server.ServerStartedEvent;
|
||||
import net.neoforged.neoforge.event.server.ServerStartingEvent;
|
||||
@@ -405,6 +406,16 @@ public class LivingWorldMod {
|
||||
LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
|
||||
bootstrap.handleSaplingPlaced(coord, level.getGameTime() / 24000L);
|
||||
});
|
||||
NeoForge.EVENT_BUS.addListener(CropGrowEvent.Pre.class, event -> {
|
||||
if (!bootstrap.isServerReady() || !(event.getLevel() instanceof ServerLevel level)) return;
|
||||
RegionCoordinate coord = RegionCoordinate.fromBlock(
|
||||
level.dimension().location().toString(),
|
||||
event.getPos().getX(), event.getPos().getZ(),
|
||||
LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
|
||||
if (bootstrap.isCropGrowthSuppressed(coord)) {
|
||||
event.setResult(CropGrowEvent.Pre.Result.DO_NOT_GROW);
|
||||
}
|
||||
});
|
||||
|
||||
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World Bootstrap initialized successfully.");
|
||||
}
|
||||
@@ -486,6 +497,7 @@ public class LivingWorldMod {
|
||||
if (hasWaterBody(serverLevel, coord)) {
|
||||
bootstrap.applyWaterBodyBoost(coord);
|
||||
}
|
||||
bootstrap.markFarmlandRegion(coord, hasFarmland(serverLevel, coord));
|
||||
waterBodyLastScan.put(coord, playerCheckTick);
|
||||
}
|
||||
}
|
||||
@@ -711,10 +723,24 @@ public class LivingWorldMod {
|
||||
}
|
||||
|
||||
float pitch = 0.75f + random.nextFloat() * 0.5f;
|
||||
if (stage != null && stage.ordinal() <= SuccessionStage.SPARSE_GRASS.ordinal()
|
||||
&& pollution > 50.0) {
|
||||
return false;
|
||||
}
|
||||
if (stage != null && stage.ordinal() >= SuccessionStage.YOUNG_WOODLAND.ordinal()
|
||||
&& health > 50.0 && random.nextInt(10) == 0) {
|
||||
// Rustling leaves — healthy forest ambience.
|
||||
playAmbientSound(player, Holder.direct(SoundEvents.AZALEA_LEAVES_STEP), 0.18f, pitch);
|
||||
&& health > 50.0 && random.nextInt(Math.max(3, 14 - (int) (health / 10))) == 0) {
|
||||
int blend = random.nextInt(100);
|
||||
if (stage == SuccessionStage.MATURE_FOREST && health > 70.0 && blend < 45) {
|
||||
playAmbientSound(player, Holder.direct(SoundEvents.PARROT_AMBIENT), 0.12f, pitch);
|
||||
} else if (stage == SuccessionStage.MATURE_FOREST && blend < 75) {
|
||||
playAmbientSound(player, Holder.direct(SoundEvents.BEE_LOOP), 0.10f, pitch);
|
||||
} else {
|
||||
playAmbientSound(player, Holder.direct(SoundEvents.AZALEA_LEAVES_STEP), 0.18f, pitch);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (stage == SuccessionStage.GRASSLAND && health > 35.0 && random.nextInt(12) == 0) {
|
||||
playAmbientSound(player, Holder.direct(SoundEvents.GRASS_STEP), 0.12f, pitch);
|
||||
return true;
|
||||
}
|
||||
if (stage != null && stage.ordinal() <= SuccessionStage.SPARSE_GRASS.ordinal()
|
||||
@@ -904,6 +930,22 @@ public class LivingWorldMod {
|
||||
return waterCount >= 5;
|
||||
}
|
||||
|
||||
private boolean hasFarmland(ServerLevel level, RegionCoordinate coord) {
|
||||
int baseX = coord.x() * REGION_BLOCKS;
|
||||
int baseZ = coord.z() * REGION_BLOCKS;
|
||||
for (int i = 0; i < 30; i++) {
|
||||
int x = baseX + random.nextInt(REGION_BLOCKS);
|
||||
int z = baseZ + random.nextInt(REGION_BLOCKS);
|
||||
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1;
|
||||
BlockPos pos = new BlockPos(x, y, z);
|
||||
if (level.isLoaded(pos)
|
||||
&& level.getBlockState(pos).is(net.minecraft.world.level.block.Blocks.FARMLAND)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Step 6: Samples the dominant biome at the region centre and returns a succession ceiling. */
|
||||
private SuccessionStage deriveBiomeCap(ServerLevel level, RegionCoordinate coord) {
|
||||
int cx = coord.x() * LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16 + 64;
|
||||
|
||||
@@ -159,6 +159,11 @@ public final class LivingWorldBootstrap {
|
||||
private final Map<RegionCoordinate, Integer> bareDryCycles = new HashMap<>();
|
||||
private final Map<RegionCoordinate, Integer> permafrostWarmCycles = new HashMap<>();
|
||||
private final Set<RegionCoordinate> thawedPermafrostRegions = new HashSet<>();
|
||||
private final Map<RegionCoordinate, Integer> lowTreeCycles = new HashMap<>();
|
||||
private final Set<RegionCoordinate> deforestedRegions = new HashSet<>();
|
||||
private final Set<RegionCoordinate> farmlandRegions = new HashSet<>();
|
||||
private final Set<RegionCoordinate> exhaustedFarmlandRegions = new HashSet<>();
|
||||
private final Map<RegionCoordinate, Integer> farmingInactiveCycles = new HashMap<>();
|
||||
|
||||
private PlatformAdapter platformAdapter;
|
||||
private Path worldSaveDirectory;
|
||||
@@ -350,6 +355,11 @@ public final class LivingWorldBootstrap {
|
||||
bareDryCycles.clear();
|
||||
permafrostWarmCycles.clear();
|
||||
thawedPermafrostRegions.clear();
|
||||
lowTreeCycles.clear();
|
||||
deforestedRegions.clear();
|
||||
farmlandRegions.clear();
|
||||
exhaustedFarmlandRegions.clear();
|
||||
farmingInactiveCycles.clear();
|
||||
simSpeedMultiplier = 1;
|
||||
serverReady = false;
|
||||
LivingWorldLogger.info(
|
||||
@@ -417,6 +427,8 @@ public final class LivingWorldBootstrap {
|
||||
if (soil == null) return;
|
||||
double gain = isPollinatorCollapse(coord) ? 1.0 : 2.0;
|
||||
soil.setFertility(Math.min(100, soil.getFertility() + gain));
|
||||
exhaustedFarmlandRegions.remove(coord);
|
||||
farmingInactiveCycles.put(coord, 0);
|
||||
region.getModuleData().put(SoilModule.MODULE_ID, soil);
|
||||
regionManager.markDirty(region);
|
||||
});
|
||||
@@ -427,6 +439,7 @@ public final class LivingWorldBootstrap {
|
||||
if (!serverReady) return;
|
||||
RegionCoordinate coord = RegionCoordinate.fromBlock(
|
||||
dimensionId, x, z, LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
|
||||
farmingInactiveCycles.put(coord, 0);
|
||||
regionManager.resolve(coord).ifPresent(region -> {
|
||||
SoilRegionData soil = region.getModuleData()
|
||||
.get(SoilModule.MODULE_ID, SoilRegionData.class)
|
||||
@@ -519,6 +532,7 @@ public final class LivingWorldBootstrap {
|
||||
applyHydrologyExpansion();
|
||||
applyEcologyExpansion();
|
||||
applyLongCycleEffects();
|
||||
applyPlayerFeedbackLoops();
|
||||
}
|
||||
|
||||
private void applyLongCycleEffects() {
|
||||
@@ -735,6 +749,79 @@ public final class LivingWorldBootstrap {
|
||||
}
|
||||
}
|
||||
|
||||
public void markFarmlandRegion(RegionCoordinate coord, boolean present) {
|
||||
if (coord == null) return;
|
||||
if (present) farmlandRegions.add(coord);
|
||||
else farmlandRegions.remove(coord);
|
||||
}
|
||||
|
||||
public boolean isCropGrowthSuppressed(RegionCoordinate coord) {
|
||||
return coord != null && exhaustedFarmlandRegions.contains(coord);
|
||||
}
|
||||
|
||||
public boolean isDeforestedRegion(RegionCoordinate coord) {
|
||||
return coord != null && deforestedRegions.contains(coord);
|
||||
}
|
||||
|
||||
private void applyPlayerFeedbackLoops() {
|
||||
if (worldEffectsModule == null) return;
|
||||
for (Region region : regionManager.getActiveRegions()) {
|
||||
RegionCoordinate coord = region.getCoordinate();
|
||||
VegetationRegionData vegetation = region.getModuleData()
|
||||
.get(VegetationModule.MODULE_ID, VegetationRegionData.class).orElse(null);
|
||||
SoilRegionData soil = region.getModuleData()
|
||||
.get(SoilModule.MODULE_ID, SoilRegionData.class).orElse(null);
|
||||
RecoveryRegionData recovery = region.getModuleData()
|
||||
.get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElse(null);
|
||||
ResourceRegionData resources = region.getModuleData()
|
||||
.get(ResourceDepletionModule.MODULE_ID, ResourceRegionData.class).orElse(null);
|
||||
if (vegetation == null || soil == null || recovery == null || resources == null) continue;
|
||||
|
||||
if (vegetation.getTreePressure() < 5.0 && resources.getLoggingDepletion() > 20.0) {
|
||||
int cycles = lowTreeCycles.merge(coord, 1, Integer::sum);
|
||||
if (cycles >= 10 && deforestedRegions.add(coord)) {
|
||||
pendingEventMessages.add("[LW] Deforestation detected at ("
|
||||
+ coord.x() + "," + coord.z() + ") - soil erosion accelerating.");
|
||||
}
|
||||
} else if (vegetation.getTreePressure() >= 15.0) {
|
||||
lowTreeCycles.remove(coord);
|
||||
deforestedRegions.remove(coord);
|
||||
}
|
||||
|
||||
if (deforestedRegions.contains(coord)) {
|
||||
recovery.setMaxSuccessionStage(SuccessionStage.SPARSE_GRASS);
|
||||
soil.setContamination(Math.min(100, soil.getContamination() + 0.10));
|
||||
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||
WorldEffectType.RIVER_SILTS, coord, 0.6));
|
||||
}
|
||||
|
||||
if (farmlandRegions.contains(coord)) {
|
||||
int inactive = farmingInactiveCycles.merge(coord, 1, Integer::sum);
|
||||
boolean exhausted = soil.getFertility() < 20.0 && soil.getContamination() > 40.0;
|
||||
if (exhausted) {
|
||||
if (exhaustedFarmlandRegions.add(coord)) {
|
||||
pendingEventMessages.add("[LW] Crop exhaustion near region ("
|
||||
+ coord.x() + "," + coord.z()
|
||||
+ ") - leave fields fallow or use bone meal.");
|
||||
}
|
||||
worldEffectsModule.queueEffect(new WorldEffectRequest(
|
||||
WorldEffectType.CROPLAND_EXHAUSTS, coord, 0.7));
|
||||
}
|
||||
if (inactive >= 50) {
|
||||
soil.setFertility(Math.min(100, soil.getFertility() + 0.15));
|
||||
soil.setContamination(Math.max(0, soil.getContamination() - 0.08));
|
||||
if (soil.getFertility() >= 25.0 || soil.getContamination() <= 30.0) {
|
||||
exhaustedFarmlandRegions.remove(coord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
region.getModuleData().put(SoilModule.MODULE_ID, soil);
|
||||
region.getModuleData().put(RecoveryModule.MODULE_ID, recovery);
|
||||
regionManager.markDirty(region);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyHydrologyExpansion() {
|
||||
Collection<Region> active = regionManager.getActiveRegions();
|
||||
if (active.isEmpty() || worldEffectsModule == null) return;
|
||||
@@ -805,7 +892,10 @@ public final class LivingWorldBootstrap {
|
||||
lowestNeighbour = neighbour;
|
||||
}
|
||||
}
|
||||
if (atmosphere.getRainLevel() > 0.80 && lowSuccession && maxSlope > 10.0
|
||||
double floodRainThreshold = deforestedRegions.contains(coord) ? 0.60 : 0.80;
|
||||
double floodSlopeThreshold = deforestedRegions.contains(coord) ? 5.0 : 10.0;
|
||||
if (atmosphere.getRainLevel() > floodRainThreshold
|
||||
&& lowSuccession && maxSlope > floodSlopeThreshold
|
||||
&& windRandom.nextDouble() < 0.02
|
||||
&& !isClimateEventActive(coord, ClimateEventType.FLASH_FLOOD)) {
|
||||
ClimateEvent flood = new ClimateEvent(
|
||||
|
||||
@@ -205,4 +205,10 @@ public enum WorldEffectType {
|
||||
|
||||
/** 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,
|
||||
}
|
||||
|
||||
@@ -172,6 +172,10 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
|
||||
breakSoilCrust(level, baseX, baseZ, request.intensity());
|
||||
case PERMAFROST_THAWS ->
|
||||
thawPermafrost(level, baseX, baseZ, request.intensity());
|
||||
case RIVER_SILTS ->
|
||||
siltRiver(level, baseX, baseZ, request.intensity());
|
||||
case CROPLAND_EXHAUSTS ->
|
||||
exhaustCropland(level, baseX, baseZ, request.intensity());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1274,6 +1278,35 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
|
||||
}
|
||||
}
|
||||
|
||||
private void siltRiver(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++) {
|
||||
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 water = new BlockPos(x, y, z);
|
||||
BlockPos bed = water.below();
|
||||
if (level.isLoaded(water) && level.getFluidState(water).is(FluidTags.WATER)
|
||||
&& level.getBlockState(bed).is(Blocks.SAND)) {
|
||||
level.setBlock(bed, Blocks.GRAVEL.defaultBlockState(), Block.UPDATE_ALL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void exhaustCropland(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++) {
|
||||
BlockPos pos = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS),
|
||||
baseZ + random.nextInt(REGION_BLOCKS));
|
||||
if (pos == null || !level.getBlockState(pos).is(Blocks.FARMLAND)) continue;
|
||||
BlockPos crop = pos.above();
|
||||
if (level.isLoaded(crop) && level.getBlockState(crop).is(BlockTags.CROPS)) {
|
||||
level.setBlock(crop, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
|
||||
}
|
||||
level.setBlock(pos, Blocks.COARSE_DIRT.defaultBlockState(), Block.UPDATE_ALL);
|
||||
}
|
||||
}
|
||||
|
||||
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