From b004a65e962d78b89698597af3dec31074ac94c5 Mon Sep 17 00:00:00 2001 From: Floppyrj45 Date: Sun, 19 Apr 2026 17:07:08 +0200 Subject: [PATCH] feat(dolphin): swim controller + third-person camera + HUD stats Co-Authored-By: Claude Sonnet 4.6 --- project.godot | 73 +++++++- scenes/Dolphin.tscn | 176 ++++++++++++++++++ scripts/dolphin/DolphinController.gd | 261 +++++++++++++++++++++++++++ scripts/dolphin/EcholocationPulse.gd | 46 +++++ scripts/dolphin/HUD.gd | 39 ++++ 5 files changed, 594 insertions(+), 1 deletion(-) create mode 100644 scenes/Dolphin.tscn create mode 100644 scripts/dolphin/DolphinController.gd create mode 100644 scripts/dolphin/EcholocationPulse.gd create mode 100644 scripts/dolphin/HUD.gd diff --git a/project.godot b/project.godot index 5022fef..6cf6e8e 100644 --- a/project.godot +++ b/project.godot @@ -14,7 +14,78 @@ window/size/viewport_height=720 window/size/mode=2 [input] -; Bindings custom — à compléter plus tard par le module dauphin + +move_forward={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null) +] +} + +move_backward={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) +] +} + +strafe_left={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) +] +} + +strafe_right={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) +] +} + +ascend={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null) +] +} + +descend={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":16777237,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} + +boost={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":16777238,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} + +echolocate={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null) +] +} + +break_block={ +"deadzone": 0.5, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"button_index":1,"factor":1.0,"script":null) +] +} + +place_block={ +"deadzone": 0.5, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"button_index":2,"factor":1.0,"script":null) +] +} + +toggle_inventory={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":16777217,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} + +escape={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":16777217,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} [autoload] BlockDatabase="*res://scripts/world/BlockDatabase.gd" diff --git a/scenes/Dolphin.tscn b/scenes/Dolphin.tscn new file mode 100644 index 0000000..c4f8b24 --- /dev/null +++ b/scenes/Dolphin.tscn @@ -0,0 +1,176 @@ +[gd_scene load_steps=22 format=3 uid="uid://dolphin_main"] + +[ext_resource type="Script" path="res://scripts/dolphin/DolphinController.gd" id="1_controller"] +[ext_resource type="Script" path="res://scripts/dolphin/HUD.gd" id="2_hud"] +[ext_resource type="Script" path="res://scripts/dolphin/EcholocationPulse.gd" id="3_echo"] + +[sub_resource type="CapsuleShape3D" id="1_colshape"] +radius = 0.4 +height = 2.0 + +[sub_resource type="CapsuleMesh" id="2_body_mesh"] +radius = 0.4 +height = 2.0 + +[sub_resource type="StandardMaterial3D" id="3_body_mat"] +albedo_color = Color(0.29, 0.478, 0.584, 1) + +[sub_resource type="CapsuleMesh" id="4_belly_mesh"] +radius = 0.35 +height = 1.6 + +[sub_resource type="StandardMaterial3D" id="5_belly_mat"] +albedo_color = Color(0.91, 0.933, 0.949, 1) + +[sub_resource type="CapsuleMesh" id="6_tail_mesh"] +radius = 0.2 +height = 0.9 + +[sub_resource type="StandardMaterial3D" id="7_tail_mat"] +albedo_color = Color(0.29, 0.478, 0.584, 1) + +[sub_resource type="PrismMesh" id="8_fin_mesh"] +size = Vector3(0.15, 0.4, 0.08) + +[sub_resource type="StandardMaterial3D" id="9_fin_mat"] +albedo_color = Color(0.29, 0.478, 0.584, 1) + +[sub_resource type="CapsuleMesh" id="10_pec_mesh"] +radius = 0.1 +height = 0.5 + +[sub_resource type="StandardMaterial3D" id="11_pec_mat"] +albedo_color = Color(0.29, 0.478, 0.584, 1) + +[sub_resource type="StyleBoxFlat" id="12_panel_style"] +bg_color = Color(0.05, 0.05, 0.05, 0.7) +corner_radius_top_left = 10 +corner_radius_top_right = 10 +corner_radius_bottom_left = 10 +corner_radius_bottom_right = 10 + +[node name="Dolphin" type="CharacterBody3D"] +script = ExtResource("1_controller") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="."] +shape = SubResource("1_colshape") + +[node name="DolphinBody" type="Node3D" parent="."] +rotation = Vector3(0, 0, 0) + +[node name="Body" type="MeshInstance3D" parent="DolphinBody"] +rotation = Vector3(1.5708, 0, 0) +mesh = SubResource("2_body_mesh") +surface_material_override/0 = SubResource("3_body_mat") + +[node name="Belly" type="MeshInstance3D" parent="DolphinBody"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0.15) +rotation = Vector3(1.5708, 0, 0) +mesh = SubResource("4_belly_mesh") +surface_material_override/0 = SubResource("5_belly_mat") + +[node name="Tail" type="MeshInstance3D" parent="DolphinBody"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -1.1) +rotation = Vector3(1.5708, 0, 0) +mesh = SubResource("6_tail_mesh") +surface_material_override/0 = SubResource("7_tail_mat") + +[node name="DorsalFin" type="MeshInstance3D" parent="DolphinBody"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.45, -0.1) +mesh = SubResource("8_fin_mesh") +surface_material_override/0 = SubResource("9_fin_mat") + +[node name="PecFinLeft" type="MeshInstance3D" parent="DolphinBody"] +transform = Transform3D(1, 0, 0, 0, 0.866, -0.5, 0, 0.5, 0.866, -0.45, 0.0, 0.1) +mesh = SubResource("10_pec_mesh") +surface_material_override/0 = SubResource("11_pec_mat") + +[node name="PecFinRight" type="MeshInstance3D" parent="DolphinBody"] +transform = Transform3D(1, 0, 0, 0, 0.866, 0.5, 0, -0.5, 0.866, 0.45, 0.0, 0.1) +mesh = SubResource("10_pec_mesh") +surface_material_override/0 = SubResource("11_pec_mat") + +[node name="CameraPivot" type="Node3D" parent="."] + +[node name="SpringArm" type="SpringArm3D" parent="CameraPivot"] +spring_length = 3.5 +rotation = Vector3(0, 3.14159, 0) + +[node name="Camera" type="Camera3D" parent="CameraPivot/SpringArm"] + +[node name="EcholocationPulse" type="Node3D" parent="."] +script = ExtResource("3_echo") + +[node name="HUD" type="CanvasLayer" parent="."] +script = ExtResource("2_hud") + +[node name="Panel" type="PanelContainer" parent="HUD"] +anchors_preset = 0 +anchor_left = 0.0 +anchor_top = 1.0 +anchor_right = 0.0 +anchor_bottom = 1.0 +offset_left = 16.0 +offset_top = -160.0 +offset_right = 220.0 +offset_bottom = -16.0 +theme_override_styles/panel = SubResource("12_panel_style") + +[node name="VBox" type="VBoxContainer" parent="HUD/Panel"] +layout_mode = 2 +theme_override_constants/separation = 8 +offset_left = 10.0 +offset_top = 10.0 +offset_right = -10.0 +offset_bottom = -10.0 + +[node name="OxygenRow" type="HBoxContainer" parent="HUD/Panel/VBox"] +layout_mode = 2 + +[node name="OxygenLabel" type="Label" parent="HUD/Panel/VBox/OxygenRow"] +layout_mode = 2 +text = "O2:" +custom_minimum_size = Vector2(36, 0) + +[node name="OxygenBar" type="ProgressBar" parent="HUD/Panel/VBox/OxygenRow"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +custom_minimum_size = Vector2(130, 18) +max_value = 100.0 +value = 100.0 +show_percentage = false + +[node name="HealthRow" type="HBoxContainer" parent="HUD/Panel/VBox"] +layout_mode = 2 + +[node name="HealthLabel" type="Label" parent="HUD/Panel/VBox/HealthRow"] +layout_mode = 2 +text = "HP:" +custom_minimum_size = Vector2(36, 0) + +[node name="HealthBar" type="ProgressBar" parent="HUD/Panel/VBox/HealthRow"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +custom_minimum_size = Vector2(130, 18) +max_value = 100.0 +value = 100.0 +show_percentage = false + +[node name="HungerRow" type="HBoxContainer" parent="HUD/Panel/VBox"] +layout_mode = 2 + +[node name="HungerLabel" type="Label" parent="HUD/Panel/VBox/HungerRow"] +layout_mode = 2 +text = "Food:" +custom_minimum_size = Vector2(36, 0) + +[node name="HungerBar" type="ProgressBar" parent="HUD/Panel/VBox/HungerRow"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +custom_minimum_size = Vector2(130, 18) +max_value = 100.0 +value = 100.0 +show_percentage = false diff --git a/scripts/dolphin/DolphinController.gd b/scripts/dolphin/DolphinController.gd new file mode 100644 index 0000000..77eb9d1 --- /dev/null +++ b/scripts/dolphin/DolphinController.gd @@ -0,0 +1,261 @@ +extends CharacterBody3D + +# --- Exported Properties --- +@export var swim_speed: float = 8.0 +@export var boost_multiplier: float = 2.5 +@export var mouse_sensitivity: float = 0.002 +@export var max_oxygen: float = 100.0 +@export var oxygen_drain: float = 0.8 +@export var max_health: float = 100.0 +@export var max_hunger: float = 100.0 +@export var hunger_drain: float = 0.3 +@export var water_ceiling_y: float = 60.0 + +# --- Signals --- +signal block_break_requested(hit_position: Vector3, block_normal: Vector3) +signal block_place_requested(hit_position: Vector3, block_normal: Vector3) +signal hotbar_scroll(direction: int) +signal echolocation_triggered(position: Vector3, radius: float) +signal stats_changed(oxygen: float, health: float, hunger: float) +signal player_died() + +# --- Internal State --- +var oxygen: float +var health: float +var hunger: float +var is_boosting: bool = false +var is_echolocating: bool = false + +var _yaw: float = 0.0 +var _pitch: float = 0.0 +var _time: float = 0.0 +var _is_dead: bool = false + +@onready var _camera_pivot: Node3D = $CameraPivot +@onready var _spring_arm: SpringArm3D = $CameraPivot/SpringArm +@onready var _camera: Camera3D = $CameraPivot/SpringArm/Camera +@onready var _dolphin_body: Node3D = $DolphinBody +@onready var _echo_pulse: Node3D = $EcholocationPulse + + +func _ready() -> void: + oxygen = max_oxygen + health = max_health + hunger = max_hunger + Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) + _setup_input_actions() + _emit_stats() + + +func _setup_input_actions() -> void: + var actions: Dictionary = { + "move_forward": KEY_W, + "move_backward": KEY_S, + "strafe_left": KEY_A, + "strafe_right": KEY_D, + "ascend": KEY_SPACE, + "descend": KEY_SHIFT, + "boost": KEY_CTRL, + "echolocate": KEY_E, + "toggle_inventory": KEY_TAB, + } + for action_name: String in actions: + if not InputMap.has_action(action_name): + InputMap.add_action(action_name) + var ev := InputEventKey.new() + ev.keycode = actions[action_name] + InputMap.action_add_event(action_name, ev) + + var mouse_actions: Dictionary = { + "break_block": MOUSE_BUTTON_LEFT, + "place_block": MOUSE_BUTTON_RIGHT, + } + for action_name: String in mouse_actions: + if not InputMap.has_action(action_name): + InputMap.add_action(action_name) + var ev := InputEventMouseButton.new() + ev.button_index = mouse_actions[action_name] + InputMap.action_add_event(action_name, ev) + + if not InputMap.has_action("escape"): + InputMap.add_action("escape") + var ev := InputEventKey.new() + ev.keycode = KEY_ESCAPE + InputMap.action_add_event("escape", ev) + + +func _input(event: InputEvent) -> void: + if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED: + var motion := event as InputEventMouseMotion + _yaw -= motion.relative.x * mouse_sensitivity + _pitch -= motion.relative.y * mouse_sensitivity + _pitch = clamp(_pitch, -PI * 0.45, PI * 0.45) + rotation.y = _yaw + _camera_pivot.rotation.x = _pitch + + if event is InputEventMouseButton: + var mb := event as InputEventMouseButton + if mb.pressed: + if mb.button_index == MOUSE_BUTTON_WHEEL_UP: + hotbar_scroll.emit(1) + elif mb.button_index == MOUSE_BUTTON_WHEEL_DOWN: + hotbar_scroll.emit(-1) + + if event.is_action_pressed("break_block"): + _do_raycast_break() + if event.is_action_pressed("place_block"): + _do_raycast_place() + if event.is_action_pressed("echolocate"): + _trigger_echolocation() + if event.is_action_pressed("escape"): + if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED: + Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) + else: + Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) + + +func _physics_process(delta: float) -> void: + if _is_dead: + return + + _time += delta + _update_stats(delta) + _update_movement(delta) + _animate_body() + + if position.y > water_ceiling_y: + position.y = water_ceiling_y + velocity.y = min(velocity.y, 0.0) + + +func _update_movement(delta: float) -> void: + is_boosting = Input.is_action_pressed("boost") + var speed: float = swim_speed * (boost_multiplier if is_boosting else 1.0) + + var forward: Vector3 = -global_transform.basis.z + var right: Vector3 = global_transform.basis.x + + # Recompute forward/right using camera pitch for intuitive swim direction + var cam_basis: Basis = _camera_pivot.global_transform.basis + var swim_forward: Vector3 = -cam_basis.z + var swim_right: Vector3 = cam_basis.x + + var input_dir: Vector3 = Vector3.ZERO + if Input.is_action_pressed("move_forward"): + input_dir += swim_forward + if Input.is_action_pressed("move_backward"): + input_dir -= swim_forward + if Input.is_action_pressed("strafe_right"): + input_dir += swim_right + if Input.is_action_pressed("strafe_left"): + input_dir -= swim_right + if Input.is_action_pressed("ascend"): + input_dir += Vector3.UP + if Input.is_action_pressed("descend"): + input_dir += Vector3.DOWN + + if input_dir.length_squared() > 0.0: + input_dir = input_dir.normalized() + + var target_velocity: Vector3 = input_dir * speed + velocity = velocity.lerp(target_velocity, 4.0 * delta) + + move_and_slide() + + +func _update_stats(delta: float) -> void: + var changed: bool = false + + # Hunger drain + hunger = max(0.0, hunger - hunger_drain * delta) + changed = true + + # Oxygen + if position.y < 55.0: + oxygen = max(0.0, oxygen - oxygen_drain * delta) + else: + oxygen = min(max_oxygen, oxygen + 20.0 * delta) + + # Health drain from oxygen/hunger + if oxygen <= 0.0: + health = max(0.0, health - 5.0 * delta) + changed = true + if hunger <= 0.0: + health = max(0.0, health - 1.0 * delta) + changed = true + + if changed: + _emit_stats() + + if health <= 0.0 and not _is_dead: + _die() + + +func _emit_stats() -> void: + stats_changed.emit(oxygen, health, hunger) + + +func _die() -> void: + _is_dead = true + player_died.emit() + # Respawn after short delay + await get_tree().create_timer(2.0).timeout + _respawn() + + +func _respawn() -> void: + position = Vector3(0.0, 50.0, 0.0) + velocity = Vector3.ZERO + oxygen = max_oxygen + health = max_health + hunger = max_hunger + _is_dead = false + _emit_stats() + + +func _animate_body() -> void: + if not is_instance_valid(_dolphin_body): + return + # Body bob + _dolphin_body.position.y = sin(_time * 2.5) * 0.05 + # Tail wag via last child if present + var child_count: int = _dolphin_body.get_child_count() + if child_count > 1: + var tail_node: Node3D = _dolphin_body.get_child(child_count - 1) as Node3D + if tail_node != null: + tail_node.rotation.y = sin(_time * 3.0) * 0.3 + + +func _do_raycast_break() -> void: + var result: Dictionary = _raycast(5.0) + if not result.is_empty(): + block_break_requested.emit(result["position"], result["normal"]) + + +func _do_raycast_place() -> void: + var result: Dictionary = _raycast(5.0) + if not result.is_empty(): + block_place_requested.emit(result["position"], result["normal"]) + + +func _raycast(reach: float) -> Dictionary: + if not is_instance_valid(_camera): + return {} + var space_state: PhysicsDirectSpaceState3D = get_world_3d().direct_space_state + var cam_pos: Vector3 = _camera.global_position + var cam_fwd: Vector3 = -_camera.global_transform.basis.z + var query := PhysicsRayQueryParameters3D.create(cam_pos, cam_pos + cam_fwd * reach) + query.exclude = [self] + var result: Dictionary = space_state.intersect_ray(query) + return result + + +func _trigger_echolocation() -> void: + if is_echolocating: + return + is_echolocating = true + echolocation_triggered.emit(global_position, 20.0) + if is_instance_valid(_echo_pulse): + _echo_pulse.trigger() + await get_tree().create_timer(1.2).timeout + is_echolocating = false diff --git a/scripts/dolphin/EcholocationPulse.gd b/scripts/dolphin/EcholocationPulse.gd new file mode 100644 index 0000000..ff47065 --- /dev/null +++ b/scripts/dolphin/EcholocationPulse.gd @@ -0,0 +1,46 @@ +extends Node3D + +const MAX_RADIUS: float = 20.0 +const DURATION: float = 1.0 + +signal pulse_finished() + + +func trigger() -> void: + var mesh_instance := MeshInstance3D.new() + var sphere_mesh := SphereMesh.new() + sphere_mesh.radius = 0.1 + sphere_mesh.height = 0.2 + mesh_instance.mesh = sphere_mesh + + var mat := StandardMaterial3D.new() + mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + mat.albedo_color = Color(0.2, 0.85, 1.0, 0.5) + mat.emission_enabled = true + mat.emission = Color(0.1, 0.6, 1.0) + mat.emission_energy_multiplier = 1.5 + mat.cull_mode = BaseMaterial3D.CULL_DISABLED + mesh_instance.material_override = mat + + add_child(mesh_instance) + + var tween: Tween = create_tween() + tween.set_parallel(true) + tween.tween_method( + func(r: float) -> void: + mesh_instance.scale = Vector3(r, r, r), + 0.1, + MAX_RADIUS, + DURATION + ) + tween.tween_method( + func(a: float) -> void: + mat.albedo_color = Color(0.2, 0.85, 1.0, a), + 0.5, + 0.0, + DURATION + ) + tween.chain().tween_callback(func() -> void: + mesh_instance.queue_free() + pulse_finished.emit() + ) diff --git a/scripts/dolphin/HUD.gd b/scripts/dolphin/HUD.gd new file mode 100644 index 0000000..dbb87f5 --- /dev/null +++ b/scripts/dolphin/HUD.gd @@ -0,0 +1,39 @@ +extends CanvasLayer + +@onready var _oxygen_bar: ProgressBar = %OxygenBar +@onready var _health_bar: ProgressBar = %HealthBar +@onready var _hunger_bar: ProgressBar = %HungerBar + + +func _ready() -> void: + _style_bar(_oxygen_bar, Color(0.31, 0.76, 0.97)) + _style_bar(_health_bar, Color(0.91, 0.30, 0.24)) + _style_bar(_hunger_bar, Color(0.95, 0.61, 0.07)) + + +func _style_bar(bar: ProgressBar, color: Color) -> void: + var style := StyleBoxFlat.new() + style.bg_color = color + style.corner_radius_top_left = 4 + style.corner_radius_top_right = 4 + style.corner_radius_bottom_left = 4 + style.corner_radius_bottom_right = 4 + bar.add_theme_stylebox_override("fill", style) + + var bg_style := StyleBoxFlat.new() + bg_style.bg_color = Color(0.1, 0.1, 0.1, 0.6) + bg_style.corner_radius_top_left = 4 + bg_style.corner_radius_top_right = 4 + bg_style.corner_radius_bottom_left = 4 + bg_style.corner_radius_bottom_right = 4 + bar.add_theme_stylebox_override("background", bg_style) + + +func connect_to_dolphin(dolphin: CharacterBody3D) -> void: + dolphin.stats_changed.connect(_on_stats_changed) + + +func _on_stats_changed(oxygen: float, hp: float, hunger: float) -> void: + _oxygen_bar.value = oxygen + _health_bar.value = hp + _hunger_bar.value = hunger