From 51634820bd7f967a9a4438e145de207a8917407c Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 6 Oct 2026 20:16:45 +0200 Subject: [PATCH] feat(composition): dive world assembled from a selected site definition The calibration site spawn sat 16 m from the south bounds edge, inside the 40 m warning margin. The vehicle_test recipe now extends the terrain and playable bounds 42 m to the south so the spawn is inside the zone. Co-Authored-By: Claude Sonnet 5.5 --- content/sites/vehicle_test.tres | 2 +- content/sites/vehicle_test_terrain.res | Bin 21626 -> 25894 bytes .../captures/vehicle_test_terrain_preview.png | Bin 2516 -> 2459 bytes .../vertical_slice/dive_world.tscn | 43 +++++ .../vertical_slice/vehicle_test_world.tscn | 20 --- .../vertical_slice/vertical_slice_root.gd | 56 ++++++- game/presentation/cockpit/cockpit_hud.gd | 18 +++ game/presentation/cockpit/cockpit_hud.tscn | 21 +++ game/world/sites/vehicle_test_environment.gd | 153 ------------------ .../world/sites/vehicle_test_environment.tscn | 75 --------- .../run_vertical_slice_foundation.gd | 20 ++- tools/sites/vehicle_test_terrain_recipe.gd | 4 +- 12 files changed, 154 insertions(+), 258 deletions(-) create mode 100644 game/composition/vertical_slice/dive_world.tscn delete mode 100644 game/composition/vertical_slice/vehicle_test_world.tscn delete mode 100644 game/world/sites/vehicle_test_environment.gd delete mode 100644 game/world/sites/vehicle_test_environment.tscn diff --git a/content/sites/vehicle_test.tres b/content/sites/vehicle_test.tres index 2d2df84..eb82678 100644 --- a/content/sites/vehicle_test.tres +++ b/content/sites/vehicle_test.tres @@ -23,6 +23,6 @@ script = ExtResource("3_usfet") site_id = &"vehicle_test" display_name = "Site de calibration du véhicule" terrain = ExtResource("4_awe7g") -playable_bounds_m = Rect2(-90, -176, 180, 204) +playable_bounds_m = Rect2(-90, -176, 180, 246) spawn_position_m = Vector3(0, -12, 12) placements = Array[ExtResource("1_j2ii4")]([SubResource("Resource_5juth"), SubResource("Resource_tboiu")]) diff --git a/content/sites/vehicle_test_terrain.res b/content/sites/vehicle_test_terrain.res index 02a77626635e640f298a8b54e795b8ce70dcde41..fd14ed2396939e1742e59d64ca0a673eaeb69fab 100644 GIT binary patch delta 3499 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 SfsdIcE4UQ1G6V$&IRgOhkY9-a delta 117 zcmZ2>it*P9#tr`%8C@s;XVhmZ6xeLQbj*>5lYxOjo`Hd(P=JA9vtsBG#>ojW?2~te zYfUZ)@ov%g#m|COySQ-Zc7@9`H5RgC+j_U{?SE5)Yv%ugGUJy*M~>n{`)JrE}3%3GJTQ9 zl;SXDxm|&Lu|GE)H5dHpCzqG()LExo^`KaJk$mZb4i`b`-(f%hwR)(p@x54m>Uzne z$9qnhmw9iVIE(X1Zl|?UlCD|XuR7&ZF1@GI8t(sokZ?C`Xoyrk)$M0Ui?SQ;#wP8D%@AGE(Puh6ni~W0j%j&}H zDOKFxF8&C6(R-Zne)!wzy^nb}UEO>1?yuJ@IvXpp|6aR%`sAE-*VJv_B|gykzVNh) zisKeY_17B1#cJSO3EVe0gvZA^LK7}1Ydu|56sH9RtzEa_ntIC#(eF0r zFWVR!cgXa+yXYvnp66A{QH=WZHEy46URoW`!7PW{`hA+Z+v=1=JvkC*Ez6u|f5y0- zLFo)n$IdBpCOxZOxAfrKn)NSlpQz(mzU-a%UJs7;W?wh;1y>h!rOaFQ^KiRBQ-{Fj zjwe%sdNh|#`RuG2_elJQnY^4~+0P@7>k|65X72FPWIr;exMyOvOo@?8NswpHXPf#N z=||7H1bKPAtgw;mKRzKS@tNLpzLRpVk2pLws*iq>JMn1drJ_wwdO9qh+w->{SaYe~ z?aC(`1NW;ZP0osL|7I`4r|8xzZ5*t*?7a1hBeS%kPX@~vy1ATLs*vQR8TL_w!}j^= zML9klfuF0qG>=I*C;XjmJl*I?b*Osw6#np(H8tlyulZ^5e2u1()u$a@)r_{swoBM| zHk>d#CZOD|rK6Pg&HJ;}+yh>AjxK3+1{~gZl!{jD@V_9N*Sya8zRSPJu5aAy_@^E^ z6ZQ0-uX@C39zMl3gPV3vQ;PJK`WVjpd2NUOY&93vuRWppeXlq}ugPD&yF&PO-j%&8 z|4e-mulaOJ&zo5a2b;e=4;0+^-6uuqVbzVDOYJ^hY<&9d9-Hkkfn=4g4$Y+(OkC`| zx7&6u+JB<-EZ4E!FI(Q2U1-Yt&tQ{qmftb*qUx(}?27~Azw+CO?EZ9;?f%|Feu<@T zzgPVZR<6DFvFZI&+iBOUTbz7ED7S~zswo3x%Sh&LwE0I^L<_G zyLP{l9V5em%^n)8Uq3CFw!lTi>4c@bpe~nBW6&7%d<_s^X9BbKaj^px7!`&Rx1%m0cuBtP9P_`S7sr#k<)=O(}J71(#zl+>^b z3U;1RWJx;q)M{HB-ClECWZ}N)=V893Y&C4vnR!DdeCX` zdrOi0?MM46^7GYlloh7$ul1={s0*3Z=94Q|hBa#pis*6;VQ`9AOcu^+5= zHpEOxtL3;`AF{ry|JCBw|CxH*v)1%oitaiVr93ItN^;YUfO`(xI}U&P=sQ{Lyn4-c zgAHPImbu-L8+WIe@7Qaz z;`hgssdICj-wW~=n!mTrE3wbZ>{#&BpQB#sesA+J-+t-oua`eur1(3pCch!>@Y}f; zKi%2eR?Sh>{eJGx>W+Qo&3d-cuTRD;K0G`3(X~hWBGvCT?$}$~oG?X_KO{y0zPe%fcIcWpEN2Hh^GbFj&EJA#(n=Yu6Mp~tyG+UM|D{R|J%2Zq<7DlwR72o8|S}Yc;>h7?3#spt6~@Z z*1UMX@BQzrjoUR}cNZ-``}_Ii?gj6Yil^p(F1dK{=nS(9PuE{O`8)ZSaZcP^|8GaG z+{?M&IP+|`W!8c3=WjgUe)65>@z=#y{xkM>2IwE#Auqtdz`)??>gTe~DWM4fCP2|~ literal 2516 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 O1_n=8KbLh*2~7Z7q!x?- diff --git a/game/composition/vertical_slice/dive_world.tscn b/game/composition/vertical_slice/dive_world.tscn new file mode 100644 index 0000000..a4454d3 --- /dev/null +++ b/game/composition/vertical_slice/dive_world.tscn @@ -0,0 +1,43 @@ +[gd_scene load_steps=7 format=3] + +[ext_resource type="PackedScene" path="res://game/world/sites/dive_site.tscn" id="1_site"] +[ext_resource type="PackedScene" path="res://game/world/submarine/submarine_actor.tscn" id="2_submarine"] +[ext_resource type="PackedScene" path="res://game/presentation/cockpit/cockpit_camera_rig.tscn" id="3_camera"] +[ext_resource type="PackedScene" path="res://game/presentation/cockpit/cockpit_interior.tscn" id="4_cockpit"] + +[sub_resource type="ParticleProcessMaterial" id="SuspendedMotion"] +emission_shape = 3 +emission_box_extents = Vector3(45, 28, 70) +direction = Vector3(0.1, 0.04, 0) +spread = 180.0 +initial_velocity_min = 0.02 +initial_velocity_max = 0.09 +gravity = Vector3(0, 0, 0) +scale_min = 0.025 +scale_max = 0.08 + +[sub_resource type="SphereMesh" id="SuspendedParticleMesh"] +radius = 0.035 +height = 0.07 + +[node name="DiveWorld" type="Node3D"] + +[node name="DiveSite" parent="." instance=ExtResource("1_site")] +unique_name_in_owner = true + +[node name="SubmarineActor" parent="." instance=ExtResource("2_submarine")] +unique_name_in_owner = true + +[node name="SuspendedParticles" type="GPUParticles3D" parent="SubmarineActor"] +amount = 360 +lifetime = 14.0 +preprocess = 14.0 +visibility_aabb = AABB(-50, -35, -78, 100, 70, 156) +process_material = SubResource("SuspendedMotion") +draw_pass_1 = SubResource("SuspendedParticleMesh") + +[node name="CockpitInterior" parent="SubmarineActor" instance=ExtResource("4_cockpit")] +unique_name_in_owner = true + +[node name="CockpitCameraRig" parent="SubmarineActor" instance=ExtResource("3_camera")] +unique_name_in_owner = true diff --git a/game/composition/vertical_slice/vehicle_test_world.tscn b/game/composition/vertical_slice/vehicle_test_world.tscn deleted file mode 100644 index 74dbbbb..0000000 --- a/game/composition/vertical_slice/vehicle_test_world.tscn +++ /dev/null @@ -1,20 +0,0 @@ -[gd_scene load_steps=5 format=3] - -[ext_resource type="PackedScene" path="res://game/world/sites/vehicle_test_environment.tscn" id="1_environment"] -[ext_resource type="PackedScene" path="res://game/world/submarine/submarine_actor.tscn" id="2_submarine"] -[ext_resource type="PackedScene" path="res://game/presentation/cockpit/cockpit_camera_rig.tscn" id="3_camera"] -[ext_resource type="PackedScene" path="res://game/presentation/cockpit/cockpit_interior.tscn" id="4_cockpit"] - -[node name="VehicleTestWorld" type="Node3D"] - -[node name="Environment" parent="." instance=ExtResource("1_environment")] - -[node name="SubmarineActor" parent="." instance=ExtResource("2_submarine")] -unique_name_in_owner = true -position = Vector3(0, -12, 12) - -[node name="CockpitInterior" parent="SubmarineActor" instance=ExtResource("4_cockpit")] -unique_name_in_owner = true - -[node name="CockpitCameraRig" parent="SubmarineActor" instance=ExtResource("3_camera")] -unique_name_in_owner = true diff --git a/game/composition/vertical_slice/vertical_slice_root.gd b/game/composition/vertical_slice/vertical_slice_root.gd index 2c2c4e8..860bd05 100644 --- a/game/composition/vertical_slice/vertical_slice_root.gd +++ b/game/composition/vertical_slice/vertical_slice_root.gd @@ -29,6 +29,12 @@ const SubmarineTelemetryAdapter = preload( const ReferenceBathymetryAcquisition = preload( "res://game/world/sonar/reference_bathymetry_acquisition.gd" ) +const DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition.gd") +const DiveSite = preload("res://game/world/sites/dive_site.gd") +const PlayableBoundsRule = preload("res://game/application/sites/playable_bounds_rule.gd") +const PlayableBoundsStatus = preload( + "res://game/application/sites/playable_bounds_status.gd" +) const ProductionInputAdapter = preload( "res://game/infrastructure/input/production_input_adapter.gd" ) @@ -51,9 +57,8 @@ const ConfirmationDialogScreen = preload( const RebindResult = preload("res://game/application/input/rebind_result.gd") const MAIN_MENU_SCENE = preload("res://game/presentation/main_menu/main_menu_screen.tscn") -const VEHICLE_TEST_WORLD_SCENE_PATH := ( - "res://game/composition/vertical_slice/vehicle_test_world.tscn" -) +const DIVE_WORLD_SCENE_PATH := "res://game/composition/vertical_slice/dive_world.tscn" +const DEFAULT_SITE_PATH := "res://content/sites/vehicle_test.tres" const COCKPIT_HUD_SCENE = preload("res://game/presentation/cockpit/cockpit_hud.tscn") const SONAR_DISPLAY_SCENE = preload( "res://game/presentation/sonar/sonar_display.tscn" @@ -87,6 +92,10 @@ var _sonar_service: SonarService var _sonar_display: SonarDisplay var _latest_update: DiveUpdate var _active_world: Node3D +var _selected_site_path := DEFAULT_SITE_PATH +var _dive_site: DiveSite +var _bounds_rule: PlayableBoundsRule +var _bounds_status: PlayableBoundsStatus var _active_screen: Control var _active_overlay: Control var _vehicle_command_sequence := 0 @@ -105,7 +114,7 @@ func _ready() -> void: _show_main_menu() -func _start_vehicle_test() -> void: +func _start_dive() -> void: var rules = DiveRulesResolver.new().resolve( DiveDifficultyProfile.standard(), DiveUpgradeEffects.new(), @@ -117,12 +126,29 @@ func _start_vehicle_test() -> void: if not session.is_ready(): return - var world_scene := load(VEHICLE_TEST_WORLD_SCENE_PATH) as PackedScene + var definition := load(_selected_site_path) as DiveSiteDefinition + if definition == null: + push_error("dive site definition %s cannot be loaded" % _selected_site_path) + return + var definition_error := definition.validation_error() + if not definition_error.is_empty(): + push_error("dive site %s is invalid: %s" % [_selected_site_path, definition_error]) + return + var world_scene := load(DIVE_WORLD_SCENE_PATH) as PackedScene if world_scene == null: return var world := world_scene.instantiate() as Node3D _replace_active_world(world) + _dive_site = world.get_node("%DiveSite") as DiveSite + var site_error := _dive_site.configure(definition) + if not site_error.is_empty(): + push_error("dive site %s cannot be built: %s" % [_selected_site_path, site_error]) + _show_main_menu() + return _actor = world.get_node("%SubmarineActor") as SubmarineActor + _actor.global_transform = _dive_site.spawn_transform() + _bounds_rule = PlayableBoundsRule.new(definition.playable_bounds_m) + _bounds_status = _bounds_rule.evaluate(_actor.global_position) _camera_controller = world.get_node("%CockpitCameraRig") as CockpitCameraController _instrument_panel = world.get_node("%CockpitInterior") as CockpitInstrumentPanel _hud = COCKPIT_HUD_SCENE.instantiate() as CockpitHud @@ -154,6 +180,7 @@ func _start_vehicle_test() -> void: _runner.next_sequence() ) _hud.present(_latest_update, initial_telemetry) + _hud.present_bounds(_bounds_status) _instrument_panel.present(_latest_update, initial_telemetry) _camera_controller.present_telemetry(initial_telemetry) _input_adapter.set_gameplay_enabled(true) @@ -196,6 +223,8 @@ func _physics_process(delta: float) -> void: return_route ) _hud.present(_latest_update, telemetry) + _bounds_status = _bounds_rule.evaluate(_actor.global_position) + _hud.present_bounds(_bounds_status) _instrument_panel.present(_latest_update, telemetry) _sonar_service.advance(delta) _present_sonar() @@ -243,6 +272,18 @@ func sonar_display() -> SonarDisplay: return _sonar_display +func select_dive_site(definition_path: String) -> void: + _selected_site_path = definition_path + + +func dive_site() -> DiveSite: + return _dive_site + + +func latest_bounds_status() -> PlayableBoundsStatus: + return _bounds_status + + func _finish_dive_motion() -> void: _dive_terminal = true _input_adapter.set_gameplay_enabled(false) @@ -444,6 +485,9 @@ func _show_main_menu() -> void: _pilot_use_case = null _telemetry_adapter = null _actor = null + _dive_site = null + _bounds_rule = null + _bounds_status = null _camera_controller = null _instrument_panel = null _hud = null @@ -457,7 +501,7 @@ func _show_main_menu() -> void: _replace_active_world(null) var screen := MAIN_MENU_SCENE.instantiate() as MainMenuScreen _replace_active_screen(screen) - screen.new_game_requested.connect(_start_vehicle_test) + screen.new_game_requested.connect(_start_dive) screen.quit_requested.connect(_quit_game) diff --git a/game/presentation/cockpit/cockpit_hud.gd b/game/presentation/cockpit/cockpit_hud.gd index f79e3c0..747f533 100644 --- a/game/presentation/cockpit/cockpit_hud.gd +++ b/game/presentation/cockpit/cockpit_hud.gd @@ -5,9 +5,12 @@ const DiveUpdate = preload("res://game/domain/dive/dive_update.gd") const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd") const DiveTerminalReason = preload("res://game/domain/dive/dive_terminal_reason.gd") const VehicleTelemetry = preload("res://game/application/dive/vehicle_telemetry.gd") +const PlayableBoundsStatus = preload("res://game/application/sites/playable_bounds_status.gd") @onready var _alert_panel: PanelContainer = %AlertPanel @onready var _alert_label: Label = %AlertLabel +@onready var _bounds_panel: PanelContainer = %BoundsPanel +@onready var _bounds_label: Label = %BoundsLabel @onready var _ejection_panel: PanelContainer = %EjectionPanel @onready var _ejection_label: Label = %EjectionLabel @onready var _terminal_panel: PanelContainer = %TerminalPanel @@ -29,6 +32,21 @@ func present(update: DiveUpdate, telemetry: VehicleTelemetry) -> void: ] +func present_bounds(status: PlayableBoundsStatus) -> void: + if status == null or not status.requires_warning(): + _bounds_panel.visible = false + return + _bounds_panel.visible = true + if status.state == PlayableBoundsStatus.State.OUTSIDE: + _bounds_label.text = tr("HORS ZONE · FAITES DEMI-TOUR") + else: + _bounds_label.text = "%s · %d m" % [tr("LIMITE DE ZONE"), int(status.distance_to_edge_m)] + + +func bounds_warning_visible() -> bool: + return _bounds_panel.visible + + func alert_overlay_visible() -> bool: return _alert_panel.visible diff --git a/game/presentation/cockpit/cockpit_hud.tscn b/game/presentation/cockpit/cockpit_hud.tscn index ddc071b..c62e728 100644 --- a/game/presentation/cockpit/cockpit_hud.tscn +++ b/game/presentation/cockpit/cockpit_hud.tscn @@ -57,6 +57,27 @@ theme_override_font_sizes/font_size = 20 text = "ALERTE · ATTENTION" horizontal_alignment = 1 +[node name="BoundsPanel" type="PanelContainer" parent="."] +unique_name_in_owner = true +visible = false +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_left = 560.0 +offset_top = 88.0 +offset_right = -560.0 +offset_bottom = 136.0 +grow_horizontal = 2 +theme_override_styles/panel = SubResource("AlertStyle") + +[node name="BoundsLabel" type="Label" parent="BoundsPanel"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.82, 0.42, 1) +theme_override_font_sizes/font_size = 18 +text = "LIMITE DE ZONE" +horizontal_alignment = 1 + [node name="EjectionPanel" type="PanelContainer" parent="."] unique_name_in_owner = true visible = false diff --git a/game/world/sites/vehicle_test_environment.gd b/game/world/sites/vehicle_test_environment.gd deleted file mode 100644 index aeceefd..0000000 --- a/game/world/sites/vehicle_test_environment.gd +++ /dev/null @@ -1,153 +0,0 @@ -class_name VehicleTestEnvironment -extends Node3D - -const TERRAIN_COLUMNS := 61 -const TERRAIN_ROWS := 69 -const TERRAIN_WIDTH_M := 180.0 -const TERRAIN_LENGTH_M := 200.0 -const TERRAIN_NEAR_Z := 28.0 - - -func _ready() -> void: - _add_bathymetric_terrain() - _place_calibration_target() - _add_suspended_particles() - - -func _add_suspended_particles() -> void: - var particles := GPUParticles3D.new() - particles.name = "SuspendedParticles" - particles.position = Vector3(0.0, -34.0, -42.0) - particles.amount = 360 - particles.lifetime = 14.0 - particles.preprocess = 14.0 - particles.visibility_aabb = AABB( - Vector3(-50.0, -35.0, -78.0), - Vector3(100.0, 70.0, 156.0) - ) - - var motion := ParticleProcessMaterial.new() - motion.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_BOX - motion.emission_box_extents = Vector3(45.0, 28.0, 70.0) - motion.direction = Vector3(0.1, 0.04, 0.0) - motion.spread = 180.0 - motion.initial_velocity_min = 0.02 - motion.initial_velocity_max = 0.09 - motion.gravity = Vector3.ZERO - motion.scale_min = 0.025 - motion.scale_max = 0.08 - particles.process_material = motion - - var particle_mesh := SphereMesh.new() - particle_mesh.radius = 0.035 - particle_mesh.height = 0.07 - particles.draw_pass_1 = particle_mesh - add_child(particles) - - -func _add_bathymetric_terrain() -> void: - var surface := SurfaceTool.new() - surface.begin(Mesh.PRIMITIVE_TRIANGLES) - for row in range(TERRAIN_ROWS - 1): - for column in range(TERRAIN_COLUMNS - 1): - var top_left := _terrain_vertex(row, column) - var top_right := _terrain_vertex(row, column + 1) - var bottom_left := _terrain_vertex(row + 1, column) - var bottom_right := _terrain_vertex(row + 1, column + 1) - _add_terrain_triangle(surface, top_left, top_right, bottom_left) - _add_terrain_triangle(surface, top_right, bottom_right, bottom_left) - surface.generate_normals() - var mesh := surface.commit() - - var body := StaticBody3D.new() - body.name = "BathymetricTerrain" - body.set_meta("sonar_material", 1) - body.set_meta("sonar_reflectivity", 0.72) - var mesh_instance := MeshInstance3D.new() - mesh_instance.name = "Mesh" - mesh_instance.mesh = mesh - var material := StandardMaterial3D.new() - material.albedo_color = Color(0.075, 0.19, 0.19) - material.roughness = 0.92 - mesh_instance.material_override = material - body.add_child(mesh_instance) - var collision := CollisionShape3D.new() - collision.name = "Collision" - var height_map := HeightMapShape3D.new() - height_map.map_width = TERRAIN_COLUMNS - height_map.map_depth = TERRAIN_ROWS - var height_data := PackedFloat32Array() - height_data.resize(TERRAIN_COLUMNS * TERRAIN_ROWS) - for row in TERRAIN_ROWS: - for column in TERRAIN_COLUMNS: - var vertex := _terrain_vertex(row, column) - height_data[row * TERRAIN_COLUMNS + column] = vertex.y - height_map.map_data = height_data - collision.shape = height_map - collision.position = Vector3( - 0.0, - 0.0, - TERRAIN_NEAR_Z - TERRAIN_LENGTH_M * 0.5 - ) - collision.scale = Vector3( - TERRAIN_WIDTH_M / float(TERRAIN_COLUMNS - 1), - 1.0, - TERRAIN_LENGTH_M / float(TERRAIN_ROWS - 1) - ) - body.add_child(collision) - add_child(body) - - -func _terrain_vertex(row: int, column: int) -> Vector3: - var x := lerpf( - -TERRAIN_WIDTH_M * 0.5, - TERRAIN_WIDTH_M * 0.5, - float(column) / float(TERRAIN_COLUMNS - 1) - ) - var z := TERRAIN_NEAR_Z - ( - TERRAIN_LENGTH_M * float(row) / float(TERRAIN_ROWS - 1) - ) - return Vector3(x, _terrain_height(x, z), z) - - -func _terrain_height(x: float, z: float) -> float: - var distance_ratio := clampf( - (TERRAIN_NEAR_Z - z) / TERRAIN_LENGTH_M, - 0.0, - 1.0 - ) - var base := lerpf(-40.0, -78.0, distance_ratio) - var left_ridge := 13.0 * exp( - -(pow(x + 24.0, 2.0) / 210.0 + pow(z + 48.0, 2.0) / 980.0) - ) - var right_ridge := 15.0 * exp( - -(pow(x - 25.0, 2.0) / 250.0 + pow(z + 58.0, 2.0) / 1150.0) - ) - var channel := -8.5 * exp( - -(pow(x, 2.0) / 120.0 + pow(z + 66.0, 2.0) / 2500.0) - ) - var rear_rise := 8.0 * exp( - -(pow(x + 5.0, 2.0) / 900.0 + pow(z + 125.0, 2.0) / 620.0) - ) - var undulation := 1.8 * sin(x * 0.075) * cos(z * 0.048) - return base + left_ridge + right_ridge + channel + rear_rise + undulation - - -func _add_terrain_triangle( - surface: SurfaceTool, - a: Vector3, - b: Vector3, - c: Vector3 -) -> void: - surface.set_uv(Vector2(a.x, a.z) * 0.02) - surface.add_vertex(a) - surface.set_uv(Vector2(b.x, b.z) * 0.02) - surface.add_vertex(b) - surface.set_uv(Vector2(c.x, c.z) * 0.02) - surface.add_vertex(c) - - -func _place_calibration_target() -> void: - var target := get_node_or_null("SonarCalibrationTarget") as Node3D - if target != null: - target.position.y = _terrain_height(target.position.x, target.position.z) + 2.5 diff --git a/game/world/sites/vehicle_test_environment.tscn b/game/world/sites/vehicle_test_environment.tscn deleted file mode 100644 index ac53448..0000000 --- a/game/world/sites/vehicle_test_environment.tscn +++ /dev/null @@ -1,75 +0,0 @@ -[gd_scene load_steps=8 format=3] - -[ext_resource type="Script" path="res://game/world/sites/vehicle_test_environment.gd" id="1_environment"] - -[sub_resource type="Environment" id="EnvironmentUnderwater"] -background_mode = 1 -background_color = Color(0.005, 0.045, 0.065, 1) -ambient_light_source = 3 -ambient_light_color = Color(0.12, 0.32, 0.37, 1) -ambient_light_energy = 0.42 -fog_enabled = true -fog_light_color = Color(0.07, 0.25, 0.3, 1) -fog_light_energy = 0.48 -fog_density = 0.014 -fog_height = -4.0 -fog_height_density = 0.025 -volumetric_fog_enabled = true -volumetric_fog_density = 0.025 -volumetric_fog_length = 72.0 - -[sub_resource type="SphereMesh" id="BeaconMesh"] -radius = 0.55 -height = 1.1 - -[sub_resource type="StandardMaterial3D" id="MetalTargetMaterial"] -albedo_color = Color(0.24, 0.18, 0.08, 1) -metallic = 0.88 -roughness = 0.27 - -[sub_resource type="BoxMesh" id="MetalTargetMesh"] -size = Vector3(8, 5, 7) - -[sub_resource type="BoxShape3D" id="MetalTargetShape"] -size = Vector3(8, 5, 7) - -[sub_resource type="StandardMaterial3D" id="BeaconMaterial"] -shading_mode = 0 -albedo_color = Color(0.2, 0.78, 0.9, 1) -emission_enabled = true -emission = Color(0.2, 0.78, 0.9, 1) -emission_energy_multiplier = 5.0 - -[node name="VehicleTestEnvironment" type="Node3D"] -script = ExtResource("1_environment") - -[node name="WorldEnvironment" type="WorldEnvironment" parent="."] -environment = SubResource("EnvironmentUnderwater") - -[node name="AmbientDirection" type="DirectionalLight3D" parent="."] -rotation_degrees = Vector3(-52, -24, 0) -light_color = Color(0.39, 0.65, 0.7, 1) -light_energy = 0.38 -shadow_enabled = true - -[node name="NavigationBeacon" type="MeshInstance3D" parent="."] -position = Vector3(0, -15, -22) -mesh = SubResource("BeaconMesh") -material_override = SubResource("BeaconMaterial") - -[node name="BeaconLight" type="OmniLight3D" parent="NavigationBeacon"] -omni_range = 12.0 -light_color = Color(0.2, 0.78, 0.9, 1) -light_energy = 4.0 - -[node name="SonarCalibrationTarget" type="StaticBody3D" parent="."] -position = Vector3(0, -60, -58) -metadata/sonar_material = 2 -metadata/sonar_reflectivity = 1.0 - -[node name="Mesh" type="MeshInstance3D" parent="SonarCalibrationTarget"] -mesh = SubResource("MetalTargetMesh") -material_override = SubResource("MetalTargetMaterial") - -[node name="Collision" type="CollisionShape3D" parent="SonarCalibrationTarget"] -shape = SubResource("MetalTargetShape") diff --git a/tests/integration/run_vertical_slice_foundation.gd b/tests/integration/run_vertical_slice_foundation.gd index adbf33b..e1a5dae 100644 --- a/tests/integration/run_vertical_slice_foundation.gd +++ b/tests/integration/run_vertical_slice_foundation.gd @@ -5,6 +5,7 @@ const VerticalSliceRoot = preload( ) const MainMenuScreen = preload("res://game/presentation/main_menu/main_menu_screen.gd") const CockpitHud = preload("res://game/presentation/cockpit/cockpit_hud.gd") +const PlayableBoundsStatus = preload("res://game/application/sites/playable_bounds_status.gd") const CockpitDetailing = preload( "res://game/presentation/cockpit/cockpit_detailing.gd" ) @@ -19,7 +20,7 @@ const ProductionInputAdapter = preload( "res://game/infrastructure/input/production_input_adapter.gd" ) -const TEST_COUNT := 78 +const TEST_COUNT := 83 var _failures: Array[String] = [] @@ -75,6 +76,7 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void: var continue_button := menu.get_node("%ContinueButton") as Button _expect(continue_button.disabled, "continue is disabled without save") var new_game_button := menu.get_node("%NewGameButton") as Button + composition.select_dive_site("res://content/sites/vehicle_test.tres") new_game_button.pressed.emit() await process_frame await physics_frame @@ -110,6 +112,22 @@ func _test_vehicle_slice(composition: VerticalSliceRoot) -> void: _expect(sonar_service != null, "production sonar service is assembled") _expect(sonar_display != null, "shared sonar presentation is assembled") _expect(composition.latest_update() != null, "dive session starts with a typed update") + _expect(composition.dive_site() != null, "composition instantiates the selected dive site") + _expect( + composition.dive_site() != null + and composition.dive_site().definition().site_id == &"vehicle_test", + "selected site definition drives the world" + ) + _expect( + actor != null and actor.global_position.is_equal_approx(Vector3(0.0, -12.0, 12.0)), + "submarine spawns at the site spawn point" + ) + _expect( + composition.latest_bounds_status() != null + and composition.latest_bounds_status().state == PlayableBoundsStatus.State.INSIDE, + "spawn is inside the playable bounds" + ) + _expect(hud != null and not hud.bounds_warning_visible(), "no bounds warning inside the zone") if ( actor == null or camera == null diff --git a/tools/sites/vehicle_test_terrain_recipe.gd b/tools/sites/vehicle_test_terrain_recipe.gd index b207867..a13a716 100644 --- a/tools/sites/vehicle_test_terrain_recipe.gd +++ b/tools/sites/vehicle_test_terrain_recipe.gd @@ -11,7 +11,7 @@ const SitePlacement = preload("res://game/domain/sites/site_placement.gd") static func recipe() -> SiteTerrainBaker.Recipe: var result := SiteTerrainBaker.Recipe.new() result.columns = 61 - result.rows = 69 + result.rows = 83 result.cell_size_m = 3.0 result.origin_x_m = -90.0 result.origin_z_m = -176.0 @@ -35,7 +35,7 @@ static func definition(terrain: SiteTerrainData) -> DiveSiteDefinition: result.site_id = &"vehicle_test" result.display_name = "Site de calibration du véhicule" result.terrain = terrain - result.playable_bounds_m = Rect2(-90.0, -176.0, 180.0, 204.0) + result.playable_bounds_m = Rect2(-90.0, -176.0, 180.0, 246.0) result.spawn_position_m = Vector3(0.0, -12.0, 12.0) result.spawn_yaw_radians = 0.0 var target := SitePlacement.new()