feat: added ragdoll, health, death, and adjusted mortar
This commit is contained in:
@@ -10,6 +10,23 @@ var head_pivot: Node3D = null # FPSCameraRig node
|
||||
var camera: Camera3D = null
|
||||
var _damage_layer: CanvasLayer = null
|
||||
|
||||
# Health and Shield
|
||||
var max_health: float = 100.0
|
||||
var health: float = 100.0
|
||||
var max_shield: float = 100.0
|
||||
var shield: float = 100.0
|
||||
var time_since_last_damage: float = 0.0
|
||||
var is_dead: bool = false
|
||||
|
||||
# UI
|
||||
var health_bar: ProgressBar
|
||||
var shield_bar: ProgressBar
|
||||
var health_label: Label
|
||||
var shield_label: Label
|
||||
var death_screen: Control
|
||||
|
||||
var ragdoll_instance: Node3D
|
||||
|
||||
# Audio
|
||||
var footstep_player: AudioStreamPlayer
|
||||
var dash_player: AudioStreamPlayer
|
||||
@@ -50,6 +67,7 @@ func _ready() -> void:
|
||||
_setup_audio()
|
||||
_setup_hit_marker()
|
||||
_setup_grapple()
|
||||
_setup_hud()
|
||||
|
||||
set_process(true)
|
||||
set_physics_process(true)
|
||||
@@ -188,9 +206,30 @@ func apply_impulse(force: Vector3) -> void:
|
||||
# Tiny upward bump helps move_and_slide detach from the floor
|
||||
global_position.y += 0.1
|
||||
|
||||
func take_damage(amount: float, _hit_pos: Vector3, _source: Node3D = null) -> void:
|
||||
func take_damage(amount: float, _hit_pos: Vector3, _source: Node3D = null, impulse: Vector3 = Vector3.ZERO) -> void:
|
||||
if is_dead:
|
||||
if is_instance_valid(ragdoll_instance):
|
||||
ragdoll_instance.apply_initial_velocities(Vector3.ZERO, impulse)
|
||||
return
|
||||
|
||||
# Show damage number for self-damage
|
||||
spawn_damage_number(amount, _hit_pos)
|
||||
|
||||
time_since_last_damage = 0.0
|
||||
|
||||
if shield > 0.0:
|
||||
if shield >= amount:
|
||||
shield -= amount
|
||||
amount = 0.0
|
||||
else:
|
||||
amount -= shield
|
||||
shield = 0.0
|
||||
|
||||
if amount > 0.0:
|
||||
health -= amount
|
||||
if health <= 0.0:
|
||||
health = 0.0
|
||||
die(impulse)
|
||||
|
||||
func _ensure_machine() -> MovementStateMachine:
|
||||
if is_instance_valid(_machine):
|
||||
@@ -292,3 +331,221 @@ func _on_movement_event(ev: String, data: Dictionary) -> void:
|
||||
grapple_shoot_player.play()
|
||||
elif ev == "grapple_latch":
|
||||
grapple_latch_player.play()
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if is_dead:
|
||||
# Continuously follow the ragdoll torso
|
||||
if is_instance_valid(ragdoll_instance) and is_instance_valid(camera):
|
||||
var torso = ragdoll_instance.torso_body
|
||||
if is_instance_valid(torso) and ragdoll_instance.has_meta("spring_arm"):
|
||||
var spring_arm = ragdoll_instance.get_meta("spring_arm")
|
||||
if is_instance_valid(spring_arm):
|
||||
# Follow torso position exactly to avoid lerp bobbing
|
||||
spring_arm.global_position = torso.global_position
|
||||
return
|
||||
|
||||
# Shield recharge logic
|
||||
time_since_last_damage += delta
|
||||
if time_since_last_damage >= 5.0 and shield < max_shield:
|
||||
shield += 20.0 * delta
|
||||
if shield > max_shield:
|
||||
shield = max_shield
|
||||
|
||||
# Update UI
|
||||
if is_instance_valid(health_bar):
|
||||
health_bar.value = health
|
||||
health_label.text = "%d / %d" % [ceil(health), max_health]
|
||||
if is_instance_valid(shield_bar):
|
||||
shield_bar.value = shield
|
||||
shield_label.text = "%d / %d" % [ceil(shield), max_shield]
|
||||
|
||||
func _setup_hud() -> void:
|
||||
var margin = MarginContainer.new()
|
||||
margin.set_anchors_preset(Control.PRESET_BOTTOM_LEFT)
|
||||
margin.offset_left = 20
|
||||
margin.offset_bottom = -20
|
||||
margin.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
||||
|
||||
var vbox = VBoxContainer.new()
|
||||
margin.add_child(vbox)
|
||||
|
||||
# Shield Bar (Top)
|
||||
var shield_box = VBoxContainer.new()
|
||||
var s_title = Label.new()
|
||||
s_title.text = "SHIELD"
|
||||
s_title.add_theme_font_size_override("font_size", 12)
|
||||
s_title.add_theme_color_override("font_color", Color(0.4, 0.7, 1.0))
|
||||
shield_box.add_child(s_title)
|
||||
|
||||
shield_bar = ProgressBar.new()
|
||||
shield_bar.custom_minimum_size = Vector2(200, 20)
|
||||
shield_bar.max_value = max_shield
|
||||
shield_bar.value = shield
|
||||
shield_bar.show_percentage = false
|
||||
|
||||
var s_sb = StyleBoxFlat.new()
|
||||
s_sb.bg_color = Color(0.1, 0.4, 0.8)
|
||||
shield_bar.add_theme_stylebox_override("fill", s_sb)
|
||||
|
||||
shield_label = Label.new()
|
||||
shield_label.text = "100 / 100"
|
||||
shield_label.set_anchors_preset(Control.PRESET_CENTER)
|
||||
shield_bar.add_child(shield_label)
|
||||
|
||||
shield_box.add_child(shield_bar)
|
||||
vbox.add_child(shield_box)
|
||||
|
||||
# Spacer
|
||||
vbox.add_child(Control.new())
|
||||
|
||||
# Health Bar (Bottom)
|
||||
var health_box = VBoxContainer.new()
|
||||
var h_title = Label.new()
|
||||
h_title.text = "HEALTH"
|
||||
h_title.add_theme_font_size_override("font_size", 12)
|
||||
h_title.add_theme_color_override("font_color", Color(1.0, 0.3, 0.3))
|
||||
health_box.add_child(h_title)
|
||||
|
||||
health_bar = ProgressBar.new()
|
||||
health_bar.custom_minimum_size = Vector2(200, 20)
|
||||
health_bar.max_value = max_health
|
||||
health_bar.value = health
|
||||
health_bar.show_percentage = false
|
||||
|
||||
var h_sb = StyleBoxFlat.new()
|
||||
h_sb.bg_color = Color(0.8, 0.1, 0.1)
|
||||
health_bar.add_theme_stylebox_override("fill", h_sb)
|
||||
|
||||
health_label = Label.new()
|
||||
health_label.text = "100 / 100"
|
||||
health_label.set_anchors_preset(Control.PRESET_CENTER)
|
||||
health_bar.add_child(health_label)
|
||||
|
||||
health_box.add_child(health_bar)
|
||||
vbox.add_child(health_box)
|
||||
|
||||
_damage_layer.add_child(margin)
|
||||
|
||||
# Setup Death Screen
|
||||
death_screen = ColorRect.new()
|
||||
death_screen.color = Color(0, 0, 0, 0.7)
|
||||
death_screen.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
death_screen.visible = false
|
||||
|
||||
var center = CenterContainer.new()
|
||||
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
death_screen.add_child(center)
|
||||
|
||||
var d_vbox = VBoxContainer.new()
|
||||
d_vbox.add_theme_constant_override("separation", 20)
|
||||
center.add_child(d_vbox)
|
||||
|
||||
var d_title = Label.new()
|
||||
d_title.text = "SYSTEM FAILURE"
|
||||
d_title.add_theme_font_size_override("font_size", 48)
|
||||
d_title.add_theme_color_override("font_color", Color(1.0, 0.2, 0.2))
|
||||
d_vbox.add_child(d_title)
|
||||
|
||||
var respawn_btn = Button.new()
|
||||
respawn_btn.text = "REBOOT"
|
||||
respawn_btn.add_theme_font_size_override("font_size", 24)
|
||||
respawn_btn.pressed.connect(_on_respawn_pressed)
|
||||
d_vbox.add_child(respawn_btn)
|
||||
|
||||
_damage_layer.add_child(death_screen)
|
||||
|
||||
func die(impulse: Vector3 = Vector3.ZERO) -> void:
|
||||
if is_dead: return
|
||||
is_dead = true
|
||||
|
||||
# Disable movement state machine inputs
|
||||
if _machine:
|
||||
_machine.process_mode = Node.PROCESS_MODE_DISABLED
|
||||
|
||||
# Hide old animated model
|
||||
var humanoid = get_node_or_null("HumanoidModel")
|
||||
if humanoid:
|
||||
humanoid.visible = false
|
||||
|
||||
# Disable collision so player doesn't block bullets
|
||||
var col = get_node_or_null("CollisionShape3D")
|
||||
if col:
|
||||
col.set_deferred("disabled", true)
|
||||
|
||||
# Spawn true physics ragdoll
|
||||
var ragdoll = load("res://characters/procedural_ragdoll.gd").new()
|
||||
ragdoll_instance = ragdoll
|
||||
get_tree().current_scene.add_child(ragdoll)
|
||||
ragdoll.global_transform = global_transform
|
||||
ragdoll.build_ragdoll(Color(0.2, 0.4, 0.8)) # Blueish player color
|
||||
|
||||
# Wait a frame for physics to initialize then apply velocity
|
||||
get_tree().create_timer(0.01).timeout.connect(func():
|
||||
if is_instance_valid(ragdoll):
|
||||
ragdoll.apply_initial_velocities(velocity, impulse)
|
||||
)
|
||||
|
||||
# Move camera to 3rd person view using SpringArm3D
|
||||
if is_instance_valid(camera):
|
||||
# Hide the weapon manager so it isn't floating in front of the 3rd person camera
|
||||
var wman = camera.get_node_or_null("WeaponManager")
|
||||
if wman:
|
||||
wman.visible = false
|
||||
|
||||
var cam_trans = camera.global_transform
|
||||
camera.get_parent().remove_child(camera)
|
||||
|
||||
# Determine fixed offset direction for camera to follow from
|
||||
var back_dir = cam_trans.basis.z.normalized()
|
||||
back_dir.y = 0 # Flatten it
|
||||
if back_dir.length_squared() < 0.1:
|
||||
back_dir = Vector3.BACK
|
||||
else:
|
||||
back_dir = back_dir.normalized()
|
||||
|
||||
var spring_arm = SpringArm3D.new()
|
||||
spring_arm.name = "DeathSpringArm"
|
||||
spring_arm.spring_length = 1.5
|
||||
spring_arm.margin = 0.5
|
||||
|
||||
# Give it collision capability against the world but ignore the ragdoll shapes
|
||||
spring_arm.collision_mask = 1 # Environment mask
|
||||
for child in ragdoll.get_children():
|
||||
if child is CollisionObject3D:
|
||||
spring_arm.add_excluded_object(child.get_rid())
|
||||
|
||||
get_tree().current_scene.add_child(spring_arm)
|
||||
|
||||
# Put the arm at the torso's current position
|
||||
spring_arm.global_position = ragdoll.torso_body.global_position
|
||||
|
||||
# Calculate upper-right position
|
||||
var right_dir = back_dir.cross(Vector3.UP).normalized()
|
||||
if right_dir.length_squared() < 0.1:
|
||||
right_dir = Vector3.RIGHT
|
||||
|
||||
# The vector only defines direction, length is controlled by spring_length
|
||||
var desired_cam_pos = spring_arm.global_position + back_dir * 2.5 + right_dir * 1.5 + Vector3.UP * 1.5
|
||||
|
||||
# SpringArm3D extends along its local +Z axis.
|
||||
# look_at() points the local -Z axis at the target.
|
||||
# To point +Z at desired_cam_pos, we must look_at the OPPOSITE direction.
|
||||
var opposite_dir = spring_arm.global_position + (spring_arm.global_position - desired_cam_pos)
|
||||
spring_arm.look_at(opposite_dir, Vector3.UP)
|
||||
|
||||
spring_arm.add_child(camera)
|
||||
|
||||
# Since +Z points outwards to the camera, -Z points inwards to the torso.
|
||||
# Camera3D natively looks down -Z. Setting rotation to ZERO makes it perfectly look at the torso!
|
||||
camera.position = Vector3.ZERO
|
||||
camera.rotation = Vector3.ZERO
|
||||
|
||||
ragdoll_instance.set_meta("spring_arm", spring_arm)
|
||||
|
||||
# Show death screen
|
||||
death_screen.visible = true
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
|
||||
func _on_respawn_pressed() -> void:
|
||||
# Reload scene
|
||||
get_tree().reload_current_scene()
|
||||
|
||||
Reference in New Issue
Block a user