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:
1 parent
51634820bd
commit
a1493434e4
7 files changed
+454
No files matched your search
@@ -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)
|
||||
Reference in new issue
Block a user