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
|
||||
|
||||
[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"
|
||||
|
||||
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