feat: rethink cave generation - latitude extends underground, Deep Dark legendary

The previous implementation forced a uniform cave-biome slab (lush/dripstone/
deep_dark) across all of Y<30, which overrode the latitude logic for a huge
volume and made the Deep Dark far too common.

New 3-zone underground model keeps the latitude surface biome as the default
at every depth (mining under a desert feels like the desert) and only carves
rare cave features on top:

- Near surface (Y >= 0): latitude biome as-is, no override.
- Mid caves (-30 <= Y < 0): lush/dripstone pockets (~8%) matching the climate
  (humid/warm bands -> lush, dry/cold bands -> dripstone).
- Deep zone (Y < -30): legendary Deep Dark pockets (~1%, very low-frequency
  noise so they form large rare regions suitable for Ancient Cities); the
  surface latitude biome otherwise.

Deep Dark is removed from the biome band pools and driven solely by its own
noise, restoring it to a rare, legendary discovery. Cave biomes are now
resolved once and declared in possibleBiomes. BiomeBand.underground() removed
(caves are climate-driven, not band-pool-driven).

Validated by GameTest (BOP + Tectonic): spawn=birch_forest | north=100% |
south=100% | swamp=1.26%.
This commit is contained in:
feldenr
2026-06-16 21:12:17 +02:00
parent 45213d3580
commit 060b77b506
2 changed files with 112 additions and 34 deletions
@@ -115,23 +115,6 @@ public enum BiomeBand {
}
}
public List<ResourceKey<Biome>> underground() {
switch (this) {
case FROZEN:
return List.of(Biomes.DRIPSTONE_CAVES, Biomes.DEEP_DARK);
case COLD:
return List.of(Biomes.DRIPSTONE_CAVES);
case TEMPERATE:
return List.of(Biomes.LUSH_CAVES, Biomes.DRIPSTONE_CAVES);
case WARM:
return List.of(Biomes.LUSH_CAVES);
case HOT:
return List.of(Biomes.LUSH_CAVES, Biomes.DEEP_DARK);
default:
return List.of(Biomes.LUSH_CAVES);
}
}
/** Resolve a list of (possibly absent) biome keys into actual holders, skipping missing ones. */
public static List<Holder<Biome>> resolve(HolderGetter<Biome> getter, List<ResourceKey<Biome>> keys) {
List<Holder<Biome>> holders = new ArrayList<>();
@@ -36,10 +36,16 @@ import java.util.stream.Stream;
* selector noise picks among the band's biomes, producing extremely large biomes that
* encourage exploration and long-distance travel (railways, roads, nether hubs).</p>
*
* <p><b>Underground philosophy:</b> the surface latitude biome extends downward by default, so
* mining under a desert feels like being in the desert. Cave biomes (lush / dripstone) only
* appear as rare climatic pockets in the mid underground, and the Deep Dark is a legendary
* (~1%) feature of the deep zone. This keeps the latitude logic coherent at every depth
* instead of overriding it with a uniform cave-biome slab.</p>
*
* <p>Surface biomes come from dedicated climate tags (see {@code data/custom_ore_gen/tags/
* worldgen/biome/latitude_*_surface.json}), which support optional Biomes O' Plenty biomes
* via {@code "required": false} without ever creating unbound holders. Ocean and underground
* (cave) pools are vanilla-only.</p>
* via {@code "required": false} without ever creating unbound holders. Ocean pools are
* vanilla-only.</p>
*/
public class LatitudeBiomeSource extends BiomeSource {
@@ -49,7 +55,7 @@ public class LatitudeBiomeSource extends BiomeSource {
).apply(instance, instance.stable(LatitudeBiomeSource::new)));
// ------------------------------------------------------------------
// Tunable constants
// Surface / climate tunables
// ------------------------------------------------------------------
/** Number of blocks for the temperature to go from 0 (equator) to +-1 (pole). */
@@ -76,8 +82,34 @@ public class LatitudeBiomeSource extends BiomeSource {
/** Above this moisture value a wet biome (swamp/mangrove) overrides the surface. ~8% of land. */
private static final double MOISTURE_THRESHOLD = 0.55;
/** Below this block Y we resolve underground (cave) biomes. */
private static final int UNDERGROUND_Y = 30;
// ------------------------------------------------------------------
// Underground tunables (3-zone model)
// ------------------------------------------------------------------
/** Top of the deep zone (Y &lt; this = deep underground). Deep Dark can live here. */
private static final int DEEP_ZONE_TOP = -30;
/** Top of the mid-cave zone (DEEP_ZONE_TOP &le; Y &lt; this = mid caves, lush/dripstone pockets). */
private static final int MID_CAVE_TOP = 0;
/** 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;
/** Frequency of the Deep Dark noise (very low = large, rare regions). */
private static final double DEEP_DARK_SCALE = 0.0011;
/** 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;
// ------------------------------------------------------------------
// Spawn safe zone
// ------------------------------------------------------------------
/** Half-size of the guaranteed safe spawn square around the origin (plains/forest). */
private static final int SPAWN_SAFE_RADIUS = 96;
@@ -96,6 +128,8 @@ public class LatitudeBiomeSource extends BiomeSource {
private final ImprovedNoise selectorNoise;
private final ImprovedNoise oceanNoise;
private final ImprovedNoise moistureNoise;
private final ImprovedNoise caveNoise;
private final ImprovedNoise deepDarkNoise;
/** Vanilla-only pools resolved eagerly (safe: vanilla biomes always exist and are bound). */
private final EnumMap<BiomeBand, ResolvedBand> resolvedVanilla;
@@ -109,6 +143,11 @@ public class LatitudeBiomeSource extends BiomeSource {
private final Holder<Biome> swampBiome;
private final Holder<Biome> mangroveBiome;
/** Cave biomes resolved eagerly (vanilla, always present). Lush = humid bands, Dripstone = dry bands. */
private final Holder<Biome> lushCavesBiome;
private final Holder<Biome> dripstoneCavesBiome;
private final Holder<Biome> deepDarkBiome;
/** Mod-aware surface pools resolved lazily from climate tags (safe: tags are bound by then). */
private volatile EnumMap<BiomeBand, List<Holder<Biome>>> surfaceFromTag;
@@ -121,19 +160,25 @@ public class LatitudeBiomeSource extends BiomeSource {
this.selectorNoise = new ImprovedNoise(RandomSource.create(seed ^ 0x4C415449L));
this.oceanNoise = new ImprovedNoise(RandomSource.create(seed ^ 0x4F434541L));
this.moistureNoise = new ImprovedNoise(RandomSource.create(seed ^ 0x57455421L));
this.caveNoise = new ImprovedNoise(RandomSource.create(seed ^ 0x43415645L));
this.deepDarkNoise = new ImprovedNoise(RandomSource.create(seed ^ 0x44454550L));
this.resolvedVanilla = new EnumMap<>(BiomeBand.class);
Set<Holder<Biome>> all = new HashSet<>();
for (BiomeBand band : BiomeBand.values()) {
List<Holder<Biome>> surface = BiomeBand.resolve(biomeGetter, band.surfaceVanilla());
List<Holder<Biome>> ocean = BiomeBand.resolve(biomeGetter, band.ocean());
List<Holder<Biome>> underground = BiomeBand.resolve(biomeGetter, band.underground());
resolvedVanilla.put(band, new ResolvedBand(surface, ocean, underground));
resolvedVanilla.put(band, new ResolvedBand(surface, ocean));
all.addAll(surface);
all.addAll(ocean);
all.addAll(underground);
}
// Cave biomes (vanilla) declared so the source advertises them as possible.
this.lushCavesBiome = resolveAndAdd(biomeGetter, Biomes.LUSH_CAVES, all);
this.dripstoneCavesBiome = resolveAndAdd(biomeGetter, Biomes.DRIPSTONE_CAVES, all);
this.deepDarkBiome = resolveAndAdd(biomeGetter, Biomes.DEEP_DARK, all);
this.possibleBiomes = all;
List<Holder<Biome>> safe = new ArrayList<>();
@@ -151,6 +196,14 @@ public class LatitudeBiomeSource extends BiomeSource {
this.fallback = plains != null ? plains : (possibleBiomes.isEmpty() ? null : possibleBiomes.iterator().next());
}
private static Holder<Biome> resolveAndAdd(HolderGetter<Biome> getter, net.minecraft.resources.ResourceKey<Biome> key, Set<Holder<Biome>> into) {
Holder<Biome> holder = getter.get(key).orElse(null);
if (holder != null) {
into.add(holder);
}
return holder;
}
@Override
protected MapCodec<? extends BiomeSource> codec() {
return CODEC;
@@ -182,19 +235,20 @@ public class LatitudeBiomeSource extends BiomeSource {
return pickBiome(spawnSafeBiomes, blockX, blockZ, SPAWN_SELECTOR_SCALE);
}
// Underground (caves) layer.
if (blockY < UNDERGROUND_Y && !pool.underground().isEmpty()) {
return pickBiome(pool.underground(), blockX, blockZ, 0.004);
// Determine the column's base biome: ocean mask, rare wet pocket, or latitude surface biome.
Holder<Biome> base = baseSurfaceBiome(band, pool, blockX, blockZ);
// Underground cave overrides (3-zone model) extend latitude rather than replace it.
return applyCaveOverrides(base, band, blockX, blockY, blockZ);
}
// Land vs ocean mask.
/** Resolve the surface biome for a column: ocean, swamp/mangrove pocket, or the band's surface tag. */
private Holder<Biome> baseSurfaceBiome(BiomeBand band, ResolvedBand pool, int blockX, int blockZ) {
double oceanValue = oceanNoise.noise(blockX * OCEAN_NOISE_SCALE, 0.0, blockZ * OCEAN_NOISE_SCALE);
if (oceanValue > OCEAN_THRESHOLD && !pool.ocean().isEmpty()) {
return pickBiome(pool.ocean(), blockX, blockZ, 0.002);
}
// Rare wet pockets: high moisture carves a swamp (temperate) or mangrove swamp (warm/hot),
// matching vanilla where these are uncommon humid biomes rather than equal-weighted surface ones.
double moisture = moistureNoise.noise(blockX * MOISTURE_SCALE, 0.0, blockZ * MOISTURE_SCALE);
if (moisture > MOISTURE_THRESHOLD) {
if (band == BiomeBand.TEMPERATE && swampBiome != null) {
@@ -205,7 +259,6 @@ public class LatitudeBiomeSource extends BiomeSource {
}
}
// Surface biome from the (mod-aware) climate tag, with a vanilla fallback.
List<Holder<Biome>> surface = resolveSurfaceTag(band);
if (surface == null || surface.isEmpty()) {
surface = pool.surface();
@@ -216,6 +269,48 @@ public class LatitudeBiomeSource extends BiomeSource {
return pickBiome(surface, blockX, blockZ, SURFACE_SELECTOR_SCALE);
}
/**
* Underground overrides keep the latitude biome by default and only carve rare cave features:
* <ul>
* <li>Deep zone (Y &lt; {@value #DEEP_ZONE_TOP}): legendary Deep Dark pockets (~1%), else surface biome.</li>
* <li>Mid caves (DEEP_ZONE_TOP &le; Y &lt; {@value #MID_CAVE_TOP}): lush/dripstone pockets (~8%) matching the climate.</li>
* <li>Near surface: no override, latitude biome as-is.</li>
* </ul>
*/
private Holder<Biome> applyCaveOverrides(Holder<Biome> surfaceBiome, BiomeBand band, int blockX, int blockY, int blockZ) {
// Deep zone: legendary Deep Dark.
if (blockY < DEEP_ZONE_TOP) {
if (deepDarkBiome != null) {
double dd = deepDarkNoise.noise(blockX * DEEP_DARK_SCALE, blockY * DEEP_DARK_SCALE_Y, blockZ * DEEP_DARK_SCALE);
if (dd > DEEP_DARK_THRESHOLD) {
return deepDarkBiome;
}
}
return surfaceBiome;
}
// Mid caves: rare lush/dripstone pockets by climate.
if (blockY < MID_CAVE_TOP) {
Holder<Biome> caveBiome = caveBiomeFor(band);
if (caveBiome != null) {
double cave = caveNoise.noise(blockX * CAVE_SCALE, 0.0, blockZ * CAVE_SCALE);
if (cave > CAVE_THRESHOLD) {
return caveBiome;
}
}
}
return surfaceBiome;
}
/** Lush caves for humid/warm bands, dripstone for dry/cold bands. */
private Holder<Biome> caveBiomeFor(BiomeBand band) {
if (band == BiomeBand.FROZEN || band == BiomeBand.COLD) {
return dripstoneCavesBiome;
}
return lushCavesBiome;
}
/** Lazily resolve surface biomes from climate tags. Tags are bound after the registry freezes,
* so this is safe and never creates unbound holders. */
private List<Holder<Biome>> resolveSurfaceTag(BiomeBand band) {
@@ -258,6 +353,6 @@ public class LatitudeBiomeSource extends BiomeSource {
return value < min ? min : (value > max ? max : value);
}
private record ResolvedBand(List<Holder<Biome>> surface, List<Holder<Biome>> ocean, List<Holder<Biome>> underground) {
private record ResolvedBand(List<Holder<Biome>> surface, List<Holder<Biome>> ocean) {
}
}