Implement production vertical slice foundation

This commit is contained in:
chindit committed 2026-10-02 15:27:48 +02:00
1 parent 9d4a89f355
commit a18e82eb44
104 files changed
+10595 -268

No files matched your search

+75
View File
@@ -0,0 +1,75 @@
class_name DiveTickRunner
extends RefCounted
const VehicleTelemetry = preload("res://game/application/dive/vehicle_telemetry.gd")
const DiveSession = preload("res://game/domain/dive/dive_session.gd")
const DiveTick = preload("res://game/domain/dive/dive_tick.gd")
const DiveTelemetrySample = preload("res://game/domain/dive/dive_telemetry_sample.gd")
const CollisionObservation = preload("res://game/domain/dive/collision_observation.gd")
const ToolPowerDemand = preload("res://game/domain/dive/tool_power_demand.gd")
const EjectionIntent = preload("res://game/domain/dive/ejection_intent.gd")
const ReturnRouteSample = preload("res://game/domain/dive/return_route_sample.gd")
const DiveStep = preload("res://game/domain/dive/dive_step.gd")
const DiveUpdate = preload("res://game/domain/dive/dive_update.gd")
var _session: DiveSession
var _next_sequence := 0
func _init(session: DiveSession) -> void:
_session = session
func next_sequence() -> int:
return _next_sequence
func initial_update() -> DiveUpdate:
return DiveUpdate.initial(_session.current_snapshot())
func advance(
telemetry: VehicleTelemetry,
elapsed_seconds: float,
collisions: Array[CollisionObservation] = [],
tool_power_demands: Array[ToolPowerDemand] = [],
ejection_intent: EjectionIntent = null,
return_route: ReturnRouteSample = null
) -> DiveUpdate:
if telemetry == null:
return DiveUpdate.rejected(
_next_sequence,
"vehicle telemetry is missing",
_session.current_snapshot()
)
var telemetry_error: String = telemetry.validation_error(
_session.session_id,
_next_sequence
)
if not telemetry_error.is_empty():
return DiveUpdate.rejected(
_next_sequence,
telemetry_error,
_session.current_snapshot()
)
var tick := DiveTick.new(_session.session_id, _next_sequence, elapsed_seconds)
var sample := DiveTelemetrySample.new(
telemetry.depth_m,
telemetry.longitudinal_effort,
telemetry.steering_effort,
telemetry.vertical_effort,
telemetry.high_thrust_active
)
var step := DiveStep.new(
tick,
sample,
collisions,
tool_power_demands,
ejection_intent,
return_route
)
var update: DiveUpdate = _session.advance(step)
if update.accepted:
_next_sequence += 1
return update
@@ -0,0 +1,74 @@
class_name VehicleTelemetry
extends RefCounted
var session_id: StringName
var sequence: int
var world_position_m: Vector3
var world_orientation: Quaternion
var world_linear_velocity_mps: Vector3
var local_angular_velocity_rps: Vector3
var depth_m: float
var longitudinal_effort: float
var steering_effort: float
var vertical_effort: float
var high_thrust_active: bool
func _init(
p_session_id: StringName,
p_sequence: int,
p_world_position_m: Vector3,
p_world_orientation: Quaternion,
p_world_linear_velocity_mps: Vector3,
p_local_angular_velocity_rps: Vector3,
p_depth_m: float,
p_longitudinal_effort: float,
p_steering_effort: float,
p_vertical_effort: float,
p_high_thrust_active: bool
) -> void:
session_id = p_session_id
sequence = p_sequence
world_position_m = p_world_position_m
world_orientation = p_world_orientation
world_linear_velocity_mps = p_world_linear_velocity_mps
local_angular_velocity_rps = p_local_angular_velocity_rps
depth_m = p_depth_m
longitudinal_effort = p_longitudinal_effort
steering_effort = p_steering_effort
vertical_effort = p_vertical_effort
high_thrust_active = p_high_thrust_active
func validation_error(expected_session_id: StringName, expected_sequence: int) -> String:
if session_id != expected_session_id:
return "telemetry session_id does not match"
if sequence != expected_sequence:
return "telemetry sequence does not match expected tick"
if not _vector_is_finite(world_position_m):
return "telemetry world position is not finite"
if not _quaternion_is_finite(world_orientation):
return "telemetry orientation is not finite"
if not _vector_is_finite(world_linear_velocity_mps):
return "telemetry linear velocity is not finite"
if not _vector_is_finite(local_angular_velocity_rps):
return "telemetry angular velocity is not finite"
if not is_finite(depth_m) or depth_m < 0.0:
return "telemetry depth must be finite and non-negative"
for effort: float in [longitudinal_effort, steering_effort, vertical_effort]:
if not is_finite(effort) or effort < -1.0 or effort > 1.0:
return "telemetry efforts must be between -1 and 1"
return ""
func _vector_is_finite(value: Vector3) -> bool:
return is_finite(value.x) and is_finite(value.y) and is_finite(value.z)
func _quaternion_is_finite(value: Quaternion) -> bool:
return (
is_finite(value.x)
and is_finite(value.y)
and is_finite(value.z)
and is_finite(value.w)
)
@@ -0,0 +1,12 @@
class_name InputBindingSnapshot
extends RefCounted
var action_id: StringName
var label: String
var binding_text: String
func _init(p_action_id: StringName, p_label: String, p_binding_text: String) -> void:
action_id = p_action_id
label = p_label
binding_text = p_binding_text
+22
View File
@@ -0,0 +1,22 @@
class_name RebindResult
extends RefCounted
var accepted: bool
var action_id: StringName
var binding_text: String
var conflicting_action_id: StringName
var message: String
func _init(
p_accepted: bool,
p_action_id: StringName,
p_binding_text: String = "",
p_conflicting_action_id: StringName = &"",
p_message: String = ""
) -> void:
accepted = p_accepted
action_id = p_action_id
binding_text = p_binding_text
conflicting_action_id = p_conflicting_action_id
message = p_message
@@ -0,0 +1,8 @@
class_name VehicleMotionPort
extends RefCounted
const VehicleCommand = preload("res://game/application/vehicle/vehicle_command.gd")
func apply_command(_command: VehicleCommand) -> void:
pass
@@ -0,0 +1,22 @@
class_name CameraLookCommand
extends RefCounted
var yaw_delta_radians: float
var pitch_delta_radians: float
var recenter_requested: bool
func _init(
p_yaw_delta_radians: float = 0.0,
p_pitch_delta_radians: float = 0.0,
p_recenter_requested: bool = false
) -> void:
yaw_delta_radians = p_yaw_delta_radians
pitch_delta_radians = p_pitch_delta_radians
recenter_requested = p_recenter_requested
func validation_error() -> String:
if not is_finite(yaw_delta_radians) or not is_finite(pitch_delta_radians):
return "camera look deltas must be finite"
return ""
@@ -0,0 +1,32 @@
class_name PilotUseCase
extends RefCounted
const VehicleCommand = preload("res://game/application/vehicle/vehicle_command.gd")
const VehicleCommandResult = preload(
"res://game/application/vehicle/vehicle_command_result.gd"
)
const VehicleMotionPort = preload("res://game/application/interfaces/vehicle_motion_port.gd")
var _motion_port: VehicleMotionPort
var _last_sequence := -1
func _init(motion_port: VehicleMotionPort) -> void:
_motion_port = motion_port
func execute(command: VehicleCommand) -> VehicleCommandResult:
if command == null:
return VehicleCommandResult.new(false, -1, "vehicle command is missing")
var error: String = command.validation_error()
if not error.is_empty():
return VehicleCommandResult.new(false, command.sequence, error)
if command.sequence != _last_sequence + 1:
return VehicleCommandResult.new(
false,
command.sequence,
"vehicle command sequence is not consecutive"
)
_motion_port.apply_command(command)
_last_sequence = command.sequence
return VehicleCommandResult.new(true, command.sequence)
@@ -0,0 +1,16 @@
class_name VehicleCollisionSample
extends RefCounted
var sample_id: StringName
var sequence: int
var normal_impact_speed_mps: float
func _init(
p_sample_id: StringName,
p_sequence: int,
p_normal_impact_speed_mps: float
) -> void:
sample_id = p_sample_id
sequence = p_sequence
normal_impact_speed_mps = p_normal_impact_speed_mps
@@ -0,0 +1,35 @@
class_name VehicleCommand
extends RefCounted
var sequence: int
var longitudinal: float
var steering: float
var vertical: float
var high_thrust_active: bool
func _init(
p_sequence: int,
p_longitudinal: float,
p_steering: float,
p_vertical: float,
p_high_thrust_active: bool
) -> void:
sequence = p_sequence
longitudinal = p_longitudinal
steering = p_steering
vertical = p_vertical
high_thrust_active = p_high_thrust_active
func validation_error() -> String:
if sequence < 0:
return "vehicle command sequence is negative"
for value: float in [longitudinal, steering, vertical]:
if not is_finite(value) or value < -1.0 or value > 1.0:
return "vehicle command axes must be finite and normalized"
return ""
static func neutral(p_sequence: int) -> VehicleCommand:
return new(p_sequence, 0.0, 0.0, 0.0, false)
@@ -0,0 +1,12 @@
class_name VehicleCommandResult
extends RefCounted
var accepted: bool
var sequence: int
var rejection_reason: String
func _init(p_accepted: bool, p_sequence: int, p_rejection_reason: String = "") -> void:
accepted = p_accepted
sequence = p_sequence
rejection_reason = p_rejection_reason
@@ -0,0 +1,20 @@
class_name DiagnosticTelemetrySource
extends RefCounted
const VehicleTelemetry = preload("res://game/application/dive/vehicle_telemetry.gd")
func sample(session_id: StringName, sequence: int) -> VehicleTelemetry:
return VehicleTelemetry.new(
session_id,
sequence,
Vector3(0.0, -24.0, 0.0),
Quaternion.IDENTITY,
Vector3(0.0, 0.0, -1.5),
Vector3.ZERO,
24.0,
0.25,
0.0,
0.0,
false
)
@@ -0,0 +1,20 @@
[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
@@ -0,0 +1,426 @@
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 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 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 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 VEHICLE_TEST_WORLD_SCENE_PATH := (
"res://game/composition/vertical_slice/vehicle_test_world.tscn"
)
const COCKPIT_HUD_SCENE = preload("res://game/presentation/cockpit/cockpit_hud.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 _latest_update: DiveUpdate
var _active_world: Node3D
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)
_show_main_menu()
func _start_vehicle_test() -> 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 world_scene := load(VEHICLE_TEST_WORLD_SCENE_PATH) as PackedScene
if world_scene == null:
return
var world := world_scene.instantiate() as Node3D
_replace_active_world(world)
_actor = world.get_node("%SubmarineActor") as SubmarineActor
_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)
var motion_adapter := SubmarineMotionAdapter.new(_actor)
_pilot_use_case = PilotUseCase.new(motion_adapter)
_telemetry_adapter = SubmarineTelemetryAdapter.new(_actor)
_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)
_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)
_instrument_panel.present(_latest_update, telemetry)
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 _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_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
_camera_controller = null
_instrument_panel = null
_hud = 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_vehicle_test)
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()
@@ -0,0 +1,35 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="Script" path="res://game/composition/vertical_slice/vertical_slice_root.gd" id="1_root"]
[ext_resource type="Script" path="res://game/infrastructure/input/production_input_adapter.gd" id="2_input"]
[node name="VerticalSliceRoot" type="Node"]
script = ExtResource("1_root")
[node name="WorldContent" type="Node3D" parent="."]
unique_name_in_owner = true
[node name="Content" type="Control" parent="."]
unique_name_in_owner = true
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="OverlayContent" type="Control" parent="."]
unique_name_in_owner = true
process_mode = 3
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
[node name="ProductionInputAdapter" type="Node" parent="."]
unique_name_in_owner = true
process_mode = 3
script = ExtResource("2_input")
+22
View File
@@ -0,0 +1,22 @@
class_name BatterySnapshot
extends RefCounted
var remaining_kwh: float
var maximum_kwh: float
var ratio: float
var current_power_kw: float
var endurance_minutes: float
func _init(
p_remaining_kwh: float,
p_maximum_kwh: float,
p_current_power_kw: float
) -> void:
remaining_kwh = p_remaining_kwh
maximum_kwh = p_maximum_kwh
ratio = remaining_kwh / maximum_kwh if maximum_kwh > 0.0 else 0.0
current_power_kw = p_current_power_kw
endurance_minutes = (
remaining_kwh / current_power_kw * 60.0 if current_power_kw > 0.0 else INF
)
+31
View File
@@ -0,0 +1,31 @@
class_name CollisionObservation
extends RefCounted
var observation_id: StringName
var sequence: int
var tick_sequence: int
var normal_impact_speed_mps: float
func _init(
p_observation_id: StringName,
p_sequence: int,
p_tick_sequence: int,
p_normal_impact_speed_mps: float
) -> void:
observation_id = p_observation_id
sequence = p_sequence
tick_sequence = p_tick_sequence
normal_impact_speed_mps = p_normal_impact_speed_mps
func validation_error(expected_tick_sequence: int) -> String:
if observation_id.is_empty():
return "collision observation_id is empty"
if sequence < 0:
return "collision sequence is negative"
if tick_sequence != expected_tick_sequence:
return "collision tick sequence does not match DiveTick"
if not is_finite(normal_impact_speed_mps) or normal_impact_speed_mps < 0.0:
return "collision speed must be finite and non-negative"
return ""
+19
View File
@@ -0,0 +1,19 @@
class_name DepthSnapshot
extends RefCounted
var current_m: float
var certified_m: float
var critical_m: float
var crush_m: float
func _init(
p_current_m: float,
p_certified_m: float,
p_critical_m: float,
p_crush_m: float
) -> void:
current_m = p_current_m
certified_m = p_certified_m
critical_m = p_critical_m
crush_m = p_crush_m
+18
View File
@@ -0,0 +1,18 @@
class_name DiveAlertLevel
extends RefCounted
enum Value {
NORMAL,
ATTENTION,
RETURN,
CRITICAL,
FAILURE,
}
static func highest(levels: Array[Value]) -> Value:
var result := Value.NORMAL
for level: Value in levels:
if level > result:
result = level
return result
+23
View File
@@ -0,0 +1,23 @@
class_name DiveAlertState
extends RefCounted
const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd")
var oxygen: DiveAlertLevel.Value
var battery: DiveAlertLevel.Value
var hull: DiveAlertLevel.Value
var depth: DiveAlertLevel.Value
var maximum: DiveAlertLevel.Value
func _init(
p_oxygen: DiveAlertLevel.Value,
p_battery: DiveAlertLevel.Value,
p_hull: DiveAlertLevel.Value,
p_depth: DiveAlertLevel.Value
) -> void:
oxygen = p_oxygen
battery = p_battery
hull = p_hull
depth = p_depth
maximum = DiveAlertLevel.highest([oxygen, battery, hull, depth])
+101
View File
@@ -0,0 +1,101 @@
class_name DiveEvent
extends RefCounted
const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd")
const DiveTerminalReason = preload("res://game/domain/dive/dive_terminal_reason.gd")
enum Kind {
ALERT_LEVEL_CHANGED,
RESOURCE_DEPLETED,
HULL_DAMAGED,
DIVE_TERMINATED,
}
var event_id: StringName
var kind: Kind
var tick_sequence: int
var subject: StringName
var amount: float
var previous_level: DiveAlertLevel.Value
var current_level: DiveAlertLevel.Value
var terminal_reason: DiveTerminalReason.Value
func _init(
p_event_id: StringName,
p_kind: Kind,
p_tick_sequence: int,
p_subject: StringName = &"",
p_amount: float = 0.0,
p_previous_level: DiveAlertLevel.Value = DiveAlertLevel.Value.NORMAL,
p_current_level: DiveAlertLevel.Value = DiveAlertLevel.Value.NORMAL,
p_terminal_reason: DiveTerminalReason.Value = DiveTerminalReason.Value.NONE
) -> void:
event_id = p_event_id
kind = p_kind
tick_sequence = p_tick_sequence
subject = p_subject
amount = p_amount
previous_level = p_previous_level
current_level = p_current_level
terminal_reason = p_terminal_reason
static func alert_changed(
tick_sequence: int,
subject: StringName,
previous_level: DiveAlertLevel.Value,
current_level: DiveAlertLevel.Value
):
return new(
_event_id(tick_sequence, &"alert", subject),
Kind.ALERT_LEVEL_CHANGED,
tick_sequence,
subject,
0.0,
previous_level,
current_level
)
static func resource_depleted(tick_sequence: int, subject: StringName):
return new(
_event_id(tick_sequence, &"depleted", subject),
Kind.RESOURCE_DEPLETED,
tick_sequence,
subject
)
static func hull_damaged(tick_sequence: int, amount: float):
return new(
_event_id(tick_sequence, &"damage", &"hull"),
Kind.HULL_DAMAGED,
tick_sequence,
&"hull",
amount
)
static func dive_terminated(
tick_sequence: int,
reason: DiveTerminalReason.Value
):
return new(
_event_id(tick_sequence, &"terminal", &"dive"),
Kind.DIVE_TERMINATED,
tick_sequence,
&"dive",
0.0,
DiveAlertLevel.Value.NORMAL,
DiveAlertLevel.Value.NORMAL,
reason
)
static func _event_id(
tick_sequence: int,
event_name: StringName,
subject_name: StringName
) -> StringName:
return StringName("%d:%s:%s" % [tick_sequence, event_name, subject_name])
+370
View File
@@ -0,0 +1,370 @@
class_name DiveSession
extends RefCounted
const ResolvedDiveRules = preload("res://game/domain/rules/resolved_dive_rules.gd")
const DiveStep = preload("res://game/domain/dive/dive_step.gd")
const CollisionObservation = preload("res://game/domain/dive/collision_observation.gd")
const BatterySnapshot = preload("res://game/domain/dive/battery_snapshot.gd")
const OxygenSnapshot = preload("res://game/domain/dive/oxygen_snapshot.gd")
const HullSnapshot = preload("res://game/domain/dive/hull_snapshot.gd")
const DepthSnapshot = preload("res://game/domain/dive/depth_snapshot.gd")
const ReturnEstimate = preload("res://game/domain/dive/return_estimate.gd")
const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd")
const DiveAlertState = preload("res://game/domain/dive/dive_alert_state.gd")
const EjectionSnapshot = preload("res://game/domain/dive/ejection_snapshot.gd")
const DiveTerminalReason = preload("res://game/domain/dive/dive_terminal_reason.gd")
const DiveSnapshot = preload("res://game/domain/dive/dive_snapshot.gd")
const DiveEvent = preload("res://game/domain/dive/dive_event.gd")
const DiveUpdate = preload("res://game/domain/dive/dive_update.gd")
var session_id: StringName
var rules: ResolvedDiveRules
var initialization_error: String = ""
var _battery_kwh: float
var _oxygen_l: float
var _hull_integrity: float
var _depth_m := 0.0
var _current_power_kw := 0.0
var _ejection_held_seconds := 0.0
var _crush_exposure_seconds := 0.0
var _collision_cooldown_seconds := 0.0
var _last_tick_sequence := -1
var _terminal_reason := DiveTerminalReason.Value.NONE
var _return_estimate: ReturnEstimate = ReturnEstimate.unknown()
var _processed_collision_ids: Dictionary[StringName, bool] = {}
var _alerts: DiveAlertState
var _snapshot: DiveSnapshot
func _init(
p_session_id: StringName,
p_rules: ResolvedDiveRules,
initial_battery_kwh: float = -1.0,
initial_oxygen_l: float = -1.0,
initial_hull_integrity: float = -1.0
) -> void:
session_id = p_session_id
rules = p_rules
initialization_error = _validate_initialization(
initial_battery_kwh,
initial_oxygen_l,
initial_hull_integrity
)
if not initialization_error.is_empty():
return
_battery_kwh = (
rules.battery_capacity_kwh if initial_battery_kwh < 0.0 else initial_battery_kwh
)
_oxygen_l = rules.oxygen_capacity_l if initial_oxygen_l < 0.0 else initial_oxygen_l
_hull_integrity = (
rules.hull_max_integrity if initial_hull_integrity < 0.0 else initial_hull_integrity
)
_alerts = _build_alert_state()
_snapshot = _build_snapshot()
func is_ready() -> bool:
return initialization_error.is_empty()
func current_snapshot() -> DiveSnapshot:
return _snapshot
func advance(step: DiveStep) -> DiveUpdate:
var requested_sequence := step.tick.sequence if step != null and step.tick != null else -1
if not is_ready():
return DiveUpdate.rejected(requested_sequence, initialization_error, _snapshot)
if _terminal_reason != DiveTerminalReason.Value.NONE:
return DiveUpdate.rejected(requested_sequence, "dive session is terminal", _snapshot)
if step == null:
return DiveUpdate.rejected(requested_sequence, "DiveStep is null", _snapshot)
var validation_error: String = step.validation_error()
if not validation_error.is_empty():
return DiveUpdate.rejected(requested_sequence, validation_error, _snapshot)
if step.tick.session_id != session_id:
return DiveUpdate.rejected(requested_sequence, "tick session_id does not match", _snapshot)
if step.tick.sequence != _last_tick_sequence + 1:
return DiveUpdate.rejected(requested_sequence, "tick sequence is not consecutive", _snapshot)
var previous_battery := _battery_kwh
var previous_oxygen := _oxygen_l
var previous_hull := _hull_integrity
var previous_alerts := _alerts
var delta_seconds := step.tick.elapsed_seconds
var events: Array[DiveEvent] = []
_last_tick_sequence = step.tick.sequence
_depth_m = step.telemetry.depth_m
_current_power_kw = _calculate_power_kw(step)
_battery_kwh = maxf(
0.0,
_battery_kwh - _current_power_kw * delta_seconds / 3600.0
)
_oxygen_l = maxf(
0.0,
_oxygen_l - rules.oxygen_consumption_l_per_min * delta_seconds / 60.0
)
var hull_damage := _apply_pressure_damage(delta_seconds)
hull_damage += _apply_collision_damage(step, delta_seconds)
if hull_damage > 0.0:
events.append(DiveEvent.hull_damaged(_last_tick_sequence, hull_damage))
if step.ejection_intent.confirmed:
_ejection_held_seconds = rules.ejection_hold_seconds
elif step.ejection_intent.active:
_ejection_held_seconds = minf(
rules.ejection_hold_seconds,
_ejection_held_seconds + delta_seconds
)
else:
_ejection_held_seconds = 0.0
_return_estimate = _calculate_return_estimate(step)
_alerts = _build_alert_state()
_append_depletion_events(events, previous_battery, previous_oxygen, previous_hull)
_append_alert_events(events, previous_alerts)
_update_terminal_reason()
if _terminal_reason != DiveTerminalReason.Value.NONE:
events.append(DiveEvent.dive_terminated(_last_tick_sequence, _terminal_reason))
_snapshot = _build_snapshot()
return DiveUpdate.new(true, _last_tick_sequence, "", _snapshot, events)
func _validate_initialization(
initial_battery_kwh: float,
initial_oxygen_l: float,
initial_hull_integrity: float
) -> String:
if session_id.is_empty():
return "session_id is empty"
if rules == null:
return "resolved dive rules are missing"
var rules_error: String = rules.validation_error()
if not rules_error.is_empty():
return rules_error
if not _valid_initial_value(initial_battery_kwh, rules.battery_capacity_kwh):
return "initial battery is outside resolved capacity"
if not _valid_initial_value(initial_oxygen_l, rules.oxygen_capacity_l):
return "initial oxygen is outside resolved capacity"
if not _valid_initial_value(initial_hull_integrity, rules.hull_max_integrity):
return "initial hull is outside resolved capacity"
return ""
func _valid_initial_value(value: float, maximum: float) -> bool:
return is_finite(value) and (is_equal_approx(value, -1.0) or (value >= 0.0 and value <= maximum))
func _calculate_power_kw(step: DiveStep) -> float:
var power := (
absf(step.telemetry.longitudinal_effort) * rules.longitudinal_power_kw
+ absf(step.telemetry.steering_effort) * rules.steering_power_kw
+ absf(step.telemetry.vertical_effort) * rules.vertical_power_kw
)
if step.telemetry.high_thrust_active:
power += rules.high_thrust_power_kw
for demand in step.tool_power_demands:
power += demand.effective_power_kw
return power
func _apply_pressure_damage(delta_seconds: float) -> float:
var damage_rate := 0.0
if _depth_m <= rules.certified_depth_m:
_crush_exposure_seconds = 0.0
elif _depth_m <= rules.critical_depth_m:
_crush_exposure_seconds = 0.0
var range_size := rules.critical_depth_m - rules.certified_depth_m
var ratio := (_depth_m - rules.certified_depth_m) / range_size
damage_rate = lerpf(0.15, 5.0, ratio * ratio)
elif _depth_m <= rules.crush_depth_m:
_crush_exposure_seconds = 0.0
var range_size := rules.crush_depth_m - rules.critical_depth_m
var ratio := (_depth_m - rules.critical_depth_m) / range_size
damage_rate = lerpf(5.0, 18.0, ratio)
else:
_crush_exposure_seconds += delta_seconds
if _crush_exposure_seconds >= rules.crush_grace_seconds:
damage_rate = 100.0
var damage := damage_rate * rules.pressure_damage_multiplier * delta_seconds
return _damage_hull(damage)
func _apply_collision_damage(step: DiveStep, delta_seconds: float) -> float:
_collision_cooldown_seconds = maxf(0.0, _collision_cooldown_seconds - delta_seconds)
var ordered_collisions: Array[CollisionObservation] = step.collisions.duplicate()
ordered_collisions.sort_custom(_collision_comes_first)
var total_damage := 0.0
for collision: CollisionObservation in ordered_collisions:
if _processed_collision_ids.has(collision.observation_id):
continue
_processed_collision_ids[collision.observation_id] = true
if collision.normal_impact_speed_mps <= rules.collision_speed_threshold_mps:
continue
if _collision_cooldown_seconds > 0.0:
continue
var excess_speed := (
collision.normal_impact_speed_mps - rules.collision_speed_threshold_mps
)
var requested_damage := (
excess_speed
* rules.collision_damage_per_excess_mps
* rules.collision_damage_multiplier
)
total_damage += _damage_hull(requested_damage)
_collision_cooldown_seconds = rules.collision_repeat_delay_seconds
return total_damage
func _collision_comes_first(left: CollisionObservation, right: CollisionObservation) -> bool:
return left.sequence < right.sequence
func _damage_hull(requested_damage: float) -> float:
if requested_damage <= 0.0:
return 0.0
var applied_damage := minf(requested_damage, _hull_integrity)
_hull_integrity -= applied_damage
return applied_damage
func _calculate_return_estimate(step: DiveStep) -> ReturnEstimate:
if not step.return_route.is_known:
return ReturnEstimate.unknown()
var required := (
step.return_route.distance_m * rules.return_energy_kwh_per_m
+ step.return_route.ascent_m * rules.ascent_energy_kwh_per_m
)
var margin := _battery_kwh - required
var margin_ratio := margin / rules.battery_capacity_kwh
var status := ReturnEstimate.Status.SAFE
if margin < 0.0:
status = ReturnEstimate.Status.INSUFFICIENT
elif margin_ratio <= rules.return_ratio:
status = ReturnEstimate.Status.TIGHT
return ReturnEstimate.new(required, margin, margin_ratio, status)
func _append_depletion_events(
events: Array[DiveEvent],
previous_battery: float,
previous_oxygen: float,
previous_hull: float
) -> void:
if previous_battery > 0.0 and _battery_kwh <= 0.0:
events.append(DiveEvent.resource_depleted(_last_tick_sequence, &"battery"))
if previous_oxygen > 0.0 and _oxygen_l <= 0.0:
events.append(DiveEvent.resource_depleted(_last_tick_sequence, &"oxygen"))
if previous_hull > 0.0 and _hull_integrity <= 0.0:
events.append(DiveEvent.resource_depleted(_last_tick_sequence, &"hull"))
func _append_alert_events(
events: Array[DiveEvent],
previous_alerts: DiveAlertState
) -> void:
_append_alert_event(events, &"oxygen", previous_alerts.oxygen, _alerts.oxygen)
_append_alert_event(events, &"battery", previous_alerts.battery, _alerts.battery)
_append_alert_event(events, &"hull", previous_alerts.hull, _alerts.hull)
_append_alert_event(events, &"depth", previous_alerts.depth, _alerts.depth)
func _append_alert_event(
events: Array[DiveEvent],
subject: StringName,
previous_level: DiveAlertLevel.Value,
current_level: DiveAlertLevel.Value
) -> void:
if previous_level == current_level:
return
events.append(
DiveEvent.alert_changed(
_last_tick_sequence,
subject,
previous_level,
current_level
)
)
func _update_terminal_reason() -> void:
if _hull_integrity <= 0.0:
_terminal_reason = DiveTerminalReason.Value.HULL_DESTROYED
elif _oxygen_l <= 0.0:
_terminal_reason = DiveTerminalReason.Value.OXYGEN_DEPLETED
elif _ejection_held_seconds >= rules.ejection_hold_seconds:
_terminal_reason = DiveTerminalReason.Value.VOLUNTARY_EJECTION
func _build_alert_state() -> DiveAlertState:
return DiveAlertState.new(
_resource_alert(_ratio(_oxygen_l, rules.oxygen_capacity_l)),
_resource_alert(_ratio(_battery_kwh, rules.battery_capacity_kwh)),
_resource_alert(_ratio(_hull_integrity, rules.hull_max_integrity)),
_depth_alert(_depth_m)
)
func _resource_alert(ratio: float) -> DiveAlertLevel.Value:
if ratio <= 0.0:
return DiveAlertLevel.Value.FAILURE
if ratio <= rules.critical_ratio:
return DiveAlertLevel.Value.CRITICAL
if ratio <= rules.return_ratio:
return DiveAlertLevel.Value.RETURN
if ratio <= rules.attention_ratio:
return DiveAlertLevel.Value.ATTENTION
return DiveAlertLevel.Value.NORMAL
func _depth_alert(depth_m: float) -> DiveAlertLevel.Value:
if depth_m >= rules.crush_depth_m:
return DiveAlertLevel.Value.FAILURE
if depth_m >= rules.critical_depth_m:
return DiveAlertLevel.Value.CRITICAL
if depth_m > rules.certified_depth_m:
return DiveAlertLevel.Value.RETURN
if depth_m >= rules.certified_depth_m * 0.85:
return DiveAlertLevel.Value.ATTENTION
return DiveAlertLevel.Value.NORMAL
func _build_snapshot() -> DiveSnapshot:
var exterior_pressure := 1.0 + _depth_m / 10.0
return DiveSnapshot.new(
session_id,
_last_tick_sequence,
BatterySnapshot.new(_battery_kwh, rules.battery_capacity_kwh, _current_power_kw),
OxygenSnapshot.new(
_oxygen_l,
rules.oxygen_capacity_l,
rules.oxygen_consumption_l_per_min
),
HullSnapshot.new(
_hull_integrity,
rules.hull_max_integrity,
exterior_pressure,
rules.certified_pressure_bar,
rules.critical_pressure_bar,
rules.crush_pressure_bar
),
DepthSnapshot.new(
_depth_m,
rules.certified_depth_m,
rules.critical_depth_m,
rules.crush_depth_m
),
_return_estimate,
_alerts,
EjectionSnapshot.new(_ejection_held_seconds, rules.ejection_hold_seconds),
_terminal_reason
)
func _ratio(current: float, maximum: float) -> float:
return current / maximum if maximum > 0.0 else 0.0
+50
View File
@@ -0,0 +1,50 @@
class_name DiveSnapshot
extends RefCounted
const BatterySnapshot = preload("res://game/domain/dive/battery_snapshot.gd")
const OxygenSnapshot = preload("res://game/domain/dive/oxygen_snapshot.gd")
const HullSnapshot = preload("res://game/domain/dive/hull_snapshot.gd")
const DepthSnapshot = preload("res://game/domain/dive/depth_snapshot.gd")
const ReturnEstimate = preload("res://game/domain/dive/return_estimate.gd")
const DiveAlertState = preload("res://game/domain/dive/dive_alert_state.gd")
const EjectionSnapshot = preload("res://game/domain/dive/ejection_snapshot.gd")
const DiveTerminalReason = preload("res://game/domain/dive/dive_terminal_reason.gd")
var session_id: StringName
var tick_sequence: int
var battery: BatterySnapshot
var oxygen: OxygenSnapshot
var hull: HullSnapshot
var depth: DepthSnapshot
var return_estimate: ReturnEstimate
var alerts: DiveAlertState
var ejection: EjectionSnapshot
var terminal_reason: DiveTerminalReason.Value
func _init(
p_session_id: StringName,
p_tick_sequence: int,
p_battery: BatterySnapshot,
p_oxygen: OxygenSnapshot,
p_hull: HullSnapshot,
p_depth: DepthSnapshot,
p_return_estimate: ReturnEstimate,
p_alerts: DiveAlertState,
p_ejection: EjectionSnapshot,
p_terminal_reason: DiveTerminalReason.Value
) -> void:
session_id = p_session_id
tick_sequence = p_tick_sequence
battery = p_battery
oxygen = p_oxygen
hull = p_hull
depth = p_depth
return_estimate = p_return_estimate
alerts = p_alerts
ejection = p_ejection
terminal_reason = p_terminal_reason
func is_terminal() -> bool:
return terminal_reason != DiveTerminalReason.Value.NONE
+70
View File
@@ -0,0 +1,70 @@
class_name DiveStep
extends RefCounted
const DiveTick = preload("res://game/domain/dive/dive_tick.gd")
const DiveTelemetrySample = preload("res://game/domain/dive/dive_telemetry_sample.gd")
const CollisionObservation = preload("res://game/domain/dive/collision_observation.gd")
const ToolPowerDemand = preload("res://game/domain/dive/tool_power_demand.gd")
const EjectionIntent = preload("res://game/domain/dive/ejection_intent.gd")
const ReturnRouteSample = preload("res://game/domain/dive/return_route_sample.gd")
var tick: DiveTick
var telemetry: DiveTelemetrySample
var collisions: Array[CollisionObservation]
var tool_power_demands: Array[ToolPowerDemand]
var ejection_intent: EjectionIntent
var return_route: ReturnRouteSample
func _init(
p_tick: DiveTick,
p_telemetry: DiveTelemetrySample,
p_collisions: Array[CollisionObservation] = [],
p_tool_power_demands: Array[ToolPowerDemand] = [],
p_ejection_intent: EjectionIntent = null,
p_return_route: ReturnRouteSample = null
) -> void:
tick = p_tick
telemetry = p_telemetry
collisions = p_collisions.duplicate()
tool_power_demands = p_tool_power_demands.duplicate()
ejection_intent = p_ejection_intent if p_ejection_intent != null else EjectionIntent.new()
return_route = p_return_route if p_return_route != null else ReturnRouteSample.new()
func validation_error() -> String:
if tick == null:
return "DiveStep has no tick"
var error: String = tick.validation_error()
if not error.is_empty():
return error
if telemetry == null:
return "DiveStep has no telemetry"
error = telemetry.validation_error()
if not error.is_empty():
return error
if ejection_intent == null:
return "DiveStep has no ejection intent"
if return_route == null:
return "DiveStep has no return route sample"
error = return_route.validation_error()
if not error.is_empty():
return error
for collision: CollisionObservation in collisions:
if collision == null:
return "DiveStep contains a null collision"
error = collision.validation_error(tick.sequence)
if not error.is_empty():
return error
for demand: ToolPowerDemand in tool_power_demands:
if demand == null:
return "DiveStep contains a null tool power demand"
error = demand.validation_error()
if not error.is_empty():
return error
var tool_ids: Dictionary[StringName, bool] = {}
for demand: ToolPowerDemand in tool_power_demands:
if tool_ids.has(demand.tool_id):
return "DiveStep contains duplicate tool power demands"
tool_ids[demand.tool_id] = true
return ""
+38
View File
@@ -0,0 +1,38 @@
class_name DiveTelemetrySample
extends RefCounted
var depth_m: float
var longitudinal_effort: float
var steering_effort: float
var vertical_effort: float
var high_thrust_active: bool
func _init(
p_depth_m: float = 0.0,
p_longitudinal_effort: float = 0.0,
p_steering_effort: float = 0.0,
p_vertical_effort: float = 0.0,
p_high_thrust_active: bool = false
) -> void:
depth_m = p_depth_m
longitudinal_effort = p_longitudinal_effort
steering_effort = p_steering_effort
vertical_effort = p_vertical_effort
high_thrust_active = p_high_thrust_active
func validation_error() -> String:
if not is_finite(depth_m) or depth_m < 0.0:
return "telemetry depth must be finite and non-negative"
if not _is_normalized(longitudinal_effort):
return "longitudinal effort must be between -1 and 1"
if not _is_normalized(steering_effort):
return "steering effort must be between -1 and 1"
if not _is_normalized(vertical_effort):
return "vertical effort must be between -1 and 1"
return ""
func _is_normalized(value: float) -> bool:
return is_finite(value) and value >= -1.0 and value <= 1.0
+10
View File
@@ -0,0 +1,10 @@
class_name DiveTerminalReason
extends RefCounted
enum Value {
NONE,
RETURNED,
VOLUNTARY_EJECTION,
OXYGEN_DEPLETED,
HULL_DESTROYED,
}
+28
View File
@@ -0,0 +1,28 @@
class_name DiveTick
extends RefCounted
var session_id: StringName
var sequence: int
var elapsed_seconds: float
func _init(
p_session_id: StringName,
p_sequence: int,
p_elapsed_seconds: float
) -> void:
session_id = p_session_id
sequence = p_sequence
elapsed_seconds = p_elapsed_seconds
func validation_error() -> String:
if session_id.is_empty():
return "tick session_id is empty"
if sequence < 0:
return "tick sequence is negative"
if not is_finite(elapsed_seconds) or elapsed_seconds <= 0.0:
return "tick duration must be finite and positive"
if elapsed_seconds > 0.1:
return "tick duration exceeds 0.1 seconds"
return ""
+37
View File
@@ -0,0 +1,37 @@
class_name DiveUpdate
extends RefCounted
const DiveSnapshot = preload("res://game/domain/dive/dive_snapshot.gd")
const DiveEvent = preload("res://game/domain/dive/dive_event.gd")
var accepted: bool
var requested_tick_sequence: int
var rejection_reason: String
var snapshot: DiveSnapshot
var events: Array[DiveEvent]
func _init(
p_accepted: bool,
p_requested_tick_sequence: int,
p_rejection_reason: String,
p_snapshot: DiveSnapshot,
p_events: Array[DiveEvent] = []
) -> void:
accepted = p_accepted
requested_tick_sequence = p_requested_tick_sequence
rejection_reason = p_rejection_reason
snapshot = p_snapshot
events = p_events.duplicate()
static func initial(snapshot: DiveSnapshot):
return new(true, snapshot.tick_sequence, "", snapshot)
static func rejected(
requested_tick_sequence: int,
reason: String,
snapshot: DiveSnapshot
):
return new(false, requested_tick_sequence, reason, snapshot)
+10
View File
@@ -0,0 +1,10 @@
class_name EjectionIntent
extends RefCounted
var active: bool
var confirmed: bool
func _init(p_active: bool = false, p_confirmed: bool = false) -> void:
active = p_active
confirmed = p_confirmed
+12
View File
@@ -0,0 +1,12 @@
class_name EjectionSnapshot
extends RefCounted
var held_seconds: float
var required_seconds: float
var ratio: float
func _init(p_held_seconds: float, p_required_seconds: float) -> void:
held_seconds = p_held_seconds
required_seconds = p_required_seconds
ratio = clampf(held_seconds / required_seconds, 0.0, 1.0) if required_seconds > 0.0 else 0.0
+27
View File
@@ -0,0 +1,27 @@
class_name HullSnapshot
extends RefCounted
var current_integrity: float
var maximum_integrity: float
var ratio: float
var exterior_pressure_bar: float
var certified_pressure_bar: float
var critical_pressure_bar: float
var crush_pressure_bar: float
func _init(
p_current_integrity: float,
p_maximum_integrity: float,
p_exterior_pressure_bar: float,
p_certified_pressure_bar: float,
p_critical_pressure_bar: float,
p_crush_pressure_bar: float
) -> void:
current_integrity = p_current_integrity
maximum_integrity = p_maximum_integrity
ratio = current_integrity / maximum_integrity if maximum_integrity > 0.0 else 0.0
exterior_pressure_bar = p_exterior_pressure_bar
certified_pressure_bar = p_certified_pressure_bar
critical_pressure_bar = p_critical_pressure_bar
crush_pressure_bar = p_crush_pressure_bar
+22
View File
@@ -0,0 +1,22 @@
class_name OxygenSnapshot
extends RefCounted
var remaining_l: float
var maximum_l: float
var ratio: float
var consumption_l_per_min: float
var endurance_minutes: float
func _init(
p_remaining_l: float,
p_maximum_l: float,
p_consumption_l_per_min: float
) -> void:
remaining_l = p_remaining_l
maximum_l = p_maximum_l
ratio = remaining_l / maximum_l if maximum_l > 0.0 else 0.0
consumption_l_per_min = p_consumption_l_per_min
endurance_minutes = (
remaining_l / consumption_l_per_min if consumption_l_per_min > 0.0 else INF
)
+30
View File
@@ -0,0 +1,30 @@
class_name ReturnEstimate
extends RefCounted
enum Status {
UNKNOWN,
SAFE,
TIGHT,
INSUFFICIENT,
}
var required_kwh: float
var margin_kwh: float
var margin_ratio: float
var status: Status
func _init(
p_required_kwh: float,
p_margin_kwh: float,
p_margin_ratio: float,
p_status: Status
) -> void:
required_kwh = p_required_kwh
margin_kwh = p_margin_kwh
margin_ratio = p_margin_ratio
status = p_status
static func unknown():
return new(0.0, 0.0, 0.0, Status.UNKNOWN)
+24
View File
@@ -0,0 +1,24 @@
class_name ReturnRouteSample
extends RefCounted
var is_known: bool
var distance_m: float
var ascent_m: float
func _init(
p_is_known: bool = false,
p_distance_m: float = 0.0,
p_ascent_m: float = 0.0
) -> void:
is_known = p_is_known
distance_m = p_distance_m
ascent_m = p_ascent_m
func validation_error() -> String:
if not is_finite(distance_m) or distance_m < 0.0:
return "return distance must be finite and non-negative"
if not is_finite(ascent_m) or ascent_m < 0.0:
return "return ascent must be finite and non-negative"
return ""
+18
View File
@@ -0,0 +1,18 @@
class_name ToolPowerDemand
extends RefCounted
var tool_id: StringName
var effective_power_kw: float
func _init(p_tool_id: StringName, p_effective_power_kw: float) -> void:
tool_id = p_tool_id
effective_power_kw = p_effective_power_kw
func validation_error() -> String:
if tool_id.is_empty():
return "tool_id is empty"
if not is_finite(effective_power_kw) or effective_power_kw < 0.0:
return "tool power must be finite and non-negative"
return ""
@@ -0,0 +1,27 @@
class_name DiveAccessibilityAdjustments
extends RefCounted
var oxygen_consumption_multiplier: float
var battery_consumption_multiplier: float
var pressure_damage_multiplier: float
func _init(
p_oxygen_consumption_multiplier: float = 1.0,
p_battery_consumption_multiplier: float = 1.0,
p_pressure_damage_multiplier: float = 1.0
) -> void:
oxygen_consumption_multiplier = p_oxygen_consumption_multiplier
battery_consumption_multiplier = p_battery_consumption_multiplier
pressure_damage_multiplier = p_pressure_damage_multiplier
func validation_error() -> String:
for multiplier: float in [
oxygen_consumption_multiplier,
battery_consumption_multiplier,
pressure_damage_multiplier,
]:
if not is_finite(multiplier) or multiplier <= 0.0:
return "accessibility multipliers must be finite and positive"
return ""
@@ -0,0 +1,52 @@
class_name DiveDifficultyProfile
extends RefCounted
var profile_id: StringName
var oxygen_consumption_multiplier: float
var battery_consumption_multiplier: float
var collision_damage_multiplier: float
var pressure_damage_multiplier: float
var crush_grace_multiplier: float
func _init(
p_profile_id: StringName,
p_oxygen_consumption_multiplier: float,
p_battery_consumption_multiplier: float,
p_collision_damage_multiplier: float,
p_pressure_damage_multiplier: float,
p_crush_grace_multiplier: float
) -> void:
profile_id = p_profile_id
oxygen_consumption_multiplier = p_oxygen_consumption_multiplier
battery_consumption_multiplier = p_battery_consumption_multiplier
collision_damage_multiplier = p_collision_damage_multiplier
pressure_damage_multiplier = p_pressure_damage_multiplier
crush_grace_multiplier = p_crush_grace_multiplier
static func exploration():
return new(&"exploration", 0.75, 0.80, 0.65, 0.50, 1.50)
static func standard():
return new(&"standard", 1.0, 1.0, 1.0, 1.0, 1.0)
static func expert():
return new(&"expert", 1.20, 1.15, 1.25, 1.35, 0.75)
func validation_error() -> String:
if profile_id.is_empty():
return "difficulty profile_id is empty"
for value: float in [
oxygen_consumption_multiplier,
battery_consumption_multiplier,
collision_damage_multiplier,
pressure_damage_multiplier,
crush_grace_multiplier,
]:
if not is_finite(value) or value <= 0.0:
return "difficulty multipliers must be finite and positive"
return ""
+99
View File
@@ -0,0 +1,99 @@
class_name DiveRulesResolver
extends RefCounted
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 ResolvedDiveRules = preload("res://game/domain/rules/resolved_dive_rules.gd")
const BASE_BATTERY_CAPACITY_KWH := 100.0
const BASE_OXYGEN_CAPACITY_L := 480.0
const BASE_OXYGEN_CONSUMPTION_L_PER_MIN := 30.0
const BASE_HULL_INTEGRITY := 100.0
const BASE_CERTIFIED_DEPTH_M := 60.0
const BASE_LONGITUDINAL_POWER_KW := 1800.0
const BASE_STEERING_POWER_KW := 180.0
const BASE_VERTICAL_POWER_KW := 360.0
const BASE_HIGH_THRUST_POWER_KW := 900.0
const BASE_RETURN_ENERGY_KWH_PER_M := 100.0 / 1200.0
const BASE_ASCENT_ENERGY_KWH_PER_M := 0.035
func resolve(
difficulty: DiveDifficultyProfile,
upgrades: DiveUpgradeEffects = null,
accessibility: DiveAccessibilityAdjustments = null
) -> ResolvedDiveRules:
var resolved_upgrades: DiveUpgradeEffects = (
upgrades if upgrades != null else DiveUpgradeEffects.new()
)
var resolved_accessibility: DiveAccessibilityAdjustments = (
accessibility if accessibility != null else DiveAccessibilityAdjustments.new()
)
if difficulty == null:
return null
if not difficulty.validation_error().is_empty():
return null
if not resolved_upgrades.validation_error().is_empty():
return null
if not resolved_accessibility.validation_error().is_empty():
return null
# Base values, then submarine upgrades, difficulty and gameplay accessibility.
var battery_capacity: float = (
BASE_BATTERY_CAPACITY_KWH + resolved_upgrades.battery_capacity_bonus_kwh
)
var oxygen_capacity: float = (
BASE_OXYGEN_CAPACITY_L + resolved_upgrades.oxygen_capacity_bonus_l
)
var hull_integrity: float = BASE_HULL_INTEGRITY + resolved_upgrades.hull_integrity_bonus
var certified_depth: float = (
BASE_CERTIFIED_DEPTH_M + resolved_upgrades.certified_depth_bonus_m
)
var battery_multiplier: float = (
resolved_upgrades.battery_consumption_multiplier
* difficulty.battery_consumption_multiplier
* resolved_accessibility.battery_consumption_multiplier
)
var oxygen_multiplier: float = (
difficulty.oxygen_consumption_multiplier
* resolved_accessibility.oxygen_consumption_multiplier
)
var pressure_multiplier: float = (
resolved_upgrades.pressure_damage_multiplier
* difficulty.pressure_damage_multiplier
* resolved_accessibility.pressure_damage_multiplier
)
var collision_multiplier: float = (
resolved_upgrades.collision_damage_multiplier
* difficulty.collision_damage_multiplier
)
var result: ResolvedDiveRules = ResolvedDiveRules.new(
battery_capacity,
oxygen_capacity,
BASE_OXYGEN_CONSUMPTION_L_PER_MIN * oxygen_multiplier,
hull_integrity,
certified_depth,
BASE_LONGITUDINAL_POWER_KW * battery_multiplier,
BASE_STEERING_POWER_KW * battery_multiplier,
BASE_VERTICAL_POWER_KW * battery_multiplier,
BASE_HIGH_THRUST_POWER_KW * battery_multiplier,
BASE_RETURN_ENERGY_KWH_PER_M * battery_multiplier,
BASE_ASCENT_ENERGY_KWH_PER_M * battery_multiplier,
pressure_multiplier,
collision_multiplier,
3.5,
4.0,
0.45,
1.5 * difficulty.crush_grace_multiplier,
0.30,
0.15,
0.05,
3.0
)
return result if result.validation_error().is_empty() else null
+47
View File
@@ -0,0 +1,47 @@
class_name DiveUpgradeEffects
extends RefCounted
var battery_capacity_bonus_kwh: float
var oxygen_capacity_bonus_l: float
var hull_integrity_bonus: float
var certified_depth_bonus_m: float
var battery_consumption_multiplier: float
var collision_damage_multiplier: float
var pressure_damage_multiplier: float
func _init(
p_battery_capacity_bonus_kwh: float = 0.0,
p_oxygen_capacity_bonus_l: float = 0.0,
p_hull_integrity_bonus: float = 0.0,
p_certified_depth_bonus_m: float = 0.0,
p_battery_consumption_multiplier: float = 1.0,
p_collision_damage_multiplier: float = 1.0,
p_pressure_damage_multiplier: float = 1.0
) -> void:
battery_capacity_bonus_kwh = p_battery_capacity_bonus_kwh
oxygen_capacity_bonus_l = p_oxygen_capacity_bonus_l
hull_integrity_bonus = p_hull_integrity_bonus
certified_depth_bonus_m = p_certified_depth_bonus_m
battery_consumption_multiplier = p_battery_consumption_multiplier
collision_damage_multiplier = p_collision_damage_multiplier
pressure_damage_multiplier = p_pressure_damage_multiplier
func validation_error() -> String:
for bonus: float in [
battery_capacity_bonus_kwh,
oxygen_capacity_bonus_l,
hull_integrity_bonus,
certified_depth_bonus_m,
]:
if not is_finite(bonus) or bonus < 0.0:
return "upgrade bonuses must be finite and non-negative"
for multiplier: float in [
battery_consumption_multiplier,
collision_damage_multiplier,
pressure_damage_multiplier,
]:
if not is_finite(multiplier) or multiplier <= 0.0:
return "upgrade multipliers must be finite and positive"
return ""
+125
View File
@@ -0,0 +1,125 @@
class_name ResolvedDiveRules
extends RefCounted
var battery_capacity_kwh: float
var oxygen_capacity_l: float
var oxygen_consumption_l_per_min: float
var hull_max_integrity: float
var certified_depth_m: float
var critical_depth_m: float
var crush_depth_m: float
var certified_pressure_bar: float
var critical_pressure_bar: float
var crush_pressure_bar: float
var longitudinal_power_kw: float
var steering_power_kw: float
var vertical_power_kw: float
var high_thrust_power_kw: float
var return_energy_kwh_per_m: float
var ascent_energy_kwh_per_m: float
var pressure_damage_multiplier: float
var collision_damage_multiplier: float
var collision_speed_threshold_mps: float
var collision_damage_per_excess_mps: float
var collision_repeat_delay_seconds: float
var crush_grace_seconds: float
var attention_ratio: float
var return_ratio: float
var critical_ratio: float
var ejection_hold_seconds: float
func _init(
p_battery_capacity_kwh: float,
p_oxygen_capacity_l: float,
p_oxygen_consumption_l_per_min: float,
p_hull_max_integrity: float,
p_certified_depth_m: float,
p_longitudinal_power_kw: float,
p_steering_power_kw: float,
p_vertical_power_kw: float,
p_high_thrust_power_kw: float,
p_return_energy_kwh_per_m: float,
p_ascent_energy_kwh_per_m: float,
p_pressure_damage_multiplier: float,
p_collision_damage_multiplier: float,
p_collision_speed_threshold_mps: float,
p_collision_damage_per_excess_mps: float,
p_collision_repeat_delay_seconds: float,
p_crush_grace_seconds: float,
p_attention_ratio: float,
p_return_ratio: float,
p_critical_ratio: float,
p_ejection_hold_seconds: float
) -> void:
battery_capacity_kwh = p_battery_capacity_kwh
oxygen_capacity_l = p_oxygen_capacity_l
oxygen_consumption_l_per_min = p_oxygen_consumption_l_per_min
hull_max_integrity = p_hull_max_integrity
certified_depth_m = p_certified_depth_m
critical_depth_m = certified_depth_m * 1.15
crush_depth_m = certified_depth_m * 1.25
certified_pressure_bar = _pressure_at_depth(certified_depth_m)
critical_pressure_bar = _pressure_at_depth(critical_depth_m)
crush_pressure_bar = _pressure_at_depth(crush_depth_m)
longitudinal_power_kw = p_longitudinal_power_kw
steering_power_kw = p_steering_power_kw
vertical_power_kw = p_vertical_power_kw
high_thrust_power_kw = p_high_thrust_power_kw
return_energy_kwh_per_m = p_return_energy_kwh_per_m
ascent_energy_kwh_per_m = p_ascent_energy_kwh_per_m
pressure_damage_multiplier = p_pressure_damage_multiplier
collision_damage_multiplier = p_collision_damage_multiplier
collision_speed_threshold_mps = p_collision_speed_threshold_mps
collision_damage_per_excess_mps = p_collision_damage_per_excess_mps
collision_repeat_delay_seconds = p_collision_repeat_delay_seconds
crush_grace_seconds = p_crush_grace_seconds
attention_ratio = p_attention_ratio
return_ratio = p_return_ratio
critical_ratio = p_critical_ratio
ejection_hold_seconds = p_ejection_hold_seconds
func validation_error() -> String:
for positive_value: float in [
battery_capacity_kwh,
oxygen_capacity_l,
oxygen_consumption_l_per_min,
hull_max_integrity,
certified_depth_m,
longitudinal_power_kw,
return_energy_kwh_per_m,
collision_speed_threshold_mps,
collision_damage_per_excess_mps,
collision_repeat_delay_seconds,
crush_grace_seconds,
ejection_hold_seconds,
]:
if not is_finite(positive_value) or positive_value <= 0.0:
return "resolved positive values must be finite and greater than zero"
for non_negative_value: float in [
steering_power_kw,
vertical_power_kw,
high_thrust_power_kw,
ascent_energy_kwh_per_m,
]:
if not is_finite(non_negative_value) or non_negative_value < 0.0:
return "resolved non-negative values must be finite"
if not (
critical_ratio > 0.0
and critical_ratio < return_ratio
and return_ratio < attention_ratio
and attention_ratio < 1.0
):
return "alert ratios must be ordered between zero and one"
if pressure_damage_multiplier <= 0.0 or collision_damage_multiplier <= 0.0:
return "damage multipliers must be positive"
return ""
static func _pressure_at_depth(depth_m: float) -> float:
return 1.0 + depth_m / 10.0
@@ -0,0 +1,279 @@
class_name ProductionInputAdapter
extends Node
signal pause_requested
signal rebind_result_available(result: RebindResult)
const VehicleCommand = preload("res://game/application/vehicle/vehicle_command.gd")
const CameraLookCommand = preload("res://game/application/vehicle/camera_look_command.gd")
const InputBindingSnapshot = preload(
"res://game/application/input/input_binding_snapshot.gd"
)
const RebindResult = preload("res://game/application/input/rebind_result.gd")
const ACTION_FORWARD := &"vehicle_forward"
const ACTION_BACKWARD := &"vehicle_backward"
const ACTION_LEFT := &"vehicle_left"
const ACTION_RIGHT := &"vehicle_right"
const ACTION_ASCEND := &"vehicle_ascend"
const ACTION_DESCEND := &"vehicle_descend"
const ACTION_HIGH_THRUST := &"vehicle_high_thrust"
const ACTION_EJECT := &"dive_eject"
const ACTION_RECENTER_CAMERA := &"camera_recenter"
const ACTION_PAUSE := &"pause_game"
const BINDABLE_ACTIONS: Array[StringName] = [
ACTION_FORWARD,
ACTION_BACKWARD,
ACTION_LEFT,
ACTION_RIGHT,
ACTION_ASCEND,
ACTION_DESCEND,
ACTION_HIGH_THRUST,
ACTION_EJECT,
ACTION_RECENTER_CAMERA,
]
const ACTION_LABELS := {
ACTION_FORWARD: "Avancer",
ACTION_BACKWARD: "Reculer",
ACTION_LEFT: "Gouverner à gauche",
ACTION_RIGHT: "Gouverner à droite",
ACTION_ASCEND: "Monter",
ACTION_DESCEND: "Descendre",
ACTION_HIGH_THRUST: "Forte poussée",
ACTION_EJECT: "Éjection maintenue",
ACTION_RECENTER_CAMERA: "Recentrer le regard",
}
const DEFAULT_KEYS := {
ACTION_FORWARD: KEY_W,
ACTION_BACKWARD: KEY_S,
ACTION_LEFT: KEY_A,
ACTION_RIGHT: KEY_D,
ACTION_ASCEND: KEY_SPACE,
ACTION_DESCEND: KEY_CTRL,
ACTION_HIGH_THRUST: KEY_SHIFT,
ACTION_EJECT: KEY_X,
ACTION_RECENTER_CAMERA: KEY_R,
ACTION_PAUSE: KEY_ESCAPE,
}
@export var mouse_sensitivity_radians_per_pixel := 0.0025
var _gameplay_enabled := false
var _look_delta := Vector2.ZERO
var _rebind_action_id: StringName = &""
var _pending_key_event: InputEventKey
var _pending_conflicting_action_id: StringName = &""
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
_ensure_default_actions()
func set_gameplay_enabled(enabled: bool) -> void:
_gameplay_enabled = enabled
_look_delta = Vector2.ZERO
if enabled:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
else:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
func sample_vehicle_command(sequence: int) -> VehicleCommand:
if not _gameplay_enabled or get_tree().paused:
return VehicleCommand.neutral(sequence)
return VehicleCommand.new(
sequence,
Input.get_axis(ACTION_BACKWARD, ACTION_FORWARD),
Input.get_axis(ACTION_LEFT, ACTION_RIGHT),
Input.get_axis(ACTION_DESCEND, ACTION_ASCEND),
Input.is_action_pressed(ACTION_HIGH_THRUST)
)
func take_camera_look_command() -> CameraLookCommand:
var command := CameraLookCommand.new(
-_look_delta.x * mouse_sensitivity_radians_per_pixel,
-_look_delta.y * mouse_sensitivity_radians_per_pixel,
_gameplay_enabled and Input.is_action_just_pressed(ACTION_RECENTER_CAMERA)
)
_look_delta = Vector2.ZERO
return command
func ejection_active() -> bool:
return (
_gameplay_enabled
and not get_tree().paused
and Input.is_action_pressed(ACTION_EJECT)
)
func binding_snapshots() -> Array[InputBindingSnapshot]:
var snapshots: Array[InputBindingSnapshot] = []
for action_id: StringName in BINDABLE_ACTIONS:
snapshots.append(
InputBindingSnapshot.new(
action_id,
String(ACTION_LABELS[action_id]),
_binding_text(action_id)
)
)
return snapshots
func begin_rebind(action_id: StringName) -> RebindResult:
if action_id not in BINDABLE_ACTIONS:
return RebindResult.new(false, action_id, "", &"", "Action inconnue")
_rebind_action_id = action_id
_pending_key_event = null
_pending_conflicting_action_id = &""
return RebindResult.new(false, action_id, "", &"", "Appuyez sur une touche")
func resolve_pending_conflict(swap_bindings: bool) -> RebindResult:
if _pending_key_event == null or _rebind_action_id.is_empty():
return RebindResult.new(false, &"", "", &"", "Aucun conflit en attente")
if not swap_bindings:
var cancelled_action := _rebind_action_id
_clear_pending_rebind()
return RebindResult.new(false, cancelled_action, "", &"", "Remappage annulé")
var target_action := _rebind_action_id
var conflicting_action := _pending_conflicting_action_id
var previous_target_event := _first_key_event(target_action)
_set_single_key_binding(target_action, _pending_key_event)
if not conflicting_action.is_empty():
if previous_target_event != null:
_set_single_key_binding(conflicting_action, previous_target_event)
else:
InputMap.action_erase_events(conflicting_action)
var result := RebindResult.new(
true,
target_action,
_binding_text(target_action),
conflicting_action,
"Touches permutées"
)
_clear_pending_rebind()
return result
func restore_default_bindings() -> void:
for action_id: StringName in BINDABLE_ACTIONS:
_set_single_key_binding(action_id, _key_event(int(DEFAULT_KEYS[action_id])))
func _unhandled_input(event: InputEvent) -> void:
if not _rebind_action_id.is_empty():
_handle_rebind_input(event)
return
if event.is_action_pressed(ACTION_PAUSE):
pause_requested.emit()
get_viewport().set_input_as_handled()
return
if (
_gameplay_enabled
and not get_tree().paused
and event is InputEventMouseMotion
):
_look_delta += (event as InputEventMouseMotion).relative
func _handle_rebind_input(event: InputEvent) -> void:
if not (event is InputEventKey):
return
var key_event := event as InputEventKey
if not key_event.pressed or key_event.echo:
return
get_viewport().set_input_as_handled()
if key_event.physical_keycode == KEY_ESCAPE:
var cancelled_action := _rebind_action_id
_clear_pending_rebind()
rebind_result_available.emit(
RebindResult.new(false, cancelled_action, "", &"", "Remappage annulé")
)
return
var normalized_event := _key_event(key_event.physical_keycode)
var conflicting_action := _find_conflicting_action(
_rebind_action_id,
normalized_event.physical_keycode
)
if not conflicting_action.is_empty():
_pending_key_event = normalized_event
_pending_conflicting_action_id = conflicting_action
rebind_result_available.emit(
RebindResult.new(
false,
_rebind_action_id,
normalized_event.as_text_physical_keycode(),
conflicting_action,
"Cette touche est déjà utilisée"
)
)
return
_set_single_key_binding(_rebind_action_id, normalized_event)
var result := RebindResult.new(
true,
_rebind_action_id,
_binding_text(_rebind_action_id),
&"",
"Commande mise à jour"
)
_clear_pending_rebind()
rebind_result_available.emit(result)
func _ensure_default_actions() -> void:
for action_id: StringName in DEFAULT_KEYS:
if not InputMap.has_action(action_id):
InputMap.add_action(action_id)
if InputMap.action_get_events(action_id).is_empty():
InputMap.action_add_event(action_id, _key_event(int(DEFAULT_KEYS[action_id])))
func _find_conflicting_action(
target_action: StringName,
physical_keycode: Key
) -> StringName:
for action_id: StringName in BINDABLE_ACTIONS:
if action_id == target_action:
continue
var event := _first_key_event(action_id)
if event != null and event.physical_keycode == physical_keycode:
return action_id
return &""
func _binding_text(action_id: StringName) -> String:
var event := _first_key_event(action_id)
return event.as_text_physical_keycode() if event != null else "Non assignée"
func _first_key_event(action_id: StringName) -> InputEventKey:
for event: InputEvent in InputMap.action_get_events(action_id):
if event is InputEventKey:
return (event as InputEventKey).duplicate()
return null
func _set_single_key_binding(action_id: StringName, event: InputEventKey) -> void:
InputMap.action_erase_events(action_id)
InputMap.action_add_event(action_id, event)
func _key_event(physical_keycode: Key) -> InputEventKey:
var event := InputEventKey.new()
event.physical_keycode = physical_keycode
return event
func _clear_pending_rebind() -> void:
_rebind_action_id = &""
_pending_key_event = null
_pending_conflicting_action_id = &""
@@ -0,0 +1,82 @@
class_name CockpitCameraController
extends Node3D
const CameraLookCommand = preload(
"res://game/application/vehicle/camera_look_command.gd"
)
const VehicleTelemetry = preload("res://game/application/dive/vehicle_telemetry.gd")
@export var minimum_pitch_radians := -0.30
@export var maximum_pitch_radians := 0.20
@export var maximum_yaw_radians := deg_to_rad(30.0)
@export var feedback_response := 4.5
@export var maximum_feedback_pitch_radians := deg_to_rad(18.0)
@export var maximum_feedback_roll_radians := deg_to_rad(16.0) * 0.55
var _look_yaw := 0.0
var _look_pitch := -0.04
var _feedback_pitch := 0.0
var _feedback_roll := 0.0
var _target_feedback_pitch := 0.0
var _target_feedback_roll := 0.0
func apply_look(command: CameraLookCommand) -> void:
if command == null or not command.validation_error().is_empty():
return
if command.recenter_requested:
_look_yaw = 0.0
_look_pitch = -0.04
else:
_look_yaw = clampf(
_look_yaw + command.yaw_delta_radians,
-maximum_yaw_radians,
maximum_yaw_radians
)
_look_pitch = clampf(
_look_pitch + command.pitch_delta_radians,
minimum_pitch_radians,
maximum_pitch_radians
)
_apply_rotation()
func present_telemetry(telemetry: VehicleTelemetry) -> void:
if telemetry == null:
return
_target_feedback_pitch = (
-telemetry.vertical_effort * maximum_feedback_pitch_radians
)
_target_feedback_roll = (
-telemetry.steering_effort * maximum_feedback_roll_radians
)
func look_yaw_radians() -> float:
return _look_yaw
func look_pitch_radians() -> float:
return _look_pitch
func _process(delta: float) -> void:
_feedback_pitch = lerpf(
_feedback_pitch,
_target_feedback_pitch,
1.0 - exp(-feedback_response * delta)
)
_feedback_roll = lerpf(
_feedback_roll,
_target_feedback_roll,
1.0 - exp(-feedback_response * delta)
)
_apply_rotation()
func _apply_rotation() -> void:
rotation = Vector3(
_look_pitch + _feedback_pitch,
_look_yaw,
_feedback_roll
)
@@ -0,0 +1,12 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://game/presentation/cockpit/cockpit_camera_controller.gd" id="1_camera"]
[node name="CockpitCameraRig" type="Node3D"]
position = Vector3(0, 0.52, -0.48)
script = ExtResource("1_camera")
[node name="Camera" type="Camera3D" parent="."]
current = true
fov = 82.0
near = 0.05
+61
View File
@@ -0,0 +1,61 @@
class_name CockpitHud
extends Control
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")
@onready var _alert_panel: PanelContainer = %AlertPanel
@onready var _alert_label: Label = %AlertLabel
@onready var _ejection_panel: PanelContainer = %EjectionPanel
@onready var _ejection_label: Label = %EjectionLabel
@onready var _terminal_panel: PanelContainer = %TerminalPanel
@onready var _terminal_label: Label = %TerminalLabel
func present(update: DiveUpdate, telemetry: VehicleTelemetry) -> void:
if update == null or update.snapshot == null or telemetry == null:
return
var snapshot = update.snapshot
_alert_panel.visible = snapshot.alerts.maximum != DiveAlertLevel.Value.NORMAL
_alert_label.text = "ALERTE · %s" % _alert_text(snapshot.alerts.maximum)
_ejection_panel.visible = snapshot.ejection.held_seconds > 0.0
_ejection_label.text = "ÉJECTION %.0f %%" % (snapshot.ejection.ratio * 100.0)
_terminal_panel.visible = snapshot.is_terminal()
if snapshot.is_terminal():
_terminal_label.text = "%s\nÉchap pour ouvrir le menu de pause." % [
_terminal_text(snapshot.terminal_reason)
]
func alert_overlay_visible() -> bool:
return _alert_panel.visible
func _alert_text(level: DiveAlertLevel.Value) -> String:
match level:
DiveAlertLevel.Value.NORMAL:
return tr("NORMAL")
DiveAlertLevel.Value.ATTENTION:
return tr("ATTENTION")
DiveAlertLevel.Value.RETURN:
return tr("RETOUR")
DiveAlertLevel.Value.CRITICAL:
return tr("CRITIQUE")
DiveAlertLevel.Value.FAILURE:
return tr("DÉFAILLANCE")
return tr("INCONNU")
func _terminal_text(reason: DiveTerminalReason.Value) -> String:
match reason:
DiveTerminalReason.Value.VOLUNTARY_EJECTION:
return tr("ÉJECTION DÉCLENCHÉE")
DiveTerminalReason.Value.OXYGEN_DEPLETED:
return tr("OXYGÈNE ÉPUISÉ")
DiveTerminalReason.Value.HULL_DESTROYED:
return tr("COQUE DÉTRUITE")
DiveTerminalReason.Value.RETURNED:
return tr("RETOUR TERMINÉ")
return tr("PLONGÉE TERMINÉE")
+109
View File
@@ -0,0 +1,109 @@
[gd_scene load_steps=4 format=3]
[ext_resource type="Script" path="res://game/presentation/cockpit/cockpit_hud.gd" id="1_hud"]
[sub_resource type="StyleBoxFlat" id="AlertStyle"]
bg_color = Color(0.14, 0.075, 0.02, 0.92)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(1, 0.75, 0.2, 1)
content_margin_left = 22.0
content_margin_top = 10.0
content_margin_right = 22.0
content_margin_bottom = 10.0
[sub_resource type="StyleBoxFlat" id="DangerStyle"]
bg_color = Color(0.16, 0.025, 0.018, 0.94)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(1, 0.42, 0.31, 1)
content_margin_left = 24.0
content_margin_top = 12.0
content_margin_right = 24.0
content_margin_bottom = 12.0
[node name="CockpitHud" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
script = ExtResource("1_hud")
[node name="AlertPanel" type="PanelContainer" parent="."]
unique_name_in_owner = true
visible = false
layout_mode = 1
anchors_preset = 10
anchor_right = 1.0
offset_left = 590.0
offset_top = 26.0
offset_right = -590.0
offset_bottom = 78.0
grow_horizontal = 2
theme_override_styles/panel = SubResource("AlertStyle")
[node name="AlertLabel" type="Label" parent="AlertPanel"]
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 = 20
text = "ALERTE · ATTENTION"
horizontal_alignment = 1
[node name="EjectionPanel" type="PanelContainer" parent="."]
unique_name_in_owner = true
visible = false
layout_mode = 1
anchors_preset = 7
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -190.0
offset_top = -94.0
offset_right = 190.0
offset_bottom = -36.0
grow_horizontal = 2
grow_vertical = 0
theme_override_styles/panel = SubResource("DangerStyle")
[node name="EjectionLabel" type="Label" parent="EjectionPanel"]
unique_name_in_owner = true
layout_mode = 2
theme_override_colors/font_color = Color(1, 0.48, 0.38, 1)
theme_override_font_sizes/font_size = 20
text = "ÉJECTION —"
horizontal_alignment = 1
[node name="TerminalPanel" type="PanelContainer" parent="."]
unique_name_in_owner = true
visible = false
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -280.0
offset_top = -82.0
offset_right = 280.0
offset_bottom = 82.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/panel = SubResource("DangerStyle")
[node name="TerminalLabel" type="Label" parent="TerminalPanel"]
unique_name_in_owner = true
layout_mode = 2
theme_override_colors/font_color = Color(1, 0.74, 0.68, 1)
theme_override_font_sizes/font_size = 24
text = "PLONGÉE TERMINÉE"
horizontal_alignment = 1
vertical_alignment = 1
@@ -0,0 +1,118 @@
class_name CockpitInstrumentPanel
extends Node3D
const DiveUpdate = preload("res://game/domain/dive/dive_update.gd")
const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd")
const ReturnEstimate = preload("res://game/domain/dive/return_estimate.gd")
const VehicleTelemetry = preload("res://game/application/dive/vehicle_telemetry.gd")
const COLOR_NORMAL := Color(0.45, 0.89, 0.90, 1.0)
const COLOR_ATTENTION := Color(1.0, 0.75, 0.20, 1.0)
const COLOR_CRITICAL := Color(1.0, 0.42, 0.31, 1.0)
@onready var _resources_label: Label3D = %ResourcesLabel
@onready var _navigation_label: Label3D = %NavigationLabel
@onready var _integrity_label: Label3D = %IntegrityLabel
@onready var _return_label: Label3D = %ReturnLabel
@onready var _status_label: Label3D = %StatusLabel
func present(update: DiveUpdate, telemetry: VehicleTelemetry) -> void:
if update == null or update.snapshot == null or telemetry == null:
return
var snapshot = update.snapshot
_resources_label.text = (
"BATTERIE\n%.1f / %.1f kWh · %.0f %%\n%.0f kW\n\nOXYGÈNE\n%.1f / %.1f L · %.0f %%\n%.1f L/min"
% [
snapshot.battery.remaining_kwh,
snapshot.battery.maximum_kwh,
snapshot.battery.ratio * 100.0,
snapshot.battery.current_power_kw,
snapshot.oxygen.remaining_l,
snapshot.oxygen.maximum_l,
snapshot.oxygen.ratio * 100.0,
snapshot.oxygen.consumption_l_per_min,
]
)
_navigation_label.text = (
"PROFONDEUR\n%.1f m\nMAX. CONSEILLÉ %.1f m\n\nVITESSE\n%.2f m/s"
% [
snapshot.depth.current_m,
snapshot.depth.certified_m,
telemetry.world_linear_velocity_mps.length(),
]
)
_integrity_label.text = (
"COQUE\n%.1f / %.1f · %.0f %%\n\nPRESSION\n%.2f / %.2f bar"
% [
snapshot.hull.current_integrity,
snapshot.hull.maximum_integrity,
snapshot.hull.ratio * 100.0,
snapshot.hull.exterior_pressure_bar,
snapshot.hull.certified_pressure_bar,
]
)
_return_label.text = _return_text(snapshot.return_estimate)
_status_label.text = "SYSTÈMES · %s" % _alert_text(snapshot.alerts.maximum)
_resources_label.modulate = _alert_color(
maxi(snapshot.alerts.battery, snapshot.alerts.oxygen)
)
_integrity_label.modulate = _alert_color(
maxi(snapshot.alerts.hull, snapshot.alerts.depth)
)
_status_label.modulate = _alert_color(snapshot.alerts.maximum)
func resources_text() -> String:
return _resources_label.text
func navigation_text() -> String:
return _navigation_label.text
func integrity_text() -> String:
return _integrity_label.text
func return_text() -> String:
return _return_label.text
func _return_text(estimate: ReturnEstimate) -> String:
match estimate.status:
ReturnEstimate.Status.SAFE:
return "RETOUR %.1f kWh · MARGE %.1f kWh" % [
estimate.required_kwh,
estimate.margin_kwh,
]
ReturnEstimate.Status.TIGHT:
return "RETOUR SERRÉ · MARGE %.1f kWh" % estimate.margin_kwh
ReturnEstimate.Status.INSUFFICIENT:
return "RETOUR INSUFFISANT · MANQUE %.1f kWh" % absf(
estimate.margin_kwh
)
return "RETOUR · INCONNU"
func _alert_text(level: DiveAlertLevel.Value) -> String:
match level:
DiveAlertLevel.Value.NORMAL:
return "NORMAL"
DiveAlertLevel.Value.ATTENTION:
return "ATTENTION"
DiveAlertLevel.Value.RETURN:
return "RETOUR"
DiveAlertLevel.Value.CRITICAL:
return "CRITIQUE"
DiveAlertLevel.Value.FAILURE:
return "DÉFAILLANCE"
return "INCONNU"
func _alert_color(level: DiveAlertLevel.Value) -> Color:
if level >= DiveAlertLevel.Value.CRITICAL:
return COLOR_CRITICAL
if level >= DiveAlertLevel.Value.ATTENTION:
return COLOR_ATTENTION
return COLOR_NORMAL
@@ -0,0 +1,450 @@
[gd_scene load_steps=19 format=3]
[ext_resource type="Script" path="res://game/presentation/cockpit/cockpit_instrument_panel.gd" id="1_instruments"]
[sub_resource type="StandardMaterial3D" id="SteelMaterial"]
albedo_color = Color(0.095, 0.12, 0.12, 1)
metallic = 0.82
roughness = 0.34
[sub_resource type="StandardMaterial3D" id="PaintedMaterial"]
albedo_color = Color(0.085, 0.185, 0.19, 1)
metallic = 0.58
roughness = 0.48
[sub_resource type="StandardMaterial3D" id="BrassMaterial"]
albedo_color = Color(0.36, 0.24, 0.075, 1)
metallic = 0.78
roughness = 0.31
[sub_resource type="StandardMaterial3D" id="RubberMaterial"]
albedo_color = Color(0.018, 0.024, 0.025, 1)
roughness = 0.88
[sub_resource type="StandardMaterial3D" id="GlassMaterial"]
transparency = 1
shading_mode = 0
albedo_color = Color(0.12, 0.42, 0.47, 0.09)
metallic = 0.12
roughness = 0.08
cull_mode = 2
[sub_resource type="StandardMaterial3D" id="ScreenMaterial"]
albedo_color = Color(0.006, 0.025, 0.028, 1)
metallic = 0.15
roughness = 0.22
emission_enabled = true
emission = Color(0.008, 0.07, 0.075, 1)
emission_energy_multiplier = 1.7
[sub_resource type="StandardMaterial3D" id="CyanMaterial"]
shading_mode = 0
albedo_color = Color(0.2, 0.78, 0.82, 1)
emission_enabled = true
emission = Color(0.12, 0.72, 0.76, 1)
emission_energy_multiplier = 3.2
[sub_resource type="StandardMaterial3D" id="AmberMaterial"]
shading_mode = 0
albedo_color = Color(0.95, 0.52, 0.08, 1)
emission_enabled = true
emission = Color(0.95, 0.33, 0.02, 1)
emission_energy_multiplier = 2.6
[sub_resource type="StandardMaterial3D" id="RedMaterial"]
shading_mode = 0
albedo_color = Color(0.82, 0.08, 0.045, 1)
emission_enabled = true
emission = Color(0.92, 0.055, 0.025, 1)
emission_energy_multiplier = 2.8
[sub_resource type="BoxMesh" id="UnitBoxMesh"]
size = Vector3(1, 1, 1)
[sub_resource type="TorusMesh" id="PortholeTorusMesh"]
inner_radius = 0.56
outer_radius = 0.71
rings = 48
ring_segments = 16
[sub_resource type="CylinderMesh" id="GlassCylinderMesh"]
top_radius = 0.56
bottom_radius = 0.56
height = 0.012
radial_segments = 64
[sub_resource type="CylinderMesh" id="BoltMesh"]
top_radius = 0.026
bottom_radius = 0.026
height = 0.045
radial_segments = 12
[sub_resource type="QuadMesh" id="ScreenQuadMesh"]
size = Vector2(1, 1)
[sub_resource type="CylinderMesh" id="LeverMesh"]
top_radius = 0.018
bottom_radius = 0.023
height = 0.24
radial_segments = 12
[sub_resource type="SphereMesh" id="KnobMesh"]
radius = 0.046
height = 0.092
radial_segments = 16
rings = 8
[sub_resource type="CylinderMesh" id="DialMesh"]
top_radius = 0.105
bottom_radius = 0.105
height = 0.035
radial_segments = 32
[node name="CockpitInterior" type="Node3D"]
script = ExtResource("1_instruments")
[node name="CabinShell" type="Node3D" parent="."]
[node name="LeftWall" type="MeshInstance3D" parent="CabinShell"]
position = Vector3(-1.03, 0.24, -0.15)
scale = Vector3(0.16, 1.82, 2.7)
mesh = SubResource("UnitBoxMesh")
material_override = SubResource("PaintedMaterial")
[node name="RightWall" type="MeshInstance3D" parent="CabinShell"]
position = Vector3(1.03, 0.24, -0.15)
scale = Vector3(0.16, 1.82, 2.7)
mesh = SubResource("UnitBoxMesh")
material_override = SubResource("PaintedMaterial")
[node name="Ceiling" type="MeshInstance3D" parent="CabinShell"]
position = Vector3(0, 1.22, -0.2)
scale = Vector3(2.15, 0.16, 2.6)
mesh = SubResource("UnitBoxMesh")
material_override = SubResource("SteelMaterial")
[node name="Floor" type="MeshInstance3D" parent="CabinShell"]
position = Vector3(0, -0.7, -0.1)
scale = Vector3(2.05, 0.18, 2.5)
mesh = SubResource("UnitBoxMesh")
material_override = SubResource("RubberMaterial")
[node name="RearBulkhead" type="MeshInstance3D" parent="CabinShell"]
position = Vector3(0, 0.22, 1.08)
scale = Vector3(2.08, 1.75, 0.16)
mesh = SubResource("UnitBoxMesh")
material_override = SubResource("SteelMaterial")
[node name="LeftRib" type="MeshInstance3D" parent="CabinShell"]
position = Vector3(-0.88, 0.26, -0.45)
scale = Vector3(0.11, 1.75, 0.18)
mesh = SubResource("UnitBoxMesh")
material_override = SubResource("BrassMaterial")
[node name="RightRib" type="MeshInstance3D" parent="CabinShell"]
position = Vector3(0.88, 0.26, -0.45)
scale = Vector3(0.11, 1.75, 0.18)
mesh = SubResource("UnitBoxMesh")
material_override = SubResource("BrassMaterial")
[node name="CeilingRib" type="MeshInstance3D" parent="CabinShell"]
position = Vector3(0, 1.05, -0.45)
scale = Vector3(1.82, 0.1, 0.18)
mesh = SubResource("UnitBoxMesh")
material_override = SubResource("BrassMaterial")
[node name="PortholeAssembly" type="Node3D" parent="."]
position = Vector3(0, 0.47, -1.32)
[node name="LeftBulkhead" type="MeshInstance3D" parent="PortholeAssembly"]
position = Vector3(-0.94, 0, 0)
scale = Vector3(0.6, 1.82, 0.2)
mesh = SubResource("UnitBoxMesh")
material_override = SubResource("SteelMaterial")
[node name="RightBulkhead" type="MeshInstance3D" parent="PortholeAssembly"]
position = Vector3(0.94, 0, 0)
scale = Vector3(0.6, 1.82, 0.2)
mesh = SubResource("UnitBoxMesh")
material_override = SubResource("SteelMaterial")
[node name="TopBulkhead" type="MeshInstance3D" parent="PortholeAssembly"]
position = Vector3(0, 0.91, 0)
scale = Vector3(1.55, 0.52, 0.2)
mesh = SubResource("UnitBoxMesh")
material_override = SubResource("SteelMaterial")
[node name="BottomBulkhead" type="MeshInstance3D" parent="PortholeAssembly"]
position = Vector3(0, -0.91, 0)
scale = Vector3(1.55, 0.52, 0.2)
mesh = SubResource("UnitBoxMesh")
material_override = SubResource("SteelMaterial")
[node name="WindowFrame" type="MeshInstance3D" parent="PortholeAssembly"]
unique_name_in_owner = true
rotation_degrees = Vector3(90, 0, 0)
mesh = SubResource("PortholeTorusMesh")
material_override = SubResource("BrassMaterial")
[node name="RubberSeal" type="MeshInstance3D" parent="PortholeAssembly"]
scale = Vector3(0.91, 0.91, 0.91)
rotation_degrees = Vector3(90, 0, 0)
mesh = SubResource("PortholeTorusMesh")
material_override = SubResource("RubberMaterial")
[node name="WindowGlass" type="MeshInstance3D" parent="PortholeAssembly"]
position = Vector3(0, 0, -0.01)
rotation_degrees = Vector3(90, 0, 0)
cast_shadow = 0
mesh = SubResource("GlassCylinderMesh")
material_override = SubResource("GlassMaterial")
[node name="BoltTop" type="MeshInstance3D" parent="PortholeAssembly"]
position = Vector3(0, 0.665, 0.1)
rotation_degrees = Vector3(90, 0, 0)
mesh = SubResource("BoltMesh")
material_override = SubResource("SteelMaterial")
[node name="BoltBottom" type="MeshInstance3D" parent="PortholeAssembly"]
position = Vector3(0, -0.665, 0.1)
rotation_degrees = Vector3(90, 0, 0)
mesh = SubResource("BoltMesh")
material_override = SubResource("SteelMaterial")
[node name="BoltLeft" type="MeshInstance3D" parent="PortholeAssembly"]
position = Vector3(-0.665, 0, 0.1)
rotation_degrees = Vector3(90, 0, 0)
mesh = SubResource("BoltMesh")
material_override = SubResource("SteelMaterial")
[node name="BoltRight" type="MeshInstance3D" parent="PortholeAssembly"]
position = Vector3(0.665, 0, 0.1)
rotation_degrees = Vector3(90, 0, 0)
mesh = SubResource("BoltMesh")
material_override = SubResource("SteelMaterial")
[node name="BoltUpperLeft" type="MeshInstance3D" parent="PortholeAssembly"]
position = Vector3(-0.47, 0.47, 0.1)
rotation_degrees = Vector3(90, 0, 0)
mesh = SubResource("BoltMesh")
material_override = SubResource("SteelMaterial")
[node name="BoltUpperRight" type="MeshInstance3D" parent="PortholeAssembly"]
position = Vector3(0.47, 0.47, 0.1)
rotation_degrees = Vector3(90, 0, 0)
mesh = SubResource("BoltMesh")
material_override = SubResource("SteelMaterial")
[node name="BoltLowerLeft" type="MeshInstance3D" parent="PortholeAssembly"]
position = Vector3(-0.47, -0.47, 0.1)
rotation_degrees = Vector3(90, 0, 0)
mesh = SubResource("BoltMesh")
material_override = SubResource("SteelMaterial")
[node name="BoltLowerRight" type="MeshInstance3D" parent="PortholeAssembly"]
position = Vector3(0.47, -0.47, 0.1)
rotation_degrees = Vector3(90, 0, 0)
mesh = SubResource("BoltMesh")
material_override = SubResource("SteelMaterial")
[node name="Dashboard" type="Node3D" parent="."]
unique_name_in_owner = true
position = Vector3(0, 0.05, -1.22)
rotation_degrees = Vector3(-12, 0, 0)
[node name="MainPanel" type="MeshInstance3D" parent="Dashboard"]
scale = Vector3(1.56, 0.44, 0.14)
mesh = SubResource("UnitBoxMesh")
material_override = SubResource("PaintedMaterial")
[node name="ResourcesFrame" type="MeshInstance3D" parent="Dashboard"]
position = Vector3(-0.5, 0.03, 0.088)
scale = Vector3(0.45, 0.31, 0.025)
mesh = SubResource("UnitBoxMesh")
material_override = SubResource("BrassMaterial")
[node name="ResourcesScreen" type="MeshInstance3D" parent="Dashboard"]
position = Vector3(-0.5, 0.03, 0.102)
scale = Vector3(0.41, 0.27, 1)
mesh = SubResource("ScreenQuadMesh")
material_override = SubResource("ScreenMaterial")
[node name="ResourcesLabel" type="Label3D" parent="Dashboard"]
unique_name_in_owner = true
position = Vector3(-0.5, 0.03, 0.108)
pixel_size = 0.00115
font_size = 20
outline_size = 2
text = "BATTERIE\n— / — kWh\n— kW\n\nOXYGÈNE\n— / — L\n— L/min"
modulate = Color(0.45, 0.89, 0.9, 1)
[node name="NavigationFrame" type="MeshInstance3D" parent="Dashboard"]
position = Vector3(0, 0.03, 0.088)
scale = Vector3(0.4, 0.31, 0.025)
mesh = SubResource("UnitBoxMesh")
material_override = SubResource("BrassMaterial")
[node name="NavigationScreen" type="MeshInstance3D" parent="Dashboard"]
position = Vector3(0, 0.03, 0.102)
scale = Vector3(0.36, 0.27, 1)
mesh = SubResource("ScreenQuadMesh")
material_override = SubResource("ScreenMaterial")
[node name="NavigationLabel" type="Label3D" parent="Dashboard"]
unique_name_in_owner = true
position = Vector3(0, 0.03, 0.108)
pixel_size = 0.00115
font_size = 20
outline_size = 2
text = "PROFONDEUR\n— m\nMAX. — m\n\nVITESSE\n— m/s"
modulate = Color(0.45, 0.89, 0.9, 1)
[node name="IntegrityFrame" type="MeshInstance3D" parent="Dashboard"]
position = Vector3(0.5, 0.03, 0.088)
scale = Vector3(0.45, 0.31, 0.025)
mesh = SubResource("UnitBoxMesh")
material_override = SubResource("BrassMaterial")
[node name="IntegrityScreen" type="MeshInstance3D" parent="Dashboard"]
position = Vector3(0.5, 0.03, 0.102)
scale = Vector3(0.41, 0.27, 1)
mesh = SubResource("ScreenQuadMesh")
material_override = SubResource("ScreenMaterial")
[node name="IntegrityLabel" type="Label3D" parent="Dashboard"]
unique_name_in_owner = true
position = Vector3(0.5, 0.03, 0.108)
pixel_size = 0.00115
font_size = 20
outline_size = 2
text = "COQUE\n— / —\n\nPRESSION\n— / — bar"
modulate = Color(0.45, 0.89, 0.9, 1)
[node name="ReturnLabel" type="Label3D" parent="Dashboard"]
unique_name_in_owner = true
position = Vector3(0.32, 0.205, 0.108)
pixel_size = 0.00082
font_size = 15
outline_size = 2
text = "RETOUR · INCONNU"
modulate = Color(1, 0.75, 0.2, 1)
[node name="StatusLabel" type="Label3D" parent="Dashboard"]
unique_name_in_owner = true
position = Vector3(-0.34, 0.205, 0.108)
pixel_size = 0.00082
font_size = 15
outline_size = 2
text = "SYSTÈMES · NORMAL"
modulate = Color(0.45, 0.89, 0.9, 1)
[node name="LeftConsole" type="MeshInstance3D" parent="."]
position = Vector3(-0.82, -0.23, -1.19)
rotation_degrees = Vector3(-8, 18, 4)
scale = Vector3(0.32, 0.16, 0.56)
mesh = SubResource("UnitBoxMesh")
material_override = SubResource("PaintedMaterial")
[node name="RightConsole" type="MeshInstance3D" parent="."]
position = Vector3(0.82, -0.23, -1.19)
rotation_degrees = Vector3(-8, -18, -4)
scale = Vector3(0.32, 0.16, 0.56)
mesh = SubResource("UnitBoxMesh")
material_override = SubResource("PaintedMaterial")
[node name="ThrottleLever" type="MeshInstance3D" parent="."]
position = Vector3(-0.79, -0.06, -1.13)
rotation_degrees = Vector3(18, 0, -10)
scale = Vector3(0.72, 0.72, 0.72)
mesh = SubResource("LeverMesh")
material_override = SubResource("SteelMaterial")
[node name="ThrottleKnob" type="MeshInstance3D" parent="."]
position = Vector3(-0.81, 0.025, -1.1)
scale = Vector3(0.72, 0.72, 0.72)
mesh = SubResource("KnobMesh")
material_override = SubResource("BrassMaterial")
[node name="VerticalLever" type="MeshInstance3D" parent="."]
position = Vector3(0.79, -0.06, -1.13)
rotation_degrees = Vector3(18, 0, 10)
scale = Vector3(0.72, 0.72, 0.72)
mesh = SubResource("LeverMesh")
material_override = SubResource("SteelMaterial")
[node name="VerticalKnob" type="MeshInstance3D" parent="."]
position = Vector3(0.81, 0.025, -1.1)
scale = Vector3(0.72, 0.72, 0.72)
mesh = SubResource("KnobMesh")
material_override = SubResource("BrassMaterial")
[node name="SonarMonitor" type="Node3D" parent="."]
position = Vector3(0.72, 0.4, -1.24)
rotation_degrees = Vector3(-5, -22, -2)
[node name="Housing" type="MeshInstance3D" parent="SonarMonitor"]
scale = Vector3(0.32, 0.24, 0.08)
mesh = SubResource("UnitBoxMesh")
material_override = SubResource("RubberMaterial")
[node name="SonarScreen" type="MeshInstance3D" parent="SonarMonitor"]
unique_name_in_owner = true
position = Vector3(0, 0.025, 0.06)
scale = Vector3(0.265, 0.165, 1)
mesh = SubResource("ScreenQuadMesh")
material_override = SubResource("ScreenMaterial")
[node name="SonarPlaceholder" type="Label3D" parent="SonarMonitor"]
position = Vector3(0, 0.025, 0.068)
pixel_size = 0.0009
font_size = 16
outline_size = 2
text = "SONAR\nMODULE EN ATTENTE"
modulate = Color(0.34, 0.6, 0.6, 1)
[node name="EmergencyDial" type="MeshInstance3D" parent="."]
position = Vector3(-0.6, 0.39, -1.15)
scale = Vector3(0.38, 0.38, 0.38)
rotation_degrees = Vector3(90, 0, 0)
mesh = SubResource("DialMesh")
material_override = SubResource("RedMaterial")
[node name="EmergencyLabel" type="Label3D" parent="."]
position = Vector3(-0.6, 0.3, -1.125)
pixel_size = 0.0009
font_size = 15
outline_size = 2
text = "ÉJECTION · X"
modulate = Color(1, 0.48, 0.38, 1)
[node name="CyanIndicator" type="MeshInstance3D" parent="."]
position = Vector3(-0.15, 0.305, -1.11)
scale = Vector3(0.25, 0.25, 0.25)
mesh = SubResource("KnobMesh")
material_override = SubResource("CyanMaterial")
[node name="AmberIndicator" type="MeshInstance3D" parent="."]
position = Vector3(0, 0.305, -1.11)
scale = Vector3(0.25, 0.25, 0.25)
mesh = SubResource("KnobMesh")
material_override = SubResource("AmberMaterial")
[node name="RedIndicator" type="MeshInstance3D" parent="."]
position = Vector3(0.15, 0.305, -1.11)
scale = Vector3(0.25, 0.25, 0.25)
mesh = SubResource("KnobMesh")
material_override = SubResource("RedMaterial")
[node name="CabinLight" type="OmniLight3D" parent="."]
position = Vector3(0, 0.92, -0.28)
light_color = Color(0.42, 0.75, 0.74, 1)
light_energy = 2.2
shadow_enabled = true
omni_range = 3.3
[node name="PanelLight" type="OmniLight3D" parent="."]
position = Vector3(0, 0.18, -0.63)
light_color = Color(0.23, 0.72, 0.76, 1)
light_energy = 0.8
shadow_enabled = false
omni_range = 1.35
@@ -0,0 +1,26 @@
class_name ConfirmationDialogScreen
extends Control
signal cancelled
signal confirmed(action_id: StringName)
var _action_id: StringName
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
%CancelButton.pressed.connect(func() -> void: cancelled.emit())
%ConfirmButton.pressed.connect(func() -> void: confirmed.emit(_action_id))
%CancelButton.grab_focus()
func configure(
action_id: StringName,
title: String,
message: String,
confirm_label: String
) -> void:
_action_id = action_id
%Title.text = title
%Message.text = message
%ConfirmButton.text = confirm_label
@@ -0,0 +1,74 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://game/presentation/confirmation/confirmation_dialog.gd" id="1_confirm"]
[node name="ConfirmationDialog" type="Control"]
process_mode = 3
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_confirm")
[node name="Dim" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0, 0, 0.72)
[node name="Center" type="CenterContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Panel" type="PanelContainer" parent="Center"]
custom_minimum_size = Vector2(650, 280)
layout_mode = 2
[node name="Margin" type="MarginContainer" parent="Center/Panel"]
layout_mode = 2
theme_override_constants/margin_left = 34
theme_override_constants/margin_top = 28
theme_override_constants/margin_right = 34
theme_override_constants/margin_bottom = 28
[node name="Layout" type="VBoxContainer" parent="Center/Panel/Margin"]
layout_mode = 2
theme_override_constants/separation = 18
[node name="Title" type="Label" parent="Center/Panel/Margin/Layout"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 28
text = "CONFIRMER"
[node name="Message" type="Label" parent="Center/Panel/Margin/Layout"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
text = "Cette action doit être confirmée."
autowrap_mode = 2
[node name="Actions" type="HBoxContainer" parent="Center/Panel/Margin/Layout"]
layout_mode = 2
alignment = 2
[node name="CancelButton" type="Button" parent="Center/Panel/Margin/Layout/Actions"]
unique_name_in_owner = true
custom_minimum_size = Vector2(190, 46)
layout_mode = 2
text = "RESTER EN PLONGÉE"
[node name="ConfirmButton" type="Button" parent="Center/Panel/Margin/Layout/Actions"]
unique_name_in_owner = true
custom_minimum_size = Vector2(210, 46)
layout_mode = 2
text = "CONFIRMER"
@@ -0,0 +1,141 @@
class_name DiagnosticDiveScreen
extends Control
signal menu_requested
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 ReturnEstimate = preload("res://game/domain/dive/return_estimate.gd")
@onready var _status_label: Label = %StatusLabel
@onready var _tick_label: Label = %TickLabel
@onready var _battery_label: Label = %BatteryLabel
@onready var _oxygen_label: Label = %OxygenLabel
@onready var _hull_label: Label = %HullLabel
@onready var _depth_label: Label = %DepthLabel
@onready var _return_label: Label = %ReturnLabel
@onready var _alert_label: Label = %AlertLabel
@onready var _event_label: Label = %EventLabel
@onready var _menu_button: Button = %MenuButton
func _ready() -> void:
_menu_button.pressed.connect(_on_menu_pressed)
func present(update: DiveUpdate) -> void:
if update == null or update.snapshot == null:
present_setup_error("Mise à jour de plongée absente")
return
if not update.accepted:
_status_label.text = "ÉTAT · PAS REFUSÉ · %s" % update.rejection_reason
else:
_status_label.text = "ÉTAT · SIMULATION DÉTERMINISTE · 60 HZ"
var snapshot = update.snapshot
_tick_label.text = "SESSION %s · TICK %d" % [snapshot.session_id, snapshot.tick_sequence]
_battery_label.text = (
"BATTERIE %.2f / %.2f kWh · %.0f %% · %.1f kW"
% [
snapshot.battery.remaining_kwh,
snapshot.battery.maximum_kwh,
snapshot.battery.ratio * 100.0,
snapshot.battery.current_power_kw,
]
)
_oxygen_label.text = (
"OXYGÈNE %.1f / %.1f L · %.0f %% · %.1f L/min · %.1f min"
% [
snapshot.oxygen.remaining_l,
snapshot.oxygen.maximum_l,
snapshot.oxygen.ratio * 100.0,
snapshot.oxygen.consumption_l_per_min,
snapshot.oxygen.endurance_minutes,
]
)
_hull_label.text = (
"COQUE %.1f / %.1f · %.0f %% · %.2f / %.2f bar recommandés"
% [
snapshot.hull.current_integrity,
snapshot.hull.maximum_integrity,
snapshot.hull.ratio * 100.0,
snapshot.hull.exterior_pressure_bar,
snapshot.hull.certified_pressure_bar,
]
)
_depth_label.text = (
"PROFONDEUR %.1f m · CERTIFIÉE %.1f m · CRITIQUE %.1f m"
% [
snapshot.depth.current_m,
snapshot.depth.certified_m,
snapshot.depth.critical_m,
]
)
_return_label.text = _format_return_estimate(snapshot.return_estimate)
_alert_label.text = "ALERTE MAXIMALE · %s" % _alert_level_text(snapshot.alerts.maximum)
_event_label.text = (
"TERMINAISON · %s · ÉVÉNEMENTS DU PAS · %d"
% [_terminal_reason_text(snapshot.terminal_reason), update.events.size()]
)
func present_setup_error(message: String) -> void:
_status_label.text = "ÉTAT · INITIALISATION IMPOSSIBLE"
_tick_label.text = message
func _on_menu_pressed() -> void:
menu_requested.emit()
func _format_return_estimate(estimate: ReturnEstimate) -> String:
match estimate.status:
ReturnEstimate.Status.UNKNOWN:
return "RETOUR · ESTIMATION INCONNUE"
ReturnEstimate.Status.SAFE:
return "RETOUR %.2f kWh requis · MARGE %.2f kWh · SÛR" % [
estimate.required_kwh,
estimate.margin_kwh,
]
ReturnEstimate.Status.TIGHT:
return "RETOUR %.2f kWh requis · MARGE %.2f kWh · SERRÉ" % [
estimate.required_kwh,
estimate.margin_kwh,
]
ReturnEstimate.Status.INSUFFICIENT:
return "RETOUR %.2f kWh requis · MANQUE %.2f kWh" % [
estimate.required_kwh,
absf(estimate.margin_kwh),
]
return "RETOUR · ÉTAT INCONNU"
func _alert_level_text(level: DiveAlertLevel.Value) -> String:
match level:
DiveAlertLevel.Value.NORMAL:
return tr("NORMAL")
DiveAlertLevel.Value.ATTENTION:
return tr("ATTENTION")
DiveAlertLevel.Value.RETURN:
return tr("RETOUR")
DiveAlertLevel.Value.CRITICAL:
return tr("CRITIQUE")
DiveAlertLevel.Value.FAILURE:
return tr("DÉFAILLANCE")
return tr("INCONNU")
func _terminal_reason_text(reason: DiveTerminalReason.Value) -> String:
match reason:
DiveTerminalReason.Value.NONE:
return tr("EN COURS")
DiveTerminalReason.Value.RETURNED:
return tr("RETOUR")
DiveTerminalReason.Value.VOLUNTARY_EJECTION:
return tr("ÉJECTION VOLONTAIRE")
DiveTerminalReason.Value.OXYGEN_DEPLETED:
return tr("OXYGÈNE ÉPUISÉ")
DiveTerminalReason.Value.HULL_DESTROYED:
return tr("COQUE DÉTRUITE")
return tr("INCONNU")
@@ -0,0 +1,119 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://game/presentation/diagnostic/diagnostic_dive_screen.gd" id="1_screen"]
[node name="DiagnosticDiveScreen" type="Control"]
unique_name_in_owner = true
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_screen")
[node name="Background" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.0118, 0.0549, 0.0745, 1)
mouse_filter = 2
[node name="Margin" type="MarginContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 72.0
offset_top = 56.0
offset_right = -72.0
offset_bottom = -56.0
grow_horizontal = 2
grow_vertical = 2
[node name="Rows" type="VBoxContainer" parent="Margin"]
layout_mode = 2
theme_override_constants/separation = 18
[node name="Title" type="Label" parent="Margin/Rows"]
layout_mode = 2
theme_override_colors/font_color = Color(0.4549, 0.8902, 0.898, 1)
theme_override_font_sizes/font_size = 30
text = "RANSOM'S WAKE · DIAGNOSTIC DE PLONGÉE"
[node name="StatusLabel" type="Label" parent="Margin/Rows"]
unique_name_in_owner = true
layout_mode = 2
theme_override_colors/font_color = Color(0.7647, 0.8157, 0.8275, 1)
theme_override_font_sizes/font_size = 16
text = "ÉTAT · INITIALISATION"
[node name="Separator" type="HSeparator" parent="Margin/Rows"]
layout_mode = 2
[node name="TickLabel" type="Label" parent="Margin/Rows"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "SESSION — · TICK —"
[node name="BatteryLabel" type="Label" parent="Margin/Rows"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 22
text = "BATTERIE —"
[node name="OxygenLabel" type="Label" parent="Margin/Rows"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 22
text = "OXYGÈNE —"
[node name="HullLabel" type="Label" parent="Margin/Rows"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 22
text = "COQUE —"
[node name="DepthLabel" type="Label" parent="Margin/Rows"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 22
text = "PROFONDEUR —"
[node name="ReturnLabel" type="Label" parent="Margin/Rows"]
unique_name_in_owner = true
layout_mode = 2
theme_override_colors/font_color = Color(0.9098, 0.7686, 0.4039, 1)
theme_override_font_sizes/font_size = 20
text = "RETOUR —"
[node name="AlertLabel" type="Label" parent="Margin/Rows"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 20
text = "ALERTE MAXIMALE —"
[node name="EventLabel" type="Label" parent="Margin/Rows"]
unique_name_in_owner = true
layout_mode = 2
theme_override_colors/font_color = Color(0.7647, 0.8157, 0.8275, 1)
theme_override_font_sizes/font_size = 16
text = "TERMINAISON — · ÉVÉNEMENTS —"
[node name="MenuButton" type="Button" parent="Margin/Rows"]
unique_name_in_owner = true
custom_minimum_size = Vector2(260, 44)
layout_mode = 2
size_flags_horizontal = 0
text = "RETOUR AU MENU"
[node name="Note" type="Label" parent="Margin/Rows"]
layout_mode = 2
size_flags_vertical = 10
theme_override_colors/font_color = Color(0.4824, 0.5804, 0.6078, 1)
text = "Écran technique temporaire · aucune règle de gameplay n'est calculée ici."
vertical_alignment = 2
@@ -0,0 +1,24 @@
class_name MainMenuScreen
extends Control
signal new_game_requested
signal quit_requested
@onready var _continue_button: Button = %ContinueButton
@onready var _new_game_button: Button = %NewGameButton
@onready var _quit_button: Button = %QuitButton
func _ready() -> void:
_continue_button.disabled = true
_continue_button.tooltip_text = "Aucune sauvegarde disponible"
_new_game_button.pressed.connect(_on_new_game_pressed)
_quit_button.pressed.connect(_on_quit_pressed)
func _on_new_game_pressed() -> void:
new_game_requested.emit()
func _on_quit_pressed() -> void:
quit_requested.emit()
@@ -0,0 +1,85 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://game/presentation/main_menu/main_menu_screen.gd" id="1_menu"]
[node name="MainMenuScreen" type="Control"]
unique_name_in_owner = true
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_menu")
[node name="Background" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.0078, 0.0353, 0.051, 1)
mouse_filter = 2
[node name="Center" type="CenterContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Menu" type="VBoxContainer" parent="Center"]
custom_minimum_size = Vector2(460, 0)
layout_mode = 2
theme_override_constants/separation = 14
[node name="Title" type="Label" parent="Center/Menu"]
layout_mode = 2
theme_override_colors/font_color = Color(0.4549, 0.8902, 0.898, 1)
theme_override_font_sizes/font_size = 46
text = "RANSOM'S WAKE"
horizontal_alignment = 1
[node name="Subtitle" type="Label" parent="Center/Menu"]
layout_mode = 2
theme_override_colors/font_color = Color(0.6588, 0.7333, 0.749, 1)
theme_override_font_sizes/font_size = 16
text = "FONDATION DE LA VERTICALE"
horizontal_alignment = 1
[node name="Spacer" type="Control" parent="Center/Menu"]
custom_minimum_size = Vector2(0, 24)
layout_mode = 2
[node name="ContinueButton" type="Button" parent="Center/Menu"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 52)
layout_mode = 2
text = "CONTINUER · AUCUNE SAUVEGARDE"
[node name="NewGameButton" type="Button" parent="Center/Menu"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 52)
layout_mode = 2
text = "NOUVELLE PARTIE"
[node name="SettingsButton" type="Button" parent="Center/Menu"]
custom_minimum_size = Vector2(0, 52)
layout_mode = 2
disabled = true
tooltip_text = "Écran de réglages prévu dans une tranche ultérieure"
text = "RÉGLAGES · À VENIR"
[node name="QuitButton" type="Button" parent="Center/Menu"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 52)
layout_mode = 2
text = "QUITTER"
[node name="TechnicalNote" type="Label" parent="Center/Menu"]
layout_mode = 2
theme_override_colors/font_color = Color(0.3882, 0.4824, 0.5059, 1)
text = "Build technique · Forward+ · simulation 60 Hz"
horizontal_alignment = 1
+18
View File
@@ -0,0 +1,18 @@
class_name PauseMenu
extends Control
signal resume_requested
signal controls_requested
signal abandon_requested
signal main_menu_requested
signal quit_requested
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
%ResumeButton.pressed.connect(func() -> void: resume_requested.emit())
%ControlsButton.pressed.connect(func() -> void: controls_requested.emit())
%AbandonButton.pressed.connect(func() -> void: abandon_requested.emit())
%MainMenuButton.pressed.connect(func() -> void: main_menu_requested.emit())
%QuitButton.pressed.connect(func() -> void: quit_requested.emit())
%ResumeButton.grab_focus()
+82
View File
@@ -0,0 +1,82 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://game/presentation/pause/pause_menu.gd" id="1_pause"]
[node name="PauseMenu" type="Control"]
process_mode = 3
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_pause")
[node name="Dim" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.01, 0.025, 0.04, 0.88)
[node name="Center" type="CenterContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Menu" type="VBoxContainer" parent="Center"]
custom_minimum_size = Vector2(430, 0)
layout_mode = 2
theme_override_constants/separation = 12
[node name="Title" type="Label" parent="Center/Menu"]
layout_mode = 2
theme_override_colors/font_color = Color(0.45, 0.89, 0.9, 1)
theme_override_font_sizes/font_size = 34
text = "PLONGÉE EN PAUSE"
horizontal_alignment = 1
[node name="State" type="Label" parent="Center/Menu"]
layout_mode = 2
theme_override_colors/font_color = Color(0.65, 0.73, 0.75, 1)
text = "SIMULATION ET CONSOMMATIONS SUSPENDUES"
horizontal_alignment = 1
[node name="Spacer" type="Control" parent="Center/Menu"]
custom_minimum_size = Vector2(0, 18)
layout_mode = 2
[node name="ResumeButton" type="Button" parent="Center/Menu"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 48)
layout_mode = 2
text = "REPRENDRE"
[node name="ControlsButton" type="Button" parent="Center/Menu"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 48)
layout_mode = 2
text = "RÉGLAGES · COMMANDES"
[node name="AbandonButton" type="Button" parent="Center/Menu"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 48)
layout_mode = 2
text = "ABANDONNER LA PLONGÉE"
[node name="MainMenuButton" type="Button" parent="Center/Menu"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 48)
layout_mode = 2
text = "RETOUR AU MENU PRINCIPAL"
[node name="QuitButton" type="Button" parent="Center/Menu"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 48)
layout_mode = 2
text = "QUITTER LE JEU"
@@ -0,0 +1,67 @@
class_name ControlsSettingsScreen
extends Control
signal rebind_requested(action_id: StringName)
signal restore_defaults_requested
signal conflict_resolution_requested(swap_bindings: bool)
signal back_requested
const InputBindingSnapshot = preload(
"res://game/application/input/input_binding_snapshot.gd"
)
const RebindResult = preload("res://game/application/input/rebind_result.gd")
@onready var _rows: VBoxContainer = %Rows
@onready var _status_label: Label = %StatusLabel
@onready var _conflict_panel: PanelContainer = %ConflictPanel
@onready var _conflict_label: Label = %ConflictLabel
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
%RestoreButton.pressed.connect(func() -> void: restore_defaults_requested.emit())
%BackButton.pressed.connect(func() -> void: back_requested.emit())
%SwapButton.pressed.connect(func() -> void: conflict_resolution_requested.emit(true))
%CancelConflictButton.pressed.connect(
func() -> void: conflict_resolution_requested.emit(false)
)
_conflict_panel.visible = false
func present_bindings(bindings: Array[InputBindingSnapshot]) -> void:
for child: Node in _rows.get_children():
child.queue_free()
for binding: InputBindingSnapshot in bindings:
var row := HBoxContainer.new()
row.custom_minimum_size.y = 42.0
var label := Label.new()
label.text = binding.label
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var button := Button.new()
button.custom_minimum_size.x = 180.0
button.text = binding.binding_text
button.pressed.connect(_on_binding_button_pressed.bind(binding.action_id))
row.add_child(label)
row.add_child(button)
_rows.add_child(row)
func _on_binding_button_pressed(action_id: StringName) -> void:
rebind_requested.emit(action_id)
func present_rebind_result(result: RebindResult) -> void:
_status_label.text = result.message
if result.conflicting_action_id.is_empty():
_conflict_panel.visible = false
return
_conflict_label.text = (
"La touche %s est déjà utilisée. Permuter les deux commandes ?"
% result.binding_text
)
_conflict_panel.visible = true
func show_waiting_for_key() -> void:
_status_label.text = "Appuyez sur une touche · Échap pour annuler"
_conflict_panel.visible = false
@@ -0,0 +1,108 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://game/presentation/settings/controls_settings_screen.gd" id="1_controls"]
[node name="ControlsSettingsScreen" type="Control"]
process_mode = 3
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_controls")
[node name="Dim" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.01, 0.025, 0.04, 0.96)
[node name="Margin" type="MarginContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 190.0
offset_top = 42.0
offset_right = -190.0
offset_bottom = -42.0
grow_horizontal = 2
grow_vertical = 2
[node name="Layout" type="VBoxContainer" parent="Margin"]
layout_mode = 2
theme_override_constants/separation = 12
[node name="Title" type="Label" parent="Margin/Layout"]
layout_mode = 2
theme_override_colors/font_color = Color(0.45, 0.89, 0.9, 1)
theme_override_font_sizes/font_size = 32
text = "RÉGLAGES · COMMANDES"
[node name="Description" type="Label" parent="Margin/Layout"]
layout_mode = 2
theme_override_colors/font_color = Color(0.65, 0.73, 0.75, 1)
text = "Le clavier oriente le sous-marin. La souris déplace uniquement le regard."
[node name="Rows" type="VBoxContainer" parent="Margin/Layout"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
theme_override_constants/separation = 5
[node name="StatusLabel" type="Label" parent="Margin/Layout"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 30)
layout_mode = 2
theme_override_colors/font_color = Color(0.91, 0.77, 0.4, 1)
text = "Sélectionnez une commande à remapper."
[node name="ConflictPanel" type="PanelContainer" parent="Margin/Layout"]
unique_name_in_owner = true
visible = false
layout_mode = 2
[node name="ConflictLayout" type="VBoxContainer" parent="Margin/Layout/ConflictPanel"]
layout_mode = 2
[node name="ConflictLabel" type="Label" parent="Margin/Layout/ConflictPanel/ConflictLayout"]
unique_name_in_owner = true
layout_mode = 2
text = "Cette touche est déjà utilisée."
horizontal_alignment = 1
[node name="Actions" type="HBoxContainer" parent="Margin/Layout/ConflictPanel/ConflictLayout"]
layout_mode = 2
alignment = 1
[node name="CancelConflictButton" type="Button" parent="Margin/Layout/ConflictPanel/ConflictLayout/Actions"]
unique_name_in_owner = true
custom_minimum_size = Vector2(150, 42)
layout_mode = 2
text = "ANNULER"
[node name="SwapButton" type="Button" parent="Margin/Layout/ConflictPanel/ConflictLayout/Actions"]
unique_name_in_owner = true
custom_minimum_size = Vector2(150, 42)
layout_mode = 2
text = "PERMUTER"
[node name="Footer" type="HBoxContainer" parent="Margin/Layout"]
layout_mode = 2
alignment = 2
[node name="RestoreButton" type="Button" parent="Margin/Layout/Footer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(220, 44)
layout_mode = 2
text = "VALEURS PAR DÉFAUT"
[node name="BackButton" type="Button" parent="Margin/Layout/Footer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(160, 44)
layout_mode = 2
text = "RETOUR"
@@ -0,0 +1,33 @@
class_name VehicleTestEnvironment
extends Node3D
func _ready() -> 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)
@@ -0,0 +1,132 @@
[gd_scene load_steps=12 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="StandardMaterial3D" id="RockMaterial"]
albedo_color = Color(0.075, 0.19, 0.19, 1)
roughness = 0.92
[sub_resource type="StandardMaterial3D" id="RockMaterialLight"]
albedo_color = Color(0.105, 0.25, 0.24, 1)
roughness = 0.88
[sub_resource type="BoxMesh" id="SeafloorMesh"]
size = Vector3(220, 8, 250)
[sub_resource type="BoxShape3D" id="SeafloorShape"]
size = Vector3(220, 8, 250)
[sub_resource type="BoxMesh" id="RockMeshA"]
size = Vector3(12, 30, 14)
[sub_resource type="BoxShape3D" id="RockShapeA"]
size = Vector3(12, 30, 14)
[sub_resource type="BoxMesh" id="RockMeshB"]
size = Vector3(18, 42, 16)
[sub_resource type="BoxShape3D" id="RockShapeB"]
size = Vector3(18, 42, 16)
[sub_resource type="SphereMesh" id="BeaconMesh"]
radius = 0.55
height = 1.1
[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="Seafloor" type="StaticBody3D" parent="."]
position = Vector3(0, -84, -58)
[node name="Mesh" type="MeshInstance3D" parent="Seafloor"]
mesh = SubResource("SeafloorMesh")
material_override = SubResource("RockMaterial")
[node name="Collision" type="CollisionShape3D" parent="Seafloor"]
shape = SubResource("SeafloorShape")
[node name="RockNearLeft" type="StaticBody3D" parent="."]
position = Vector3(-10, -18, -18)
rotation_degrees = Vector3(0, -14, 8)
[node name="Mesh" type="MeshInstance3D" parent="RockNearLeft"]
mesh = SubResource("RockMeshA")
material_override = SubResource("RockMaterialLight")
[node name="Collision" type="CollisionShape3D" parent="RockNearLeft"]
shape = SubResource("RockShapeA")
[node name="RockNearRight" type="StaticBody3D" parent="."]
position = Vector3(12, -22, -32)
rotation_degrees = Vector3(0, 18, -5)
[node name="Mesh" type="MeshInstance3D" parent="RockNearRight"]
mesh = SubResource("RockMeshB")
material_override = SubResource("RockMaterial")
[node name="Collision" type="CollisionShape3D" parent="RockNearRight"]
shape = SubResource("RockShapeB")
[node name="RockFarLeft" type="StaticBody3D" parent="."]
position = Vector3(-18, -48, -68)
rotation_degrees = Vector3(0, 9, 4)
[node name="Mesh" type="MeshInstance3D" parent="RockFarLeft"]
mesh = SubResource("RockMeshA")
material_override = SubResource("RockMaterialLight")
[node name="Collision" type="CollisionShape3D" parent="RockFarLeft"]
shape = SubResource("RockShapeA")
[node name="RockFarRight" type="StaticBody3D" parent="."]
position = Vector3(20, -58, -94)
rotation_degrees = Vector3(0, -11, -7)
[node name="Mesh" type="MeshInstance3D" parent="RockFarRight"]
mesh = SubResource("RockMeshB")
material_override = SubResource("RockMaterial")
[node name="Collision" type="CollisionShape3D" parent="RockFarRight"]
shape = SubResource("RockShapeB")
[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
+139
View File
@@ -0,0 +1,139 @@
class_name SubmarineActor
extends CharacterBody3D
signal collision_sampled(sample: VehicleCollisionSample)
const VehicleCommand = preload("res://game/application/vehicle/vehicle_command.gd")
const VehicleCollisionSample = preload(
"res://game/application/vehicle/vehicle_collision_sample.gd"
)
@export var cruise_speed_mps := 6.0
@export var high_thrust_speed_mps := 9.5
@export var acceleration_mps2 := 2.0
@export var braking_mps2 := 1.35
@export var vertical_speed_mps := 3.5
@export var vertical_acceleration_mps2 := 1.8
@export var lateral_grip_mps2 := 2.2
@export var maximum_yaw_speed_degrees := 45.0
@export var yaw_acceleration_degrees := 30.0
@export var yaw_braking_degrees := 24.0
@export var maximum_depth_m := 500.0
var _command: VehicleCommand = VehicleCommand.neutral(0)
var _yaw_velocity_rps := 0.0
var _collision_sequence := 0
var _last_longitudinal_effort := 0.0
var _last_steering_effort := 0.0
var _last_vertical_effort := 0.0
var _last_high_thrust_active := false
func apply_vehicle_command(command: VehicleCommand) -> void:
_command = command
func current_depth_m() -> float:
return maxf(-global_position.y, 0.0)
func current_yaw_velocity_rps() -> float:
return _yaw_velocity_rps
func actual_longitudinal_effort() -> float:
return _last_longitudinal_effort
func actual_steering_effort() -> float:
return _last_steering_effort
func actual_vertical_effort() -> float:
return _last_vertical_effort
func high_thrust_active() -> bool:
return _last_high_thrust_active
func _physics_process(delta: float) -> void:
_update_yaw(delta)
_update_velocity(delta)
var velocity_before_move := velocity
move_and_slide()
_keep_inside_vertical_bounds()
_publish_collisions(velocity_before_move)
func _update_yaw(delta: float) -> void:
var maximum_yaw_speed := deg_to_rad(maximum_yaw_speed_degrees)
var target_yaw_velocity := -_command.steering * maximum_yaw_speed
var yaw_change_degrees := (
yaw_acceleration_degrees
if not is_zero_approx(_command.steering)
else yaw_braking_degrees
)
_yaw_velocity_rps = move_toward(
_yaw_velocity_rps,
target_yaw_velocity,
deg_to_rad(yaw_change_degrees) * delta
)
rotation.y = wrapf(rotation.y + _yaw_velocity_rps * delta, -PI, PI)
_last_steering_effort = _command.steering
func _update_velocity(delta: float) -> void:
var local_velocity := global_basis.inverse() * velocity
var requested_speed := (
high_thrust_speed_mps if _command.high_thrust_active else cruise_speed_mps
)
var target_forward_speed := _command.longitudinal * requested_speed
var current_forward_speed := -local_velocity.z
var forward_change_rate := (
acceleration_mps2
if not is_zero_approx(target_forward_speed)
else braking_mps2
)
current_forward_speed = move_toward(
current_forward_speed,
target_forward_speed,
forward_change_rate * delta
)
local_velocity.z = -current_forward_speed
var target_vertical_speed := _command.vertical * vertical_speed_mps
local_velocity.y = move_toward(
local_velocity.y,
target_vertical_speed,
vertical_acceleration_mps2 * delta
)
local_velocity.x = move_toward(local_velocity.x, 0.0, lateral_grip_mps2 * delta)
velocity = global_basis * local_velocity
_last_longitudinal_effort = _command.longitudinal
_last_vertical_effort = _command.vertical
_last_high_thrust_active = _command.high_thrust_active
func _keep_inside_vertical_bounds() -> void:
if global_position.y > -2.0:
global_position.y = -2.0
velocity.y = minf(velocity.y, 0.0)
if global_position.y < -maximum_depth_m:
global_position.y = -maximum_depth_m
velocity.y = maxf(velocity.y, 0.0)
func _publish_collisions(previous_velocity: Vector3) -> void:
for collision_index in get_slide_collision_count():
var collision := get_slide_collision(collision_index)
var impact_speed := absf(previous_velocity.dot(collision.get_normal()))
if impact_speed <= 0.01:
continue
var sample_id := StringName(
"collision-%d-%d" % [_collision_sequence, collision_index]
)
collision_sampled.emit(
VehicleCollisionSample.new(sample_id, _collision_sequence, impact_speed)
)
_collision_sequence += 1
+75
View File
@@ -0,0 +1,75 @@
[gd_scene load_steps=8 format=3]
[ext_resource type="Script" path="res://game/world/submarine/submarine_actor.gd" id="1_actor"]
[sub_resource type="CapsuleShape3D" id="CapsuleCollision"]
radius = 0.72
height = 3.8
[sub_resource type="CapsuleMesh" id="CapsuleHull"]
radius = 0.72
height = 3.8
[sub_resource type="StandardMaterial3D" id="HullMaterial"]
albedo_color = Color(0.64, 0.48, 0.17, 1)
metallic = 0.72
roughness = 0.34
[sub_resource type="SphereMesh" id="CanopyMesh"]
radius = 0.58
height = 1.05
[sub_resource type="StandardMaterial3D" id="CanopyMaterial"]
transparency = 1
albedo_color = Color(0.04, 0.18, 0.25, 0.72)
metallic = 0.18
roughness = 0.16
[sub_resource type="BoxMesh" id="FinMesh"]
size = Vector3(1.8, 0.12, 1.2)
[node name="SubmarineActor" type="CharacterBody3D"]
script = ExtResource("1_actor")
[node name="CollisionShape" type="CollisionShape3D" parent="."]
rotation = Vector3(1.5708, 0, 0)
shape = SubResource("CapsuleCollision")
[node name="Hull" type="MeshInstance3D" parent="."]
rotation = Vector3(1.5708, 0, 0)
mesh = SubResource("CapsuleHull")
material_override = SubResource("HullMaterial")
[node name="Canopy" type="MeshInstance3D" parent="."]
position = Vector3(0, 0.36, -0.45)
scale = Vector3(0.85, 0.55, 1)
mesh = SubResource("CanopyMesh")
material_override = SubResource("CanopyMaterial")
[node name="LeftFin" type="MeshInstance3D" parent="."]
position = Vector3(-0.92, 0, 0.85)
mesh = SubResource("FinMesh")
material_override = SubResource("HullMaterial")
[node name="RightFin" type="MeshInstance3D" parent="."]
position = Vector3(0.92, 0, 0.85)
mesh = SubResource("FinMesh")
material_override = SubResource("HullMaterial")
[node name="LeftProjector" type="SpotLight3D" parent="."]
position = Vector3(-0.42, 0.05, -2.02)
rotation_degrees = Vector3(-3, 6, 0)
light_color = Color(0.72, 0.91, 1, 1)
light_energy = 22.0
shadow_enabled = true
spot_range = 58.0
spot_angle = 24.0
[node name="RightProjector" type="SpotLight3D" parent="."]
position = Vector3(0.42, 0.05, -2.02)
rotation_degrees = Vector3(-3, -6, 0)
light_color = Color(0.72, 0.91, 1, 1)
light_energy = 22.0
shadow_enabled = true
spot_range = 58.0
spot_angle = 24.0
@@ -0,0 +1,14 @@
class_name SubmarineMotionAdapter
extends "res://game/application/interfaces/vehicle_motion_port.gd"
const SubmarineActor = preload("res://game/world/submarine/submarine_actor.gd")
var _actor: SubmarineActor
func _init(actor: SubmarineActor) -> void:
_actor = actor
func apply_command(command: VehicleCommand) -> void:
_actor.apply_vehicle_command(command)
@@ -0,0 +1,27 @@
class_name SubmarineTelemetryAdapter
extends RefCounted
const VehicleTelemetry = preload("res://game/application/dive/vehicle_telemetry.gd")
const SubmarineActor = preload("res://game/world/submarine/submarine_actor.gd")
var _actor: SubmarineActor
func _init(actor: SubmarineActor) -> void:
_actor = actor
func sample(session_id: StringName, sequence: int) -> VehicleTelemetry:
return VehicleTelemetry.new(
session_id,
sequence,
_actor.global_position,
_actor.global_transform.basis.get_rotation_quaternion(),
_actor.velocity,
Vector3(0.0, _actor.current_yaw_velocity_rps(), 0.0),
_actor.current_depth_m(),
_actor.actual_longitudinal_effort(),
_actor.actual_steering_effort(),
_actor.actual_vertical_effort(),
_actor.high_thrust_active()
)