feat(tools): Outcrop and BoulderField primitives with forced rock material
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
aae9bcd301
commit
6dd2e86f91
2 files changed
+135
-1
No files matched your search
@@ -8,7 +8,7 @@ const SiteTerrainData = preload("res://game/domain/sites/site_terrain_data.gd")
|
|||||||
|
|
||||||
const ZONE1_TERRAIN_PATH := "res://content/sites/zone1_vigilant_terrain.res"
|
const ZONE1_TERRAIN_PATH := "res://content/sites/zone1_vigilant_terrain.res"
|
||||||
|
|
||||||
const TEST_COUNT := 26
|
const TEST_COUNT := 36
|
||||||
|
|
||||||
var _failures: Array[String] = []
|
var _failures: Array[String] = []
|
||||||
|
|
||||||
@@ -20,6 +20,8 @@ func _init() -> void:
|
|||||||
_test_zone1_artefact_is_stable()
|
_test_zone1_artefact_is_stable()
|
||||||
_test_canyon()
|
_test_canyon()
|
||||||
_test_escarpment()
|
_test_escarpment()
|
||||||
|
_test_outcrop()
|
||||||
|
_test_boulder_field()
|
||||||
|
|
||||||
if _failures.is_empty():
|
if _failures.is_empty():
|
||||||
print("Site baker unit tests: %d passed, 0 failed." % TEST_COUNT)
|
print("Site baker unit tests: %d passed, 0 failed." % TEST_COUNT)
|
||||||
@@ -144,6 +146,34 @@ func _test_escarpment() -> void:
|
|||||||
_expect(is_equal_approx(cliff.elevation_delta(0.0, 12.0), -15.0), "escarpment keeps falling at slope_beyond")
|
_expect(is_equal_approx(cliff.elevation_delta(0.0, 12.0), -15.0), "escarpment keeps falling at slope_beyond")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_outcrop() -> void:
|
||||||
|
var mesa := SiteTerrainBaker.Outcrop.new(Vector2.ZERO, 10.0, 10.0, 6.0, 4.0, 0, 0.0, true)
|
||||||
|
_expect(is_equal_approx(mesa.elevation_delta(0.0, 0.0), 6.0), "outcrop top is flat at height_m")
|
||||||
|
_expect(is_equal_approx(mesa.elevation_delta(0.0, 8.0), 3.0), "outcrop flank is half way down in the middle of the wall")
|
||||||
|
_expect(is_equal_approx(mesa.elevation_delta(0.0, 25.0), 0.0), "outcrop is zero past its outline")
|
||||||
|
_expect(mesa.material_at(0.0, 0.0) == SonarMaterial.Value.ROCK, "rock_top makes the flat summit rock")
|
||||||
|
_expect(mesa.material_at(0.0, 25.0) == SiteTerrainBaker.NO_MATERIAL, "outcrop leaves the surroundings to the slope rule")
|
||||||
|
var sand_top := SiteTerrainBaker.Outcrop.new(Vector2.ZERO, 10.0, 10.0, 6.0, 4.0, 0, 0.0, false)
|
||||||
|
_expect(sand_top.material_at(0.0, 0.0) == SiteTerrainBaker.NO_MATERIAL, "without rock_top the summit follows the slope rule")
|
||||||
|
var pit := SiteTerrainBaker.Outcrop.new(Vector2.ZERO, 10.0, 20.0, -5.0, 3.0, 0, 0.0, false)
|
||||||
|
_expect(is_equal_approx(pit.elevation_delta(0.0, 0.0), -5.0), "negative height digs a flat-bottomed pit")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_boulder_field() -> void:
|
||||||
|
var first := SiteTerrainBaker.BoulderField.new(Vector2(50.0, -50.0), 30.0, 11, 20, 2.0, 4.0, 0.8)
|
||||||
|
var second := SiteTerrainBaker.BoulderField.new(Vector2(50.0, -50.0), 30.0, 11, 20, 2.0, 4.0, 0.8)
|
||||||
|
_expect(first.boulders() == second.boulders(), "same seed scatters the same boulders")
|
||||||
|
var all_inside := true
|
||||||
|
for boulder in first.boulders():
|
||||||
|
all_inside = all_inside and Vector2(boulder.x, boulder.y).distance_to(Vector2(50.0, -50.0)) <= 30.0
|
||||||
|
_expect(all_inside and first.boulders().size() == 20, "boulders stay inside the field disc")
|
||||||
|
var top := first.boulders()[0]
|
||||||
|
_expect(first.elevation_delta(top.x, top.y) >= top.z * 0.8 - 0.001, "a boulder centre is at least its own height")
|
||||||
|
_expect(first.material_at(top.x, top.y) == SonarMaterial.Value.ROCK and first.material_at(200.0, 200.0) == SiteTerrainBaker.NO_MATERIAL, "boulders are rock, the sand around is not forced")
|
||||||
|
var stacked := SiteTerrainBaker.BoulderField.new(Vector2.ZERO, 0.0, 3, 5, 2.0, 4.0, 1.0)
|
||||||
|
_expect(stacked.elevation_delta(0.0, 0.0) <= 4.0 + 0.001, "overlapping boulders combine by maximum, not by sum")
|
||||||
|
|
||||||
|
|
||||||
func _test_zone1_artefact_is_stable() -> void:
|
func _test_zone1_artefact_is_stable() -> void:
|
||||||
var versioned := load(ZONE1_TERRAIN_PATH) as SiteTerrainData
|
var versioned := load(ZONE1_TERRAIN_PATH) as SiteTerrainData
|
||||||
_expect(versioned != null, "versioned zone 1 terrain loads")
|
_expect(versioned != null, "versioned zone 1 terrain loads")
|
||||||
|
|||||||
@@ -243,6 +243,110 @@ class Escarpment extends Primitive:
|
|||||||
return -(step + slope)
|
return -(step + slope)
|
||||||
|
|
||||||
|
|
||||||
|
## Flat-topped elliptical mound (or, with a negative height, a flat-bottomed pit) whose
|
||||||
|
## flank spans wall_width_m inside a noise-roughened outline. With rock_top the whole
|
||||||
|
## mound above 30 % of its height is rock, sommet plat compris.
|
||||||
|
class Outcrop extends Primitive:
|
||||||
|
var centre: Vector2
|
||||||
|
var radius_x_m: float
|
||||||
|
var radius_z_m: float
|
||||||
|
var height_m: float
|
||||||
|
var wall_width_m: float
|
||||||
|
var edge_amplitude_m: float
|
||||||
|
var rock_top: bool
|
||||||
|
var _noise: FastNoiseLite
|
||||||
|
var _mean_radius_m: float
|
||||||
|
|
||||||
|
func _init(
|
||||||
|
p_centre: Vector2,
|
||||||
|
p_radius_x_m: float,
|
||||||
|
p_radius_z_m: float,
|
||||||
|
p_height_m: float,
|
||||||
|
p_wall_width_m: float,
|
||||||
|
p_edge_seed: int,
|
||||||
|
p_edge_amplitude_m: float,
|
||||||
|
p_rock_top: bool
|
||||||
|
) -> void:
|
||||||
|
centre = p_centre
|
||||||
|
radius_x_m = p_radius_x_m
|
||||||
|
radius_z_m = p_radius_z_m
|
||||||
|
height_m = p_height_m
|
||||||
|
wall_width_m = p_wall_width_m
|
||||||
|
edge_amplitude_m = p_edge_amplitude_m
|
||||||
|
rock_top = p_rock_top
|
||||||
|
_noise = edge_noise(p_edge_seed, p_edge_amplitude_m)
|
||||||
|
_mean_radius_m = (p_radius_x_m + p_radius_z_m) * 0.5
|
||||||
|
|
||||||
|
func elevation_delta(x_m: float, z_m: float) -> float:
|
||||||
|
var dx := (x_m - centre.x) / radius_x_m
|
||||||
|
var dz := (z_m - centre.y) / radius_z_m
|
||||||
|
var edge_distance := (sqrt(dx * dx + dz * dz) - 1.0) * _mean_radius_m
|
||||||
|
edge_distance += edge_offset(_noise, edge_amplitude_m, x_m, z_m)
|
||||||
|
return height_m * (1.0 - smoothstep(-wall_width_m, 0.0, edge_distance))
|
||||||
|
|
||||||
|
func material_at(x_m: float, z_m: float) -> int:
|
||||||
|
if rock_top and absf(elevation_delta(x_m, z_m)) > 0.3 * absf(height_m):
|
||||||
|
return SonarMaterial.Value.ROCK
|
||||||
|
return NO_MATERIAL
|
||||||
|
|
||||||
|
|
||||||
|
## count half-ellipsoid boulders scattered uniformly in a disc by a seeded generator.
|
||||||
|
## Boulders combine by maximum, so overlapping blocks never pile into one mound, and every
|
||||||
|
## cell they cover is rock.
|
||||||
|
class BoulderField extends Primitive:
|
||||||
|
var centre: Vector2
|
||||||
|
var radius_m: float
|
||||||
|
var height_ratio: float
|
||||||
|
var _boulders := PackedVector3Array() # x, z, radius
|
||||||
|
var _reach_m: float
|
||||||
|
|
||||||
|
func _init(
|
||||||
|
p_centre: Vector2,
|
||||||
|
p_radius_m: float,
|
||||||
|
p_seed: int,
|
||||||
|
p_count: int,
|
||||||
|
p_boulder_radius_min_m: float,
|
||||||
|
p_boulder_radius_max_m: float,
|
||||||
|
p_height_ratio: float
|
||||||
|
) -> void:
|
||||||
|
centre = p_centre
|
||||||
|
radius_m = p_radius_m
|
||||||
|
height_ratio = p_height_ratio
|
||||||
|
_reach_m = p_radius_m + p_boulder_radius_max_m
|
||||||
|
var generator := RandomNumberGenerator.new()
|
||||||
|
generator.seed = p_seed
|
||||||
|
for _index in p_count:
|
||||||
|
var distance := p_radius_m * sqrt(generator.randf())
|
||||||
|
var angle := TAU * generator.randf()
|
||||||
|
var boulder_radius := generator.randf_range(p_boulder_radius_min_m, p_boulder_radius_max_m)
|
||||||
|
_boulders.append(Vector3(
|
||||||
|
p_centre.x + distance * cos(angle),
|
||||||
|
p_centre.y + distance * sin(angle),
|
||||||
|
boulder_radius
|
||||||
|
))
|
||||||
|
|
||||||
|
func boulders() -> PackedVector3Array:
|
||||||
|
return _boulders
|
||||||
|
|
||||||
|
func elevation_delta(x_m: float, z_m: float) -> float:
|
||||||
|
if Vector2(x_m, z_m).distance_to(centre) > _reach_m:
|
||||||
|
return 0.0
|
||||||
|
var highest := 0.0
|
||||||
|
for boulder in _boulders:
|
||||||
|
var dx := x_m - boulder.x
|
||||||
|
var dz := z_m - boulder.y
|
||||||
|
var squared := dx * dx + dz * dz
|
||||||
|
var radius_squared := boulder.z * boulder.z
|
||||||
|
if squared < radius_squared:
|
||||||
|
highest = maxf(highest, boulder.z * height_ratio * sqrt(1.0 - squared / radius_squared))
|
||||||
|
return highest
|
||||||
|
|
||||||
|
func material_at(x_m: float, z_m: float) -> int:
|
||||||
|
if elevation_delta(x_m, z_m) > 0.0:
|
||||||
|
return SonarMaterial.Value.ROCK
|
||||||
|
return NO_MATERIAL
|
||||||
|
|
||||||
|
|
||||||
class Step extends Primitive:
|
class Step extends Primitive:
|
||||||
var point: Vector2
|
var point: Vector2
|
||||||
var normal: Vector2
|
var normal: Vector2
|
||||||
|
|||||||
Reference in new issue
Block a user