feat(sites): typed dive site definition with placements and routes

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
OlivierandClaude Haiku 4.5 committed 2026-10-06 19:52:24 +02:00
1 parent 1c1f0b87da
commit 506f6dd0c8
5 files changed
+255 -2

No files matched your search

+101 -1
View File
@@ -3,8 +3,11 @@ extends SceneTree
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 SitePlacement = preload("res://game/domain/sites/site_placement.gd")
const SiteRoute = preload("res://game/domain/sites/site_route.gd")
const DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition.gd")
const TEST_COUNT := 20
const TEST_COUNT := 38
var _failures: Array[String] = []
@@ -12,6 +15,7 @@ var _failures: Array[String] = []
func _init() -> void:
_test_terrain_data_validation()
_test_height_field_sampling()
_test_site_definition_validation()
if _failures.is_empty():
print("Site domain unit tests: %d passed, 0 failed." % TEST_COUNT)
@@ -95,6 +99,102 @@ func _test_height_field_sampling() -> void:
_expect(is_equal_approx(field.reflectivity_for(SonarMaterial.Value.SAND), 0.45), "sand reflectivity comes from the data")
func _make_placement(id: String, kind: int, position: Vector3) -> SitePlacement:
var placement := SitePlacement.new()
placement.placement_id = StringName(id)
placement.kind = kind
placement.position_m = position
placement.footprint_m = Vector3(4.0, 2.0, 3.0)
return placement
func _make_route(id: String, waypoints: PackedVector3Array, maximum_depth: float) -> SiteRoute:
var route := SiteRoute.new()
route.route_id = StringName(id)
route.waypoints_m = waypoints
route.maximum_depth_m = maximum_depth
return route
func _make_definition() -> DiveSiteDefinition:
var definition := DiveSiteDefinition.new()
definition.site_id = &"test_site"
definition.display_name = "Site de test"
definition.terrain = _make_terrain(3, 4)
definition.playable_bounds_m = Rect2(-10.0, 100.0, 4.0, 6.0)
definition.spawn_position_m = Vector3(-8.0, -10.0, 102.0)
definition.spawn_yaw_radians = PI * 0.5
var placements_list: Array[SitePlacement] = [
_make_placement("target", SitePlacement.Kind.WRECK_TARGET, Vector3(-8.0, -23.0, 103.0)),
_make_placement("anchor", SitePlacement.Kind.VESSEL_ANCHOR, Vector3(-8.0, -20.0, 100.5)),
]
definition.placements = placements_list
var routes_list: Array[SiteRoute] = [
_make_route(
"safe",
PackedVector3Array([Vector3(-8.0, -12.0, 101.0), Vector3(-8.0, -14.0, 103.0)]),
20.0
),
]
definition.routes = routes_list
return definition
func _test_site_definition_validation() -> void:
var definition := _make_definition()
_expect(definition.validation_error().is_empty(), "complete definition is valid")
_expect(definition.height_field() != null, "definition exposes its height field")
_expect(definition.placement(&"target").kind == SitePlacement.Kind.WRECK_TARGET, "placement lookup by id")
_expect(definition.placement(&"missing") == null, "unknown placement is null")
_expect(definition.placements_of_kind(SitePlacement.Kind.VESSEL_ANCHOR).size() == 1, "placements filter by kind")
_expect(definition.route(&"safe").waypoints_m.size() == 2, "route lookup by id")
_expect(definition.spawn_transform().origin.is_equal_approx(Vector3(-8.0, -10.0, 102.0)), "spawn transform origin")
_expect(
(definition.spawn_transform().basis * Vector3.FORWARD).is_equal_approx(Vector3.LEFT),
"spawn yaw rotates the forward axis about Y"
)
_expect(definition.placement(&"target").is_sonar_metal(), "wrecks read as metal on sonar")
_expect(not definition.placement(&"anchor").is_sonar_metal(), "anchor is not a sonar body")
var no_id := _make_definition()
no_id.site_id = &""
_expect(not no_id.validation_error().is_empty(), "site id is required")
var no_terrain := _make_definition()
no_terrain.terrain = null
_expect(not no_terrain.validation_error().is_empty(), "terrain is required")
var bounds_outside := _make_definition()
bounds_outside.playable_bounds_m = Rect2(-20.0, 100.0, 4.0, 6.0)
_expect(not bounds_outside.validation_error().is_empty(), "playable bounds must lie inside the terrain")
var spawn_outside := _make_definition()
spawn_outside.spawn_position_m = Vector3(50.0, -10.0, 102.0)
_expect(not spawn_outside.validation_error().is_empty(), "spawn must lie inside the bounds")
var duplicate_ids := _make_definition()
duplicate_ids.placements.append(
_make_placement("target", SitePlacement.Kind.WRECK_DECOY, Vector3(-7.0, -23.0, 104.0))
)
_expect(not duplicate_ids.validation_error().is_empty(), "placement ids are unique")
var placement_outside := _make_definition()
placement_outside.placements.append(
_make_placement("far", SitePlacement.Kind.DEBRIS, Vector3(80.0, -23.0, 104.0))
)
_expect(not placement_outside.validation_error().is_empty(), "placements must lie inside the bounds")
var empty_route := _make_definition()
empty_route.routes.append(_make_route("empty", PackedVector3Array(), 20.0))
_expect(not empty_route.validation_error().is_empty(), "routes need at least two waypoints")
var deep_route := _make_definition()
deep_route.routes.append(
_make_route("deep", PackedVector3Array([Vector3(-8.0, -12.0, 101.0), Vector3(-8.0, -30.0, 103.0)]), 20.0)
)
_expect(not deep_route.validation_error().is_empty(), "route waypoints respect the route maximum depth")
func _expect(condition: bool, label: String) -> void:
if not condition:
_failures.append(label)