extends Node3D const SonarHudScript = preload("res://src/sonar_hud.gd") const SonarModelScript = preload("res://src/sonar_model.gd") const SonarSystemScript = preload("res://src/sonar_system.gd") const START_POSITION := Vector3(0.0, -25.0, 22.0) const METAL_CONTACT_POSITION := Vector3(0.0, -34.0, -45.0) var _submarine: SubmarineController var _sonar: Node var _hud: CanvasLayer var _ping_audio: AudioStreamPlayer var _ping_audio_time_remaining := 0.0 func _ready() -> void: _ensure_input_actions() _build_environment() _build_calibration_site() _hud = SonarHudScript.new() _hud.name = "SonarHud" _hud.expanded_changed.connect(_on_sonar_expanded_changed) add_child(_hud) _submarine = SubmarineController.new() _submarine.name = "Submarine" _submarine.position = START_POSITION _submarine.mission_origin = START_POSITION _submarine.telemetry_updated.connect(_hud.update_telemetry) add_child(_submarine) _sonar = SonarSystemScript.new() _sonar.name = "SonarSystem" _sonar.ping_started.connect(_on_ping_started) _sonar.ping_progress.connect(_on_ping_progress) _sonar.ping_completed.connect(_on_ping_completed) add_child(_sonar) _sonar.setup(_submarine, self) _ping_audio = AudioStreamPlayer.new() _ping_audio.name = "SonarAudio" add_child(_ping_audio) func _process(delta: float) -> void: _hud.update_sonar_pose(_submarine.global_transform) if _ping_audio_time_remaining > 0.0: _ping_audio_time_remaining = maxf(0.0, _ping_audio_time_remaining - delta) if _ping_audio_time_remaining <= 0.0: _ping_audio.stop() _ping_audio.stream = null func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("sonar_ping"): _sonar.request_ping() elif event.is_action_pressed("toggle_sonar_map"): _hud.toggle_expanded() get_viewport().set_input_as_handled() elif event.is_action_pressed("toggle_mouse_capture") and _hud.is_expanded(): _hud.set_expanded(false) get_viewport().set_input_as_handled() func _on_ping_started() -> void: _hud.show_ping_started(_sonar.max_range) _play_sonar_ping() func _on_ping_progress( current_cells: Array[Dictionary], previous_cells: Array[Dictionary], progress: float ) -> void: _hud.show_ping_progress( current_cells, previous_cells, _sonar.max_range, progress ) func _on_ping_completed( current_cells: Array[Dictionary], previous_cells: Array[Dictionary] ) -> void: _hud.show_ping_results(current_cells, previous_cells, _sonar.max_range) func _on_sonar_expanded_changed(is_expanded: bool) -> void: Input.mouse_mode = ( Input.MOUSE_MODE_VISIBLE if is_expanded else Input.MOUSE_MODE_CAPTURED ) func _build_environment() -> void: var world_environment := WorldEnvironment.new() var environment := Environment.new() environment.background_mode = Environment.BG_COLOR environment.background_color = Color("020d13") environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR environment.ambient_light_color = Color("2b6870") environment.ambient_light_energy = 0.46 environment.fog_enabled = true environment.fog_light_color = Color("19434b") environment.fog_light_energy = 0.46 environment.fog_density = 0.010 world_environment.environment = environment add_child(world_environment) var light := DirectionalLight3D.new() light.rotation_degrees = Vector3(-48.0, -22.0, 0.0) light.light_color = Color("86c8cf") light.light_energy = 0.34 add_child(light) _add_water_particles() func _build_calibration_site() -> void: _add_bathymetric_terrain() _add_metal_contact() func _add_bathymetric_terrain() -> void: var terrain := StaticBody3D.new() terrain.name = "ContinuousCalibrationSeafloor" terrain.set_meta("sonar_material", SonarModelScript.MaterialCategory.ROCK) terrain.set_meta("sonar_reflectivity", 0.72) add_child(terrain) var mesh := _build_bathymetric_terrain_mesh() var mesh_instance := MeshInstance3D.new() mesh_instance.mesh = mesh var material := StandardMaterial3D.new() material.albedo_color = Color("315f5c") material.roughness = 0.96 mesh_instance.material_override = material terrain.add_child(mesh_instance) var collision := CollisionShape3D.new() var shape := ConcavePolygonShape3D.new() shape.set_faces(mesh.get_faces()) shape.backface_collision = true collision.shape = shape terrain.add_child(collision) func _build_bathymetric_terrain_mesh() -> ArrayMesh: var column_count := 61 var row_count := 69 var terrain_width := 150.0 var terrain_length := 170.0 var terrain_center_z := -28.0 var vertices: Array[Vector3] = [] var normals: Array[Vector3] = [] var indices: Array[int] = [] for row in row_count: var z := terrain_center_z + lerpf( terrain_length * 0.5, -terrain_length * 0.5, float(row) / float(row_count - 1) ) for column in column_count: var x := lerpf( -terrain_width * 0.5, terrain_width * 0.5, float(column) / float(column_count - 1) ) vertices.append(Vector3(x, _terrain_height_at(x, z), z)) normals.append(Vector3.ZERO) for row in row_count - 1: for column in column_count - 1: var near_left := row * column_count + column var near_right := near_left + 1 var far_left := (row + 1) * column_count + column var far_right := far_left + 1 _append_terrain_triangle( near_left, near_right, far_left, vertices, normals, indices ) _append_terrain_triangle( near_right, far_right, far_left, vertices, normals, indices ) for normal_index in normals.size(): normals[normal_index] = normals[normal_index].normalized() var arrays: Array = [] arrays.resize(Mesh.ARRAY_MAX) arrays[Mesh.ARRAY_VERTEX] = PackedVector3Array(vertices) arrays[Mesh.ARRAY_NORMAL] = PackedVector3Array(normals) arrays[Mesh.ARRAY_INDEX] = PackedInt32Array(indices) var mesh := ArrayMesh.new() mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) return mesh func _append_terrain_triangle( first: int, second: int, third: int, vertices: Array[Vector3], normals: Array[Vector3], indices: Array[int] ) -> void: var normal := (vertices[second] - vertices[first]).cross( vertices[third] - vertices[first] ).normalized() if normal.y < 0.0: var swapped := second second = third third = swapped normal = -normal indices.append_array([first, second, third]) normals[first] += normal normals[second] += normal normals[third] += normal func _terrain_height_at(x: float, z: float) -> float: var base := -39.4 + (z + 12.0) * 0.018 base += sin(x * 0.11) * 0.28 + cos(z * 0.085) * 0.24 var height := base height += _gaussian_height(x, z, -12.0, -8.0, 9.2, 6.0, 8.0) height += _gaussian_height(x, z, 14.0, -15.0, 8.2, 6.5, 8.5) height += _gaussian_height(x, z, -10.0, -42.0, 16.0, 7.5, 14.0) height += _gaussian_height(x, z, 10.0, -42.0, 16.0, 7.5, 14.0) height += _gaussian_height(x, z, 0.0, -53.0, 12.0, 15.0, 7.5) # This narrow negative ridge cuts a readable entrance into the mound. It is # intentionally a bathymetric approximation rather than an impossible # overhang in a height field. height -= _gaussian_height(x, z, 0.0, -41.0, 19.0, 4.2, 16.0) return maxf(height, base - 1.0) func _gaussian_height( x: float, z: float, center_x: float, center_z: float, amplitude: float, width_x: float, width_z: float ) -> float: var normalized_x := (x - center_x) / maxf(width_x, 0.001) var normalized_z := (z - center_z) / maxf(width_z, 0.001) return amplitude * exp(-0.5 * (normalized_x * normalized_x + normalized_z * normalized_z)) func _add_rock_box( node_name: String, box_position: Vector3, box_size: Vector3, box_rotation_degrees: Vector3 = Vector3.ZERO ) -> StaticBody3D: var body := StaticBody3D.new() body.name = node_name body.position = box_position body.rotation_degrees = box_rotation_degrees body.set_meta("sonar_material", SonarModelScript.MaterialCategory.ROCK) body.set_meta("sonar_reflectivity", 0.68) add_child(body) var mesh_instance := MeshInstance3D.new() var mesh := BoxMesh.new() mesh.size = box_size mesh_instance.mesh = mesh var material := StandardMaterial3D.new() material.albedo_color = Color("315b59") material.roughness = 0.92 mesh_instance.material_override = material body.add_child(mesh_instance) var collision := CollisionShape3D.new() var shape := BoxShape3D.new() shape.size = box_size collision.shape = shape body.add_child(collision) return body func _add_metal_contact() -> void: var contact := StaticBody3D.new() contact.name = "MetalCalibrationObject" contact.position = Vector3( METAL_CONTACT_POSITION.x, _terrain_height_at(METAL_CONTACT_POSITION.x, METAL_CONTACT_POSITION.z) + 2.5, METAL_CONTACT_POSITION.z ) contact.rotation_degrees.y = 16.0 contact.set_meta("sonar_material", SonarModelScript.MaterialCategory.METAL) contact.set_meta("sonar_reflectivity", 1.0) add_child(contact) var mesh_instance := MeshInstance3D.new() var mesh := BoxMesh.new() mesh.size = Vector3(8.0, 5.0, 7.0) mesh_instance.mesh = mesh var material := StandardMaterial3D.new() material.albedo_color = Color("855a35") material.metallic = 0.88 material.roughness = 0.30 mesh_instance.material_override = material contact.add_child(mesh_instance) var collision := CollisionShape3D.new() var shape := BoxShape3D.new() shape.size = Vector3(8.0, 5.0, 7.0) collision.shape = shape contact.add_child(collision) func _add_water_particles() -> void: var particles := GPUParticles3D.new() particles.name = "CalibrationWaterParticles" particles.position = Vector3(0.0, -28.0, -15.0) particles.amount = 280 particles.lifetime = 13.0 particles.preprocess = 13.0 particles.visibility_aabb = AABB( Vector3(-55.0, -24.0, -65.0), Vector3(110.0, 48.0, 130.0) ) var motion := ParticleProcessMaterial.new() motion.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_BOX motion.emission_box_extents = Vector3(50.0, 20.0, 58.0) motion.direction = Vector3(0.2, 0.08, 0.05) motion.spread = 45.0 motion.initial_velocity_min = 0.02 motion.initial_velocity_max = 0.09 motion.gravity = Vector3.ZERO motion.scale_min = 0.035 motion.scale_max = 0.10 particles.process_material = motion var quad := QuadMesh.new() quad.size = Vector2(0.07, 0.07) var material := StandardMaterial3D.new() material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA material.billboard_mode = BaseMaterial3D.BILLBOARD_ENABLED material.albedo_color = Color(0.58, 0.86, 0.90, 0.35) quad.material = material particles.draw_pass_1 = quad add_child(particles) func _play_sonar_ping() -> void: var generator := AudioStreamGenerator.new() generator.mix_rate = 22050.0 generator.buffer_length = 0.5 _ping_audio.stream = generator _ping_audio.play() var playback := _ping_audio.get_stream_playback() as AudioStreamGeneratorPlayback if playback == null: return var duration := 0.42 _ping_audio_time_remaining = duration + 0.08 var frame_count := int(generator.mix_rate * duration) for frame_index in frame_count: var time := float(frame_index) / generator.mix_rate var frequency := lerpf(1180.0, 640.0, time / duration) var envelope := exp(-time * 7.5) * minf(time * 80.0, 1.0) var sample := sin(TAU * frequency * time) * envelope * 0.22 playback.push_frame(Vector2(sample, sample)) func _ensure_input_actions() -> void: _register_key_action("move_forward", KEY_W) _register_key_action("move_backward", KEY_S) _register_key_action("move_left", KEY_A) _register_key_action("move_right", KEY_D) _register_key_action("ascend", KEY_SPACE) _register_key_action("descend", KEY_CTRL) _register_key_action("boost", KEY_SHIFT) _register_key_action("abandon_vehicle", KEY_X) _register_key_action("toggle_mouse_capture", KEY_ESCAPE) _register_key_action("sonar_ping", KEY_F) _register_key_action("toggle_sonar_map", KEY_TAB) func _register_key_action(action_name: StringName, physical_keycode: Key) -> void: if not InputMap.has_action(action_name): InputMap.add_action(action_name) if InputMap.action_get_events(action_name).is_empty(): var key_event := InputEventKey.new() key_event.physical_keycode = physical_keycode InputMap.action_add_event(action_name, key_event)