fix(tools): canyons impose a floor material through rock outcrops; tighten showcase contracts

Review findings: the mesa forced rock onto the canyon bed, two runners
declared the wrong TEST_COUNT, and the escarpment check could pass on the
slope alone. Canyon gains floor_material, the showcase is re-baked with sand
beds, the contract measures the step across the crest band, counts distinct
ledges, and probes the crevasse against the open plateau. Degenerate recipe
parameters now fail an assert.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
OlivierandClaude Fable 5.1 committed 2026-10-06 22:00:24 +02:00
1 parent abffa737ec
commit d91e6bd02f
8 files changed
+73 -36

No files matched your search

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

+9 -7
View File
@@ -82,7 +82,7 @@ ou `smoothstep`) et **mono-segment**. Il manque :
| Primitive | Paramètres | Forme | | Primitive | Paramètres | Forme |
|---|---|---| |---|---|---|
| `Canyon` | `path: PackedVector2Array`, `half_width_m`, `depth_m`, `wall_softness_m`, `edge_seed`, `edge_amplitude_m` | tranchée à fond plat le long d'une polyligne ; parois `smoothstep` sur `wall_softness_m` ; distance au tracé perturbée par un bruit de contour ; se referme sur une demi-largeur à chaque bout | | `Canyon` | `path: PackedVector2Array`, `half_width_m`, `depth_m`, `wall_softness_m`, `edge_seed`, `edge_amplitude_m`, `floor_material` | tranchée à fond plat le long d'une polyligne ; parois `smoothstep` sur `wall_softness_m` ; distance au tracé perturbée par un bruit de contour ; se referme sur une demi-largeur à chaque bout ; impose `floor_material` (sable en général) sur le fond plat, même à travers une butte rocheuse |
| `Outcrop` | `centre`, `radius_x_m`, `radius_z_m`, `height_m`, `wall_width_m`, `edge_seed`, `edge_amplitude_m`, `rock_top` | butte elliptique à sommet plat, flanc de largeur `wall_width_m`, contour perturbé ; matériau roche partout où elle dépasse 30 % de sa hauteur si `rock_top` | | `Outcrop` | `centre`, `radius_x_m`, `radius_z_m`, `height_m`, `wall_width_m`, `edge_seed`, `edge_amplitude_m`, `rock_top` | butte elliptique à sommet plat, flanc de largeur `wall_width_m`, contour perturbé ; matériau roche partout où elle dépasse 30 % de sa hauteur si `rock_top` |
| `BoulderField` | `centre`, `radius_m`, `seed`, `count`, `boulder_radius_min_m`, `boulder_radius_max_m`, `height_ratio` | `count` demi-ellipsoïdes tirés par `RandomNumberGenerator` seedé dans le disque, combinés par **maximum** ; roche sur toute leur emprise | | `BoulderField` | `centre`, `radius_m`, `seed`, `count`, `boulder_radius_min_m`, `boulder_radius_max_m`, `height_ratio` | `count` demi-ellipsoïdes tirés par `RandomNumberGenerator` seedé dans le disque, combinés par **maximum** ; roche sur toute leur emprise |
| `Escarpment` | `path: PackedVector2Array`, `drop_m`, `softness_m`, `slope_beyond`, `edge_seed`, `edge_amplitude_m` | `Step` le long d'une polyligne : le terrain descend à **droite** du sens de parcours ; ligne de crête perturbée par un bruit de contour | | `Escarpment` | `path: PackedVector2Array`, `drop_m`, `softness_m`, `slope_beyond`, `edge_seed`, `edge_amplitude_m` | `Step` le long d'une polyligne : le terrain descend à **droite** du sens de parcours ; ligne de crête perturbée par un bruit de contour |
@@ -129,7 +129,7 @@ Plan de masse (nord en haut) :
| 55°, gradins 4 m, fond ridé entre buttes | | 55°, gradins 4 m, fond ridé entre buttes |
| [départ] | | [départ] |
| SW : chaos de blocs SE : éperons | | SW : chaos de blocs SE : éperons |
| 80 blocs r 2–6 m sur deux buttes | | 90 blocs r 2–6 m sur deux buttes |
| sable ridé alvéolées, | | sable ridé alvéolées, |
| chenal 20 m | | chenal 20 m |
| ====== falaise sinueuse (Escarpment) ===== | | ====== falaise sinueuse (Escarpment) ===== |
@@ -148,13 +148,15 @@ Plan de masse (nord en haut) :
gradins : au moins deux niveaux plats ; un fond plat ne bouge pas) ; gradins : au moins deux niveaux plats ; un fond plat ne bouge pas) ;
cuisson du Banc du Vigilant identique à l'artefact versionné. cuisson du Banc du Vigilant identique à l'artefact versionné.
- **Contrat (`run_site_content_tests.gd`, section showcase)** : canyon - **Contrat (`run_site_content_tests.gd`, section showcase)** : canyon
(profondeur ≥ 20 m sous la mesa, fond plat à ± 1 m sur 10 m, paroi ≥ 60° (profondeur ≥ 18 m sous le sommet bruité de la mesa, fond de sable plat à
quelque part sur une transversale, ≥ 2 replats de gradin) ; crevasse ± 1 m sur 10 m, paroi ≥ 60° quelque part sur une transversale, ≥ 2 replats
(fond ≥ 16 m sous les bords, largeur au fond ≤ 8 m) ; chaos (roche sur un distincts) ; crevasse (fond de sable ≥ 16 m sous le plateau ouvert, 3 à 8 m
de large à 10 m sous le plateau) ; chaos (roche sur un
bloc, sable entre deux blocs, ≥ 40 cellules de roche distinctes… mesuré par bloc, sable entre deux blocs, ≥ 40 cellules de roche distinctes… mesuré par
fraction de roche dans le disque) ; éperons (chenal plus bas que les deux fraction de roche dans le disque) ; éperons (chenal plus bas que les deux
buttes, roche sur chaque butte) ; falaise (drop ≥ 20 m entre deux points de buttes, roche sur chaque butte) ; falaise (marche ≥ 18 m mesurée dans la
part et d'autre) ; bornes et profondeurs globales. seule bande de crête, pour que la pente en contrebas ne masque pas une
marche absente) ; bornes et profondeurs globales.
- **Intégration (`run_site_terrain_integration.gd`)** : chargement du showcase - **Intégration (`run_site_terrain_integration.gd`)** : chargement du showcase
sous 3 s, rayon dans le canyon plus bas que sur la mesa, scan de référence sous 3 s, rayon dans le canyon plus bas que sur la mesa, scan de référence
lisant roche et sable. lisant roche et sable.
+29 -17
View File
@@ -15,20 +15,23 @@ const DiveRulesResolver = preload("res://game/domain/rules/dive_rules_resolver.g
const SITE_PATH := "res://content/sites/zone1_vigilant.tres" const SITE_PATH := "res://content/sites/zone1_vigilant.tres"
const SHOWCASE_PATH := "res://content/sites/terrain_showcase.tres" const SHOWCASE_PATH := "res://content/sites/terrain_showcase.tres"
# Showcase probe positions (docs/briefs/terrain-varied-landforms.md §5). The canyon # Showcase probe positions (docs/briefs/terrain-varied-landforms.md §5). The canyon
# transect crosses its first leg at right angles; the crevasse transect is the # transect crosses its second leg at right angles; the crevasse transect is the
# z = -120 row between the two buttes. # z = -120 row between the two buttes, compared with the open plateau on the same row;
# the escarpment is probed across its step band at x = 0, where the crest is at z = 217.
const SHOWCASE_MESA_TOP := Vector2(-80.0, -90.0) const SHOWCASE_MESA_TOP := Vector2(-80.0, -90.0)
const SHOWCASE_CANYON_MID := Vector2(-130.0, -135.0) const SHOWCASE_CANYON_MID := Vector2(-130.0, -135.0)
const SHOWCASE_CANYON_AXIS := Vector2(40.0, -30.0) const SHOWCASE_CANYON_AXIS := Vector2(40.0, -30.0)
const SHOWCASE_CREVASSE_ROW_Z := -120.0 const SHOWCASE_CREVASSE_ROW_Z := -120.0
const SHOWCASE_CREVASSE_SHELF_X := 118.0 const SHOWCASE_CREVASSE_PLAIN_X := 0.0
const SHOWCASE_CREVASSE_FLOOR_X := 124.0 const SHOWCASE_CREVASSE_FLOOR_X := 124.0
const SHOWCASE_BOULDER_FIELD_CENTRE := Vector2(-120.0, 110.0) const SHOWCASE_BOULDER_FIELD_CENTRE := Vector2(-120.0, 110.0)
const SHOWCASE_BOULDER_FIELD_RADIUS_M := 75.0 const SHOWCASE_BOULDER_FIELD_RADIUS_M := 75.0
const SHOWCASE_WEST_SPUR := Vector2(80.0, 120.0) const SHOWCASE_WEST_SPUR := Vector2(80.0, 120.0)
const SHOWCASE_CHANNEL := Vector2(125.0, 120.0) const SHOWCASE_CHANNEL := Vector2(125.0, 120.0)
const SHOWCASE_EAST_SPUR := Vector2(170.0, 125.0) const SHOWCASE_EAST_SPUR := Vector2(170.0, 125.0)
const STEEP_WALL_RISE_PER_M := 1.7 # tan(60 degrees) const SHOWCASE_ESCARPMENT_CREST_Z := 217.0
const SHOWCASE_ESCARPMENT_BAND_M := 14.0 # softness 6 m plus 8 m of outline noise
const STEEP_WALL_RISE_PER_M := 1.73 # tan(60 degrees)
const LEDGE_RISE_PER_M := 0.35 const LEDGE_RISE_PER_M := 0.35
const ROUTE_CLEARANCE_M := 3.0 const ROUTE_CLEARANCE_M := 3.0
const MINIMUM_LANDMARK_SPACING_M := 60.0 const MINIMUM_LANDMARK_SPACING_M := 60.0
@@ -36,7 +39,7 @@ const MAXIMUM_ANCHOR_TO_WRECK_M := 320.0
const MAXIMUM_WRECK_SPACING_M := 260.0 const MAXIMUM_WRECK_SPACING_M := 260.0
const HULL_HALF_LENGTH_M := 14.0 const HULL_HALF_LENGTH_M := 14.0
const HULL_SEABED_TOLERANCE_M := 2.0 const HULL_SEABED_TOLERANCE_M := 2.0
const TEST_COUNT := 52 const TEST_COUNT := 54
var _failures: Array[String] = [] var _failures: Array[String] = []
@@ -161,7 +164,7 @@ func _test_showcase_content() -> void:
return return
_expect(field.cell_size_m() == 1.0 and field.columns() == 513 and field.rows() == 513, "showcase grid is 513 x 513 cells of 1 m") _expect(field.cell_size_m() == 1.0 and field.columns() == 513 and field.rows() == 513, "showcase grid is 513 x 513 cells of 1 m")
_expect(definition.playable_bounds_m.is_equal_approx(Rect2(-220.0, -220.0, 440.0, 440.0)), "showcase playable zone is 440 x 440 m") _expect(definition.playable_bounds_m.is_equal_approx(Rect2(-220.0, -220.0, 440.0, 440.0)), "showcase playable zone is 440 x 440 m")
_expect(field.minimum_elevation_m() >= -80.5 and field.maximum_elevation_m() <= -18.5, "showcase terrain stays between 19 m and 80 m deep") _expect(field.minimum_elevation_m() >= -80.5 and field.maximum_elevation_m() <= -19.0, "showcase terrain stays between 19 m and 80 m deep")
var anchors := definition.placements_of_kind(SitePlacement.Kind.VESSEL_ANCHOR) var anchors := definition.placements_of_kind(SitePlacement.Kind.VESSEL_ANCHOR)
_expect(anchors.size() == 1 and definition.placements.size() == 1, "showcase has a single placement, the mooring beacon") _expect(anchors.size() == 1 and definition.placements.size() == 1, "showcase has a single placement, the mooring beacon")
var spawn := definition.spawn_position_m var spawn := definition.spawn_position_m
@@ -171,6 +174,7 @@ func _test_showcase_content() -> void:
var mesa_top := field.elevation_at(SHOWCASE_MESA_TOP.x, SHOWCASE_MESA_TOP.y) var mesa_top := field.elevation_at(SHOWCASE_MESA_TOP.x, SHOWCASE_MESA_TOP.y)
var canyon_floor := field.elevation_at(SHOWCASE_CANYON_MID.x, SHOWCASE_CANYON_MID.y) var canyon_floor := field.elevation_at(SHOWCASE_CANYON_MID.x, SHOWCASE_CANYON_MID.y)
_expect(mesa_top - canyon_floor >= 18.0, "canyon floor is at least 18 m below the mesa top (%.1f m)" % (mesa_top - canyon_floor)) _expect(mesa_top - canyon_floor >= 18.0, "canyon floor is at least 18 m below the mesa top (%.1f m)" % (mesa_top - canyon_floor))
_expect(field.material_at(SHOWCASE_CANYON_MID.x, SHOWCASE_CANYON_MID.y) == SonarMaterial.Value.SAND, "canyon floor is sand through the rock mesa")
var axis := SHOWCASE_CANYON_AXIS.normalized() var axis := SHOWCASE_CANYON_AXIS.normalized()
var lowest := INF var lowest := INF
var highest := -INF var highest := -INF
@@ -186,23 +190,30 @@ func _test_showcase_content() -> void:
var point := SHOWCASE_CANYON_MID + normal * float(offset) var point := SHOWCASE_CANYON_MID + normal * float(offset)
transect.append(field.elevation_at(point.x, point.y)) transect.append(field.elevation_at(point.x, point.y))
var steepest := 0.0 var steepest := 0.0
var ledge_pairs := 0 var ledges := 0
var on_ledge := false
for index in range(transect.size() - 1): for index in range(transect.size() - 1):
var rise := absf(transect[index + 1] - transect[index]) var rise := absf(transect[index + 1] - transect[index])
steepest = maxf(steepest, rise) steepest = maxf(steepest, rise)
var in_wall := transect[index] > canyon_floor + 1.0 and transect[index] < mesa_top - 1.0 var in_wall := transect[index] > canyon_floor + 1.0 and transect[index] < mesa_top - 1.0
# A ledge is a run of flat metres inside the wall, separated from the next by a riser.
if in_wall and rise < LEDGE_RISE_PER_M: if in_wall and rise < LEDGE_RISE_PER_M:
ledge_pairs += 1 if not on_ledge:
_expect(steepest >= STEEP_WALL_RISE_PER_M, "canyon wall is steeper than 60 degrees somewhere (%.1f m per m)" % steepest) ledges += 1
_expect(ledge_pairs >= 2, "canyon wall carries at least two ledges (%d flat metres)" % ledge_pairs) on_ledge = true
else:
on_ledge = false
_expect(steepest >= STEEP_WALL_RISE_PER_M, "canyon wall is steeper than 60 degrees somewhere (%.2f m per m)" % steepest)
_expect(ledges >= 2, "canyon walls carry at least two distinct ledges (%d)" % ledges)
# NE: crevasse between the buttes. # NE: crevasse between the buttes.
var shelf := field.elevation_at(SHOWCASE_CREVASSE_SHELF_X, SHOWCASE_CREVASSE_ROW_Z) var plain := field.elevation_at(SHOWCASE_CREVASSE_PLAIN_X, SHOWCASE_CREVASSE_ROW_Z)
var crevasse_floor := field.elevation_at(SHOWCASE_CREVASSE_FLOOR_X, SHOWCASE_CREVASSE_ROW_Z) var crevasse_floor := field.elevation_at(SHOWCASE_CREVASSE_FLOOR_X, SHOWCASE_CREVASSE_ROW_Z)
_expect(shelf - crevasse_floor >= 16.0, "crevasse floor is at least 16 m below the rim (%.1f m)" % (shelf - crevasse_floor)) _expect(plain - crevasse_floor >= 16.0, "crevasse floor is at least 16 m below the open plateau (%.1f m)" % (plain - crevasse_floor))
_expect(field.material_at(SHOWCASE_CREVASSE_FLOOR_X, SHOWCASE_CREVASSE_ROW_Z) == SonarMaterial.Value.SAND, "crevasse floor is sand")
var deep_cells := 0 var deep_cells := 0
for x in range(100, 146): for x in range(100, 146):
if field.elevation_at(float(x), SHOWCASE_CREVASSE_ROW_Z) < shelf - 10.0: if field.elevation_at(float(x), SHOWCASE_CREVASSE_ROW_Z) < plain - 10.0:
deep_cells += 1 deep_cells += 1
_expect(deep_cells >= 3 and deep_cells <= 8, "crevasse is 3 to 8 m wide at 10 m down (%d m)" % deep_cells) _expect(deep_cells >= 3 and deep_cells <= 8, "crevasse is 3 to 8 m wide at 10 m down (%d m)" % deep_cells)
@@ -234,10 +245,11 @@ func _test_showcase_content() -> void:
) )
_expect(field.material_at(SHOWCASE_CHANNEL.x, SHOWCASE_CHANNEL.y + 30.0) == SonarMaterial.Value.SAND, "the channel floor is sand") _expect(field.material_at(SHOWCASE_CHANNEL.x, SHOWCASE_CHANNEL.y + 30.0) == SonarMaterial.Value.SAND, "the channel floor is sand")
# S: escarpment. # S: escarpment, measured across its step band only so the slope below cannot
var above := field.elevation_at(0.0, 190.0) # stand in for a missing step (the slope alone would give about 7 m here).
var below := field.elevation_at(0.0, 250.0) var above := field.elevation_at(0.0, SHOWCASE_ESCARPMENT_CREST_Z - SHOWCASE_ESCARPMENT_BAND_M)
_expect(above - below >= 20.0, "the escarpment drops at least 20 m (%.1f m)" % (above - below)) var below := field.elevation_at(0.0, SHOWCASE_ESCARPMENT_CREST_Z + SHOWCASE_ESCARPMENT_BAND_M)
_expect(above - below >= 18.0, "the escarpment steps down at least 18 m across its crest band (%.1f m)" % (above - below))
func _rests_on_terrain(field: TerrainHeightField, placement: SitePlacement) -> bool: func _rests_on_terrain(field: TerrainHeightField, placement: SitePlacement) -> bool:
@@ -240,7 +240,7 @@ func _test_showcase_site() -> void:
var mesa_top := _terrain_height(space, terrain, -80.0, -90.0, []) var mesa_top := _terrain_height(space, terrain, -80.0, -90.0, [])
_expect(mesa_top - canyon_floor >= 18.0, "collision rays find the canyon under the mesa (%.1f m vs %.1f m)" % [canyon_floor, mesa_top]) _expect(mesa_top - canyon_floor >= 18.0, "collision rays find the canyon under the mesa (%.1f m vs %.1f m)" % [canyon_floor, mesa_top])
# Sensor above the canyon looking along its first leg (the acquisition probes along # Sensor above the canyon looking along its second leg (the acquisition probes along
# local +Z): rock walls and sand floor. # local +Z): rock walls and sand floor.
var sensor := Transform3D(Basis.IDENTITY, Vector3(-130.0, -15.0, -135.0)) var sensor := Transform3D(Basis.IDENTITY, Vector3(-130.0, -15.0, -135.0))
sensor.basis = Basis.looking_at(-Vector3(40.0, 0.0, -30.0).normalized(), Vector3.UP) sensor.basis = Basis.looking_at(-Vector3(40.0, 0.0, -30.0).normalized(), Vector3.UP)
+5 -3
View File
@@ -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 := 40 const TEST_COUNT := 44
var _failures: Array[String] = [] var _failures: Array[String] = []
@@ -121,16 +121,18 @@ func _test_modifiers_and_primitive_materials() -> void:
func _test_canyon() -> void: func _test_canyon() -> void:
var canyon := SiteTerrainBaker.Canyon.new( var canyon := SiteTerrainBaker.Canyon.new(
PackedVector2Array([Vector2(-20.0, 0.0), Vector2(0.0, 0.0), Vector2(0.0, 20.0)]), PackedVector2Array([Vector2(-20.0, 0.0), Vector2(0.0, 0.0), Vector2(0.0, 20.0)]),
4.0, 10.0, 1.0, 0, 0.0 4.0, 10.0, 1.0, 0, 0.0, SonarMaterial.Value.SAND
) )
_expect(is_equal_approx(canyon.elevation_delta(-10.0, 0.0), -10.0), "canyon floor sits at depth on the first leg") _expect(is_equal_approx(canyon.elevation_delta(-10.0, 0.0), -10.0), "canyon floor sits at depth on the first leg")
_expect(canyon.material_at(-10.0, 0.0) == SonarMaterial.Value.SAND, "canyon imposes its floor material on the flat floor")
_expect(canyon.material_at(-10.0, 4.0) == SiteTerrainBaker.NO_MATERIAL, "canyon leaves its walls to the slope rule")
_expect(is_equal_approx(canyon.elevation_delta(1.5, 10.0), -10.0), "canyon floor is flat across its width on the second leg") _expect(is_equal_approx(canyon.elevation_delta(1.5, 10.0), -10.0), "canyon floor is flat across its width on the second leg")
_expect(is_equal_approx(canyon.elevation_delta(-10.0, 6.0), 0.0), "canyon stops past half width plus softness") _expect(is_equal_approx(canyon.elevation_delta(-10.0, 6.0), 0.0), "canyon stops past half width plus softness")
_expect(is_equal_approx(canyon.elevation_delta(15.0, -15.0), 0.0), "canyon is zero far from the path") _expect(is_equal_approx(canyon.elevation_delta(15.0, -15.0), 0.0), "canyon is zero far from the path")
_expect(is_equal_approx(canyon.elevation_delta(0.0, 20.0), 0.0), "canyon closes at its last vertex") _expect(is_equal_approx(canyon.elevation_delta(0.0, 20.0), 0.0), "canyon closes at its last vertex")
_expect(canyon.elevation_delta(-10.0, 4.0) < -4.0 and canyon.elevation_delta(-10.0, 4.0) > -6.0, "canyon wall is half way down at half width") _expect(canyon.elevation_delta(-10.0, 4.0) < -4.0 and canyon.elevation_delta(-10.0, 4.0) > -6.0, "canyon wall is half way down at half width")
var rough := SiteTerrainBaker.Canyon.new( var rough := SiteTerrainBaker.Canyon.new(
PackedVector2Array([Vector2(-200.0, 0.0), Vector2(200.0, 0.0)]), 8.0, 10.0, 2.0, 7, 3.0 PackedVector2Array([Vector2(-200.0, 0.0), Vector2(200.0, 0.0)]), 8.0, 10.0, 2.0, 7, 3.0, SiteTerrainBaker.NO_MATERIAL
) )
var wall_values: Dictionary = {} var wall_values: Dictionary = {}
for x in range(-100, 101, 10): for x in range(-100, 101, 10):
+25 -5
View File
@@ -170,13 +170,15 @@ class Groove extends Primitive:
## Flat-floored trench along a polyline. The floor sits depth_m below the surroundings ## Flat-floored trench along a polyline. The floor sits depth_m below the surroundings
## wherever the (noise-perturbed) distance to the path is under half_width_m minus ## wherever the (noise-perturbed) distance to the path is under half_width_m minus
## wall_softness_m; the walls climb over 2 * wall_softness_m; the trench closes over one ## wall_softness_m; the walls climb over 2 * wall_softness_m; the trench closes over one
## half-width at each end. ## half-width at each end. floor_material (a SonarMaterial value, or NO_MATERIAL) is
## imposed on the flat floor, so a canyon cut through a rock outcrop keeps a sand bed.
class Canyon extends Primitive: class Canyon extends Primitive:
var path: PackedVector2Array var path: PackedVector2Array
var half_width_m: float var half_width_m: float
var depth_m: float var depth_m: float
var wall_softness_m: float var wall_softness_m: float
var edge_amplitude_m: float var edge_amplitude_m: float
var floor_material: int
var _noise: FastNoiseLite var _noise: FastNoiseLite
var _length_m: float var _length_m: float
@@ -186,17 +188,31 @@ class Canyon extends Primitive:
p_depth_m: float, p_depth_m: float,
p_wall_softness_m: float, p_wall_softness_m: float,
p_edge_seed: int, p_edge_seed: int,
p_edge_amplitude_m: float p_edge_amplitude_m: float,
p_floor_material: int
) -> void: ) -> void:
assert(p_path.size() >= 2, "a canyon path needs at least two vertices")
assert(p_half_width_m > 0.0, "a canyon needs a positive half width")
path = p_path path = p_path
half_width_m = p_half_width_m half_width_m = p_half_width_m
depth_m = p_depth_m depth_m = p_depth_m
wall_softness_m = p_wall_softness_m wall_softness_m = p_wall_softness_m
edge_amplitude_m = p_edge_amplitude_m edge_amplitude_m = p_edge_amplitude_m
floor_material = p_floor_material
_noise = edge_noise(p_edge_seed, p_edge_amplitude_m) _noise = edge_noise(p_edge_seed, p_edge_amplitude_m)
_length_m = path_length(p_path) _length_m = path_length(p_path)
func elevation_delta(x_m: float, z_m: float) -> float: func elevation_delta(x_m: float, z_m: float) -> float:
return -depth_m * _presence(x_m, z_m)
func material_at(x_m: float, z_m: float) -> int:
# Full presence means the flat floor: inside the walls and clear of the end tapers.
if floor_material != NO_MATERIAL and _presence(x_m, z_m) >= 0.999:
return floor_material
return NO_MATERIAL
## 1 on the flat floor, 0 outside the trench, in between on the walls and end tapers.
func _presence(x_m: float, z_m: float) -> float:
var sample := path_sample(Vector2(x_m, z_m), path) var sample := path_sample(Vector2(x_m, z_m), path)
var distance := sample.x + edge_offset(_noise, edge_amplitude_m, x_m, z_m) var distance := sample.x + edge_offset(_noise, edge_amplitude_m, x_m, z_m)
var inside := 1.0 - smoothstep( var inside := 1.0 - smoothstep(
@@ -205,13 +221,14 @@ class Canyon extends Primitive:
distance distance
) )
var taper := smoothstep(0.0, half_width_m, sample.y) * smoothstep(0.0, half_width_m, _length_m - sample.y) var taper := smoothstep(0.0, half_width_m, sample.y) * smoothstep(0.0, half_width_m, _length_m - sample.y)
return -depth_m * inside * taper return inside * taper
## Step along a polyline: the terrain drops by drop_m on the right of the travel direction ## Step along a polyline: the terrain drops by drop_m on the right of the travel direction
## (south of an eastbound path), over 2 * softness_m, then keeps falling at slope_beyond. ## (south of an eastbound path), over 2 * softness_m, then keeps falling at slope_beyond.
## The crest line is roughened by outline noise. Extend the path past the field edges: ## The crest line is roughened by outline noise. The path must extend past the field
## beyond its ends the contour wraps around the end vertex. ## edges: beyond an end vertex the drop is decided by the half-plane of the last segment,
## which leaves a seam along the path's extension.
class Escarpment extends Primitive: class Escarpment extends Primitive:
var path: PackedVector2Array var path: PackedVector2Array
var drop_m: float var drop_m: float
@@ -228,6 +245,7 @@ class Escarpment extends Primitive:
p_edge_seed: int, p_edge_seed: int,
p_edge_amplitude_m: float p_edge_amplitude_m: float
) -> void: ) -> void:
assert(p_path.size() >= 2, "an escarpment path needs at least two vertices")
path = p_path path = p_path
drop_m = p_drop_m drop_m = p_drop_m
softness_m = p_softness_m softness_m = p_softness_m
@@ -267,6 +285,7 @@ class Outcrop extends Primitive:
p_edge_amplitude_m: float, p_edge_amplitude_m: float,
p_rock_top: bool p_rock_top: bool
) -> void: ) -> void:
assert(p_radius_x_m > 0.0 and p_radius_z_m > 0.0, "an outcrop needs positive radii")
centre = p_centre centre = p_centre
radius_x_m = p_radius_x_m radius_x_m = p_radius_x_m
radius_z_m = p_radius_z_m radius_z_m = p_radius_z_m
@@ -435,6 +454,7 @@ class Strata extends Modifier:
p_centre: Vector2, p_centre: Vector2,
p_radius_m: float p_radius_m: float
) -> void: ) -> void:
assert(p_layer_height_m > 0.0, "strata need a positive layer height")
layer_height_m = p_layer_height_m layer_height_m = p_layer_height_m
sharpness = clampf(p_sharpness, 0.0, 0.999) sharpness = clampf(p_sharpness, 0.0, 0.999)
centre = p_centre centre = p_centre
+4 -3
View File
@@ -5,6 +5,7 @@ extends RefCounted
## mooring beacon at the centre. Field 512 x 512 m at 1 m, +X east, +Z south. ## mooring beacon at the centre. Field 512 x 512 m at 1 m, +X east, +Z south.
const SiteTerrainBaker = preload("res://tools/site_terrain_baker.gd") const SiteTerrainBaker = preload("res://tools/site_terrain_baker.gd")
const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
const SiteTerrainData = preload("res://game/domain/sites/site_terrain_data.gd") const SiteTerrainData = preload("res://game/domain/sites/site_terrain_data.gd")
const DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition.gd") const DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition.gd")
const SitePlacement = preload("res://game/domain/sites/site_placement.gd") const SitePlacement = preload("res://game/domain/sites/site_placement.gd")
@@ -59,15 +60,15 @@ static func recipe() -> SiteTerrainBaker.Recipe:
# modifier (one ledge every 2.8 m across, resolvable at 1 m per cell), rippled # modifier (one ledge every 2.8 m across, resolvable at 1 m per cell), rippled
# sand on the floor. # sand on the floor.
SiteTerrainBaker.Outcrop.new(MESA_CENTRE, 95.0, 85.0, 6.0, 10.0, 101, 6.0, true), SiteTerrainBaker.Outcrop.new(MESA_CENTRE, 95.0, 85.0, 6.0, 10.0, 101, 6.0, true),
SiteTerrainBaker.Canyon.new(PackedVector2Array(CANYON_PATH), CANYON_HALF_WIDTH_M, CANYON_DEPTH_M, 7.0, 102, 2.0), SiteTerrainBaker.Canyon.new(PackedVector2Array(CANYON_PATH), CANYON_HALF_WIDTH_M, CANYON_DEPTH_M, 7.0, 102, 2.0, SonarMaterial.Value.SAND),
SiteTerrainBaker.Ripples.new(Vector2(0.8, -0.6), 4.0, 0.25, Vector2(-130.0, -140.0), 70.0), SiteTerrainBaker.Ripples.new(Vector2(0.8, -0.6), 4.0, 0.25, Vector2(-130.0, -140.0), 70.0),
# NE — la Fente: two buttes 8-9 m high with a 6 m wide, 20 m deep slot between them. # NE — la Fente: two buttes 8-9 m high with a 6 m wide, 20 m deep slot between them.
SiteTerrainBaker.Outcrop.new(WEST_BUTTE_CENTRE, 38.0, 70.0, 8.0, 8.0, 201, 5.0, true), SiteTerrainBaker.Outcrop.new(WEST_BUTTE_CENTRE, 38.0, 70.0, 8.0, 8.0, 201, 5.0, true),
SiteTerrainBaker.Outcrop.new(EAST_BUTTE_CENTRE, 38.0, 75.0, 9.0, 8.0, 202, 5.0, true), SiteTerrainBaker.Outcrop.new(EAST_BUTTE_CENTRE, 38.0, 75.0, 9.0, 8.0, 202, 5.0, true),
SiteTerrainBaker.Canyon.new(PackedVector2Array(CREVASSE_PATH), CREVASSE_HALF_WIDTH_M, CREVASSE_DEPTH_M, 1.0, 203, 1.0), SiteTerrainBaker.Canyon.new(PackedVector2Array(CREVASSE_PATH), CREVASSE_HALF_WIDTH_M, CREVASSE_DEPTH_M, 1.0, 203, 1.0, SonarMaterial.Value.SAND),
# SW — le Chaos: ninety boulders 2-6 m across plus six coral heads, on rippled sand. # SW — le Chaos: ninety boulders of 2-6 m radius plus six coral heads, on rippled sand.
SiteTerrainBaker.BoulderField.new(BOULDER_FIELD_CENTRE, BOULDER_FIELD_RADIUS_M, 301, 90, 2.0, 6.0, 0.7), SiteTerrainBaker.BoulderField.new(BOULDER_FIELD_CENTRE, BOULDER_FIELD_RADIUS_M, 301, 90, 2.0, 6.0, 0.7),
SiteTerrainBaker.BoulderField.new(Vector2(-100.0, 130.0), 40.0, 302, 6, 6.0, 9.0, 0.6), SiteTerrainBaker.BoulderField.new(Vector2(-100.0, 130.0), 40.0, 302, 6, 6.0, 9.0, 0.6),
SiteTerrainBaker.Ripples.new(Vector2(1.0, 0.3), 3.5, 0.3, BOULDER_FIELD_CENTRE, 110.0), SiteTerrainBaker.Ripples.new(Vector2(1.0, 0.3), 3.5, 0.3, BOULDER_FIELD_CENTRE, 110.0),