From d1c73025a073e42bc24a4c9add81bc52033224a5 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Tue, 1 Sep 2026 23:13:47 -0400 Subject: [PATCH] Spring pools at headwaters and ring-ordered feature search - channel.springWidthRatio / springLength open every surface river in a pool that shrinks where the ground falls away - Banks settings keep structural invariants only; authoring bounds stay in the validator - Feature searches walk tiles outward and stop early; find and goto report progress per ring --- .../iris/core/commands/CommandFind.java | 4 +- .../modded/command/ModdedLocateCommands.java | 4 +- .../core/pack/PackHydrologyValidator.java | 2 + .../hydrology/HydrologyPlannerSettings.java | 21 ++++--- .../runtime/IrisHydrologyRuntime.java | 58 ++++++++++++++----- .../surface/ChannelProfileBuilder.java | 39 +++++++++++-- .../object/IrisSurfaceRiverChannelConfig.java | 10 ++++ .../core/pack/PackHydrologyValidatorTest.java | 12 +++- .../HydrologyPlannerSettingsTest.java | 21 +++---- .../hydrology/HydrologyTileCacheTest.java | 2 +- .../surface/ChannelProfileBuilderTest.java | 33 +++++++++-- .../surface/ErosionFieldCompilerTest.java | 2 +- 12 files changed, 152 insertions(+), 56 deletions(-) diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java index c25e99477..e8745569c 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java @@ -161,7 +161,9 @@ public class CommandFind implements DirectorExecutor { query.profileKey(), origin.getBlockX(), origin.getBlockZ(), - maximumDistance + maximumDistance, + (int visited) -> sendStructureMessage(target, commandSender, + C.GRAY + "Searched " + visited + " hydrology tiles for " + type + "...") ).orElse(null); if (feature == null) { sendStructureMessage(target, commandSender, diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java index be971a53f..505f4f07c 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java @@ -176,7 +176,9 @@ final class ModdedLocateCommands { MinecraftServer server = source.getServer(); IrisModdedCommands.ok(source, "Searching accepted hydrology plans for " + type + "..."); MultiBurst.burst.completeValueAsync(() -> runtime.nearestFeature( - query.types(), query.profileKey(), originX, originZ, maximumDistance).orElse(null)) + query.types(), query.profileKey(), originX, originZ, maximumDistance, + (int visited) -> server.execute(() -> IrisModdedCommands.ok(source, + "Searched " + visited + " hydrology tiles for " + type + "..."))).orElse(null)) .whenComplete((HydrologyFeatureRef feature, Throwable error) -> server.execute(() -> { if (error != null) { ModdedIrisLog.error("Hydrology locate failed for {}", type, error); diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackHydrologyValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackHydrologyValidator.java index a26d479a0..371f60af3 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/PackHydrologyValidator.java +++ b/core/src/main/java/art/arcane/iris/core/pack/PackHydrologyValidator.java @@ -365,6 +365,8 @@ final class PackHydrologyValidator { validateRange(path, channel, "depth", 1D, 64D, 2D, 4D, errors); PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "inset", 0, 3, errors); PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "maximumIncision", 1, 32, errors); + PackJsonFieldChecks.validateOptionalDoubleRange(path, channel, "springWidthRatio", 1D, 4D, errors); + PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "springLength", 4, 96, errors); PackJsonFieldChecks.validateOptionalDoubleRange(path, channel, "roughness", 0D, 1D, errors); PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "roughnessWavelength", 4, 64, errors); } diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettings.java b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettings.java index 6b1ea59f7..b288bec9d 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettings.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettings.java @@ -251,23 +251,28 @@ public record HydrologyPlannerSettings( int cascadeRun, int waterfallMinimumDrop, double mouthFlareRatio, + double springWidthRatio, + int springLength, boolean exposeCutStrata ) { + // Structural invariants only; authoring bounds live in the pack validator. public Banks { - if (inset < 1 || inset > 16 || freeboard < 1 || freeboard > 8 - || !Double.isFinite(blendSlope) || blendSlope < 1D || blendSlope > 16D - || minimumBlendWidth < 1 || maximumBlendWidth < minimumBlendWidth || maximumBlendWidth > 128 + if (inset < 0 || freeboard < 0 + || !Double.isFinite(blendSlope) || blendSlope <= 0D + || minimumBlendWidth < 1 || maximumBlendWidth < minimumBlendWidth || !Double.isFinite(roughness) || roughness < 0D || roughness > 1D - || roughnessWavelength < 3 || roughnessWavelength > 128 - || cascadeRun < 1 || cascadeRun > 16 - || waterfallMinimumDrop < 2 || waterfallMinimumDrop > 24 - || !Double.isFinite(mouthFlareRatio) || mouthFlareRatio < 1D || mouthFlareRatio > 4D) { + || roughnessWavelength < 1 + || cascadeRun < 1 + || waterfallMinimumDrop < 1 + || !Double.isFinite(mouthFlareRatio) || mouthFlareRatio < 1D + || !Double.isFinite(springWidthRatio) || springWidthRatio < 1D + || springLength < 1) { throw new IllegalArgumentException("Surface bank settings are invalid."); } } public static Banks defaults() { - return new Banks(1, 1, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, true); + return new Banks(1, 1, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true); } } diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyRuntime.java b/core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyRuntime.java index 5585c94d9..6dfbcb5c9 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyRuntime.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyRuntime.java @@ -57,6 +57,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.IntConsumer; public final class IrisHydrologyRuntime implements AutoCloseable { private static final int MAXIMUM_CACHE_TILES = 64; @@ -286,40 +287,63 @@ public final class IrisHydrologyRuntime implements AutoCloseable { int x, int z, int maximumDistance + ) { + return nearestFeature(types, profileKey, x, z, maximumDistance, (int visited) -> { }); + } + + /** + * Walks tiles outward from the origin, planning each on demand, and stops as soon as no unvisited + * ring can hold a nearer feature. {@code progress} receives the running tile count after every ring. + */ + public Optional nearestFeature( + Set types, + String profileKey, + int x, + int z, + int maximumDistance, + IntConsumer progress ) { Objects.requireNonNull(types); + Objects.requireNonNull(progress); if (types.isEmpty() || maximumDistance < 0) { return Optional.empty(); } int tileSize = settings.routing().tileSize(); int publicationRadius = settings.publicationRadius(); - int minimumTileX = tileCoordinate((long) x - maximumDistance - publicationRadius, tileSize); - int maximumTileX = tileCoordinate((long) x + maximumDistance + publicationRadius, tileSize); - int minimumTileZ = tileCoordinate((long) z - maximumDistance - publicationRadius, tileSize); - int maximumTileZ = tileCoordinate((long) z + maximumDistance + publicationRadius, tileSize); long tileCount = HydrologyFeatureSearchBounds.tileCount( x, z, maximumDistance, tileSize, publicationRadius); if (tileCount > MAXIMUM_FEATURE_SEARCH_TILES) { throw new IllegalArgumentException("Hydrology feature search exceeds the bounded tile limit."); } + int originTileX = tileCoordinate(x, tileSize); + int originTileZ = tileCoordinate(z, tileSize); + int ringLimit = HydrologyFeatureSearch.ringLimit(maximumDistance, tileSize, publicationRadius); HydrologyFeatureRef nearest = null; long nearestDistanceSquared = Long.MAX_VALUE; - for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) { - for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) { - HydrologyTile tile = cache.get(new HydrologyTileKey(tileX, tileZ)); + int visited = 0; + for (int ring = 0; ring <= ringLimit; ring++) { + long lowerBound = HydrologyFeatureSearch.lowerBound(ring, tileSize, publicationRadius); + if (nearest != null && lowerBound * lowerBound > nearestDistanceSquared) { + break; + } + for (HydrologyTileKey key : HydrologyFeatureSearch.ring(originTileX, originTileZ, ring)) { + HydrologyTile tile = cache.get(key); + visited++; HydrologyFeatureRef feature = tile.nearestFeature( types, profileKey, x, z, maximumDistance).orElse(null); - if (feature != null) { - long deltaX = (long) feature.x() - x; - long deltaZ = (long) feature.z() - z; - long distanceSquared = deltaX * deltaX + deltaZ * deltaZ; - if (nearest == null || distanceSquared < nearestDistanceSquared - || distanceSquared == nearestDistanceSquared && feature.id() < nearest.id()) { - nearest = feature; - nearestDistanceSquared = distanceSquared; - } + if (feature == null) { + continue; + } + long deltaX = (long) feature.x() - x; + long deltaZ = (long) feature.z() - z; + long distanceSquared = deltaX * deltaX + deltaZ * deltaZ; + if (nearest == null || distanceSquared < nearestDistanceSquared + || distanceSquared == nearestDistanceSquared && feature.id() < nearest.id()) { + nearest = feature; + nearestDistanceSquared = distanceSquared; } } + progress.accept(visited); } return Optional.ofNullable(nearest); } @@ -677,6 +701,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable { flow.getCascadeRun(), flow.getWaterfallMinimumDrop(), surface.getMouths().getFlareRatio(), + channel.getSpringWidthRatio(), + channel.getSpringLength(), banks.isExposeCutStrata() ) ); diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/surface/ChannelProfileBuilder.java b/core/src/main/java/art/arcane/iris/engine/hydrology/surface/ChannelProfileBuilder.java index afb713c63..31dbf3b26 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/surface/ChannelProfileBuilder.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/surface/ChannelProfileBuilder.java @@ -9,7 +9,6 @@ import java.util.Objects; public final class ChannelProfileBuilder { private static final int SMOOTHING_RADIUS = 16; - private static final int HEADWATER_TAPER = 48; private static final int MOUTH_FLARE = 32; private final HydrologyPlannerSettings.Surface surface; @@ -65,11 +64,15 @@ public final class ChannelProfileBuilder { } double[] smoothWidth = smooth(width); double[] smoothDepth = smooth(depth); - int taper = Math.min(HEADWATER_TAPER, count / 2); - for (int station = 0; station < taper; station++) { - double progress = SurfaceNoise.smoothStep(station / (double) taper); - smoothWidth[station] = 1D + (smoothWidth[station] - 1D) * progress; - smoothDepth[station] = 1D + (smoothDepth[station] - 1D) * progress; + // The headwater opens as a spring pool: wider and one block deeper, narrowing to the cruise width. + // Ground that falls away across the pool shrinks it so it never demands a cut the valley solver rejects. + int spring = Math.min(surface.banks().springLength(), count / 2); + double springRatio = surface.banks().springWidthRatio(); + for (int station = 0; station < spring; station++) { + double remaining = 1D - SurfaceNoise.smoothStep(station / (double) spring); + double localRatio = 1D + (springRatio - 1D) * springRoom(centerline, station, smoothWidth[station] * springRatio / 2D); + smoothWidth[station] *= 1D + (localRatio - 1D) * remaining; + smoothDepth[station] += remaining; } if (directOcean) { int flare = Math.min(MOUTH_FLARE, count / 3); @@ -87,6 +90,30 @@ public final class ChannelProfileBuilder { return new ChannelProfile(halfWidth, smoothDepth, bank); } + /** Fraction of the spring pool the ground allows: 1 on level ground, 0 where the pool ring would sit half the permitted cut lower. */ + private double springRoom(SurfaceCenterline centerline, int station, double reach) { + int centerX = centerline.x()[station]; + int centerZ = centerline.z()[station]; + HydrologyTerrainSample center = sampler.sample(centerX, centerZ); + if (center == null) { + return 1D; + } + double normalX = centerline.normalX(station); + double normalZ = centerline.normalZ(station); + int lowest = center.naturalHeight(); + for (double direction = -1D; direction <= 1D; direction += 2D) { + HydrologyTerrainSample side = sampler.sample( + (int) StrictMath.round(centerX + normalX * reach * direction), + (int) StrictMath.round(centerZ + normalZ * reach * direction)); + if (side != null) { + lowest = Math.min(lowest, side.naturalHeight()); + } + } + double drop = center.naturalHeight() - lowest; + double allowance = Math.max(1D, surface.maximumIncision() / 2D); + return Math.max(0D, Math.min(1D, 1D - drop / allowance)); + } + static double[] smooth(double[] values) { int count = values.length; double[] smoothed = new double[count]; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceRiverChannelConfig.java b/core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceRiverChannelConfig.java index 060344bac..3670d9302 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceRiverChannelConfig.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisSurfaceRiverChannelConfig.java @@ -38,6 +38,16 @@ public class IrisSurfaceRiverChannelConfig { @Desc("Wavelength in blocks of the outline and bed variation.") private int roughnessWavelength = 16; + @MinNumber(1) + @MaxNumber(4) + @Desc("Width of the spring pool at the headwater relative to the channel width; 1 starts the river at its normal width.") + private double springWidthRatio = 2.5D; + + @MinNumber(4) + @MaxNumber(96) + @Desc("Blocks over which the spring pool narrows back to the channel width.") + private int springLength = 24; + private static IrisStyledRange range(double min, double max, double zoom) { IrisGeneratorStyle style = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(zoom); return new IrisStyledRange(min, max, style); diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackHydrologyValidatorTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackHydrologyValidatorTest.java index 75cede05c..b7d5a230b 100644 --- a/core/src/test/java/art/arcane/iris/core/pack/PackHydrologyValidatorTest.java +++ b/core/src/test/java/art/arcane/iris/core/pack/PackHydrologyValidatorTest.java @@ -288,7 +288,9 @@ public class PackHydrologyValidatorTest { "inset": 4, "maximumIncision": 33, "roughness": 1.5, - "roughnessWavelength": 65 + "roughnessWavelength": 65, + "springWidthRatio": 5, + "springLength": 97 } } } @@ -303,7 +305,9 @@ public class PackHydrologyValidatorTest { "inset": -1, "maximumIncision": 0, "roughness": -0.1, - "roughnessWavelength": 3 + "roughnessWavelength": 3, + "springWidthRatio": 0.5, + "springLength": 3 } } } @@ -318,6 +322,10 @@ public class PackHydrologyValidatorTest { assertContains(aboveResult.errors(), "maximumIncision must be at most 32"); assertContains(aboveResult.errors(), "roughness must be at most 1.0"); assertContains(aboveResult.errors(), "roughnessWavelength must be at most 64"); + assertContains(aboveResult.errors(), "springWidthRatio must be at most 4.0"); + assertContains(aboveResult.errors(), "springLength must be at most 96"); + assertContains(belowResult.errors(), "springWidthRatio must be at least 1.0"); + assertContains(belowResult.errors(), "springLength must be at least 4"); assertContains(belowResult.errors(), "width.min must be at least 1.0"); assertContains(belowResult.errors(), "depth.min must be at least 1.0"); assertContains(belowResult.errors(), "inset must be at least 0"); diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettingsTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettingsTest.java index 1c1ae42cd..eb15696de 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettingsTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettingsTest.java @@ -144,20 +144,13 @@ public class HydrologyPlannerSettingsTest { assertEquals(32, banks.maximumBlendWidth()); assertEquals(6, banks.waterfallMinimumDrop()); assertTrue(banks.exposeCutStrata()); - assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks( - 0, 1, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, true)); - assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks( - 1, 0, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, true)); - assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks( - 1, 1, 0.5D, 4, 32, 0.25D, 16, 2, 6, 1.6D, true)); - assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks( - 1, 1, 3D, 40, 32, 0.25D, 16, 2, 6, 1.6D, true)); - assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks( - 1, 1, 3D, 4, 32, 0.25D, 16, 2, 1, 1.6D, true)); - assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks( - 1, 1, 3D, 4, 32, 0.25D, 16, 0, 6, 1.6D, true)); - assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks( - 1, 1, 3D, 4, 32, 1.5D, 16, 2, 6, 1.6D, true)); + assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(-1, 1, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true)); + assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, -1, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true)); + assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 0D, 4, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true)); + assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 3D, 40, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true)); + assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 32, 0.25D, 16, 2, 0, 1.6D, 2.5D, 24, true)); + assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 32, 0.25D, 16, 0, 6, 1.6D, 2.5D, 24, true)); + assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 32, 1.5D, 16, 2, 6, 1.6D, 2.5D, 24, true)); assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Surface( true, new HydrologyPlannerSettings.Source(true, 1D, 0, 0, 1, 0), diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTileCacheTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTileCacheTest.java index 0f685df58..33a28735c 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTileCacheTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTileCacheTest.java @@ -303,7 +303,7 @@ public class HydrologyTileCacheTest { // Small-tile settings need a blend envelope that fits inside the bounded cross-tile admission period. private static HydrologyPlannerSettings.Banks tileBoundedBanks() { - return new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 4, 0.25D, 16, 2, 6, 1.6D, true); + return new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 4, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true); } private HydrologyTerrainSample featureTerrain(int x, int z) { diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/surface/ChannelProfileBuilderTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/surface/ChannelProfileBuilderTest.java index 8610f270a..e224ef72c 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/surface/ChannelProfileBuilderTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/surface/ChannelProfileBuilderTest.java @@ -8,6 +8,7 @@ import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler; import org.junit.Test; import java.util.List; +import java.util.function.IntBinaryOperator; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -20,7 +21,7 @@ public class ChannelProfileBuilderTest { }; @Test - public void cruiseWidthIsConstantAndTapersFromTheHeadwater() { + public void headwaterOpensAsASpringPoolThatNarrowsToTheCruiseWidth() { SurfaceCenterline centerline = straight(400); ChannelProfile profile = builder(1D, 1D).build(centerline, "water", false); @@ -28,11 +29,14 @@ public class ChannelProfileBuilderTest { assertEquals(3D, profile.halfWidth()[200], 1.0E-9D); assertEquals(3D, profile.halfWidth()[399], 1.0E-9D); assertEquals(3D, profile.depth()[200], 1.0E-9D); - assertTrue(profile.halfWidth()[0] < 1D); - for (int station = 1; station < 48; station++) { - assertTrue(profile.halfWidth()[station] >= profile.halfWidth()[station - 1]); + assertEquals(3D * 2.5D, profile.halfWidth()[0], 1.0E-9D); + assertEquals(4D, profile.depth()[0], 1.0E-9D); + for (int station = 1; station <= 24; station++) { + assertTrue(profile.halfWidth()[station] <= profile.halfWidth()[station - 1]); + assertTrue(profile.depth()[station] <= profile.depth()[station - 1]); } - assertTrue(profile.depth()[0] >= 1D); + assertEquals(3D, profile.halfWidth()[24], 1.0E-9D); + assertEquals(3D, profile.depth()[24], 1.0E-9D); } @Test @@ -55,9 +59,26 @@ public class ChannelProfileBuilderTest { assertEquals(6D * 1.25D + 2D, profile.collar(200, 0.25D), 1.0E-9D); } + @Test + public void groundFallingAwayAcrossThePoolShrinksTheSpringPool() { + SurfaceCenterline centerline = straight(400); + ChannelProfile sloped = builder(1D, 1D, (int x, int z) -> 80 - Math.abs(z) / 2).build(centerline, "water", false); + ChannelProfile cliff = builder(1D, 1D, (int x, int z) -> 80 - Math.abs(z) * 8).build(centerline, "water", false); + + assertTrue(sloped.halfWidth()[0] > 3D); + assertTrue(sloped.halfWidth()[0] < 3D * 2.5D); + assertEquals(3D, cliff.halfWidth()[0], 1.0E-9D); + assertEquals(4D, cliff.depth()[0], 1.0E-9D); + assertEquals(3D, sloped.halfWidth()[24], 1.0E-9D); + } + private static ChannelProfileBuilder builder(double widthMultiplier, double depthMultiplier) { + return builder(widthMultiplier, depthMultiplier, (int x, int z) -> 80); + } + + private static ChannelProfileBuilder builder(double widthMultiplier, double depthMultiplier, IntBinaryOperator height) { HydrologyTerrainSampler sampler = (int x, int z) -> new HydrologyTerrainSample( - 80, + height.applyAsInt(x, z), 0D, false, false, diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/surface/ErosionFieldCompilerTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/surface/ErosionFieldCompilerTest.java index 6d598b5ba..eddccf880 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/surface/ErosionFieldCompilerTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/surface/ErosionFieldCompilerTest.java @@ -173,7 +173,7 @@ public class ErosionFieldCompilerTest { true, defaults.sources(), 4, 8, 2, 4, 10, 1.5D, - new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 32, 0D, 16, 2, 6, 1.6D, true) + new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 32, 0D, 16, 2, 6, 1.6D, 2.5D, 24, true) ); }