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

+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 SHOWCASE_PATH := "res://content/sites/terrain_showcase.tres"
# 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
# z = -120 row between the two buttes.
# transect crosses its second leg at right angles; the crevasse transect is the
# 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_CANYON_MID := Vector2(-130.0, -135.0)
const SHOWCASE_CANYON_AXIS := Vector2(40.0, -30.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_BOULDER_FIELD_CENTRE := Vector2(-120.0, 110.0)
const SHOWCASE_BOULDER_FIELD_RADIUS_M := 75.0
const SHOWCASE_WEST_SPUR := Vector2(80.0, 120.0)
const SHOWCASE_CHANNEL := Vector2(125.0, 120.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 ROUTE_CLEARANCE_M := 3.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 HULL_HALF_LENGTH_M := 14.0
const HULL_SEABED_TOLERANCE_M := 2.0
const TEST_COUNT := 52
const TEST_COUNT := 54
var _failures: Array[String] = []
@@ -161,7 +164,7 @@ func _test_showcase_content() -> void:
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(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)
_expect(anchors.size() == 1 and definition.placements.size() == 1, "showcase has a single placement, the mooring beacon")
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 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(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 lowest := INF
var highest := -INF
@@ -186,23 +190,30 @@ func _test_showcase_content() -> void:
var point := SHOWCASE_CANYON_MID + normal * float(offset)
transect.append(field.elevation_at(point.x, point.y))
var steepest := 0.0
var ledge_pairs := 0
var ledges := 0
var on_ledge := false
for index in range(transect.size() - 1):
var rise := absf(transect[index + 1] - transect[index])
steepest = maxf(steepest, rise)
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:
ledge_pairs += 1
_expect(steepest >= STEEP_WALL_RISE_PER_M, "canyon wall is steeper than 60 degrees somewhere (%.1f m per m)" % steepest)
_expect(ledge_pairs >= 2, "canyon wall carries at least two ledges (%d flat metres)" % ledge_pairs)
if not on_ledge:
ledges += 1
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.
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)
_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
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
_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")
# S: escarpment.
var above := field.elevation_at(0.0, 190.0)
var below := field.elevation_at(0.0, 250.0)
_expect(above - below >= 20.0, "the escarpment drops at least 20 m (%.1f m)" % (above - below))
# S: escarpment, measured across its step band only so the slope below cannot
# stand in for a missing step (the slope alone would give about 7 m here).
var above := field.elevation_at(0.0, SHOWCASE_ESCARPMENT_CREST_Z - SHOWCASE_ESCARPMENT_BAND_M)
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:
@@ -240,7 +240,7 @@ func _test_showcase_site() -> void:
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])
# 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.
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)
+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 TEST_COUNT := 40
const TEST_COUNT := 44
var _failures: Array[String] = []
@@ -121,16 +121,18 @@ func _test_modifiers_and_primitive_materials() -> void:
func _test_canyon() -> void:
var canyon := SiteTerrainBaker.Canyon.new(
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(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(-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(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")
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 = {}
for x in range(-100, 101, 10):