feat(dolphin): swim controller + third-person camera + HUD stats
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,78 @@ window/size/viewport_height=720
|
|||||||
window/size/mode=2
|
window/size/mode=2
|
||||||
|
|
||||||
[input]
|
[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]
|
[autoload]
|
||||||
BlockDatabase="*res://scripts/world/BlockDatabase.gd"
|
BlockDatabase="*res://scripts/world/BlockDatabase.gd"
|
||||||
|
|||||||
176
scenes/Dolphin.tscn
Normal file
176
scenes/Dolphin.tscn
Normal file
@@ -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
|
||||||
261
scripts/dolphin/DolphinController.gd
Normal file
261
scripts/dolphin/DolphinController.gd
Normal file
@@ -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
|
||||||
46
scripts/dolphin/EcholocationPulse.gd
Normal file
46
scripts/dolphin/EcholocationPulse.gd
Normal file
@@ -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()
|
||||||
|
)
|
||||||
39
scripts/dolphin/HUD.gd
Normal file
39
scripts/dolphin/HUD.gd
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user