diff --git a/src/main/java/net/mcreator/customoregen/worldgen/LatitudeBiomeSource.java b/src/main/java/net/mcreator/customoregen/worldgen/LatitudeBiomeSource.java
index 9f947aa4..32059803 100644
--- a/src/main/java/net/mcreator/customoregen/worldgen/LatitudeBiomeSource.java
+++ b/src/main/java/net/mcreator/customoregen/worldgen/LatitudeBiomeSource.java
@@ -95,8 +95,8 @@ public class LatitudeBiomeSource extends BiomeSource {
/** Frequency of the mid-cave pocket noise (lush/dripstone). */
private static final double CAVE_SCALE = 0.003;
- /** Above this value a lush/dripstone pocket overrides the surface biome. ~8% of the mid zone. */
- private static final double CAVE_THRESHOLD = 0.55;
+ /** Above this value a lush/dripstone pocket overrides the surface biome. ~5-8% of the mid zone. */
+ private static final double CAVE_THRESHOLD = 0.38;
/** Frequency of the Deep Dark noise (very low = large, rare regions). */
private static final double DEEP_DARK_SCALE = 0.0011;
@@ -104,8 +104,8 @@ public class LatitudeBiomeSource extends BiomeSource {
/** Vertical frequency of the Deep Dark noise (keeps it coherent in tall sections). */
private static final double DEEP_DARK_SCALE_Y = 0.012;
- /** Above this value the Deep Dark overrides. Tuned for ~1% of the deep zone (legendary). */
- private static final double DEEP_DARK_THRESHOLD = 0.88;
+ /** Above this value the Deep Dark overrides. Tuned for ~1-2% of the deep zone (legendary). */
+ private static final double DEEP_DARK_THRESHOLD = 0.55;
// ------------------------------------------------------------------
// Spawn safe zone
diff --git a/src/main/java/net/mcreator/customoregen/worldgen/LatitudeGameTest.java b/src/main/java/net/mcreator/customoregen/worldgen/LatitudeGameTest.java
index 1923c9d5..5caae0df 100644
--- a/src/main/java/net/mcreator/customoregen/worldgen/LatitudeGameTest.java
+++ b/src/main/java/net/mcreator/customoregen/worldgen/LatitudeGameTest.java
@@ -29,9 +29,9 @@ import java.util.Set;
* {@link GameTest} so it executes inside a real game server with the full biome registry.
*
*
Launch with {@code ./gradlew runGameTestServer}. The test samples a large grid through a
- * freshly built {@link LatitudeBiomeSource}, renders a PNG map, writes a distribution report and
- * asserts the climate invariants (frozen north, hot south, safe spawn, rare swamp). No manual
- * in-game testing required.
+ * freshly built {@link LatitudeBiomeSource} at several depths and renders one PNG map per depth
+ * (a "geological slice" view), writes a distribution report and asserts the climate + cave
+ * invariants. No manual in-game testing required.
*/
@GameTestHolder(CustomOreGenMod.MODID)
@PrefixGameTestTemplate(false)
@@ -42,6 +42,11 @@ public class LatitudeGameTest {
private static final long SEED = 0L;
private static final Path OUT_DIR = Path.of("gametest-results", "latitude");
+ // Sampling depths (one PNG per depth = a top-down "slice").
+ private static final int SURFACE_Y = 64;
+ private static final int MID_CAVE_Y = -15; // -30 <= Y < 0 (lush/dripstone pockets)
+ private static final int DEEP_Y = -50; // Y < -30 (legendary Deep Dark)
+
private static final Map, Integer> COLORS = new HashMap<>();
static {
@@ -61,7 +66,10 @@ public class LatitudeGameTest {
Biomes.JUNGLE, Biomes.SPARSE_JUNGLE, Biomes.BAMBOO_JUNGLE, Biomes.WARM_OCEAN);
col(0x5B7A3A, Biomes.SWAMP);
col(0x3A5A3A, Biomes.MANGROVE_SWAMP);
- col(0x6B4E3A, Biomes.LUSH_CAVES, Biomes.DRIPSTONE_CAVES, Biomes.DEEP_DARK);
+ // Cave biomes - distinct colors so the slices are readable.
+ col(0x4DB05A, Biomes.LUSH_CAVES); // lush green
+ col(0xC8782C, Biomes.DRIPSTONE_CAVES); // stalactite orange-brown
+ col(0x0A4D54, Biomes.DEEP_DARK); // dark sculk teal
}
private static void col(int rgb, ResourceKey... keys) {
@@ -85,6 +93,85 @@ public class LatitudeGameTest {
LatitudeBiomeSource source = new LatitudeBiomeSource(SEED, getter);
Files.createDirectories(OUT_DIR);
+
+ // ---- Sample one slice per depth. Each slice produces a PNG + a biome-count map. ----
+ Slice surface = sampleSlice(source, SURFACE_Y, "latitude_map_surface.png");
+ Slice midCave = sampleSlice(source, MID_CAVE_Y, "latitude_map_mid_cave.png");
+ Slice deep = sampleSlice(source, DEEP_Y, "latitude_map_deep.png");
+
+ // ---- Surface climate assertions ----
+ ResourceKey spawnKey = source.getNoiseBiome(0, SURFACE_Y >> 2, 0, null).unwrapKey().orElse(null);
+
+ Set> coldTag = resolveTagKeys(getter, BiomeBand.COLD.surfaceTag());
+ Set> hotTag = resolveTagKeys(getter, BiomeBand.HOT.surfaceTag());
+ Set> coldOceans = new HashSet<>(List.of(
+ Biomes.FROZEN_OCEAN, Biomes.DEEP_FROZEN_OCEAN, Biomes.COLD_OCEAN, Biomes.DEEP_COLD_OCEAN,
+ Biomes.FROZEN_RIVER));
+ Set> hotOceans = new HashSet<>(List.of(
+ Biomes.WARM_OCEAN, Biomes.LUKEWARM_OCEAN, Biomes.DEEP_LUKEWARM_OCEAN));
+
+ assertNotNull(spawnKey, "spawn biome resolved");
+ assertTrue(isSafeSpawn(spawnKey), "spawn must be a safe biome, was " + spawnKey);
+
+ Set> northExpected = new HashSet<>();
+ northExpected.addAll(coldTag);
+ northExpected.addAll(coldOceans);
+ double north = sliceBandRatio(surface, BiomeBand.FROZEN, northExpected);
+ assertTrue(north > 60.0, "FROZEN band should be >60%% cold/frozen (by tag), was %.1f%%".formatted(north));
+
+ Set> southExpected = new HashSet<>();
+ southExpected.addAll(hotTag);
+ southExpected.addAll(hotOceans);
+ southExpected.add(Biomes.MANGROVE_SWAMP);
+ double south = sliceBandRatio(surface, BiomeBand.HOT, southExpected);
+ assertTrue(south > 60.0, "HOT band should be >60%% warm/hot (by tag), was %.1f%%".formatted(south));
+
+ // Swamp stays rare at the surface in the temperate band.
+ double swampRatio = biomeRatioInBand(surface, BiomeBand.TEMPERATE, Biomes.SWAMP);
+ assertTrue(swampRatio < 15.0, "swamp must be <15%% of temperate band, was %.2f%%".formatted(swampRatio));
+
+ // ---- Cave assertions ----
+ // Deep Dark is legendary: a small fraction of the deep slice only.
+ double deepDarkRatio = biomeRatio(deep, Biomes.DEEP_DARK);
+ assertTrue(deepDarkRatio < 8.0,
+ "Deep Dark must be rare in the deep zone (<8%%), was %.2f%%".formatted(deepDarkRatio));
+
+ // Cave biomes (lush/dripstone) should be a minority even in the mid-cave slice - the latitude
+ // biome dominates underground by design.
+ double caveBiomeRatio = biomeRatio(midCave, Biomes.LUSH_CAVES, Biomes.DRIPSTONE_CAVES);
+ assertTrue(caveBiomeRatio < 25.0,
+ "cave biomes must stay a minority in mid-cave (<25%%), was %.2f%%".formatted(caveBiomeRatio));
+
+ // ---- Report ----
+ StringBuilder r = new StringBuilder();
+ r.append("Latitude GameTest Report (seed=").append(SEED).append(")\n\n");
+ r.append("Spawn @ (0,0): ").append(spawnKey).append("\n\n");
+
+ r.append("=== Depth slices (PNG) ===\n");
+ r.append(String.format(" Surface Y=%3d : latitude_map_surface.png%n", SURFACE_Y));
+ r.append(String.format(" Mid cave Y=%3d: latitude_map_mid_cave.png%n", MID_CAVE_Y));
+ r.append(String.format(" Deep Y=%3d : latitude_map_deep.png%n%n", DEEP_Y));
+
+ r.append("=== Surface top biomes (top 12) ===\n");
+ appendTop(r, surface.global, 12);
+
+ r.append("\n=== Mid cave (Y=").append(MID_CAVE_Y).append(") top biomes (top 10) ===\n");
+ appendTop(r, midCave.global, 10);
+
+ r.append("\n=== Deep (Y=").append(DEEP_Y).append(") top biomes (top 10) ===\n");
+ appendTop(r, deep.global, 10);
+
+ Files.writeString(OUT_DIR.resolve("latitude_report.txt"), r.toString());
+
+ // ---- One-line summary ----
+ CustomOreGenMod.LOGGER.info(String.format(
+ "Latitude game test OK: spawn=%s | north=%.0f%% | south=%.0f%% | swamp=%.2f%% | "
+ + "deepDark=%.2f%% | caveBiomes=%.1f%%",
+ spawnKey, north, south, swampRatio, deepDarkRatio, caveBiomeRatio));
+ }
+
+ /** Sample the whole grid at a single Y and write a PNG; returns the slice's biome counts. */
+ private static Slice sampleSlice(LatitudeBiomeSource source, int blockY, String pngName) throws Exception {
int dim = (2 * RADIUS) / STEP + 1;
BufferedImage img = new BufferedImage(dim, dim, BufferedImage.TYPE_INT_RGB);
Map, Integer> global = new HashMap<>();
@@ -95,89 +182,31 @@ public class LatitudeGameTest {
for (int xi = 0; xi < dim; xi++) {
int blockX = -RADIUS + xi * STEP;
int blockZ = -RADIUS + zi * STEP;
- Holder holder = source.getNoiseBiome(blockX >> 2, 64 >> 2, blockZ >> 2, null);
+ Holder holder = source.getNoiseBiome(blockX >> 2, blockY >> 2, blockZ >> 2, null);
ResourceKey key = holder.unwrapKey().orElse(null);
global.merge(key, 1, Integer::sum);
perBand.get(BiomeBand.fromTemperature(blockZ / 16000.0)).merge(key, 1, Integer::sum);
int rgb = key == null ? 0x888888 : COLORS.getOrDefault(key, 0x888888);
- img.setRGB(xi, dim - 1 - zi, rgb);
+ img.setRGB(xi, dim - 1 - zi, rgb); // flip Z so north is at the top
}
}
-
- ImageIO.write(img, "png", OUT_DIR.resolve("latitude_map.png").toFile());
-
- ResourceKey spawnKey = source.getNoiseBiome(0, 64 >> 2, 0, null).unwrapKey().orElse(null);
-
- // Resolve the three surface tags so assertions can use tag membership (the source of
- // truth) instead of a hardcoded vanilla-only biome list. This correctly counts BOP biomes.
- Set> coldTag = resolveTagKeys(getter, BiomeBand.COLD.surfaceTag());
- Set> temperateTag = resolveTagKeys(getter, BiomeBand.TEMPERATE.surfaceTag());
- Set> hotTag = resolveTagKeys(getter, BiomeBand.HOT.surfaceTag());
- Set> coldOceans = new HashSet<>(List.of(
- Biomes.FROZEN_OCEAN, Biomes.DEEP_FROZEN_OCEAN, Biomes.COLD_OCEAN, Biomes.DEEP_COLD_OCEAN,
- Biomes.FROZEN_RIVER));
- Set> hotOceans = new HashSet<>(List.of(
- Biomes.WARM_OCEAN, Biomes.LUKEWARM_OCEAN, Biomes.DEEP_LUKEWARM_OCEAN));
-
- StringBuilder r = new StringBuilder();
- r.append("Latitude GameTest Report (seed=").append(SEED).append(")\n\n");
- r.append("Spawn @ (0,0): ").append(spawnKey).append("\n\n");
- int total = global.values().stream().mapToInt(Integer::intValue).sum();
- r.append("Global biome distribution (top 20):\n");
- global.entrySet().stream()
- .sorted(Map.Entry., Integer>comparingByValue().reversed())
- .limit(20)
- .forEach(e -> r.append(String.format(" %-45s %6.2f%%%n", e.getKey(), 100.0 * e.getValue() / total)));
-
- // Per-band breakdown for diagnostics.
- for (BiomeBand band : BiomeBand.values()) {
- Map, Integer> m = perBand.get(band);
- int bt = m.values().stream().mapToInt(Integer::intValue).sum();
- if (bt == 0) continue;
- r.append("\nBand ").append(band).append(" (").append(bt).append(" samples):\n");
- m.entrySet().stream()
- .sorted(Map.Entry., Integer>comparingByValue().reversed())
- .limit(8)
- .forEach(e -> r.append(String.format(" %-43s %6.2f%%%n", e.getKey(), 100.0 * e.getValue() / bt)));
- }
- Files.writeString(OUT_DIR.resolve("latitude_report.txt"), r.toString());
-
- // ---- Assertions (tag-based: correctly counts vanilla + BOP biomes) ----
- assertNotNull(spawnKey, "spawn biome resolved");
- assertTrue(isSafeSpawn(spawnKey), "spawn must be a safe biome, was " + spawnKey);
-
- Set> northExpected = new HashSet<>();
- northExpected.addAll(coldTag);
- northExpected.addAll(coldOceans);
- Set> undergroundCold = new HashSet<>(List.of(
- Biomes.DRIPSTONE_CAVES, Biomes.DEEP_DARK));
- northExpected.addAll(undergroundCold);
- double north = bandRatio(perBand.get(BiomeBand.FROZEN), northExpected);
- assertTrue(north > 60.0, "FROZEN band should be >60%% cold/frozen (by tag), was %.1f%%".formatted(north));
-
- Set> southExpected = new HashSet<>();
- southExpected.addAll(hotTag);
- southExpected.addAll(hotOceans);
- southExpected.add(Biomes.MANGROVE_SWAMP);
- double south = bandRatio(perBand.get(BiomeBand.HOT), southExpected);
- assertTrue(south > 60.0, "HOT band should be >60%% warm/hot (by tag), was %.1f%%".formatted(south));
-
- Map, Integer> temperate = perBand.get(BiomeBand.TEMPERATE);
- int temperateTotal = temperate.values().stream().mapToInt(Integer::intValue).sum();
- int temperateSwamp = temperate.getOrDefault(Biomes.SWAMP, 0);
- double swampRatio = 100.0 * temperateSwamp / temperateTotal;
- assertTrue(swampRatio < 15.0,
- "swamp must be <15%% of temperate band, was %.2f%%".formatted(swampRatio));
-
- // Log a one-line summary so it shows up in the game test output.
- CustomOreGenMod.LOGGER.info(String.format(
- "Latitude game test OK: spawn=%s | north=%.0f%% | south=%.0f%% | swamp=%.2f%%",
- spawnKey, north, south, swampRatio));
+ ImageIO.write(img, "png", OUT_DIR.resolve(pngName).toFile());
+ return new Slice(global, perBand);
}
- private static double bandRatio(Map, Integer> m, Set> expected) {
+ private static void appendTop(StringBuilder r, Map, Integer> counts, int limit) {
+ int total = counts.values().stream().mapToInt(Integer::intValue).sum();
+ counts.entrySet().stream()
+ .sorted(Map.Entry., Integer>comparingByValue().reversed())
+ .limit(limit)
+ .forEach(e -> r.append(String.format(" %-45s %6.2f%%%n", e.getKey(), 100.0 * e.getValue() / total)));
+ }
+
+ /** Ratio of a set of biomes within a single band of a slice. */
+ private static double sliceBandRatio(Slice slice, BiomeBand band, Set> expected) {
+ Map, Integer> m = slice.perBand.get(band);
int total = m.values().stream().mapToInt(Integer::intValue).sum();
if (total == 0) return 0;
int match = 0;
@@ -185,6 +214,23 @@ public class LatitudeGameTest {
return 100.0 * match / total;
}
+ /** Ratio of one or more biomes across a whole slice. */
+ private static double biomeRatio(Slice slice, ResourceKey... keys) {
+ int total = slice.global.values().stream().mapToInt(Integer::intValue).sum();
+ if (total == 0) return 0;
+ int match = 0;
+ for (ResourceKey k : keys) match += slice.global.getOrDefault(k, 0);
+ return 100.0 * match / total;
+ }
+
+ /** Ratio of a biome within a single band of a slice. */
+ private static double biomeRatioInBand(Slice slice, BiomeBand band, ResourceKey key) {
+ Map, Integer> m = slice.perBand.get(band);
+ int total = m.values().stream().mapToInt(Integer::intValue).sum();
+ if (total == 0) return 0;
+ return 100.0 * m.getOrDefault(key, 0) / total;
+ }
+
/** Resolve all biome keys that belong to a tag, via the real registry. */
private static Set> resolveTagKeys(HolderGetter getter, net.minecraft.tags.TagKey tag) {
Set> keys = new HashSet<>();
@@ -197,6 +243,11 @@ public class LatitudeGameTest {
Biomes.FLOWER_FOREST, Biomes.MEADOW, Biomes.CHERRY_GROVE).contains(key);
}
+ /** A sampled depth slice: global counts + per-band counts. */
+ private record Slice(Map, Integer> global,
+ Map, Integer>> perBand) {
+ }
+
// Local assert helpers (no JUnit in the main classpath at runtime).
private static void assertTrue(boolean condition, String msg) {
if (!condition) throw new AssertionError(msg);