feat(content): bake the Banc du Vigilant site and its contract tests

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
OlivierandClaude Sonnet 5.5 committed 2026-10-06 20:21:29 +02:00
1 parent 51634820bd
commit a1493434e4
7 files changed
+454

No files matched your search

+121
View File
@@ -0,0 +1,121 @@
[gd_resource type="Resource" script_class="DiveSiteDefinition" format=3]
[ext_resource type="Script" path="res://game/domain/sites/site_placement.gd" id="1_n3xpu"]
[ext_resource type="Script" path="res://game/domain/sites/site_route.gd" id="2_36hd6"]
[ext_resource type="Script" path="res://game/domain/sites/dive_site_definition.gd" id="3_vix3p"]
[ext_resource type="Resource" path="res://content/sites/zone1_vigilant_terrain.res" id="4_i0v8k"]
[sub_resource type="Resource" id="Resource_rqt47"]
script = ExtResource("1_n3xpu")
placement_id = &"prospection_vessel"
kind = 0
position_m = Vector3(-12, -37.10001, 108)
yaw_radians = 0.35
footprint_m = Vector3(28, 8, 9)
[sub_resource type="Resource" id="Resource_xcojm"]
script = ExtResource("1_n3xpu")
placement_id = &"decoy_yacht"
kind = 1
position_m = Vector3(-40, -24.655516, -200)
yaw_radians = 1.1
footprint_m = Vector3(12, 3, 4)
[sub_resource type="Resource" id="Resource_yljal"]
script = ExtResource("1_n3xpu")
placement_id = &"decoy_coaster"
kind = 1
position_m = Vector3(140, -35.265244, 40)
yaw_radians = -0.6
footprint_m = Vector3(26, 6, 8)
[sub_resource type="Resource" id="Resource_aqgwj"]
script = ExtResource("1_n3xpu")
placement_id = &"secondary_item"
kind = 2
position_m = Vector3(-6, -61.61916, 157)
footprint_m = Vector3(1, 1, 1)
[sub_resource type="Resource" id="Resource_aervf"]
script = ExtResource("1_n3xpu")
placement_id = &"debris_1"
kind = 3
position_m = Vector3(-12, -45.756382, 134)
yaw_radians = 0.4
footprint_m = Vector3(3, 2, 2)
[sub_resource type="Resource" id="Resource_qgawl"]
script = ExtResource("1_n3xpu")
placement_id = &"debris_2"
kind = 3
position_m = Vector3(-10, -48.518223, 139)
yaw_radians = 1.2
footprint_m = Vector3(6, 2.5, 2.5)
[sub_resource type="Resource" id="Resource_q0ace"]
script = ExtResource("1_n3xpu")
placement_id = &"debris_3"
kind = 3
position_m = Vector3(-9, -53.75445, 146)
footprint_m = Vector3(2, 1.5, 2)
[sub_resource type="Resource" id="Resource_hd5cl"]
script = ExtResource("1_n3xpu")
placement_id = &"debris_4"
kind = 3
position_m = Vector3(-7, -58.142864, 152)
yaw_radians = 2.0
footprint_m = Vector3(4, 1.5, 1.5)
[sub_resource type="Resource" id="Resource_wy8hl"]
script = ExtResource("1_n3xpu")
placement_id = &"vessel_anchor"
kind = 5
position_m = Vector3(0, -14, -250)
[sub_resource type="Resource" id="Resource_e72vy"]
script = ExtResource("1_n3xpu")
placement_id = &"echine"
position_m = Vector3(75, -20, -50)
[sub_resource type="Resource" id="Resource_3n33i"]
script = ExtResource("1_n3xpu")
placement_id = &"deux_freres"
position_m = Vector3(-151, -18, -125)
[sub_resource type="Resource" id="Resource_cdmnh"]
script = ExtResource("1_n3xpu")
placement_id = &"rides"
position_m = Vector3(0, -25, -230)
[sub_resource type="Resource" id="Resource_u8dqp"]
script = ExtResource("1_n3xpu")
placement_id = &"saignee_entrance"
position_m = Vector3(-60, -34, -140)
[sub_resource type="Resource" id="Resource_3uttw"]
script = ExtResource("1_n3xpu")
placement_id = &"rupture"
position_m = Vector3(40, -40, 125)
[sub_resource type="Resource" id="Resource_xvesq"]
script = ExtResource("2_36hd6")
route_id = &"safe_ridge"
waypoints_m = PackedVector3Array(0, -14, -250, 70, -16, -200, 76, -16, -100, 78, -16, 0, 76, -18, 80, 40, -24, 105, 0, -26, 100, -14, -30, 98)
maximum_depth_m = 32.0
[sub_resource type="Resource" id="Resource_lcu7s"]
script = ExtResource("2_36hd6")
route_id = &"risky_groove"
waypoints_m = PackedVector3Array(0, -14, -250, -60, -20, -150, -50, -31, -100, -35, -33, -20, -22, -34, 60, -14, -33, 98)
maximum_depth_m = 48.0
[resource]
script = ExtResource("3_vix3p")
site_id = &"zone1_vigilant"
display_name = "Banc du Vigilant"
terrain = ExtResource("4_i0v8k")
playable_bounds_m = Rect2(-300, -300, 600, 600)
spawn_position_m = Vector3(0, -10, -250)
placements = Array[ExtResource("1_n3xpu")]([SubResource("Resource_rqt47"), SubResource("Resource_xcojm"), SubResource("Resource_yljal"), SubResource("Resource_aqgwj"), SubResource("Resource_aervf"), SubResource("Resource_qgawl"), SubResource("Resource_q0ace"), SubResource("Resource_hd5cl"), SubResource("Resource_wy8hl"), SubResource("Resource_e72vy"), SubResource("Resource_3n33i"), SubResource("Resource_cdmnh"), SubResource("Resource_u8dqp"), SubResource("Resource_3uttw")])
routes = Array[ExtResource("2_36hd6")]([SubResource("Resource_xvesq"), SubResource("Resource_lcu7s")])
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bseks0pf7k0t4"
path="res://.godot/imported/zone1_vigilant_terrain_preview.png-c84b076e243497f30f994bef5c7d9142.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/briefs/captures/zone1_vigilant_terrain_preview.png"
dest_files=["res://.godot/imported/zone1_vigilant_terrain_preview.png-c84b076e243497f30f994bef5c7d9142.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+8
View File
@@ -131,6 +131,14 @@ printf 'Quality gate: site baker unit tests\n'
2>&1 | tee "$QUALITY_TEMP_DIRECTORY/site-baker-unit-console.log" 2>&1 | tee "$QUALITY_TEMP_DIRECTORY/site-baker-unit-console.log"
check_godot_log "site baker unit tests" "$QUALITY_TEMP_DIRECTORY/site-baker-unit-console.log" check_godot_log "site baker unit tests" "$QUALITY_TEMP_DIRECTORY/site-baker-unit-console.log"
printf 'Quality gate: site content contract tests\n'
"$GODOT_EXECUTABLE" --headless \
--log-file "$QUALITY_TEMP_DIRECTORY/site-content-contract.log" \
--path . \
--script tests/contract/run_site_content_tests.gd \
2>&1 | tee "$QUALITY_TEMP_DIRECTORY/site-content-contract-console.log"
check_godot_log "site content contract tests" "$QUALITY_TEMP_DIRECTORY/site-content-contract-console.log"
printf 'Quality gate: production foundation integration\n' printf 'Quality gate: production foundation integration\n'
"$GODOT_EXECUTABLE" --headless \ "$GODOT_EXECUTABLE" --headless \
--log-file "$QUALITY_TEMP_DIRECTORY/foundation-integration.log" \ --log-file "$QUALITY_TEMP_DIRECTORY/foundation-integration.log" \
+142
View File
@@ -0,0 +1,142 @@
extends SceneTree
const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
const DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition.gd")
const TerrainHeightField = preload("res://game/domain/sites/terrain_height_field.gd")
const SitePlacement = preload("res://game/domain/sites/site_placement.gd")
const SiteRoute = preload("res://game/domain/sites/site_route.gd")
const DiveDifficultyProfile = preload("res://game/domain/rules/dive_difficulty_profile.gd")
const DiveUpgradeEffects = preload("res://game/domain/rules/dive_upgrade_effects.gd")
const DiveAccessibilityAdjustments = preload(
"res://game/domain/rules/dive_accessibility_adjustments.gd"
)
const DiveRulesResolver = preload("res://game/domain/rules/dive_rules_resolver.gd")
const SITE_PATH := "res://content/sites/zone1_vigilant.tres"
const ROUTE_CLEARANCE_M := 3.0
const MINIMUM_LANDMARK_SPACING_M := 60.0
const TEST_COUNT := 30
var _failures: Array[String] = []
func _init() -> void:
_test_zone1_content()
if _failures.is_empty():
print("Site content contract tests: %d passed, 0 failed." % TEST_COUNT)
quit(0)
else:
for failure: String in _failures:
push_error(failure)
print("Site content contract tests: %d failed." % _failures.size())
quit(1)
func _test_zone1_content() -> void:
var definition := load(SITE_PATH) as DiveSiteDefinition
_expect(definition != null, "zone 1 definition loads")
if definition == null:
return
_expect(definition.validation_error().is_empty(), "zone 1 definition is valid: %s" % definition.validation_error())
var field := definition.height_field()
if field == null:
return
var rules = DiveRulesResolver.new().resolve(
DiveDifficultyProfile.standard(),
DiveUpgradeEffects.new(),
DiveAccessibilityAdjustments.new()
)
var certified_m: float = rules.certified_depth_m
var critical_m: float = rules.critical_depth_m
_expect(definition.playable_bounds_m.is_equal_approx(Rect2(-300.0, -300.0, 600.0, 600.0)), "playable zone is 600 x 600 m")
_expect(field.minimum_elevation_m() >= -88.5 and field.maximum_elevation_m() <= -19.5, "terrain stays between 20 m and 88 m deep")
_expect(field.cell_size_m() == 1.5 and field.columns() == 513 and field.rows() == 513, "terrain grid is 513 x 513 cells of 1.5 m")
var targets := definition.placements_of_kind(SitePlacement.Kind.WRECK_TARGET)
var decoys := definition.placements_of_kind(SitePlacement.Kind.WRECK_DECOY)
var items := definition.placements_of_kind(SitePlacement.Kind.SECONDARY_ITEM)
var anchors := definition.placements_of_kind(SitePlacement.Kind.VESSEL_ANCHOR)
var debris := definition.placements_of_kind(SitePlacement.Kind.DEBRIS)
var landmarks := definition.placements_of_kind(SitePlacement.Kind.LANDMARK)
_expect(targets.size() == 1, "exactly one target wreck")
_expect(decoys.size() == 2, "exactly two decoy wrecks")
_expect(items.size() == 1, "exactly one secondary item")
_expect(anchors.size() == 1, "exactly one vessel anchor")
_expect(debris.size() >= 3, "a debris trail of at least three pieces")
_expect(landmarks.size() >= 4, "at least four named landmarks")
for wreck: SitePlacement in targets + decoys:
_expect(-wreck.position_m.y <= certified_m, "%s lies above the certified depth" % wreck.placement_id)
_expect(_rests_on_terrain(field, wreck), "%s rests on or in the seabed" % wreck.placement_id)
if items.size() == 1:
var item_depth := -items[0].position_m.y
_expect(item_depth > certified_m and item_depth <= critical_m, "secondary item is between 100 %% and 115 %% of the certified depth (%.1f m)" % item_depth)
_expect(_rests_on_terrain(field, items[0]), "secondary item rests on the seabed")
for piece: SitePlacement in debris:
if -piece.position_m.y < 45.0 or -piece.position_m.y > 66.0 or not _rests_on_terrain(field, piece):
_failures.append("debris %s is off the slope or floating" % piece.placement_id)
_expect(true, "debris pieces checked individually")
if anchors.size() == 1:
var seabed_under_anchor := -field.elevation_at(anchors[0].position_m.x, anchors[0].position_m.z)
_expect(seabed_under_anchor >= 20.0 and seabed_under_anchor <= 30.0, "vessel anchors over 20-30 m of seabed")
var spawn := definition.spawn_position_m
_expect(spawn.y - field.elevation_at(spawn.x, spawn.z) >= 8.0, "spawn has at least 8 m of water under the keel")
_expect(is_equal_approx(-spawn.y, 10.0), "spawn is 10 m deep")
var safe := definition.route(&"safe_ridge")
var risky := definition.route(&"risky_groove")
_expect(safe != null and safe.maximum_depth_m == 32.0, "safe route is declared with a 32 m ceiling")
_expect(risky != null and risky.maximum_depth_m == 48.0, "risky route is declared with a 48 m ceiling")
if safe != null and risky != null and targets.size() == 1:
_expect(_route_clears_terrain(field, safe), "safe route clears the terrain by %.0f m" % ROUTE_CLEARANCE_M)
_expect(_route_clears_terrain(field, risky), "risky route clears the terrain by %.0f m" % ROUTE_CLEARANCE_M)
var target := targets[0].position_m
_expect(
_xz_distance(safe.waypoints_m[-1], target) <= 25.0 and _xz_distance(risky.waypoints_m[-1], target) <= 25.0,
"both routes end within 25 m of the target"
)
_expect(_route_length(risky) < _route_length(safe) * 0.9, "risky route is shorter than the safe route")
var spaced := true
for i in landmarks.size():
for j in range(i + 1, landmarks.size()):
if _xz_distance(landmarks[i].position_m, landmarks[j].position_m) < MINIMUM_LANDMARK_SPACING_M:
spaced = false
_expect(spaced, "landmarks are at least 60 m apart")
func _rests_on_terrain(field: TerrainHeightField, placement: SitePlacement) -> bool:
var seabed := field.elevation_at(placement.position_m.x, placement.position_m.z)
var bottom := placement.position_m.y - placement.footprint_m.y * 0.5
return bottom >= seabed - placement.footprint_m.y and bottom <= seabed + 1.0
func _route_clears_terrain(field: TerrainHeightField, route: SiteRoute) -> bool:
for i in range(route.waypoints_m.size() - 1):
var a := route.waypoints_m[i]
var b := route.waypoints_m[i + 1]
var steps := maxi(int(ceilf(a.distance_to(b) / 2.0)), 1)
for step in range(steps + 1):
var point := a.lerp(b, float(step) / float(steps))
if point.y - field.elevation_at(point.x, point.z) < ROUTE_CLEARANCE_M:
return false
return true
func _route_length(route: SiteRoute) -> float:
var length := 0.0
for i in range(route.waypoints_m.size() - 1):
length += route.waypoints_m[i].distance_to(route.waypoints_m[i + 1])
return length
func _xz_distance(a: Vector3, b: Vector3) -> float:
return Vector2(a.x, a.z).distance_to(Vector2(b.x, b.z))
func _expect(condition: bool, label: String) -> void:
if not condition:
_failures.append(label)
+143
View File
@@ -0,0 +1,143 @@
class_name Zone1VigilantRecipe
extends RefCounted
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 TerrainHeightField = preload("res://game/domain/sites/terrain_height_field.gd")
const DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition.gd")
const SitePlacement = preload("res://game/domain/sites/site_placement.gd")
const SiteRoute = preload("res://game/domain/sites/site_route.gd")
# Groove axis (la Saignée): north entrance to the shelf break.
const GROOVE_START := Vector2(-60.0, -140.0)
const GROOVE_END := Vector2(-10.0, 120.0)
# Ridge axis (l'Échine). Starts south of the vessel so its crest never rises above 20 m.
const RIDGE_START := Vector2(70.0, -150.0)
const RIDGE_END := Vector2(80.0, 90.0)
const BREAK_Z := 125.0
static func recipe() -> SiteTerrainBaker.Recipe:
var result := SiteTerrainBaker.Recipe.new()
result.columns = 513
result.rows = 513
result.cell_size_m = 1.5
result.origin_x_m = -384.0
result.origin_z_m = -384.0
result.floor_elevation_m = -88.0
result.rock_slope_degrees = 20.0
result.rock_reflectivity = 0.72
result.sand_reflectivity = 0.45
result.primitives = [
# Plateau: 24 m deep at the vessel (z = -300), 6 m deeper over 400 m toward the south.
SiteTerrainBaker.SlopedPlane.new(24.0, 0.0, -0.015, 0.0, -300.0),
# L'Échine: long north-south rock ridge, 6 m high, flanks steeper than 20°.
SiteTerrainBaker.Ridge.new(RIDGE_START, RIDGE_END, 12.0, 6.0, 25.0),
# Les Deux Frères: twin pinnacles, 20 m apart, 6 m tall (crests stay below 20 m deep).
SiteTerrainBaker.Blob.new(Vector2(-160.0, -130.0), 8.4, 8.4, 6.0),
SiteTerrainBaker.Blob.new(Vector2(-142.0, -121.0), 8.4, 8.4, 6.0),
# La Saignée: 16 m wide groove, 9 m deep, steep walls, the risky route.
SiteTerrainBaker.Groove.new(GROOVE_START, GROOVE_END, 8.0, 9.0, 4.0),
# Basin and patch reef for the steel coaster decoy, east of the ridge.
SiteTerrainBaker.Blob.new(Vector2(140.0, 40.0), 30.0, 30.0, -8.0),
SiteTerrainBaker.Blob.new(Vector2(125.0, 30.0), 7.7, 7.7, 2.5),
SiteTerrainBaker.Blob.new(Vector2(155.0, 55.0), 7.7, 7.7, 2.5),
SiteTerrainBaker.Blob.new(Vector2(135.0, 60.0), 7.7, 7.7, 2.5),
# Shelf break (la rupture) then the drop-off (le tombant), clamped at -88 m.
SiteTerrainBaker.Step.new(Vector2(0.0, BREAK_Z), Vector2(0.0, 1.0), 16.0, 10.0, 0.7),
# Scree cones at the foot of the drop-off.
SiteTerrainBaker.Blob.new(Vector2(-40.0, 200.0), 6.3, 6.3, 2.0),
SiteTerrainBaker.Blob.new(Vector2(-5.0, 205.0), 6.3, 6.3, 2.0),
SiteTerrainBaker.Blob.new(Vector2(25.0, 198.0), 6.3, 6.3, 2.0),
# Les Rides: sand ripples near the vessel only.
SiteTerrainBaker.Ripples.new(Vector2(1.0, 0.35), 9.0, 0.45, Vector2(0.0, -220.0), 90.0),
# Deterministic roughness everywhere.
SiteTerrainBaker.Roughness.new(1933, 0.02, 0.6),
]
result.material_patches = [
SiteTerrainBaker.MaterialPatch.new(Vector2(140.0, 40.0), 28.0, SonarMaterial.Value.ROCK),
]
return result
static func definition(terrain: SiteTerrainData) -> DiveSiteDefinition:
var field := TerrainHeightField.from_data(terrain)
var result := DiveSiteDefinition.new()
result.site_id = &"zone1_vigilant"
result.display_name = "Banc du Vigilant"
result.terrain = terrain
result.playable_bounds_m = Rect2(-300.0, -300.0, 600.0, 600.0)
result.spawn_position_m = Vector3(0.0, -10.0, -250.0)
result.spawn_yaw_radians = 0.0
result.placements = [
_on_seabed(field, &"prospection_vessel", SitePlacement.Kind.WRECK_TARGET,
Vector2(-12.0, 108.0), 0.35, Vector3(28.0, 8.0, 9.0), 2.0),
_on_seabed(field, &"decoy_yacht", SitePlacement.Kind.WRECK_DECOY,
Vector2(-40.0, -200.0), 1.1, Vector3(12.0, 3.0, 4.0), 0.5),
_on_seabed(field, &"decoy_coaster", SitePlacement.Kind.WRECK_DECOY,
Vector2(140.0, 40.0), -0.6, Vector3(26.0, 6.0, 8.0), 1.2),
_on_seabed(field, &"secondary_item", SitePlacement.Kind.SECONDARY_ITEM,
Vector2(-6.0, 157.0), 0.0, Vector3(1.0, 1.0, 1.0), 0.0),
_on_seabed(field, &"debris_1", SitePlacement.Kind.DEBRIS,
Vector2(-12.0, 134.0), 0.4, Vector3(3.0, 2.0, 2.0), 0.3),
_on_seabed(field, &"debris_2", SitePlacement.Kind.DEBRIS,
Vector2(-10.0, 139.0), 1.2, Vector3(6.0, 2.5, 2.5), 0.4),
_on_seabed(field, &"debris_3", SitePlacement.Kind.DEBRIS,
Vector2(-9.0, 146.0), 0.0, Vector3(2.0, 1.5, 2.0), 0.2),
_on_seabed(field, &"debris_4", SitePlacement.Kind.DEBRIS,
Vector2(-7.0, 152.0), 2.0, Vector3(4.0, 1.5, 1.5), 0.3),
_marker(&"vessel_anchor", SitePlacement.Kind.VESSEL_ANCHOR, Vector3(0.0, -14.0, -250.0)),
_marker(&"echine", SitePlacement.Kind.LANDMARK, Vector3(75.0, -20.0, -50.0)),
_marker(&"deux_freres", SitePlacement.Kind.LANDMARK, Vector3(-151.0, -18.0, -125.0)),
_marker(&"rides", SitePlacement.Kind.LANDMARK, Vector3(0.0, -25.0, -230.0)),
_marker(&"saignee_entrance", SitePlacement.Kind.LANDMARK, Vector3(-60.0, -34.0, -140.0)),
_marker(&"rupture", SitePlacement.Kind.LANDMARK, Vector3(40.0, -40.0, 125.0)),
]
result.routes = [
_route(&"safe_ridge", 32.0, [
Vector3(0.0, -14.0, -250.0), Vector3(70.0, -16.0, -200.0), Vector3(76.0, -16.0, -100.0),
Vector3(78.0, -16.0, 0.0), Vector3(76.0, -18.0, 80.0), Vector3(40.0, -24.0, 105.0),
Vector3(0.0, -26.0, 100.0), Vector3(-14.0, -30.0, 98.0),
]),
_route(&"risky_groove", 48.0, [
Vector3(0.0, -14.0, -250.0), Vector3(-60.0, -20.0, -150.0), Vector3(-50.0, -31.0, -100.0),
Vector3(-35.0, -33.0, -20.0), Vector3(-22.0, -34.0, 60.0), Vector3(-14.0, -33.0, 98.0),
]),
]
return result
static func _on_seabed(
field: TerrainHeightField,
id: StringName,
kind: SitePlacement.Kind,
xz: Vector2,
yaw_radians: float,
footprint_m: Vector3,
burial_m: float
) -> SitePlacement:
var placement := SitePlacement.new()
placement.placement_id = id
placement.kind = kind
placement.footprint_m = footprint_m
placement.yaw_radians = yaw_radians
var seabed := field.elevation_at(xz.x, xz.y)
placement.position_m = Vector3(xz.x, seabed + footprint_m.y * 0.5 - burial_m, xz.y)
return placement
static func _marker(id: StringName, kind: SitePlacement.Kind, position_m: Vector3) -> SitePlacement:
var placement := SitePlacement.new()
placement.placement_id = id
placement.kind = kind
placement.position_m = position_m
return placement
static func _route(id: StringName, maximum_depth_m: float, waypoints: Array) -> SiteRoute:
var route := SiteRoute.new()
route.route_id = id
route.maximum_depth_m = maximum_depth_m
route.waypoints_m = PackedVector3Array(waypoints)
return route