fix(content): lay the Vigilant hull along la Saignee within the distance budget

The target hull now lies along the groove axis (yaw derived from the
groove constants), set back 22 m from the groove end so both hull ends
rest on the flat groove floor. The mooring, spawn and first route
waypoints move to z = -210 and the decoy yacht to (-30, -150) so no
wreck is farther than 320 m from the mooring and no two wrecks are
farther than 260 m apart (spec 4.1). The spawn faces south toward the
site. Four contract assertions pin the budget, the spawn heading and
the hull orientation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
OlivierandClaude Opus 5.5 committed 2026-10-06 20:49:28 +02:00
1 parent 5344b045d9
commit be16d2ee07
4 files changed
+63 -19

No files matched your search

+8 -7
View File
@@ -9,15 +9,15 @@
script = ExtResource("1_n3xpu") script = ExtResource("1_n3xpu")
placement_id = &"prospection_vessel" placement_id = &"prospection_vessel"
kind = 0 kind = 0
position_m = Vector3(-12, -37.10001, 108) position_m = Vector3(-14.154642, -37.198338, 98.39586)
yaw_radians = 0.35 yaw_radians = -1.380808047881126
footprint_m = Vector3(28, 8, 9) footprint_m = Vector3(28, 8, 9)
[sub_resource type="Resource" id="Resource_xcojm"] [sub_resource type="Resource" id="Resource_xcojm"]
script = ExtResource("1_n3xpu") script = ExtResource("1_n3xpu")
placement_id = &"decoy_yacht" placement_id = &"decoy_yacht"
kind = 1 kind = 1
position_m = Vector3(-40, -24.655516, -200) position_m = Vector3(-30, -25.199738, -150)
yaw_radians = 1.1 yaw_radians = 1.1
footprint_m = Vector3(12, 3, 4) footprint_m = Vector3(12, 3, 4)
@@ -71,7 +71,7 @@ footprint_m = Vector3(4, 1.5, 1.5)
script = ExtResource("1_n3xpu") script = ExtResource("1_n3xpu")
placement_id = &"vessel_anchor" placement_id = &"vessel_anchor"
kind = 5 kind = 5
position_m = Vector3(0, -14, -250) position_m = Vector3(0, -14, -210)
[sub_resource type="Resource" id="Resource_e72vy"] [sub_resource type="Resource" id="Resource_e72vy"]
script = ExtResource("1_n3xpu") script = ExtResource("1_n3xpu")
@@ -101,13 +101,13 @@ position_m = Vector3(40, -40, 125)
[sub_resource type="Resource" id="Resource_xvesq"] [sub_resource type="Resource" id="Resource_xvesq"]
script = ExtResource("2_36hd6") script = ExtResource("2_36hd6")
route_id = &"safe_ridge" 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) waypoints_m = PackedVector3Array(0, -14, -210, 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 maximum_depth_m = 32.0
[sub_resource type="Resource" id="Resource_lcu7s"] [sub_resource type="Resource" id="Resource_lcu7s"]
script = ExtResource("2_36hd6") script = ExtResource("2_36hd6")
route_id = &"risky_groove" 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) waypoints_m = PackedVector3Array(0, -14, -210, -60, -20, -150, -50, -31, -100, -35, -33, -20, -22, -34, 60, -14, -33, 98)
maximum_depth_m = 48.0 maximum_depth_m = 48.0
[resource] [resource]
@@ -116,6 +116,7 @@ site_id = &"zone1_vigilant"
display_name = "Banc du Vigilant" display_name = "Banc du Vigilant"
terrain = ExtResource("4_i0v8k") terrain = ExtResource("4_i0v8k")
playable_bounds_m = Rect2(-300, -300, 600, 600) playable_bounds_m = Rect2(-300, -300, 600, 600)
spawn_position_m = Vector3(0, -10, -250) spawn_position_m = Vector3(0, -10, -210)
spawn_yaw_radians = 3.141592653589793
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")]) 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")]) routes = Array[ExtResource("2_36hd6")]([SubResource("Resource_xvesq"), SubResource("Resource_lcu7s")])
+3 -3
View File
@@ -86,7 +86,7 @@ crédibilité, conformément à la direction artistique.
| Champ de hauteur | 513 × 513 cellules, 1,5 m par cellule, élévations float32 (`SiteTerrainData`) | | Champ de hauteur | 513 × 513 cellules, 1,5 m par cellule, élévations float32 (`SiteTerrainData`) |
| Profondeur min / max du terrain | 20 m / 88 m | | Profondeur min / max du terrain | 20 m / 88 m |
| Profondeur d'écrasement coque I | hors terrain : le fond ne tue jamais par lui-même | | Profondeur d'écrasement coque I | hors terrain : le fond ne tue jamais par lui-même |
| Mouillage du navire | centre nord du plateau, 24 m de fond | | Mouillage du navire | centre nord du plateau, 25 m de fond |
| Point de départ du sous-marin | sous le navire, 10 m de profondeur | | Point de départ du sous-marin | sous le navire, 10 m de profondeur |
Les distances sont dimensionnées pour l'autonomie Standard (environ 1,2 km, Les distances sont dimensionnées pour l'autonomie Standard (environ 1,2 km,
@@ -150,14 +150,14 @@ correspond pas au rapport.
### 4.4 Les deux routes vers la cible ### 4.4 Les deux routes vers la cible
**Route sûre — par l'Échine (≈ 470 m, 26–32 m).** Depuis le navire, suivre la **Route sûre — par l'Échine (≈ 450 m, 26–32 m).** Depuis le navire, suivre la
crête longue vers le sud. Elle est lumineuse, large, sans obstacle, et se lit crête longue vers le sud. Elle est lumineuse, large, sans obstacle, et se lit
d'un seul scan. Elle mène au bord du plateau au-dessus de la Saignée : il d'un seul scan. Elle mène au bord du plateau au-dessus de la Saignée : il
faut ensuite descendre dans le sillon par son extrémité nord, à faible faut ensuite descendre dans le sillon par son extrémité nord, à faible
pente. Coût : temps et batterie. Récompense : zéro dégât, retour facile à pente. Coût : temps et batterie. Récompense : zéro dégât, retour facile à
estimer. estimer.
**Route risquée — par la Saignée (≈ 370 m, 36–48 m).** Un sillon de 12 à **Route risquée — par la Saignée (≈ 340 m, 36–48 m).** Un sillon de 12 à
18 m de large, aux parois de 8 à 10 m, qui coupe le plateau en diagonale et 18 m de large, aux parois de 8 à 10 m, qui coupe le plateau en diagonale et
débouche directement sur la proue de la cible. Sombre (les projecteurs débouche directement sur la proue de la cible. Sombre (les projecteurs
deviennent nécessaires), étroit (collisions), en pente (la batterie de deviennent nécessaires), étroit (collisions), en pente (la batterie de
+36 -1
View File
@@ -15,7 +15,11 @@ 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 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
const TEST_COUNT := 30 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 := 34
var _failures: Array[String] = [] var _failures: Array[String] = []
@@ -107,6 +111,26 @@ func _test_zone1_content() -> void:
spaced = false spaced = false
_expect(spaced, "landmarks are at least 60 m apart") _expect(spaced, "landmarks are at least 60 m apart")
var wrecks := targets + decoys
if anchors.size() == 1:
var anchor := anchors[0].position_m
var farthest := 0.0
for wreck: SitePlacement in wrecks:
farthest = maxf(farthest, _xz_distance(anchor, wreck.position_m))
_expect(farthest <= MAXIMUM_ANCHOR_TO_WRECK_M, "every wreck lies within 320 m of the mooring (farthest %.0f m)" % farthest)
var widest := 0.0
for i in wrecks.size():
for j in range(i + 1, wrecks.size()):
widest = maxf(widest, _xz_distance(wrecks[i].position_m, wrecks[j].position_m))
_expect(widest <= MAXIMUM_WRECK_SPACING_M, "every pair of wrecks lies within 260 m (widest %.0f m)" % widest)
if targets.size() == 1:
var target_xz := Vector2(targets[0].position_m.x, targets[0].position_m.z)
var spawn_xz := Vector2(spawn.x, spawn.z)
var yaw := definition.spawn_yaw_radians
var spawn_forward := Vector2(-sin(yaw), -cos(yaw))
_expect(spawn_forward.dot(target_xz - spawn_xz) > 0.0, "the spawn faces the target wreck")
_expect(_hull_lies_along_seabed(field, targets[0]), "the target hull lies along the groove floor")
func _rests_on_terrain(field: TerrainHeightField, placement: SitePlacement) -> bool: func _rests_on_terrain(field: TerrainHeightField, placement: SitePlacement) -> bool:
var seabed := field.elevation_at(placement.position_m.x, placement.position_m.z) var seabed := field.elevation_at(placement.position_m.x, placement.position_m.z)
@@ -114,6 +138,17 @@ func _rests_on_terrain(field: TerrainHeightField, placement: SitePlacement) -> b
return bottom >= seabed - placement.footprint_m.y and bottom <= seabed + 1.0 return bottom >= seabed - placement.footprint_m.y and bottom <= seabed + 1.0
func _hull_lies_along_seabed(field: TerrainHeightField, hull: SitePlacement) -> bool:
var centre := Vector2(hull.position_m.x, hull.position_m.z)
var long_axis := Vector2(cos(hull.yaw_radians), -sin(hull.yaw_radians))
var centre_seabed := field.elevation_at(centre.x, centre.y)
for side: float in [-1.0, 1.0]:
var end := centre + long_axis * HULL_HALF_LENGTH_M * side
if absf(field.elevation_at(end.x, end.y) - centre_seabed) > HULL_SEABED_TOLERANCE_M:
return false
return true
func _route_clears_terrain(field: TerrainHeightField, route: SiteRoute) -> bool: func _route_clears_terrain(field: TerrainHeightField, route: SiteRoute) -> bool:
for i in range(route.waypoints_m.size() - 1): for i in range(route.waypoints_m.size() - 1):
var a := route.waypoints_m[i] var a := route.waypoints_m[i]
+16 -8
View File
@@ -16,6 +16,9 @@ const GROOVE_END := Vector2(-10.0, 120.0)
const RIDGE_START := Vector2(70.0, -150.0) const RIDGE_START := Vector2(70.0, -150.0)
const RIDGE_END := Vector2(80.0, 90.0) const RIDGE_END := Vector2(80.0, 90.0)
const BREAK_Z := 125.0 const BREAK_Z := 125.0
# The target's centre sits this far up the groove from GROOVE_END, so its 28 m hull
# stops one groove half-width short of the end cap and rests on the flat groove floor.
const TARGET_SET_BACK_M := 22.0
static func recipe() -> SiteTerrainBaker.Recipe: static func recipe() -> SiteTerrainBaker.Recipe:
@@ -30,7 +33,7 @@ static func recipe() -> SiteTerrainBaker.Recipe:
result.rock_reflectivity = 0.72 result.rock_reflectivity = 0.72
result.sand_reflectivity = 0.45 result.sand_reflectivity = 0.45
result.primitives = [ result.primitives = [
# Plateau: 24 m deep at the vessel (z = -300), 6 m deeper over 400 m toward the south. # Plateau: 24 m deep at z = -300 (about 25 m at the mooring), 6 m deeper over 400 m toward the south.
SiteTerrainBaker.SlopedPlane.new(24.0, 0.0, -0.015, 0.0, -300.0), 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°. # 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), SiteTerrainBaker.Ridge.new(RIDGE_START, RIDGE_END, 12.0, 6.0, 25.0),
@@ -63,18 +66,23 @@ static func recipe() -> SiteTerrainBaker.Recipe:
static func definition(terrain: SiteTerrainData) -> DiveSiteDefinition: static func definition(terrain: SiteTerrainData) -> DiveSiteDefinition:
var field := TerrainHeightField.from_data(terrain) var field := TerrainHeightField.from_data(terrain)
# The target hull's long axis (local +X) lies along la Saignée.
var groove_axis := (GROOVE_END - GROOVE_START).normalized()
var groove_yaw := atan2(-groove_axis.y, groove_axis.x)
var target_xz := GROOVE_END - groove_axis * TARGET_SET_BACK_M
var result := DiveSiteDefinition.new() var result := DiveSiteDefinition.new()
result.site_id = &"zone1_vigilant" result.site_id = &"zone1_vigilant"
result.display_name = "Banc du Vigilant" result.display_name = "Banc du Vigilant"
result.terrain = terrain result.terrain = terrain
result.playable_bounds_m = Rect2(-300.0, -300.0, 600.0, 600.0) result.playable_bounds_m = Rect2(-300.0, -300.0, 600.0, 600.0)
result.spawn_position_m = Vector3(0.0, -10.0, -250.0) # Spawn beside the mooring, facing south toward the site.
result.spawn_yaw_radians = 0.0 result.spawn_position_m = Vector3(0.0, -10.0, -210.0)
result.spawn_yaw_radians = PI
result.placements = [ result.placements = [
_on_seabed(field, &"prospection_vessel", SitePlacement.Kind.WRECK_TARGET, _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), target_xz, groove_yaw, Vector3(28.0, 8.0, 9.0), 2.0),
_on_seabed(field, &"decoy_yacht", SitePlacement.Kind.WRECK_DECOY, _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), Vector2(-30.0, -150.0), 1.1, Vector3(12.0, 3.0, 4.0), 0.5),
_on_seabed(field, &"decoy_coaster", SitePlacement.Kind.WRECK_DECOY, _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), 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, _on_seabed(field, &"secondary_item", SitePlacement.Kind.SECONDARY_ITEM,
@@ -87,7 +95,7 @@ static func definition(terrain: SiteTerrainData) -> DiveSiteDefinition:
Vector2(-9.0, 146.0), 0.0, Vector3(2.0, 1.5, 2.0), 0.2), Vector2(-9.0, 146.0), 0.0, Vector3(2.0, 1.5, 2.0), 0.2),
_on_seabed(field, &"debris_4", SitePlacement.Kind.DEBRIS, _on_seabed(field, &"debris_4", SitePlacement.Kind.DEBRIS,
Vector2(-7.0, 152.0), 2.0, Vector3(4.0, 1.5, 1.5), 0.3), 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(&"vessel_anchor", SitePlacement.Kind.VESSEL_ANCHOR, Vector3(0.0, -14.0, -210.0)),
_marker(&"echine", SitePlacement.Kind.LANDMARK, Vector3(75.0, -20.0, -50.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(&"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(&"rides", SitePlacement.Kind.LANDMARK, Vector3(0.0, -25.0, -230.0)),
@@ -96,12 +104,12 @@ static func definition(terrain: SiteTerrainData) -> DiveSiteDefinition:
] ]
result.routes = [ result.routes = [
_route(&"safe_ridge", 32.0, [ _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(0.0, -14.0, -210.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(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), Vector3(0.0, -26.0, 100.0), Vector3(-14.0, -30.0, 98.0),
]), ]),
_route(&"risky_groove", 48.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(0.0, -14.0, -210.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), Vector3(-35.0, -33.0, -20.0), Vector3(-22.0, -34.0, 60.0), Vector3(-14.0, -33.0, 98.0),
]), ]),
] ]