Add player health, lives, death, and cutscene system

Added player damage, invulnerability frames, knockback, and death handling. Enemies stop attacking after player death and player controls are disabled. Added a persistent three-life system with ambulance cutscenes after the first two deaths and a final Game Over cutscene after the third death.
This commit is contained in:
doctorbatmanwho-creator 2026-08-27 21:31:58 -04:00
parent 13f95562a7
commit 53fd05c165
19 changed files with 195 additions and 20 deletions

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
uid://bbi3yd8miilul

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
uid://b41725daretoj

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
uid://l34f82rsegd6

View File

@ -716,7 +716,7 @@ z_index = 10
texture_filter = 1 texture_filter = 1
position = Vector2(0, -6) position = Vector2(0, -6)
sprite_frames = SubResource("SpriteFrames_nn08x") sprite_frames = SubResource("SpriteFrames_nn08x")
animation = &"idle" animation = &"death"
autoplay = "idle" autoplay = "idle"
[node name="ChildGraphic" type="AnimatedSprite2D" parent="."] [node name="ChildGraphic" type="AnimatedSprite2D" parent="."]

View File

@ -0,0 +1,25 @@
[gd_scene load_steps=3 format=3 uid="uid://ci5nqe61278ke"]
[ext_resource type="VideoStream" uid="uid://l34f82rsegd6" path="res://Assets/Cut Scenes/Weewoo.ogv" id="1_3dlu2"]
[ext_resource type="Script" uid="uid://basrxhaxblk5q" path="res://Scripts/ambulance_cutscene.gd" id="1_yn2le"]
[node name="AmbulanceCutscene" type="Control"]
layout_mode = 3
anchors_preset = 0
offset_right = 960.0
offset_bottom = 540.0
script = ExtResource("1_yn2le")
[node name="VideoStreamPlayer" type="VideoStreamPlayer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
scale = Vector2(0.34, 0.34)
stream = ExtResource("1_3dlu2")
autoplay = true
expand = true
[connection signal="finished" from="VideoStreamPlayer" to="." method="_on_video_stream_player_finished"]

View File

@ -0,0 +1,21 @@
[gd_scene load_steps=2 format=3 uid="uid://dofx2pgo5xgla"]
[ext_resource type="VideoStream" uid="uid://b41725daretoj" path="res://Assets/Cut Scenes/Game over video.ogv" id="1_h6d1g"]
[node name="FinalDeathCutscene" type="Control"]
layout_mode = 3
anchors_preset = 0
offset_right = 960.0
offset_bottom = 540.0
[node name="VideoStreamPlayer" type="VideoStreamPlayer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
scale = Vector2(0.34, 0.34)
stream = ExtResource("1_h6d1g")
autoplay = true
expand = true

View File

@ -0,0 +1,13 @@
extends Control
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
pass # Replace with function body.
func _on_video_stream_player_finished() -> void:
print("AMBULANCE VIDEO FINISHED")
Gamecontroller.playerHealth = Gamecontroller.player.starting_health
get_tree().change_scene_to_file("res://Scenes/Levels/MainGame.tscn")

View File

@ -0,0 +1 @@
uid://basrxhaxblk5q

View File

@ -28,6 +28,15 @@ func _ready() -> void:
func _physics_process(_delta: float) -> void: func _physics_process(_delta: float) -> void:
if is_dead: if is_dead:
return return
if player and player.is_dead:
player = null
is_attacking = false
attack_hitbox.monitoring = false
velocity = Vector2.ZERO
sprite.play("idle")
move_and_slide()
return
if is_hurt: if is_hurt:
velocity = knockback_velocity velocity = knockback_velocity
@ -89,9 +98,9 @@ func do_attack_damage() -> void:
print("Demon bodies found: ", bodies.size()) print("Demon bodies found: ", bodies.size())
for body in bodies: for body in bodies:
if body is Player: if body is Player and not body.is_dead:
print("DEMON HIT PLAYER") print("DEMON HIT PLAYER")
body.playerTakesDamage(1) body.receive_damage(1, global_position)
attack_hitbox.monitoring = false attack_hitbox.monitoring = false

View File

@ -24,6 +24,7 @@ var player:CharacterStats
var enemy:CharacterStats var enemy:CharacterStats
var playerHealth:int var playerHealth:int
var playerLives: int = 3
var enemiesDict = {} var enemiesDict = {}
@ -112,3 +113,16 @@ func addEnemyToLevel(slime):
"damage":enemy.meleeDamage + randDamage "damage":enemy.meleeDamage + randDamage
} }
enemiesDict[slime]=enemyStat enemiesDict[slime]=enemyStat
func damage_player(amount: int) -> void:
playerHealth -= amount
if playerHealth <= 0:
playerHealth = 0
playerLives -= 1
print("Player lives remaining: ", playerLives)
playerDiesSignal.emit()
else:
playerTakesDamageSignal.emit(playerHealth)

