GodotGameWorkshop/Scripts/player.gd
doctorbatmanwho-creator 53fd05c165 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.
2026-08-27 21:31:58 -04:00

410 lines
11 KiB
GDScript

class_name Player extends CharacterBody2D
const SPEED = 300.0
const JUMP_VELOCITY = -400.0
@onready var right_cast: RayCast2D = $RightCast
@onready var left_cast: RayCast2D = $LeftCast
@onready var right_spawn: Node2D = $RightSpawn
@onready var left_spawn: Node2D = $LeftSpawn
@onready var player_graphic: AnimatedSprite2D = $PlayerGraphic
@onready var camera: Camera2D = $Camera2D
@onready var child_graphic: AnimatedSprite2D = $ChildGraphic
@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
enum State{IDLE, RUN, JUMP, FALLING, HURT, DEATH, PUNCH}
var current_state:State = State.IDLE
var pushTarget
var pushEnabled = false
var direction
var upJump = false
var gravity := ProjectSettings.get_setting("physics/2d/default_gravity") as float
var can_mount_stairs := false
var is_on_stairs := false
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:
#game loop
handle_input()
#calculate the movement
# --- STAIRS OPT-IN LOGIC ---
if can_mount_stairs and not is_on_stairs:
if Input.is_action_just_pressed("ui_up"):
print("Player: ui_up pressed")
if Input.is_action_just_pressed("ui_down"):
print("Player: ui_down pressed")
if Input.is_action_just_pressed("ui_up") or Input.is_action_just_pressed("ui_down"):
print("Player: START stairs mode requested")
_start_stairs_mode()
# ---------------------------
handle_movement(delta)
#change states
update_states()
#play animations
update_animation()
#collision with objects, raycasts
# Gravity
if not is_on_floor():
velocity.y += gravity * delta
move_and_slide()
handle_collisions()
func update_states():
match current_state:
#idle when movement in x
State.IDLE when velocity.x !=0:
current_state = State.RUN
State.RUN:
if velocity.x ==0:
current_state = State.IDLE
#jumping when reaching apex
State.JUMP when velocity.y > 0:
current_state = State.FALLING
State.FALLING when is_on_floor():
if velocity.x == 0:
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:
State.IDLE:
graphic.play("idle")
State.RUN:
graphic.play("run")
State.JUMP:
if upJump:
graphic.play("jump")
State.FALLING:
graphic.play("falling")
State.HURT:
graphic.play("hurt")
State.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:
facing = FaceDirection.LEFT
get_active_graphic().flip_h = true
if direction >0:
facing = FaceDirection.RIGHT
get_active_graphic().flip_h = false
else:
velocity.x = move_toward(velocity.x, 0, SPEED)
if direction < 0:
facing = FaceDirection.LEFT
get_active_graphic().flip_h = true
punch_hitbox.position.x = -punch_hitbox_start_x
if direction > 0:
facing = FaceDirection.RIGHT
get_active_graphic().flip_h = false
punch_hitbox.position.x = punch_hitbox_start_x
if current_state == State.PUNCH:
velocity.x = 0
return
func update_punch_hitbox_position() -> void:
if facing == FaceDirection.LEFT:
punch_hitbox.position.x = -punch_hitbox_start_x
else:
punch_hitbox.position.x = punch_hitbox_start_x
print("Facing: ", facing)
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:
facing = FaceDirection.LEFT
if direction > 0:
facing = FaceDirection.RIGHT
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = JUMP_VELOCITY
current_state = State.JUMP
upJump = true
if Input.is_action_just_pressed("attack"):
print("ATTACK PRESSED")
update_punch_hitbox_position()
current_state = State.PUNCH
await get_tree().create_timer(0.25).timeout
_do_punch_damage()
if Input.is_action_just_pressed("shove") && pushEnabled:
print("shove pressed")
if facing == FaceDirection.RIGHT:
pushTarget.apply_central_impulse(Vector2(1,0)*700)
pushEnabled = false
if facing == FaceDirection.LEFT:
pushTarget.apply_central_impulse(Vector2(-1,0)*700)
pushEnabled = false
if Input.is_action_just_pressed("shoot"):
print("shoot a bullet")
if facing == FaceDirection.RIGHT:
%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():
var c = get_slide_collision(i)
if c.get_collider() is RigidBody2D:
c.get_collider().apply_central_impulse(-c.get_normal() * 100)
if right_cast.is_colliding() && facing==FaceDirection.RIGHT:
#get the thing I am colliding with
var collider = right_cast.get_collider()
if collider is Node && collider is RigidBody2D:
print("I can shove this to the right")
pushTarget = collider
pushEnabled = true
if not right_cast.is_colliding() && not left_cast.is_colliding():
pushEnabled = false
if left_cast.is_colliding() && facing==FaceDirection.LEFT:
var collider = left_cast.get_collider()
if collider is Node && collider is RigidBody2D:
print("I can shove this to the left")
pushTarget = collider
pushEnabled = true
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:
_set_stair_collision(false)
floor_max_angle = deg_to_rad(60)
punch_hitbox.monitoring = false
punch_hitbox_start_x = abs(punch_hitbox.position.x)
func set_camera_limits(left: int, top: int, right: int, bottom: int) -> void:
camera.limit_left = left
camera.limit_top = top
camera.limit_right = right
camera.limit_bottom = bottom
print("Camera limits set:")
print("Left: ", left)
print("Top: ", top)
print("Right: ", right)
print("Bottom: ", bottom)
func _set_stair_collision(enabled: bool) -> void:
print("[Player] set stair collision to:", enabled)
set_collision_mask_value(STAIRS_LAYER, enabled)
func _start_stairs_mode() -> void:
print("[Player] _start_stairs_mode")
is_on_stairs = true
_set_stair_collision(true)
# When "on" the stairs, draw above (or tune based on your art)
if stairs_node and stairs_node.has_method("get_above_z_index"):
z_index = stairs_node.get_above_z_index()
else:
z_index = 5 # fallback
func _end_stairs_mode() -> void:
print("[Player] _end_stairs_mode")
is_on_stairs = false
_set_stair_collision(false)
# When not on stairs, draw behind (to appear "behind" stair art if overlapping)
if stairs_node and stairs_node.has_method("get_below_z_index"):
z_index = stairs_node.get_below_z_index()
else:
z_index = 0 # fallback
stairs_node = null
# Called by the stairs trigger via signals:
func on_stairs_trigger_enter(stairs: Node, above_z: int, below_z: int) -> void:
print("[Player] on_stairs_trigger_enter from:", stairs.name)
can_mount_stairs = true
stairs_node = stairs
stairs_z_above = above_z
stairs_z_below = below_z
# By default when *not* mounted, draw behind:
z_index = below_z
func on_stairs_trigger_exit(stairs: Node) -> void:
print("[Player] on_stairs_trigger_exit from:", stairs.name)
if stairs_node == stairs:
can_mount_stairs = false
func on_stairs_top_reached(stairs: Node) -> void:
print("[Player] on_stairs_top_reached from:", stairs.name)
if stairs_node == stairs and is_on_stairs:
_end_stairs_mode()
func _do_punch_damage() -> void:
if is_dead:
return
print("Punch damage started")
punch_hitbox.monitoring = true
await get_tree().physics_frame
await get_tree().physics_frame
var bodies = punch_hitbox.get_overlapping_bodies()
print("Punch overlaps: ", bodies.size())
for body in bodies:
print("Punch hit: ", body.name)
if body.has_method("take_damage"):
body.take_damage(1)
await get_tree().create_timer(0.1).timeout
punch_hitbox.monitoring = false
func _on_animation_finished() -> void:
match current_state:
State.JUMP:
upJump = false
State.HURT:
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
player_graphic.visible = false
child_graphic.visible = true
print("Player switched to CHILD form")
func become_adult() -> void:
using_child_form = false
player_graphic.visible = true
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))
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