Complete phase 7 player feedback loops
This commit is contained in:
@@ -45,4 +45,7 @@ dependencies {
|
|||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnitPlatform()
|
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.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;
|
||||||
|
import net.neoforged.neoforge.event.level.block.CropGrowEvent;
|
||||||
import net.minecraft.world.level.ChunkPos;
|
import net.minecraft.world.level.ChunkPos;
|
||||||
import net.neoforged.neoforge.event.server.ServerStartedEvent;
|
import net.neoforged.neoforge.event.server.ServerStartedEvent;
|
||||||
import net.neoforged.neoforge.event.server.ServerStartingEvent;
|
import net.neoforged.neoforge.event.server.ServerStartingEvent;
|
||||||
@@ -405,6 +406,16 @@ public class LivingWorldMod {
|
|||||||
LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
|
LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
|
||||||
bootstrap.handleSaplingPlaced(coord, level.getGameTime() / 24000L);
|
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.");
|
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World Bootstrap initialized successfully.");
|
||||||
}
|
}
|
||||||
@@ -486,6 +497,7 @@ public class LivingWorldMod {
|
|||||||
if (hasWaterBody(serverLevel, coord)) {
|
if (hasWaterBody(serverLevel, coord)) {
|
||||||
bootstrap.applyWaterBodyBoost(coord);
|
bootstrap.applyWaterBodyBoost(coord);
|
||||||
}
|
}
|
||||||
|
bootstrap.markFarmlandRegion(coord, hasFarmland(serverLevel, coord));
|
||||||
waterBodyLastScan.put(coord, playerCheckTick);
|
waterBodyLastScan.put(coord, playerCheckTick);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -711,10 +723,24 @@ public class LivingWorldMod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
&& pollution > 50.0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (stage != null && stage.ordinal() >= SuccessionStage.YOUNG_WOODLAND.ordinal()
|
if (stage != null && stage.ordinal() >= SuccessionStage.YOUNG_WOODLAND.ordinal()
|
||||||
&& health > 50.0 && random.nextInt(10) == 0) {
|
&& health > 50.0 && random.nextInt(Math.max(3, 14 - (int) (health / 10))) == 0) {
|
||||||
// Rustling leaves — healthy forest ambience.
|
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);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
if (stage != null && stage.ordinal() <= SuccessionStage.SPARSE_GRASS.ordinal()
|
if (stage != null && stage.ordinal() <= SuccessionStage.SPARSE_GRASS.ordinal()
|
||||||
@@ -904,6 +930,22 @@ public class LivingWorldMod {
|
|||||||
return waterCount >= 5;
|
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. */
|
/** Step 6: Samples the dominant biome at the region centre and returns a succession ceiling. */
|
||||||
private SuccessionStage deriveBiomeCap(ServerLevel level, RegionCoordinate coord) {
|
private SuccessionStage deriveBiomeCap(ServerLevel level, RegionCoordinate coord) {
|
||||||
int cx = coord.x() * LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS * 16 + 64;
|
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> bareDryCycles = new HashMap<>();
|
||||||
private final Map<RegionCoordinate, Integer> permafrostWarmCycles = new HashMap<>();
|
private final Map<RegionCoordinate, Integer> permafrostWarmCycles = new HashMap<>();
|
||||||
private final Set<RegionCoordinate> thawedPermafrostRegions = new HashSet<>();
|
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 PlatformAdapter platformAdapter;
|
||||||
private Path worldSaveDirectory;
|
private Path worldSaveDirectory;
|
||||||
@@ -350,6 +355,11 @@ public final class LivingWorldBootstrap {
|
|||||||
bareDryCycles.clear();
|
bareDryCycles.clear();
|
||||||
permafrostWarmCycles.clear();
|
permafrostWarmCycles.clear();
|
||||||
thawedPermafrostRegions.clear();
|
thawedPermafrostRegions.clear();
|
||||||
|
lowTreeCycles.clear();
|
||||||
|
deforestedRegions.clear();
|
||||||
|
farmlandRegions.clear();
|
||||||
|
exhaustedFarmlandRegions.clear();
|
||||||
|
farmingInactiveCycles.clear();
|
||||||
simSpeedMultiplier = 1;
|
simSpeedMultiplier = 1;
|
||||||
serverReady = false;
|
serverReady = false;
|
||||||
LivingWorldLogger.info(
|
LivingWorldLogger.info(
|
||||||
@@ -417,6 +427,8 @@ public final class LivingWorldBootstrap {
|
|||||||
if (soil == null) return;
|
if (soil == null) return;
|
||||||
double gain = isPollinatorCollapse(coord) ? 1.0 : 2.0;
|
double gain = isPollinatorCollapse(coord) ? 1.0 : 2.0;
|
||||||
soil.setFertility(Math.min(100, soil.getFertility() + gain));
|
soil.setFertility(Math.min(100, soil.getFertility() + gain));
|
||||||
|
exhaustedFarmlandRegions.remove(coord);
|
||||||
|
farmingInactiveCycles.put(coord, 0);
|
||||||
region.getModuleData().put(SoilModule.MODULE_ID, soil);
|
region.getModuleData().put(SoilModule.MODULE_ID, soil);
|
||||||
regionManager.markDirty(region);
|
regionManager.markDirty(region);
|
||||||
});
|
});
|
||||||
@@ -427,6 +439,7 @@ public final class LivingWorldBootstrap {
|
|||||||
if (!serverReady) return;
|
if (!serverReady) return;
|
||||||
RegionCoordinate coord = RegionCoordinate.fromBlock(
|
RegionCoordinate coord = RegionCoordinate.fromBlock(
|
||||||
dimensionId, x, z, LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
|
dimensionId, x, z, LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
|
||||||
|
farmingInactiveCycles.put(coord, 0);
|
||||||
regionManager.resolve(coord).ifPresent(region -> {
|
regionManager.resolve(coord).ifPresent(region -> {
|
||||||
SoilRegionData soil = region.getModuleData()
|
SoilRegionData soil = region.getModuleData()
|
||||||
.get(SoilModule.MODULE_ID, SoilRegionData.class)
|
.get(SoilModule.MODULE_ID, SoilRegionData.class)
|
||||||
@@ -519,6 +532,7 @@ public final class LivingWorldBootstrap {
|
|||||||
applyHydrologyExpansion();
|
applyHydrologyExpansion();
|
||||||
applyEcologyExpansion();
|
applyEcologyExpansion();
|
||||||
applyLongCycleEffects();
|
applyLongCycleEffects();
|
||||||
|
applyPlayerFeedbackLoops();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyLongCycleEffects() {
|
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() {
|
private void applyHydrologyExpansion() {
|
||||||
Collection<Region> active = regionManager.getActiveRegions();
|
Collection<Region> active = regionManager.getActiveRegions();
|
||||||
if (active.isEmpty() || worldEffectsModule == null) return;
|
if (active.isEmpty() || worldEffectsModule == null) return;
|
||||||
@@ -805,7 +892,10 @@ public final class LivingWorldBootstrap {
|
|||||||
lowestNeighbour = neighbour;
|
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
|
&& windRandom.nextDouble() < 0.02
|
||||||
&& !isClimateEventActive(coord, ClimateEventType.FLASH_FLOOD)) {
|
&& !isClimateEventActive(coord, ClimateEventType.FLASH_FLOOD)) {
|
||||||
ClimateEvent flood = new ClimateEvent(
|
ClimateEvent flood = new ClimateEvent(
|
||||||
|
|||||||
@@ -205,4 +205,10 @@ public enum WorldEffectType {
|
|||||||
|
|
||||||
/** Warming frozen subsoil collapses into wet mud and meltwater. */
|
/** Warming frozen subsoil collapses into wet mud and meltwater. */
|
||||||
PERMAFROST_THAWS,
|
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());
|
breakSoilCrust(level, baseX, baseZ, request.intensity());
|
||||||
case PERMAFROST_THAWS ->
|
case PERMAFROST_THAWS ->
|
||||||
thawPermafrost(level, baseX, baseZ, request.intensity());
|
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) {
|
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