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:
@@ -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 < 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 ≤ Y < 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);
|
||||
|
||||
// Land vs ocean mask.
|
||||
// Underground cave overrides (3-zone model) extend latitude rather than replace it.
|
||||
return applyCaveOverrides(base, band, blockX, blockY, blockZ);
|
||||
}
|
||||
|
||||
/** 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 < {@value #DEEP_ZONE_TOP}): legendary Deep Dark pockets (~1%), else surface biome.</li>
|
||||
* <li>Mid caves (DEEP_ZONE_TOP ≤ Y < {@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) {
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user