View File

@ -14,6 +14,12 @@ const JUMP_VELOCITY = -400.0
@onready var punch_hitbox: Area2D = $PunchHitbox @onready var punch_hitbox: Area2D = $PunchHitbox
var using_child_form := false var using_child_form := false
var punch_hitbox_start_x := 0.0 var punch_hitbox_start_x := 0.0
var is_invulnerable := false
var is_knocked_back := false
@export var knockback_strength := 250.0
@export var knockback_upward := 100.0
@export var knockback_duration := 0.25
enum FaceDirection{LEFT, RIGHT} enum FaceDirection{LEFT, RIGHT}
var facing:FaceDirection = FaceDirection.RIGHT var facing:FaceDirection = FaceDirection.RIGHT
@ -34,6 +40,8 @@ var stairs_z_above := 0 # filled by stairs trigger
var stairs_z_below := -1 # filled by stairs trigger var stairs_z_below := -1 # filled by stairs trigger
var stairs_node: Node = null var stairs_node: Node = null
var is_dead := false
signal deathAnimationCompleteSignal signal deathAnimationCompleteSignal
func _physics_process(delta: float) -> void: func _physics_process(delta: float) -> void:
@ -68,7 +76,7 @@ func _physics_process(delta: float) -> void:
move_and_slide() move_and_slide()
handle_collisions() handle_collisions()
func update_states(): func update_states():
match current_state: match current_state:
#idle when movement in x #idle when movement in x
@ -85,12 +93,12 @@ func update_states():
current_state = State.IDLE current_state = State.IDLE
else: else:
current_state = State.RUN current_state = State.RUN
func get_active_graphic() -> AnimatedSprite2D: func get_active_graphic() -> AnimatedSprite2D:
if using_child_form: if using_child_form:
return child_graphic return child_graphic
return player_graphic return player_graphic
func update_animation(): func update_animation():
var graphic = get_active_graphic() var graphic = get_active_graphic()
match current_state: match current_state:
@ -106,11 +114,22 @@ func update_animation():
State.HURT: State.HURT:
graphic.play("hurt") graphic.play("hurt")
State.DEATH: State.DEATH:
graphic.play("death") if graphic.animation != "death":
graphic.play("death")
State.PUNCH: State.PUNCH:
graphic.play("punch") graphic.play("punch")
func handle_movement(_delta): func handle_movement(_delta):
if is_dead:
velocity = Vector2.ZERO
return
if is_knocked_back:
return
if is_knocked_back:
return
if direction: if direction:
velocity.x = direction * SPEED velocity.x = direction * SPEED
if direction <0: if direction <0:
@ -146,6 +165,9 @@ func update_punch_hitbox_position() -> void:
print("PunchHitbox X: ", punch_hitbox.position.x) print("PunchHitbox X: ", punch_hitbox.position.x)
func handle_input(): func handle_input():
if is_dead:
return
direction = Input.get_axis("ui_left", "ui_right") direction = Input.get_axis("ui_left", "ui_right")
if direction < 0: if direction < 0:
@ -181,7 +203,6 @@ func handle_input():
%SceneManager.makeBullet(right_spawn.global_transform, 700) %SceneManager.makeBullet(right_spawn.global_transform, 700)
if facing == FaceDirection.LEFT: if facing == FaceDirection.LEFT:
%SceneManager.makeBullet(left_spawn.global_transform, -700) %SceneManager.makeBullet(left_spawn.global_transform, -700)
func handle_collisions(): func handle_collisions():
for i in get_slide_collision_count(): for i in get_slide_collision_count():
@ -207,7 +228,6 @@ func handle_collisions():
if not left_cast.is_colliding() && not right_cast.is_colliding(): if not left_cast.is_colliding() && not right_cast.is_colliding():
pushEnabled = false pushEnabled = false
const STAIRS_LAYER := 3 # correct layer number for "stairs" const STAIRS_LAYER := 3 # correct layer number for "stairs"
func _ready() -> void: func _ready() -> void:
@ -274,6 +294,9 @@ func on_stairs_top_reached(stairs: Node) -> void:
_end_stairs_mode() _end_stairs_mode()
func _do_punch_damage() -> void: func _do_punch_damage() -> void:
if is_dead:
return
print("Punch damage started") print("Punch damage started")
punch_hitbox.monitoring = true punch_hitbox.monitoring = true
@ -300,9 +323,15 @@ func _on_animation_finished() -> void:
current_state = State.IDLE current_state = State.IDLE
State.DEATH: State.DEATH:
deathAnimationCompleteSignal.emit() deathAnimationCompleteSignal.emit()
# Wait 2 seconds after the death animation
await get_tree().create_timer(2.0).timeout
if Gamecontroller.playerLives > 0:
get_tree().change_scene_to_file("res://Scenes/ambulance_cutscene.tscn")
else:
get_tree().change_scene_to_file("res://Scenes/final_death_cutscene.tscn")
State.PUNCH: State.PUNCH:
current_state = State.IDLE current_state = State.IDLE
func become_child() -> void: func become_child() -> void:
using_child_form = true using_child_form = true
@ -318,12 +347,63 @@ func become_adult() -> void:
child_graphic.visible = false child_graphic.visible = false
print("Player switched to ADULT form") print("Player switched to ADULT form")
func flash_while_invulnerable() -> void:
var graphic = get_active_graphic()
while is_invulnerable:
graphic.visible = false
await get_tree().create_timer(0.1).timeout
graphic.visible = true
await get_tree().create_timer(0.1).timeout
func receive_damage(amount: int, attacker_position: Vector2) -> void:
if is_dead:
return
if is_invulnerable:
print("Player is invulnerable - damage ignored")
return
is_invulnerable = true
is_knocked_back = true
# Push player AWAY from whoever attacked
var knockback_direction: float = sign(global_position.x - attacker_position.x)
# Fallback in case both are exactly on top of one another
if knockback_direction == 0:
knockback_direction = -1
velocity.x = knockback_direction * knockback_strength
velocity.y = -knockback_upward
Gamecontroller.damage_player(amount)
flash_while_invulnerable()
await get_tree().create_timer(knockback_duration).timeout
is_knocked_back = false
await get_tree().create_timer(1.0 - knockback_duration).timeout
is_invulnerable = false
get_active_graphic().visible = true
print("Player can take damage again")
func playerTakesDamage(health): func playerTakesDamage(health):
print("Player sees remaining health "+str(health)) print("Player sees remaining health " + str(health))
current_state = State.HURT current_state = State.HURT
func playerDies(): func playerDies():
if is_dead:
return
is_dead = true
is_invulnerable = true
is_knocked_back = false
velocity = Vector2.ZERO
print("Player sees they died.") print("Player sees they died.")
current_state = State.DEATH current_state = State.DEATH

