hero-mark-2/objects/player/player.gd
2023-06-25 16:13:46 -04:00

416 lines
14 KiB
GDScript

extends KinematicBody2D
# SIGNALS #
signal died()
# CONSTANTS #
const ArrowProjectile = preload("res://objects/player/arrow_projectile.tscn")
const DeathSplatter = preload("res://objects/player/player_death_particles.tscn")
# EXPORTS #
## whether to be temporarily invulnerable after respawning
export var use_iframes: bool = false
## horizontal movement speed
export var walk_speed: float = 50.0
## frames until walk speed peak (at 60fps reference)
export var walk_acceleration_frames: float = 1.0
## speed to push pushable objects at
export var push_speed: float = 25.0
## climbing speed
export var climb_speed: float = 39.0
## gravity force
export var gravity: float = 720.0
## SG's terminal velocity
export var max_fall_speed: float = 255.0
## upward added by jump
export var jump_force: float = 150.0
## proportion of remaining force retained when jump is released
export var jump_release_force: float = 0.25
## impulse added when double jumping
export var double_jump_force: float = 122.0
## if on turn on oxygentimer to kill player
export var underwater = false
# velocity
var velocity: Vector2 = Vector2.ZERO
# snap vector
var snap: Vector2 = Vector2.ZERO
# ladder currently attached to
var _attached_ladder: Node2D = null
# NODE REFERENCES #
onready var state_chart: StateChart = $StateChart
onready var animation_player: AnimationPlayer = $AnimationPlayer
onready var graphics: Node2D = $Graphics
onready var sprite: Sprite = $"%Sprite"
onready var arrow_position: Position2D = $"%ArrowPosition"
onready var dust_particles: CPUParticles2D = $"%DustParticles"
onready var grounded_shape: CollisionShape2D = $"%GroundedShape"
onready var airborne_shape: CollisionShape2D = $"%AirborneShape"
onready var ladder_detector: RayCast2D = $"%LadderDetector"
onready var death_splatter_position: Position2D = $"%DeathSplatterPosition"
onready var pushable_detector: RayCast2D = $"%PushableDetector"
onready var oxygen_timer = $OxygenTimer
onready var low_oxygen_label = $LowOxygenLabel
onready var edge_detector = $Graphics/EdgeDetector
# OVERRIDES #
func _ready() -> void:
#set palette
var palette = load("res://graphics/player/palettes/%s.png" % Game.current_palette)
sprite.material.set_shader_param("palette", palette)
# death handling
Game.respawn_point = global_position
connect("died", Game, "_on_player_died")
# to detect floor on first frame
move_and_slide(Vector2(0.0, 1.0), Vector2.UP)
# make certain pushable detector will not detect player
pushable_detector.add_exception(self)
# set up state chart
state_chart.initialize()
state_chart.set_guard_property("can_respawn", true)
state_chart.set_guard_property("use_iframes", use_iframes)
state_chart.set_guard_property("red_feather", false)
# state chart debug
$StateDebugLayer/StateChartDebug.target = state_chart
func _physics_process(delta: float) -> void:
# update transition guard properties
# whether player can currently shoot an arrow
var can_shoot = Game.arrows > 0 and get_tree().get_nodes_in_group("player_arrow").size() == 0
state_chart.set_guard_property("can_shoot", can_shoot)
# check for and propagate input events
if Input.is_action_just_pressed("jump"): # jumping
state_chart.send_event("jump")
if Input.is_action_just_pressed("shoot"): # shooting
state_chart.send_event("shoot")
if Input.is_action_pressed("ui_down"):
state_chart.send_event("duck_pressed")
if Input.is_action_just_released("ui_down"):
state_chart.send_event("duck_released")
# send relevant events
if is_on_floor(): # check on floor status
state_chart.send_event("grounded")
else:
state_chart.send_event("airborne")
# check if in contact with ladder
if ladder_detector.is_colliding():
state_chart.send_event("ladder_touched")
# show oxygen count on low oxygen
if underwater:
if oxygen_timer.time_left < 5:
low_oxygen_label.text = str(floor(oxygen_timer.time_left) + 1)
set_underwater_audio(true)
else:
low_oxygen_label.text = ""
set_underwater_audio(false)
else:
#NOT UNDERWATER
low_oxygen_label.text = ""
#Cheats
#CFox mode
if Debug.cfox_mode == true:
animation_player.play("idle")
animation_player.set_speed_scale(0)
# HELPER FUNCTIONS #
## spawns an arrow
func spawn_arrow() -> void:
var arrow = ArrowProjectile.instance()
arrow.global_position = arrow_position.global_position
arrow.direction = sign(arrow_position.global_position.x - global_position.x)
arrow.add_to_group("player_arrow")
get_parent().add_child(arrow)
Audio.play_sound(Audio.a_shoot, Audio.ac_jump)
func die() -> void:
state_chart.send_event("hurt")
func set_underwater_audio(value):
var idx = AudioServer.get_bus_index("Master")
AudioServer.set_bus_effect_enabled(idx,0,value)
AudioServer.set_bus_effect_enabled(idx,1,value)
# STATE ENTERS/EXITS #
func _on_Grounded_state_entered() -> void:
# still jump if pressed frame hit ground
if Input.is_action_just_pressed("jump"):
state_chart.send_event("jump")
# toggle hurtbox shapes
grounded_shape.disabled = false
airborne_shape.disabled = true
snap.y = 2.5 # snap when in grounded state
velocity.y = 1.0
func _on_Still_state_entered() -> void:
animation_player.play("idle")
func _on_Walking_state_entered() -> void:
animation_player.play("walk")
func _on_Blinking_state_entered() -> void:
if $"%Blinking".active:
animation_player.play("blink")
var blink_timer = get_tree().create_timer(rand_range(1.0, 2.0), false)
blink_timer.connect("timeout", self,"_on_Blinking_state_entered")
func _on_Stimming_state_entered() -> void:
animation_player.play("stim")
func _on_Ducking_state_entered():
velocity.x = 0
animation_player.play("duck")
func _on_Pushing_state_entered() -> void:
animation_player.play("push")
func _on_Airborne_state_entered() -> void:
grounded_shape.disabled = true
airborne_shape.disabled = false
snap.y = 0.0 # do not snap when in air
velocity.y = 0.0
func _on_NormalJump_state_entered() -> void:
velocity.y = -jump_force
Audio.play_sound(Audio.a_jump, Audio.ac_jump)
animation_player.play("jump")
dust_particles.restart()
func _on_NormalJump_state_exited() -> void:
# add bit of force proportional to how much of the jump is left
if Input.is_action_just_released("jump"):
var factor = inverse_lerp(0.0, -jump_force, velocity.y)
velocity.y = -jump_force * factor * jump_release_force
func _on_LadderJump_state_entered() -> void:
velocity.y = -jump_force
Audio.play_sound(Audio.a_jump, Audio.ac_jump)
animation_player.play("ladder_jump")
func _on_DoubleJump_state_entered() -> void:
velocity.y = -double_jump_force
Audio.play_sound(Audio.a_doublejump, Audio.ac_jump)
animation_player.play("double_jump")
func _on_CoyoteFalling_state_entered() -> void:
velocity.x = 0.0
animation_player.play("fall")
func _on_NormalFalling_state_entered() -> void:
animation_player.play("fall")
func _on_ScaredFalling_state_entered() -> void:
velocity.x = 0.0
animation_player.play("fall_scared")
func _on_Shooting_state_entered() -> void:
velocity.x = 0.0
animation_player.play("shoot_grounded")
func _on_AirShooting_state_entered() -> void:
spawn_arrow()
animation_player.play("shoot_airborne")
func _on_Climbing_state_entered() -> void:
if ladder_detector.get_collider().is_in_group("ladder"):
_attached_ladder = ladder_detector.get_collider()
# move a tiny bit up if on ground to detach from falling blocks
if is_on_floor():
global_position.y -= get("collision/safe_margin")
velocity = Vector2.ZERO
snap = Vector2.ZERO
var input_dir = sign(Input.get_axis("ui_left", "ui_right"))
var ladder_dir = sign(_attached_ladder.middle - global_position.x)
var flip = global_position.y - 1.0 <= _attached_ladder.global_position.y and input_dir == ladder_dir and is_on_floor()
print(ladder_dir)
print(flip)
if ladder_dir >= 0.0 != flip:
global_position.x = _attached_ladder.left_snap
graphics.scale.x = 1.0
else:
global_position.x = _attached_ladder.right_snap
graphics.scale.x = -1.0
animation_player.play("climb")
func _on_Climbing_state_exited() -> void:
_attached_ladder = null
animation_player.playback_speed = 1.0 # restore playback speed
Audio.ac_climb.stream = null # stop audio
# all the stuff that happens when they DIE
func _on_Dead_state_entered() -> void:
# send signals
emit_signal("died")
state_chart.send_event("died")
# spawn death particles
var particles = DeathSplatter.instance()
particles.global_position = death_splatter_position.global_position
particles.emitting = true
get_parent().add_child(particles)
# fade into the ether
graphics.visible = false
state_chart.send_event("respawn")
#refill oxygen
oxygen_timer.start()
func _on_Respawn_state_entered() -> void:
global_position = Game.respawn_point
graphics.visible = true
state_chart.call_deferred("send_event", "get_real")
func _on_Edge_state_entered():
animation_player.play("edge")
# STATE PROCESSING #
## when on ground
func _process_grounded(delta: float) -> void:
# make sure is_on_floor detected still
velocity.y = 1.0
#play edge sprite if hanging of edge
if !edge_detector.is_colliding():
state_chart.send_event("edge")
## called when player can move left and rightpass # Repass # Rpass # Replace with function body.eplace with function body.place with function body.
func _process_horizontal_movement(delta: float) -> void:
var input_dir = sign(Input.get_axis("ui_left", "ui_right")) # sign() to normalize
velocity.x = input_dir * walk_speed
if input_dir != 0.0:
graphics.scale.x = input_dir
## player movement with acceleration
func _process_horizontal_movement_grounded(delta: float) -> void:
var input_dir = sign(Input.get_axis("ui_left", "ui_right")) # sign() to normalize
if input_dir == 0.0 or input_dir != sign(velocity.x):
velocity.x = 0.0
var acceleration = lerp(0.0, walk_speed, 1.0 / walk_acceleration_frames) * 60.0
velocity.x += input_dir * acceleration * delta
velocity.x = clamp(velocity.x, -walk_speed, walk_speed)
if input_dir != 0.0:
graphics.scale.x = input_dir
## walk/idle state
func _process_can_walk(delta: float) -> void:
if sign(Input.get_axis("ui_left", "ui_right")) != 0.0:
state_chart.send_event("walk_start")
else:
state_chart.send_event("walk_stop")
## rubbing up against a wall or pushing an object
func _process_pushing(delta: float) -> void:
if not is_on_wall():
state_chart.send_event("push_stop")
var input_dir = sign(Input.get_axis("ui_left", "ui_right"))
if input_dir != 0.0:
pushable_detector.force_raycast_update()
if pushable_detector.is_colliding():
var col = pushable_detector.get_collider()
if col.is_in_group("pushable"):
col.push(input_dir * push_speed)
velocity.x = input_dir * push_speed * 2.0
else:
state_chart.send_event("push_stop")
## climbing on ladders
func _process_climbing(delta: float) -> void:
# climbing movement
var input_dir = sign(Input.get_axis("ui_up", "ui_down"))
move_and_slide(Vector2(0.0, input_dir * climb_speed), Vector2.UP) # move
animation_player.playback_speed = abs(input_dir) # play/pause animation
# play sound
if input_dir < 0.0:
if Audio.ac_climb.stream != Audio.a_climb_up:
Audio.play_sound(Audio.a_climb_up, Audio.ac_climb)
if Audio.ac_climb.get_playback_position() >= Audio.a_climb_up.get_length():
Audio.ac_climb.play()
elif input_dir > 0.0:
if Audio.ac_climb.stream != Audio.a_climb_down:
Audio.play_sound(Audio.a_climb_down, Audio.ac_climb)
if Audio.ac_climb.get_playback_position() >= Audio.a_climb_down.get_length():
Audio.ac_climb.play()
else:
Audio.ac_climb.stream = null
# check if still on ladder
ladder_detector.force_raycast_update()
if ladder_detector.get_collider() != _attached_ladder:
if input_dir == -1.0:
state_chart.send_event("ladder_jump")
else:
state_chart.send_event("ladder_detach")
else:
if Input.is_action_just_pressed("jump"):
var horizontal_dir = sign(Input.get_axis("ui_left", "ui_right"))
if sign(_attached_ladder.middle - global_position.x) != horizontal_dir:
global_position.x -= graphics.scale.x * 3.0
state_chart.send_event("ladder_jump")
elif Input.is_action_just_pressed("shoot"):
global_position.x -= graphics.scale.x * 3.0
state_chart.send_event("ladder_detach")
# # auto-dismount on ground
# elif Input.is_action_pressed("ui_down") and is_on_floor():
# var horizontal_dir = sign(Input.get_axis("ui_left", "ui_right"))
# if sign(_attached_ladder.middle - global_position.x) != horizontal_dir:
# global_position.x -= graphics.scale.x * 3.0
# state_chart.send_event("ladder_detach")#
else:
var ladder_dir = sign(_attached_ladder.middle - global_position.x)
if ladder_dir >= 0.0:
global_position.x = _attached_ladder.left_snap
graphics.scale.x = 1.0
else:
global_position.x = _attached_ladder.right_snap
graphics.scale.x = -1.0
func _process_jump(delta: float) -> void:
if velocity.y >= 0.0:
state_chart.send_event("jump_peak")
if not Input.is_action_pressed("jump"):
state_chart.send_event("jump_released")
## called by states SG will fall during
func _process_gravity(delta: float) -> void:
velocity.y = min(velocity.y + gravity * delta, max_fall_speed)
## called after all other physics things
func _process_movement(delta: float) -> void:
# apply velocity and react to collisions
velocity = move_and_slide_with_snap(velocity, snap, Vector2.UP)
# deal with that STUPID landing exactly on corner bug
var col = get_last_slide_collision()
if col != null:
if col.remainder.y >= 1.0 and col.normal.y == 0.0:
position.x += col.normal.x * 0.001
# check for wall
if is_on_wall():
state_chart.send_event("push_start")
# COLLISION CALLBACKS #
func _on_Hitbox_body_entered(body: Node) -> void:
if body.is_in_group("death"):
die()
func _on_Ducking_event_received(event):
if event == "jump":
position.y -= 1
func _on_OxygenTimer_timeout():
if underwater: die()
#Reset low oxygen effect when leaving level
func _on_Player_tree_exited():
set_underwater_audio(false)