diff --git a/modules/world-worldgen/src/biome_registry.zig b/modules/world-worldgen/src/biome_registry.zig index 92c30120..51b5113f 100644 --- a/modules/world-worldgen/src/biome_registry.zig +++ b/modules/world-worldgen/src/biome_registry.zig @@ -62,6 +62,15 @@ pub const TerrainModifier = struct { clamp_to_sea_level: bool = false, /// Additional height offset height_offset: f32 = 0.0, + + /// Apply biome terrain shaping to a sampled height without mutating the + /// live generation pipeline. + pub fn applyHeight(self: TerrainModifier, base_height: f32, sea_level: f32) f32 { + var height = sea_level + (base_height - sea_level) * self.height_amplitude; + height += (sea_level - height) * self.smoothing; + if (self.clamp_to_sea_level) height = sea_level; + return height + self.height_offset; + } }; /// Surface block configuration diff --git a/modules/world-worldgen/src/root.zig b/modules/world-worldgen/src/root.zig index 92712397..141d985d 100644 --- a/modules/world-worldgen/src/root.zig +++ b/modules/world-worldgen/src/root.zig @@ -32,6 +32,7 @@ pub const shadow_test_world = @import("shadow_test_world.zig"); pub const surface_builder = @import("surface_builder.zig"); pub const terrain_shape_generator = @import("terrain_shape_generator.zig"); pub const terrain_shape_generator_tests = @import("terrain_shape_generator_tests.zig"); +pub const terrain_modifier_tests = @import("terrain_modifier_tests.zig"); pub const tree_registry = @import("tree_registry.zig"); pub const world_class = @import("world_class.zig"); pub const world_map = @import("world_map.zig"); diff --git a/modules/world-worldgen/src/terrain_modifier_tests.zig b/modules/world-worldgen/src/terrain_modifier_tests.zig new file mode 100644 index 00000000..8ea1a13d --- /dev/null +++ b/modules/world-worldgen/src/terrain_modifier_tests.zig @@ -0,0 +1,56 @@ +//! Unit tests for terrain modifier height semantics. + +const std = @import("std"); +const testing = std.testing; +const TerrainModifier = @import("biome_registry.zig").TerrainModifier; + +const SEA_LEVEL: f32 = 62.0; + +test "TerrainModifier defaults preserve sampled height" { + const modifier = TerrainModifier{}; + + try testing.expectApproxEqAbs(@as(f32, 62.0), modifier.applyHeight(62.0, SEA_LEVEL), 0.0001); + try testing.expectApproxEqAbs(@as(f32, 144.0), modifier.applyHeight(144.0, SEA_LEVEL), 0.0001); +} + +test "TerrainModifier height_amplitude scales relief around sea level" { + const flat = TerrainModifier{ .height_amplitude = 0.0 }; + const doubled = TerrainModifier{ .height_amplitude = 2.0 }; + + try testing.expectApproxEqAbs(@as(f32, 62.0), flat.applyHeight(144.0, SEA_LEVEL), 0.0001); + try testing.expectApproxEqAbs(@as(f32, 226.0), doubled.applyHeight(144.0, SEA_LEVEL), 0.0001); + try testing.expectApproxEqAbs(@as(f32, 42.0), doubled.applyHeight(52.0, SEA_LEVEL), 0.0001); +} + +test "TerrainModifier smoothing blends height toward sea level" { + const unchanged = TerrainModifier{ .smoothing = 0.0 }; + const half = TerrainModifier{ .smoothing = 0.5 }; + const full = TerrainModifier{ .smoothing = 1.0 }; + + try testing.expectApproxEqAbs(@as(f32, 102.0), unchanged.applyHeight(102.0, SEA_LEVEL), 0.0001); + try testing.expectApproxEqAbs(@as(f32, 82.0), half.applyHeight(102.0, SEA_LEVEL), 0.0001); + try testing.expectApproxEqAbs(@as(f32, 62.0), full.applyHeight(102.0, SEA_LEVEL), 0.0001); +} + +test "TerrainModifier clamp_to_sea_level forces height before offset" { + const modifier = TerrainModifier{ .clamp_to_sea_level = true, .height_offset = -2.0 }; + + try testing.expectApproxEqAbs(@as(f32, 60.0), modifier.applyHeight(61.0, SEA_LEVEL), 0.0001); + try testing.expectApproxEqAbs(@as(f32, 60.0), modifier.applyHeight(180.0, SEA_LEVEL), 0.0001); +} + +test "TerrainModifier height_offset applies after shaping" { + const raised = TerrainModifier{ .height_offset = 8.0 }; + const lowered = TerrainModifier{ .height_offset = -3.0 }; + + try testing.expectApproxEqAbs(@as(f32, 70.0), raised.applyHeight(62.0, SEA_LEVEL), 0.0001); + try testing.expectApproxEqAbs(@as(f32, 141.0), lowered.applyHeight(144.0, SEA_LEVEL), 0.0001); +} + +test "TerrainModifier combines amplitude smoothing clamp and offset deterministically" { + const shaped = TerrainModifier{ .height_amplitude = 1.5, .smoothing = 0.5, .height_offset = 4.0 }; + const clamped = TerrainModifier{ .height_amplitude = 1.5, .smoothing = 0.5, .clamp_to_sea_level = true, .height_offset = 4.0 }; + + try testing.expectApproxEqAbs(@as(f32, 96.0), shaped.applyHeight(102.0, SEA_LEVEL), 0.0001); + try testing.expectApproxEqAbs(@as(f32, 66.0), clamped.applyHeight(102.0, SEA_LEVEL), 0.0001); +} diff --git a/src/tests.zig b/src/tests.zig index 3d961095..e059df49 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -60,6 +60,7 @@ test { _ = @import("world-worldgen").biome_registry_tests; _ = @import("world-worldgen").biome_selector_tests; _ = @import("world-worldgen").height_sampler_tests; + _ = @import("world-worldgen").terrain_modifier_tests; _ = @import("world-worldgen").terrain_shape_generator_tests; _ = @import("world-lod").lod_manager_tests; _ = @import("world-lod").lod_seam;