Files
Ransom-s-wake/game/composition/vertical_slice/vertical_slice_root.gd
OlivierandClaude Opus 5.5 682e1f18e5 perf(sites): validate the site once and hand collision the field array
DiveSite.configure() builds the height field once (TerrainHeightField
validates the data) instead of running the definition validation and
then the field construction again; the composition root relies on the
error returned by configure(). The collision shape takes the field's
row-major elevations directly instead of copying 263k cells one by one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:52:04 +02:00

534 lines
17 KiB
GDScript

class_name VerticalSliceRoot
extends Node
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 DiveSession = preload("res://game/domain/dive/dive_session.gd")
const DiveTickRunner = preload("res://game/application/dive/dive_tick_runner.gd")
const DiveUpdate = preload("res://game/domain/dive/dive_update.gd")
const CollisionObservation = preload("res://game/domain/dive/collision_observation.gd")
const EjectionIntent = preload("res://game/domain/dive/ejection_intent.gd")
const ReturnRouteSample = preload("res://game/domain/dive/return_route_sample.gd")
const VehicleCommand = preload("res://game/application/vehicle/vehicle_command.gd")
const VehicleCollisionSample = preload(
"res://game/application/vehicle/vehicle_collision_sample.gd"
)
const PilotUseCase = preload("res://game/application/vehicle/pilot_use_case.gd")
const SonarService = preload("res://game/application/sonar/sonar_service.gd")
const SubmarineActor = preload("res://game/world/submarine/submarine_actor.gd")
const SubmarineMotionAdapter = preload(
"res://game/world/submarine/submarine_motion_adapter.gd"
)
const SubmarineTelemetryAdapter = preload(
"res://game/world/submarine/submarine_telemetry_adapter.gd"
)
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"
)
const CockpitCameraController = preload(
"res://game/presentation/cockpit/cockpit_camera_controller.gd"
)
const CockpitInstrumentPanel = preload(
"res://game/presentation/cockpit/cockpit_instrument_panel.gd"
)
const CockpitHud = preload("res://game/presentation/cockpit/cockpit_hud.gd")
const SonarDisplay = preload("res://game/presentation/sonar/sonar_display.gd")
const MainMenuScreen = preload("res://game/presentation/main_menu/main_menu_screen.gd")
const PauseMenu = preload("res://game/presentation/pause/pause_menu.gd")
const ControlsSettingsScreen = preload(
"res://game/presentation/settings/controls_settings_screen.gd"
)
const ConfirmationDialogScreen = preload(
"res://game/presentation/confirmation/confirmation_dialog.gd"
)
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 DIVE_WORLD_SCENE_PATH := "res://game/composition/vertical_slice/dive_world.tscn"
const DEFAULT_SITE_PATH := "res://content/sites/zone1_vigilant.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"
)
const PAUSE_MENU_SCENE = preload("res://game/presentation/pause/pause_menu.tscn")
const CONTROLS_SETTINGS_SCENE = preload(
"res://game/presentation/settings/controls_settings_screen.tscn"
)
const CONFIRMATION_SCENE = preload(
"res://game/presentation/confirmation/confirmation_dialog.tscn"
)
const DIVE_SESSION_ID := &"vertical_slice_vehicle_test"
const CONFIRM_ABANDON := &"abandon_dive"
const CONFIRM_MAIN_MENU := &"return_main_menu"
const CONFIRM_QUIT := &"quit_game"
@onready var _world_content: Node3D = %WorldContent
@onready var _content: Control = %Content
@onready var _overlay_content: Control = %OverlayContent
@onready var _input_adapter: ProductionInputAdapter = %ProductionInputAdapter
var _runner: DiveTickRunner
var _pilot_use_case: PilotUseCase
var _telemetry_adapter: SubmarineTelemetryAdapter
var _actor: SubmarineActor
var _camera_controller: CockpitCameraController
var _instrument_panel: CockpitInstrumentPanel
var _hud: CockpitHud
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
var _pending_collision_samples: Array[VehicleCollisionSample] = []
var _dive_start_position := Vector3.ZERO
var _dive_start_depth_m := 0.0
var _confirmed_ejection_pending := false
var _dive_terminal := false
func _ready() -> void:
_input_adapter.pause_requested.connect(_on_pause_requested)
_input_adapter.rebind_result_available.connect(_on_rebind_result_available)
_input_adapter.sonar_scan_requested.connect(_on_sonar_scan_requested)
_input_adapter.sonar_view_toggle_requested.connect(_on_sonar_view_toggle_requested)
_show_main_menu()
func _start_dive() -> void:
var rules = DiveRulesResolver.new().resolve(
DiveDifficultyProfile.standard(),
DiveUpgradeEffects.new(),
DiveAccessibilityAdjustments.new()
)
if rules == null:
return
var session := DiveSession.new(DIVE_SESSION_ID, rules)
if not session.is_ready():
return
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 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
_replace_active_screen(_hud)
_sonar_display = SONAR_DISPLAY_SCENE.instantiate() as SonarDisplay
_hud.add_child(_sonar_display)
_instrument_panel.attach_sonar_texture(_sonar_display.sonar_texture())
var motion_adapter := SubmarineMotionAdapter.new(_actor)
_pilot_use_case = PilotUseCase.new(motion_adapter)
_telemetry_adapter = SubmarineTelemetryAdapter.new(_actor)
var sonar_acquisition := ReferenceBathymetryAcquisition.new(
_actor.get_world_3d(),
_actor
)
_sonar_service = SonarService.new(sonar_acquisition, DIVE_SESSION_ID)
_runner = DiveTickRunner.new(session)
_latest_update = _runner.initial_update()
_vehicle_command_sequence = 0
_pending_collision_samples.clear()
_confirmed_ejection_pending = false
_dive_terminal = false
_dive_start_position = _actor.global_position
_dive_start_depth_m = _actor.current_depth_m()
_actor.collision_sampled.connect(_on_collision_sampled)
var initial_telemetry = _telemetry_adapter.sample(
DIVE_SESSION_ID,
_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)
set_physics_process(true)
func _physics_process(delta: float) -> void:
if _runner == null or _actor == null or _dive_terminal:
return
var command: VehicleCommand = _input_adapter.sample_vehicle_command(
_vehicle_command_sequence
)
if _latest_update.snapshot.battery.remaining_kwh <= 0.0:
command = VehicleCommand.neutral(_vehicle_command_sequence)
var pilot_result = _pilot_use_case.execute(command)
if pilot_result.accepted:
_vehicle_command_sequence += 1
_camera_controller.apply_look(_input_adapter.take_camera_look_command())
var telemetry = _telemetry_adapter.sample(DIVE_SESSION_ID, _runner.next_sequence())
_camera_controller.present_telemetry(telemetry)
var collisions := _consume_collision_observations(_runner.next_sequence())
var ejection := EjectionIntent.new(
_input_adapter.ejection_active(),
_confirmed_ejection_pending
)
_confirmed_ejection_pending = false
var return_route := ReturnRouteSample.new(
true,
_actor.global_position.distance_to(_dive_start_position),
maxf(_actor.current_depth_m() - _dive_start_depth_m, 0.0)
)
_latest_update = _runner.advance(
telemetry,
delta,
collisions,
[],
ejection,
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()
if _latest_update.snapshot.is_terminal():
_finish_dive_motion()
func latest_update() -> DiveUpdate:
return _latest_update
func active_screen() -> Control:
return _active_screen
func active_world() -> Node3D:
return _active_world
func submarine_actor() -> SubmarineActor:
return _actor
func camera_controller() -> CockpitCameraController:
return _camera_controller
func cockpit_instrument_panel() -> CockpitInstrumentPanel:
return _instrument_panel
func input_adapter() -> ProductionInputAdapter:
return _input_adapter
func active_overlay() -> Control:
return _active_overlay
func sonar_service() -> SonarService:
return _sonar_service
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)
var neutral: VehicleCommand = VehicleCommand.neutral(_vehicle_command_sequence)
if _pilot_use_case.execute(neutral).accepted:
_vehicle_command_sequence += 1
func _consume_collision_observations(
tick_sequence: int
) -> Array[CollisionObservation]:
var observations: Array[CollisionObservation] = []
for sample: VehicleCollisionSample in _pending_collision_samples:
observations.append(
CollisionObservation.new(
sample.sample_id,
sample.sequence,
tick_sequence,
sample.normal_impact_speed_mps
)
)
_pending_collision_samples.clear()
return observations
func _on_collision_sampled(sample: VehicleCollisionSample) -> void:
_pending_collision_samples.append(sample)
func _on_sonar_scan_requested() -> void:
if _sonar_service == null or _actor == null or _dive_terminal:
return
var result = _sonar_service.request_scan(_sonar_sensor_transform())
if result.accepted:
_present_sonar()
func _on_sonar_view_toggle_requested() -> void:
if _sonar_display != null and not _dive_terminal:
_sonar_display.toggle_expanded()
func _present_sonar() -> void:
if _sonar_service == null or _sonar_display == null:
return
_sonar_display.present(
_sonar_service.current_scan(),
_sonar_service.previous_scan(),
_sonar_service.visible_row_count(),
_sonar_service.previous_scan_opacity()
)
func _sonar_sensor_transform() -> Transform3D:
var world_basis := _actor.global_basis
var contract_basis := Basis(
world_basis.x,
world_basis.y,
-world_basis.z
)
var sensor_origin := _actor.global_transform * Vector3(0.0, -0.75, -2.2)
return Transform3D(contract_basis, sensor_origin)
func _on_pause_requested() -> void:
if _runner == null:
return
if not get_tree().paused:
_open_pause_menu()
elif _active_overlay is ConfirmationDialogScreen:
_show_pause_menu_overlay()
elif _active_overlay is ControlsSettingsScreen:
_show_pause_menu_overlay()
else:
_resume_dive()
func _open_pause_menu() -> void:
get_tree().paused = true
_input_adapter.set_gameplay_enabled(false)
_show_pause_menu_overlay()
func _show_pause_menu_overlay() -> void:
var menu := PAUSE_MENU_SCENE.instantiate() as PauseMenu
_replace_overlay(menu)
menu.resume_requested.connect(_resume_dive)
menu.controls_requested.connect(_show_controls_overlay)
menu.abandon_requested.connect(_confirm_abandon)
menu.main_menu_requested.connect(_confirm_main_menu)
menu.quit_requested.connect(_confirm_quit)
func _resume_dive() -> void:
_replace_overlay(null)
get_tree().paused = false
_input_adapter.set_gameplay_enabled(not _dive_terminal)
func _show_controls_overlay() -> void:
var screen := CONTROLS_SETTINGS_SCENE.instantiate() as ControlsSettingsScreen
_replace_overlay(screen)
screen.present_bindings(_input_adapter.binding_snapshots())
screen.rebind_requested.connect(_on_rebind_requested)
screen.restore_defaults_requested.connect(_on_restore_defaults_requested)
screen.conflict_resolution_requested.connect(_on_conflict_resolution_requested)
screen.back_requested.connect(_show_pause_menu_overlay)
func _on_rebind_requested(action_id: StringName) -> void:
var result: RebindResult = _input_adapter.begin_rebind(action_id)
var screen := _active_overlay as ControlsSettingsScreen
if screen != null:
screen.present_rebind_result(result)
screen.show_waiting_for_key()
func _on_rebind_result_available(result: RebindResult) -> void:
var screen := _active_overlay as ControlsSettingsScreen
if screen == null:
return
screen.present_rebind_result(result)
if result.accepted:
screen.present_bindings(_input_adapter.binding_snapshots())
func _on_conflict_resolution_requested(swap_bindings: bool) -> void:
var result: RebindResult = _input_adapter.resolve_pending_conflict(swap_bindings)
var screen := _active_overlay as ControlsSettingsScreen
if screen != null:
screen.present_rebind_result(result)
screen.present_bindings(_input_adapter.binding_snapshots())
func _on_restore_defaults_requested() -> void:
_input_adapter.restore_default_bindings()
var screen := _active_overlay as ControlsSettingsScreen
if screen != null:
screen.present_bindings(_input_adapter.binding_snapshots())
func _confirm_abandon() -> void:
_show_confirmation(
CONFIRM_ABANDON,
"Abandonner la plongée ?",
"Vous serez éjecté puis évacué. Le sous-marin et sa cargaison non sécurisée resteront au fond. Une franchise sera appliquée.",
"S'ÉJECTER ET ABANDONNER"
)
func _confirm_main_menu() -> void:
_show_confirmation(
CONFIRM_MAIN_MENU,
"Retourner au menu principal ?",
"La plongée actuelle sera interrompue et aucune progression sous-marine ne sera sauvegardée.",
"RETOURNER AU MENU"
)
func _confirm_quit() -> void:
_show_confirmation(
CONFIRM_QUIT,
"Quitter le jeu ?",
"La plongée actuelle sera interrompue.",
"QUITTER LE JEU"
)
func _show_confirmation(
action_id: StringName,
title: String,
message: String,
confirm_label: String
) -> void:
var dialog := CONFIRMATION_SCENE.instantiate() as ConfirmationDialogScreen
_replace_overlay(dialog)
dialog.configure(action_id, title, message, confirm_label)
dialog.cancelled.connect(_show_pause_menu_overlay)
dialog.confirmed.connect(_on_confirmation_accepted)
func _on_confirmation_accepted(action_id: StringName) -> void:
match action_id:
CONFIRM_ABANDON:
_confirmed_ejection_pending = true
_replace_overlay(null)
get_tree().paused = false
_input_adapter.set_gameplay_enabled(true)
CONFIRM_MAIN_MENU:
_show_main_menu()
CONFIRM_QUIT:
_quit_game()
func _show_main_menu() -> void:
get_tree().paused = false
_input_adapter.set_gameplay_enabled(false)
_runner = null
_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
_sonar_service = null
_sonar_display = null
_latest_update = null
_pending_collision_samples.clear()
_dive_terminal = false
set_physics_process(false)
_replace_overlay(null)
_replace_active_world(null)
var screen := MAIN_MENU_SCENE.instantiate() as MainMenuScreen
_replace_active_screen(screen)
screen.new_game_requested.connect(_start_dive)
screen.quit_requested.connect(_quit_game)
func _replace_active_world(next_world: Node3D) -> void:
if _active_world != null:
_world_content.remove_child(_active_world)
_active_world.queue_free()
_active_world = next_world
if _active_world != null:
_world_content.add_child(_active_world)
func _replace_active_screen(next_screen: Control) -> void:
if _active_screen != null:
_content.remove_child(_active_screen)
_active_screen.queue_free()
_active_screen = next_screen
if _active_screen != null:
_content.add_child(_active_screen)
func _replace_overlay(next_overlay: Control) -> void:
if _active_overlay != null:
_overlay_content.remove_child(_active_overlay)
_active_overlay.queue_free()
_active_overlay = next_overlay
if _active_overlay != null:
_overlay_content.add_child(_active_overlay)
func _quit_game() -> void:
get_tree().paused = false
get_tree().quit()