View File

@ -4,9 +4,9 @@
[resource] [resource]
script = ExtResource("1_eiyfu") script = ExtResource("1_eiyfu")
max_health = 100 max_health = 3
starting_health = 100 starting_health = 3
health = 100 health = 3
meleeDamage = 5 meleeDamage = 5
rangeDamage = 10 rangeDamage = 10
metadata/_custom_type_script = "uid://b4t8cviiuvt18" metadata/_custom_type_script = "uid://b4t8cviiuvt18"

View File

@ -35,6 +35,15 @@ func _ready() -> void:
func _physics_process(delta: float) -> void: func _physics_process(delta: float) -> void:
if is_dead: if is_dead:
return return
if player and player.is_dead:
player = null
is_attacking = false
attack_hitbox.monitoring = false
velocity.x = 0
sprite.play("idle")
move_and_slide()
return
if not is_on_floor(): if not is_on_floor():
velocity.y += gravity * delta velocity.y += gravity * delta
@ -100,9 +109,9 @@ func do_attack_damage() -> void:
print("Bodies found: ", bodies.size()) print("Bodies found: ", bodies.size())
for body in bodies: for body in bodies:
if body is Player: if body is Player and not body.is_dead:
print("PLAYER HIT") print("SKELETON HIT PLAYER")
body.playerTakesDamage(1) body.receive_damage(1, global_position)
attack_hitbox.monitoring = false attack_hitbox.monitoring = false