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 committed 2026-08-27 21:31:58 -04:00
1 parent 13f95562a7
commit 53fd05c165
19 files changed
+195 -20

No files matched your search

+13
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")
+1
View File
@@ -0,0 +1 @@
uid://basrxhaxblk5q
+11 -2
View File
@@ -28,6 +28,15 @@ func _ready() -> void:
func _physics_process(_delta: float) -> void:
if is_dead:
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:
velocity = knockback_velocity
@@ -89,9 +98,9 @@ func do_attack_damage() -> void:
print("Demon bodies found: ", bodies.size())
for body in bodies:
if body is Player:
if body is Player and not body.is_dead:
print("DEMON HIT PLAYER")
body.playerTakesDamage(1)
body.receive_damage(1, global_position)
attack_hitbox.monitoring = false
+14
View File
@@ -24,6 +24,7 @@ var player:CharacterStats
var enemy:CharacterStats
var playerHealth:int
var playerLives: int = 3
var enemiesDict = {}
@@ -112,3 +113,16 @@ func addEnemyToLevel(slime):
"damage":enemy.meleeDamage + randDamage
}
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)
+91 -11
View File
@@ -14,6 +14,12 @@ const JUMP_VELOCITY = -400.0
@onready var punch_hitbox: Area2D = $PunchHitbox
var using_child_form := false
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}
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_node: Node = null
var is_dead := false
signal deathAnimationCompleteSignal
func _physics_process(delta: float) -> void:
@@ -68,7 +76,7 @@ func _physics_process(delta: float) -> void:
move_and_slide()
handle_collisions()
func update_states():
match current_state:
#idle when movement in x
@@ -85,12 +93,12 @@ func update_states():
current_state = State.IDLE
else:
current_state = State.RUN
func get_active_graphic() -> AnimatedSprite2D:
if using_child_form:
return child_graphic
return player_graphic
func update_animation():
var graphic = get_active_graphic()
match current_state:
@@ -106,11 +114,22 @@ func update_animation():
State.HURT:
graphic.play("hurt")
State.DEATH:
graphic.play("death")
if graphic.animation != "death":
graphic.play("death")
State.PUNCH:
graphic.play("punch")
func handle_movement(_delta):
if is_dead:
velocity = Vector2.ZERO
return
if is_knocked_back:
return
if is_knocked_back:
return
if direction:
velocity.x = direction * SPEED
if direction <0:
@@ -146,6 +165,9 @@ func update_punch_hitbox_position() -> void:
print("PunchHitbox X: ", punch_hitbox.position.x)
func handle_input():
if is_dead:
return
direction = Input.get_axis("ui_left", "ui_right")
if direction < 0:
@@ -181,7 +203,6 @@ func handle_input():
%SceneManager.makeBullet(right_spawn.global_transform, 700)
if facing == FaceDirection.LEFT:
%SceneManager.makeBullet(left_spawn.global_transform, -700)
func handle_collisions():
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():
pushEnabled = false
const STAIRS_LAYER := 3 # correct layer number for "stairs"
func _ready() -> void:
@@ -274,6 +294,9 @@ func on_stairs_top_reached(stairs: Node) -> void:
_end_stairs_mode()
func _do_punch_damage() -> void:
if is_dead:
return
print("Punch damage started")
punch_hitbox.monitoring = true
@@ -300,9 +323,15 @@ func _on_animation_finished() -> void:
current_state = State.IDLE
State.DEATH:
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:
current_state = State.IDLE
func become_child() -> void:
using_child_form = true
@@ -318,12 +347,63 @@ func become_adult() -> void:
child_graphic.visible = false
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):
print("Player sees remaining health "+str(health))
print("Player sees remaining health " + str(health))
current_state = State.HURT
func playerDies():
if is_dead:
return
is_dead = true
is_invulnerable = true
is_knocked_back = false
velocity = Vector2.ZERO
print("Player sees they died.")
current_state = State.DEATH
+3 -3
View File
@@ -4,9 +4,9 @@
[resource]
script = ExtResource("1_eiyfu")
max_health = 100
starting_health = 100
health = 100
max_health = 3
starting_health = 3
health = 3
meleeDamage = 5
rangeDamage = 10
metadata/_custom_type_script = "uid://b4t8cviiuvt18"
+12 -3
View File
@@ -35,6 +35,15 @@ func _ready() -> void:
func _physics_process(delta: float) -> void:
if is_dead:
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():
velocity.y += gravity * delta
@@ -100,9 +109,9 @@ func do_attack_damage() -> void:
print("Bodies found: ", bodies.size())
for body in bodies:
if body is Player:
print("PLAYER HIT")
body.playerTakesDamage(1)
if body is Player and not body.is_dead:
print("SKELETON HIT PLAYER")
body.receive_damage(1, global_position)
attack_hitbox.monitoring = false