From 1716b575d8b99886a895ca356e226d85073ab886 Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Sat, 2 May 2026 23:20:46 +0100 Subject: [PATCH 1/2] fix: preserve biome priority boundaries --- modules/world-worldgen/src/biome_registry.zig | 3 + modules/world-worldgen/src/biome_selector.zig | 15 +++ .../src/biome_selector_tests.zig | 108 ++++++++++++++++++ 3 files changed, 126 insertions(+) diff --git a/modules/world-worldgen/src/biome_registry.zig b/modules/world-worldgen/src/biome_registry.zig index 1ddb15a2..dc0af481 100644 --- a/modules/world-worldgen/src/biome_registry.zig +++ b/modules/world-worldgen/src/biome_registry.zig @@ -368,6 +368,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .humidity = Range.any(), .elevation = .{ .min = 0.28, .max = 0.38 }, .continentalness = .{ .min = 0.35, .max = 0.42 }, // NARROW beach band + .max_height = 70, .max_slope = 2, .priority = 10, .surface = .{ .top = .sand, .filler = .sand, .depth_range = 2 }, @@ -380,6 +381,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .humidity = Range.any(), .elevation = .{ .min = 0.28, .max = 0.45 }, .continentalness = .{ .min = 0.35, .max = 0.45 }, + .max_height = 82, .max_slope = 8, .priority = 11, .surface = .{ .top = .stone, .filler = .gravel, .depth_range = 2 }, @@ -394,6 +396,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .humidity = Range.any(), .elevation = .{ .min = 0.28, .max = 0.38 }, .continentalness = .{ .min = 0.35, .max = 0.42 }, + .max_height = 70, .max_slope = 2, .priority = 12, .surface = .{ .top = .snow_block, .filler = .sand, .depth_range = 2 }, diff --git a/modules/world-worldgen/src/biome_selector.zig b/modules/world-worldgen/src/biome_selector.zig index fdf0fdc9..2c15d55a 100644 --- a/modules/world-worldgen/src/biome_selector.zig +++ b/modules/world-worldgen/src/biome_selector.zig @@ -11,6 +11,9 @@ const BIOME_REGISTRY = registry.BIOME_REGISTRY; const BIOME_POINTS = registry.BIOME_POINTS; const BLEND_EPSILON = registry.BLEND_EPSILON; +const OCEAN_CONTINENTALNESS_MAX: f32 = 0.35; +const NORMALIZED_SEA_LEVEL: f32 = 0.30; + // ============================================================================ // Voronoi Biome Selection (Issue #106) // ============================================================================ @@ -98,6 +101,7 @@ pub fn selectBiomeMultiParam(climate: ClimateParams, structural: StructuralParam var best_biome: BiomeId = .plains; for (BIOME_REGISTRY) |biome| { + if (isOceanBiome(biome.id) and !isOceanStructure(params, structural)) continue; if (!biome.meetsStructuralConstraints(structural.height, structural.slope, structural.continentalness, structural.ridge_mask)) continue; const s = biome.scoreClimate(params); @@ -110,6 +114,17 @@ pub fn selectBiomeMultiParam(climate: ClimateParams, structural: StructuralParam return best_biome; } +fn isOceanStructure(climate: ClimateParams, structural: StructuralParams) bool { + return structural.continentalness < OCEAN_CONTINENTALNESS_MAX and climate.elevation <= NORMALIZED_SEA_LEVEL; +} + +fn isOceanBiome(biome: BiomeId) bool { + return switch (biome) { + .deep_ocean, .ocean, .frozen_ocean, .cold_ocean, .warm_ocean => true, + else => false, + }; +} + /// Select biome with river override pub fn selectBiomeWithRiver(params: ClimateParams, river_mask: f32) BiomeId { // River biome takes priority when river mask is active diff --git a/modules/world-worldgen/src/biome_selector_tests.zig b/modules/world-worldgen/src/biome_selector_tests.zig index 55547b23..020a54ef 100644 --- a/modules/world-worldgen/src/biome_selector_tests.zig +++ b/modules/world-worldgen/src/biome_selector_tests.zig @@ -391,6 +391,114 @@ test "selectBiomeWithConstraints locks structural edge cases" { })); } +test "selectBiomeWithConstraints preserves ocean sea-level boundary" { + const underwater_ocean = ClimateParams{ + .temperature = 0.5, + .humidity = 0.5, + .elevation = 0.30, + .continentalness = 0.25, + .ruggedness = 0.1, + }; + const inland_ocean_candidate = ClimateParams{ + .temperature = 0.5, + .humidity = 0.5, + .elevation = 0.31, + .continentalness = 0.25, + .ruggedness = 0.1, + }; + const structural = StructuralParams{ + .height = 64, + .slope = 1, + .continentalness = 0.25, + .ridge_mask = 0.1, + }; + + try testing.expectEqual(BiomeId.ocean, selectBiomeWithConstraints(underwater_ocean, structural)); + try testing.expect(selectBiomeWithConstraints(inland_ocean_candidate, structural) != .ocean); + try testing.expect(selectBiomeWithConstraints(inland_ocean_candidate, structural) != .deep_ocean); + try testing.expect(selectBiomeWithConstraints(inland_ocean_candidate, structural) != .frozen_ocean); + try testing.expect(selectBiomeWithConstraints(inland_ocean_candidate, structural) != .cold_ocean); + try testing.expect(selectBiomeWithConstraints(inland_ocean_candidate, structural) != .warm_ocean); +} + +test "selectBiomeWithConstraints preserves coast and inland water boundaries" { + const coast_climate = ClimateParams{ + .temperature = 0.6, + .humidity = 0.5, + .elevation = 0.3, + .continentalness = 0.35, + .ruggedness = 0.1, + }; + const coast_structural = StructuralParams{ + .height = 64, + .slope = 1, + .continentalness = 0.35, + .ridge_mask = 0.1, + }; + const steep_coast = StructuralParams{ + .height = 64, + .slope = 3, + .continentalness = 0.35, + .ridge_mask = 0.1, + }; + const high_coast = StructuralParams{ + .height = 71, + .slope = 1, + .continentalness = 0.35, + .ridge_mask = 0.1, + }; + const inland_water = ClimateParams{ + .temperature = 0.6, + .humidity = 0.5, + .elevation = 0.2, + .continentalness = 0.6, + .ruggedness = 0.1, + }; + + try testing.expectEqual(BiomeId.beach, selectBiomeWithConstraints(coast_climate, coast_structural)); + try testing.expect(selectBiomeWithConstraints(coast_climate, steep_coast) != .beach); + try testing.expect(selectBiomeWithConstraints(coast_climate, high_coast) != .beach); + try testing.expect(selectBiomeWithConstraints(inland_water, .{ + .height = 50, + .slope = 1, + .continentalness = 0.6, + .ridge_mask = 0.1, + }) != .beach); +} + +test "selectBiomeWithConstraintsAndRiver preserves river thresholds" { + const temperate = ClimateParams{ + .temperature = 0.21, + .humidity = 0.7, + .elevation = 0.3, + .continentalness = 0.6, + .ruggedness = 0.2, + }; + const frozen = ClimateParams{ + .temperature = 0.20, + .humidity = 0.7, + .elevation = 0.3, + .continentalness = 0.6, + .ruggedness = 0.2, + }; + const structural = StructuralParams{ + .height = 119, + .slope = 2, + .continentalness = 0.6, + .ridge_mask = 0.1, + }; + + try testing.expect(selectBiomeWithConstraintsAndRiver(temperate, structural, 0.5) != .river); + try testing.expectEqual(BiomeId.river, selectBiomeWithConstraintsAndRiver(temperate, structural, 0.5001)); + try testing.expectEqual(BiomeId.frozen_river, selectBiomeWithConstraintsAndRiver(frozen, structural, 0.5001)); + try testing.expect(selectBiomeWithConstraintsAndRiver(temperate, .{ + .height = 120, + .slope = 2, + .continentalness = 0.6, + .ridge_mask = 0.1, + }, 0.5001) != .river); +} + test "selectBiomeWithConstraints documents ridge mask baseline behavior" { const climate = ClimateParams{ .temperature = 0.4, From 9026c9d63e4ea1d4f52e3254f364ca9aa72dd01a Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Sat, 2 May 2026 23:28:57 +0100 Subject: [PATCH 2/2] fix: align simplified ocean boundaries --- modules/world-worldgen/src/biome_registry.zig | 3 +++ modules/world-worldgen/src/biome_selector.zig | 14 +++++++------- .../world-worldgen/src/biome_selector_tests.zig | 16 ++++++++++++++++ src/worldgen_tests.zig | 2 +- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/modules/world-worldgen/src/biome_registry.zig b/modules/world-worldgen/src/biome_registry.zig index dc0af481..cba65863 100644 --- a/modules/world-worldgen/src/biome_registry.zig +++ b/modules/world-worldgen/src/biome_registry.zig @@ -11,6 +11,9 @@ pub const TreeType = tree_registry.TreeType; /// Minimum sum threshold for biome blend calculation to avoid division by near-zero values pub const BLEND_EPSILON: f32 = 0.0001; +/// Normalized elevation produced by computeClimateParams at sea level. +pub const NORMALIZED_SEA_LEVEL: f32 = 0.30; + /// Represents a range of values for biome parameter matching pub const Range = struct { min: f32, diff --git a/modules/world-worldgen/src/biome_selector.zig b/modules/world-worldgen/src/biome_selector.zig index 2c15d55a..f7111998 100644 --- a/modules/world-worldgen/src/biome_selector.zig +++ b/modules/world-worldgen/src/biome_selector.zig @@ -10,9 +10,9 @@ const StructuralParams = registry.StructuralParams; const BIOME_REGISTRY = registry.BIOME_REGISTRY; 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 NORMALIZED_SEA_LEVEL: f32 = 0.30; // ============================================================================ // Voronoi Biome Selection (Issue #106) @@ -145,18 +145,18 @@ pub fn computeClimateParams( sea_level: i32, max_height: i32, ) ClimateParams { - // Normalize elevation: 0 = below sea, 0.3 = sea level, 1.0 = max height + // Normalize elevation: 0 = below sea, NORMALIZED_SEA_LEVEL = sea level, 1.0 = max height // Use conditional to avoid integer overflow when height < sea_level const height_above_sea: i32 = if (height > sea_level) height - sea_level else 0; const elevation_range = max_height - sea_level; const elevation = if (elevation_range > 0) - 0.3 + 0.7 * @as(f32, @floatFromInt(height_above_sea)) / @as(f32, @floatFromInt(elevation_range)) + NORMALIZED_SEA_LEVEL + 0.7 * @as(f32, @floatFromInt(height_above_sea)) / @as(f32, @floatFromInt(elevation_range)) else - 0.3; + NORMALIZED_SEA_LEVEL; - // For underwater: scale 0-0.3 + // For underwater: scale 0-NORMALIZED_SEA_LEVEL const final_elevation = if (height < sea_level) - 0.3 * @as(f32, @floatFromInt(@max(0, height))) / @as(f32, @floatFromInt(sea_level)) + NORMALIZED_SEA_LEVEL * @as(f32, @floatFromInt(@max(0, height))) / @as(f32, @floatFromInt(sea_level)) else elevation; @@ -286,7 +286,7 @@ pub fn selectBiomeSimple(climate: ClimateParams) BiomeId { const continental = climate.continentalness; // Ocean check - if (continental < 0.35) { + if (continental < OCEAN_CONTINENTALNESS_MAX and climate.elevation <= NORMALIZED_SEA_LEVEL) { if (heat <= 15) return .frozen_ocean; if (heat <= 30) return .cold_ocean; if (continental < 0.20) return .deep_ocean; diff --git a/modules/world-worldgen/src/biome_selector_tests.zig b/modules/world-worldgen/src/biome_selector_tests.zig index 020a54ef..d30fafad 100644 --- a/modules/world-worldgen/src/biome_selector_tests.zig +++ b/modules/world-worldgen/src/biome_selector_tests.zig @@ -624,6 +624,22 @@ test "selectBiomeSimple returns ocean for low continentalness" { try testing.expect(biome == .deep_ocean or biome == .ocean); } +test "selectBiomeSimple does not return ocean above sea level" { + const params = ClimateParams{ + .temperature = 0.5, + .humidity = 0.5, + .elevation = 0.31, + .continentalness = 0.15, + .ruggedness = 0.3, + }; + const biome = selectBiomeSimple(params); + try testing.expect(biome != .deep_ocean); + try testing.expect(biome != .ocean); + try testing.expect(biome != .frozen_ocean); + try testing.expect(biome != .cold_ocean); + try testing.expect(biome != .warm_ocean); +} + test "selectBiomeSimple returns deep_ocean for very low continentalness" { const params = ClimateParams{ .temperature = 0.5, diff --git a/src/worldgen_tests.zig b/src/worldgen_tests.zig index a37c4ae7..f6ad9358 100644 --- a/src/worldgen_tests.zig +++ b/src/worldgen_tests.zig @@ -986,7 +986,7 @@ test "BiomeSource selectBiomeSimplified returns valid biome" { const climate2 = biome_mod.ClimateParams{ .temperature = 0.5, .humidity = 0.5, - .elevation = 0.4, + .elevation = 0.2, .continentalness = 0.1, .ruggedness = 0.2, };