Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions modules/world-worldgen/src/biome_edge_detector.zig
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ pub const TRANSITION_RULES = [_]TransitionRule{
.{ .biome_a = .snowy_beach, .biome_b = .beach, .transition = .stony_shore },
.{ .biome_a = .snow_tundra, .biome_b = .beach, .transition = .snowy_beach },
.{ .biome_a = .taiga, .biome_b = .beach, .transition = .stony_shore },
.{ .biome_a = .beach, .biome_b = .plains, .transition = .coastal_plains },
.{ .biome_a = .beach, .biome_b = .forest, .transition = .coastal_plains },
.{ .biome_a = .beach, .biome_b = .birch_forest, .transition = .coastal_plains },
.{ .biome_a = .beach, .biome_b = .flower_forest, .transition = .coastal_plains },
.{ .biome_a = .beach, .biome_b = .swamp, .transition = .coastal_plains },
.{ .biome_a = .beach, .biome_b = .mangrove_swamp, .transition = .coastal_plains },
.{ .biome_a = .beach, .biome_b = .tropical, .transition = .coastal_plains },

// Wetland <-> Forest
.{ .biome_a = .swamp, .biome_b = .forest, .transition = .marsh },
Expand Down
124 changes: 62 additions & 62 deletions modules/world-worldgen/src/biome_registry.zig

Large diffs are not rendered by default.

20 changes: 14 additions & 6 deletions modules/world-worldgen/src/biome_registry_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -349,11 +349,11 @@ test "getBiomeDefinition ocean biomes have correct continentalness ranges" {
try testing.expect(deep.continentalness.max <= 0.20);

const ocean = getBiomeDefinition(.ocean);
try testing.expect(ocean.continentalness.max <= 0.35);
try testing.expect(ocean.continentalness.min < 0.35);
try testing.expect(ocean.continentalness.max <= 0.37);
try testing.expect(ocean.continentalness.min < 0.37);

const warm = getBiomeDefinition(.warm_ocean);
try testing.expect(warm.continentalness.max <= 0.35);
try testing.expect(warm.continentalness.max <= 0.37);
try testing.expect(warm.vegetation.seagrass_density > 0.0);
try testing.expect(warm.vegetation.coral_density > 0.0);
}
Expand All @@ -363,7 +363,7 @@ test "getBiomeDefinition cold ocean variants have expected identity" {
try testing.expectEqualStrings("Frozen Ocean", frozen.name);
try testing.expectEqual(.packed_ice, frozen.surface.top);
try testing.expect(frozen.temperature.max <= 0.15);
try testing.expect(frozen.continentalness.max <= 0.35);
try testing.expect(frozen.continentalness.max <= 0.37);

const cold = getBiomeDefinition(.cold_ocean);
try testing.expectEqualStrings("Cold Ocean", cold.name);
Expand All @@ -384,8 +384,8 @@ test "getBiomeDefinition cold coastal variants have expected surfaces" {
try testing.expectEqualStrings("Stony Shore", stony.name);
try testing.expectEqual(.stone, stony.surface.top);
try testing.expectEqual(.gravel, stony.surface.filler);
try testing.expect(stony.continentalness.min >= 0.35);
try testing.expect(stony.continentalness.max <= 0.45);
try testing.expect(stony.continentalness.min >= 0.37);
try testing.expect(stony.continentalness.max <= 0.46);

const snowy = getBiomeDefinition(.snowy_beach);
try testing.expectEqualStrings("Snowy Beach", snowy.name);
Expand All @@ -394,6 +394,14 @@ test "getBiomeDefinition cold coastal variants have expected surfaces" {
try testing.expect(snowy.temperature.max <= 0.20);
}

test "getBiomeDefinition coastal plains is sandy transition" {
const coastal = getBiomeDefinition(.coastal_plains);
try testing.expectEqualStrings("Coastal Plains", coastal.name);
try testing.expectEqual(.sand, coastal.surface.top);
try testing.expectEqual(.sand, coastal.surface.filler);
try testing.expectEqual(@as(usize, 0), coastal.vegetation.decoration_rules.len);
}

test "getBiomeDefinition frozen river is river override only" {
const frozen_river = getBiomeDefinition(.frozen_river);
try testing.expectEqualStrings("Frozen River", frozen_river.name);
Expand Down
2 changes: 1 addition & 1 deletion modules/world-worldgen/src/biome_selector.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const BIOME_POINTS = registry.BIOME_POINTS;
const BLEND_EPSILON = registry.BLEND_EPSILON;
const NORMALIZED_SEA_LEVEL = registry.NORMALIZED_SEA_LEVEL;

const OCEAN_CONTINENTALNESS_MAX: f32 = 0.35;
const OCEAN_CONTINENTALNESS_MAX: f32 = 0.37;

// ============================================================================
// Voronoi Biome Selection (Issue #106)
Expand Down
7 changes: 7 additions & 0 deletions modules/world-worldgen/src/biome_selector_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
const std = @import("std");
const testing = std.testing;
const selector = @import("biome_selector.zig");
const edge_detector = @import("biome_edge_detector.zig");
const registry = @import("biome_registry.zig");

const BiomeId = registry.BiomeId;
Expand Down Expand Up @@ -298,6 +299,12 @@ test "selectBiomeWithConstraints locks baseline climate and structural selection
}
}

test "beach transitions to coastal plains before common inland biomes" {
try testing.expectEqual(BiomeId.coastal_plains, edge_detector.getTransitionBiome(.beach, .plains).?);
try testing.expectEqual(BiomeId.coastal_plains, edge_detector.getTransitionBiome(.forest, .beach).?);
try testing.expectEqual(BiomeId.coastal_plains, edge_detector.getTransitionBiome(.beach, .swamp).?);
}

test "selectBiomeWithConstraintsAndRiver locks river and frozen river priority" {
const temperate = ClimateParams{
.temperature = 0.5,
Expand Down
2 changes: 1 addition & 1 deletion modules/world-worldgen/src/biome_source.zig
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub const BiomeResult = struct {
pub const BiomeSourceParams = struct {
sea_level: i32 = 64,
edge_detection_enabled: bool = true,
ocean_threshold: f32 = 0.35,
ocean_threshold: f32 = 0.37,
};

/// Unified biome selection interface.
Expand Down
41 changes: 21 additions & 20 deletions modules/world-worldgen/src/height_sampler.zig
Original file line number Diff line number Diff line change
Expand Up @@ -36,27 +36,27 @@ pub const HeightParams = struct {
sea_level: i32 = 64,

// Continental zone thresholds
ocean_threshold: f32 = 0.35,
ocean_threshold: f32 = 0.37,
continental_deep_ocean_max: f32 = 0.20,
continental_coast_max: f32 = 0.42,
continental_inland_low_max: f32 = 0.60,
continental_inland_high_max: f32 = 0.75,
continental_coast_max: f32 = 0.44,
continental_inland_low_max: f32 = 0.58,
continental_inland_high_max: f32 = 0.72,

// Mountains
mount_amp: f32 = 60.0,
mount_cap: f32 = 120.0,
mount_inland_min: f32 = 0.60,
mount_inland_max: f32 = 0.80,
mount_peak_min: f32 = 0.55,
mount_peak_max: f32 = 0.85,
mount_rugged_min: f32 = 0.35,
mount_rugged_max: f32 = 0.75,
mount_amp: f32 = 78.0,
mount_cap: f32 = 140.0,
mount_inland_min: f32 = 0.56,
mount_inland_max: f32 = 0.76,
mount_peak_min: f32 = 0.50,
mount_peak_max: f32 = 0.82,
mount_rugged_min: f32 = 0.32,
mount_rugged_max: f32 = 0.72,

// Ridges
ridge_amp: f32 = 25.0,
ridge_inland_min: f32 = 0.50,
ridge_inland_max: f32 = 0.70,
ridge_sparsity: f32 = 0.50,
ridge_amp: f32 = 34.0,
ridge_inland_min: f32 = 0.48,
ridge_inland_max: f32 = 0.68,
ridge_sparsity: f32 = 0.46,

// Detail
highland_range: f32 = 80.0,
Expand Down Expand Up @@ -276,7 +276,8 @@ pub const HeightSampler = struct {
// ============================================================
// STEP 4: LAND - Combine V7 terrain with continental base
// ============================================================
var height = self.getBaseHeight(noise.continentalness) + v7_terrain - path_effects.depth;
const coastal_ramp = smoothstep(p.ocean_threshold, p.continental_coast_max, noise.continentalness);
var height = self.getBaseHeight(noise.continentalness) + v7_terrain * coastal_ramp - path_effects.depth * coastal_ramp;

// ============================================================
// STEP 5: Mountains & Ridges - REGION-CONSTRAINED
Expand All @@ -302,7 +303,7 @@ pub const HeightSampler = struct {
// ============================================================
const erosion_smooth = smoothstep(0.5, 0.75, noise.erosion);
const land_factor = smoothstep(p.continental_coast_max, p.continental_inland_low_max, noise.continentalness);
const hills_atten = (1.0 - erosion_smooth) * land_factor * (1.0 - path_effects.slope_suppress);
const hills_atten = (1.0 - erosion_smooth) * land_factor * coastal_ramp * (1.0 - path_effects.slope_suppress);

// Small-scale detail
const elev01 = clamp01((height - sea) / p.highland_range);
Expand Down Expand Up @@ -402,8 +403,8 @@ test "HeightSampler ocean detection" {
const sampler = HeightSampler.init();

try std.testing.expect(sampler.isOcean(0.0));
try std.testing.expect(sampler.isOcean(0.34));
try std.testing.expect(!sampler.isOcean(0.35));
try std.testing.expect(sampler.isOcean(0.36));
try std.testing.expect(!sampler.isOcean(0.37));
try std.testing.expect(!sampler.isOcean(0.5));
}

Expand Down
3 changes: 2 additions & 1 deletion modules/world-worldgen/src/height_sampler_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ test "HeightSampler isOcean at deep ocean value" {

test "HeightSampler isOcean at exactly threshold" {
const sampler = HeightSampler.init();
try testing.expect(!sampler.isOcean(0.35));
try testing.expect(sampler.isOcean(0.35));
try testing.expect(!sampler.isOcean(0.37));
}

test "HeightSampler isOcean inland values return false" {
Expand Down
26 changes: 13 additions & 13 deletions modules/world-worldgen/src/noise_sampler.zig
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub const ClimateConfig = struct {
temperature_local_scale: f32 = 1.0 / 200.0,
humidity_macro_scale: f32 = 1.0 / 2000.0,
humidity_local_scale: f32 = 1.0 / 200.0,
climate_macro_weight: f32 = 0.75,
climate_macro_weight: f32 = 0.65,
};

/// Configuration parameters for terrain noise
Expand All @@ -54,8 +54,8 @@ pub const TerrainConfig = struct {
seabed_scale: f32 = 1.0 / 100.0,
seabed_amp: f32 = 2.0,
river_scale: f32 = 1.0 / 800.0,
river_min: f32 = 0.90,
river_max: f32 = 0.95,
river_min: f32 = 0.86,
river_max: f32 = 0.93,
ridge_scale: f32 = 1.0 / 400.0,
};

Expand Down Expand Up @@ -164,26 +164,26 @@ pub const NoiseSampler = struct {
.warp_noise_z = ConfiguredNoise.init(makeNoiseParams(seed, 11, 200, tc.warp_amplitude, 0, 3, 0.5)),

// Continental structure
.continentalness_noise = ConfiguredNoise.init(makeNoiseParams(seed, 20, 1500, 1.0, 0, 4, 0.5)),
.erosion_noise = ConfiguredNoise.init(makeNoiseParams(seed, 30, 400, 1.0, 0, 4, 0.5)),
.peaks_noise = ConfiguredNoise.init(makeNoiseParams(seed, 40, 300, 1.0, 0, 5, 0.5)),
.continentalness_noise = ConfiguredNoise.init(makeNoiseParams(seed, 20, 900, 1.0, 0, 4, 0.5)),
.erosion_noise = ConfiguredNoise.init(makeNoiseParams(seed, 30, 360, 1.0, 0, 4, 0.5)),
.peaks_noise = ConfiguredNoise.init(makeNoiseParams(seed, 40, 260, 1.0, 0, 5, 0.5)),

// Climate
.temperature_noise = ConfiguredNoise.init(makeNoiseParams(seed, 50, 2000, 1.0, 0, 3, 0.5)),
.humidity_noise = ConfiguredNoise.init(makeNoiseParams(seed, 60, 2000, 1.0, 0, 3, 0.5)),
.temperature_noise = ConfiguredNoise.init(makeNoiseParams(seed, 50, 1400, 1.0, 0, 3, 0.5)),
.humidity_noise = ConfiguredNoise.init(makeNoiseParams(seed, 60, 1400, 1.0, 0, 3, 0.5)),
.temperature_local_noise = ConfiguredNoise.init(makeNoiseParams(seed, 70, 200, 1.0, 0, 3, 0.5)),
.humidity_local_noise = ConfiguredNoise.init(makeNoiseParams(seed, 80, 200, 1.0, 0, 3, 0.5)),

// Terrain detail
.detail_noise = ConfiguredNoise.init(makeNoiseParams(seed, 90, 32, tc.detail_amp, 0, 3, 0.5)),
.coast_jitter_noise = ConfiguredNoise.init(makeNoiseParams(seed, 100, 150, 0.03, 0, 2, 0.5)),
.coast_jitter_noise = ConfiguredNoise.init(makeNoiseParams(seed, 100, 130, 0.035, 0, 2, 0.5)),
.seabed_noise = ConfiguredNoise.init(makeNoiseParams(seed, 110, 100, tc.seabed_amp, 0, 2, 0.5)),
.beach_exposure_noise = ConfiguredNoise.init(makeNoiseParams(seed, 130, 100, 1.0, 0, 3, 0.5)),
.filler_depth_noise = ConfiguredNoise.init(makeNoiseParams(seed, 140, 64, 1.0, 0, 3, 0.5)),

// Mountains & ridges
.mountain_lift_noise = ConfiguredNoise.init(makeNoiseParams(seed, 150, 400, 1.0, 0, 3, 0.5)),
.ridge_noise = ConfiguredNoise.init(makeNoiseParams(seed, 160, 400, 1.0, 0, 5, 0.5)),
.mountain_lift_noise = ConfiguredNoise.init(makeNoiseParams(seed, 150, 340, 1.0, 0, 3, 0.5)),
.ridge_noise = ConfiguredNoise.init(makeNoiseParams(seed, 160, 340, 1.0, 0, 5, 0.5)),

// Rivers
.river_noise = ConfiguredNoise.init(makeNoiseParams(seed, 120, 800, 1.0, 0, 4, 0.5)),
Expand Down Expand Up @@ -247,7 +247,7 @@ pub const NoiseSampler = struct {
const macro = self.temperature_noise.get2DNormalizedOctaves(x, z, macro_octaves);
const local = self.temperature_local_noise.get2DNormalizedOctaves(x, z, local_octaves);
var t = cc.climate_macro_weight * macro + (1.0 - cc.climate_macro_weight) * local;
t = (t - 0.5) * 2.2 + 0.5;
t = (t - 0.5) * 2.45 + 0.5;
return clamp01(t);
}

Expand All @@ -260,7 +260,7 @@ pub const NoiseSampler = struct {
const macro = self.humidity_noise.get2DNormalizedOctaves(x, z, macro_octaves);
const local = self.humidity_local_noise.get2DNormalizedOctaves(x, z, local_octaves);
var h = cc.climate_macro_weight * macro + (1.0 - cc.climate_macro_weight) * local;
h = (h - 0.5) * 2.2 + 0.5;
h = (h - 0.5) * 2.45 + 0.5;
return clamp01(h);
}

Expand Down
2 changes: 1 addition & 1 deletion modules/world-worldgen/src/registry.zig
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ fn initOverworld(seed: u64, allocator: std.mem.Allocator) RegistryError!Generato
OverworldGenerator.initWithParams(seed, allocator, deco_registry.StandardDecorationProvider.provider(), .{
.terrain_shape = .{
.sea_level = if (restore_water) 64 else -1,
.ocean_threshold = if (restore_water) 0.35 else -1.0,
.ocean_threshold = if (restore_water) 0.37 else -1.0,
.disable_caves = !restore_caves,
},
.basic_chunks_only = !restore_decorations,
Expand Down
31 changes: 26 additions & 5 deletions modules/world-worldgen/src/surface_builder.zig
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ pub const SurfaceParams = struct {
sea_level: i32 = 64,

// Beach constraints
beach_max_height_above_sea: i32 = 3,
beach_max_height_above_sea: i32 = 6,
beach_max_slope: i32 = 2,
cliff_min_slope: i32 = 5,
gravel_erosion_threshold: f32 = 0.7,

// Coastal zone (continentalness thresholds)
ocean_threshold: f32 = 0.35,
beach_band: f32 = 0.05, // Width of beach zone in continentalness units
ocean_threshold: f32 = 0.37,
beach_band: f32 = 0.07, // Width of beach zone in continentalness units
};

// ============================================================================
Expand Down Expand Up @@ -216,7 +216,8 @@ pub const SurfaceBuilder = struct {
var block = self.getBlockAt(y, terrain_height, biome, filler_depth, is_ocean_water, is_underwater);

const is_surface = (y == terrain_height);
const is_near_surface = (y > terrain_height - 3 and y <= terrain_height);
const coastal_fill_depth = @max(filler_depth, 6);
const is_coastal_fill = (y > terrain_height - coastal_fill_depth and y <= terrain_height);

// Apply structural coastal surface types (ocean beaches only)
if (is_surface and block != .air and block != .water and block != .bedrock) {
Expand All @@ -226,7 +227,7 @@ pub const SurfaceBuilder = struct {
.cliff => block = .stone,
.none => {},
}
} else if (is_near_surface and (coastal_type == .sand_beach or coastal_type == .gravel_beach) and block == .dirt) {
} else if (is_coastal_fill and (coastal_type == .sand_beach or coastal_type == .gravel_beach) and block != .air and block != .water and block != .bedrock) {
block = if (coastal_type == .gravel_beach) .gravel else .sand;
}

Expand Down Expand Up @@ -268,6 +269,26 @@ test "SurfaceBuilder coastal type detection" {
try std.testing.expectEqual(CoastalSurfaceType.none, high);
}

test "SurfaceBuilder beach band matches coastal biome range" {
const builder = SurfaceBuilder.init();

const upper_beach = builder.getCoastalSurfaceType(0.41, 1, 70, 0.3);
try std.testing.expectEqual(CoastalSurfaceType.sand_beach, upper_beach);

const inland = builder.getCoastalSurfaceType(0.45, 1, 70, 0.3);
try std.testing.expectEqual(CoastalSurfaceType.none, inland);
}

test "SurfaceBuilder coastal beach replaces exposed filler" {
const builder = SurfaceBuilder.init();

const deep_fill = builder.getSurfaceBlock(65, 70, .plains, 3, false, false, .sand_beach);
try std.testing.expectEqual(BlockType.sand, deep_fill);

const below_fill = builder.getSurfaceBlock(63, 70, .plains, 3, false, false, .sand_beach);
try std.testing.expectEqual(BlockType.stone, below_fill);
}

test "SurfaceBuilder bedrock at y=0" {
const builder = SurfaceBuilder.init();
const block = builder.getBlockAt(0, 50, .plains, 3, false, false);
Expand Down
34 changes: 33 additions & 1 deletion modules/world-worldgen/src/terrain_report.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const Allocator = std.mem.Allocator;
const BiomeId = biome_mod.BiomeId;
const CHUNK_SIZE_Y = world_core.CHUNK_SIZE_Y;

pub const representative_seeds = [_]u64{ 42, 424242, 987654321 };
pub const representative_seeds = [_]u64{ 42, 1337, 424242, 8675309, 987654321 };
pub const default_origin_x: i32 = -256;
pub const default_origin_z: i32 = -256;
pub const default_width: u32 = 512;
Expand Down Expand Up @@ -478,3 +478,35 @@ test "TerrainReport role profiles preserve region pacing controls" {
try std.testing.expect(forest.vegetation_multiplier > boundary.vegetation_multiplier);
try std.testing.expect(forest.subbiome_mask > transit.subbiome_mask);
}

test "representative seeds keep varied but readable spawn regions" {
const allocator = std.testing.allocator;

var total_samples: u32 = 0;
var ocean_samples: u32 = 0;
var forest_samples: u32 = 0;
var wetland_samples: u32 = 0;
var dry_samples: u32 = 0;
var mountain_samples: u32 = 0;

for (representative_seeds) |seed| {
const report = try sampleRegion(allocator, seed, -128, -128, 256, 256);
try std.testing.expect(report.ocean_ratio <= 0.30);
try std.testing.expect(report.sea_level_coverage <= 0.30);
try std.testing.expect(report.mountain_coverage <= 0.12);

total_samples += report.sample_count;
ocean_samples += report.biomeCount(.ocean) + report.biomeCount(.warm_ocean) + report.biomeCount(.cold_ocean) + report.biomeCount(.frozen_ocean) + report.biomeCount(.deep_ocean);
forest_samples += report.biomeCount(.forest) + report.biomeCount(.birch_forest) + report.biomeCount(.dark_forest) + report.biomeCount(.flower_forest) + report.biomeCount(.taiga) + report.biomeCount(.snowy_taiga) + report.biomeCount(.old_growth_taiga) + report.biomeCount(.jungle) + report.biomeCount(.bamboo_jungle) + report.biomeCount(.sparse_jungle);
wetland_samples += report.biomeCount(.swamp) + report.biomeCount(.mangrove_swamp);
dry_samples += report.biomeCount(.desert) + report.biomeCount(.savanna) + report.biomeCount(.savanna_plateau) + report.biomeCount(.windswept_savanna) + report.biomeCount(.badlands) + report.biomeCount(.wooded_badlands) + report.biomeCount(.eroded_badlands);
mountain_samples += @intFromFloat(report.mountain_coverage * @as(f64, @floatFromInt(report.sample_count)));
}

const denominator: f64 = @floatFromInt(total_samples);
try std.testing.expect(@as(f64, @floatFromInt(ocean_samples)) / denominator >= 0.03);
try std.testing.expect(@as(f64, @floatFromInt(forest_samples)) / denominator >= 0.08);
try std.testing.expect(@as(f64, @floatFromInt(wetland_samples)) / denominator >= 0.005);
try std.testing.expect(@as(f64, @floatFromInt(dry_samples)) / denominator >= 0.003);
try std.testing.expect(@as(f64, @floatFromInt(mountain_samples)) / denominator >= 0.002);
}
8 changes: 4 additions & 4 deletions modules/world-worldgen/src/terrain_shape_generator.zig
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ const CoastalGenerator = @import("coastal_generator.zig").CoastalGenerator;
pub const Params = struct {
temp_lapse: f32 = 0.25,
sea_level: i32 = 64,
ocean_threshold: f32 = 0.35,
ridge_inland_min: f32 = 0.50,
ridge_inland_max: f32 = 0.70,
ridge_sparsity: f32 = 0.50,
ocean_threshold: f32 = 0.37,
ridge_inland_min: f32 = 0.48,
ridge_inland_max: f32 = 0.68,
ridge_sparsity: f32 = 0.46,
disable_caves: bool = false,
};

Expand Down
Loading
Loading