extends KinematicBody2D # SIGNALS # signal died() signal teleport_finished() # CONSTANTS # const ArrowProjectile = preload("res://objects/player/arrow_projectile.tscn") const DeathSplatter = preload("res://objects/player/player_death_particles.tscn") const SplashParticles = preload("res://objects/environment/splash/splash_particles.tscn") const BloodSpray := preload("res://objects/environment/blood/blood_spray.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 # current falling speed var current_fall_speed: float = 0.0 # snap vector var snap: Vector2 = Vector2.ZERO # ladder currently attached to var _attached_ladder: Node2D = null # whether to skip blood splatter for this death var skip_blood: bool = false #whether sg has landed before var first_land = true # 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 onready var body_shape: CollisionShape2D = $"%BodyShape" onready var cfox: Sprite = $"%CFox" onready var hitbox: Area2D = $"%Hitbox" # OVERRIDES # func _ready() -> void: Game.can_restart = true #set palette var palette = load("res://graphics/player/palettes/%s.tex" % Game.current_palette) sprite.material.set_shader_param("palette", palette) $"%CFox".material.set_shader_param("palette", palette) $"%DissolveParticles".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 # set lung size if Game.difficulty == Game.Difficulty.SWEET: oxygen_timer.set_wait_time(25) if Game.difficulty == Game.Difficulty.PUNGENT: oxygen_timer.set_wait_time(15) oxygen_timer.start() func _physics_process(delta: float) -> void: # snap sprite sprite.global_position = graphics.global_position.round() + Vector2(0.0, -10.0) # 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("shoot"): # shooting state_chart.send_event("shoot") elif Input.is_action_just_pressed("jump"): # jumping state_chart.send_event("jump") if Input.is_action_pressed("move_down"): state_chart.send_event("duck_pressed") if Input.is_action_just_released("move_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 = "" # 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) func get_stick_input(axis): var inp = Input.get_joy_axis(0,axis) if abs(inp) >= 0.5: return sign(inp) else: return 0 func reset_fall_speed(): current_fall_speed = 0 # 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 if first_land: first_land = false else: #Landing sound Audio.play_sound(Audio.a_land,Audio.ac_land) #Landing Rumble var intensity = inverse_lerp(0.0, max_fall_speed, current_fall_speed) intensity = min(intensity * 1.1,1.0) Input.start_joy_vibration(0, 1.0, intensity, 0.05) 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: global_position.y += 1.0 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("move_left", "move_right") + get_stick_input(JOY_AXIS_0)) # sign() to normalize 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() if (ladder_dir >= 0.0 != flip or not _attached_ladder.can_climb_right) and _attached_ladder.can_climb_left: global_position.x = _attached_ladder.left_snap graphics.scale.x = 1.0 animation_player.play("climb") elif _attached_ladder.can_climb_right: 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: if Debug.fast_punishment > 0: Engine.time_scale += .05 * Debug.fast_punishment # send signals emit_signal("died") state_chart.send_event("died") Input.start_joy_vibration(0,1,1,0.2) # spawn death particles if not skip_blood: var particles = DeathSplatter.instance() particles.global_position = death_splatter_position.global_position particles.emitting = true get_parent().add_child(particles) for i in 16: var spray = BloodSpray.instance() spray.pause_mode = PAUSE_MODE_PROCESS Physics2DServer.set_active(true) spray.global_position = death_splatter_position.global_position spray.velocity = Vector2(randf() * 80.0, 0.0).rotated(randf() * TAU) spray.stains_player = false get_parent().add_child(spray) else: skip_blood = false # fade into the ether graphics.visible = false state_chart.send_event("respawn") #refill oxygen oxygen_timer.start() func _on_Drowning_state_entered() -> void: # state_chart.send_event("died") velocity = Vector2.ZERO animation_player.call_deferred("play", "drown") func _on_Respawn_state_entered() -> void: global_position = Game.respawn_point graphics.visible = true func _on_Appearing_state_entered() -> void: global_position = Game.respawn_point animation_player.play("respawn") func _on_Edge_state_entered(): animation_player.play("edge") func _on_Inactive_state_entered() -> void: velocity = Vector2.ZERO body_shape.disabled = true hitbox.monitorable = false hitbox.monitoring = false func _on_Inactive_state_exited() -> void: body_shape.disabled = false hitbox.monitorable = true hitbox.monitoring = true func _on_Teleporting_state_entered() -> void: hitbox.monitorable = false hitbox.monitoring = false velocity = Vector2.ZERO Audio.play_sound(Audio.a_teleport, Audio.ac_jump) animation_player.play("idle") var tween = create_tween() for i in 8: tween.tween_property(sprite, "position:x", 1.0, 0.0333333) tween.tween_property(sprite, "position:x", -1.0, 0.0333333) tween.tween_property(sprite, "position:x", 0.0, 0.0666667) yield(tween, "finished") sprite.visible = false $"%DissolveParticles".emitting = true yield(get_tree().create_timer(1.0, false), "timeout") emit_signal("teleport_finished") # 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") else: state_chart.send_event("off_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("move_left", "move_right") + get_stick_input(JOY_AXIS_0)) # 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("move_left", "move_right") + get_stick_input(JOY_AXIS_0)) # sign() to normalize # if Input.is_action_pressed("stick_input"): input_dir = get_stick_input(JOY_AXIS_0) 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("move_left", "move_right")) != 0.0 or get_stick_input(JOY_AXIS_0) != 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("move_left", "move_right") + get_stick_input(JOY_AXIS_0)) # sign() to normalize 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("move_up", "move_down") + get_stick_input(JOY_AXIS_1)) # sign() to normalize 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() var collider = ladder_detector.get_collider() if collider and collider.is_in_group("ladder"): _attached_ladder = collider if Input.is_action_just_pressed("jump"): var horizontal_dir = sign(Input.get_axis("move_left", "move_right") + get_stick_input(JOY_AXIS_0)) # sign() to normalize if sign(_attached_ladder.middle - global_position.x) != horizontal_dir: global_position.x -= graphics.scale.x * 3.0 state_chart.send_event("ladder_jump") return elif Input.is_action_just_pressed("shoot"): var horizontal_dir = sign(Input.get_axis("move_left", "move_right") + get_stick_input(JOY_AXIS_0)) # sign() to normalize if sign(_attached_ladder.middle - global_position.x) != horizontal_dir: global_position.x -= graphics.scale.x * 3.0 state_chart.send_event("ladder_detach") return # # auto-dismount on ground # elif Input.is_action_pressed("move_down") and is_on_floor(): # var horizontal_dir = sign(Input.get_axis("move_left", "move_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")# elif Input.is_action_just_pressed("move_left") and _attached_ladder.can_climb_left: global_position.x = _attached_ladder.left_snap graphics.scale.x = 1.0 elif Input.is_action_just_pressed("move_right") and _attached_ladder.can_climb_right: global_position.x = _attached_ladder.right_snap graphics.scale.x = -1.0 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 else: if input_dir == -1.0: state_chart.send_event("ladder_peak") return else: state_chart.send_event("ladder_detach") return 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) current_fall_speed = max(velocity.y,current_fall_speed) ## called after all other physics things func _process_movement(delta: float) -> void: # apply velocity and react to collisions velocity.y += get_floor_velocity().y 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() and (Input.get_axis("move_left", "move_right") != 0.0 or get_stick_input(JOY_AXIS_0) != 0.0): state_chart.send_event("push_start") func _process_floating_up(delta: float) -> void: graphics.global_position.y -= 50.0 * delta var sprite_sector = Game.get_sector(Vector2(global_position.x, graphics.global_position.y - 20)) if sprite_sector != Game.current_sector: graphics.visible = false graphics.position = Vector2.ZERO emit_signal("died") state_chart.send_event("respawn") # COLLISION CALLBACKS # func _on_Hitbox_body_entered(body: Node) -> void: if body.is_in_group("death"): if body.is_in_group("no_blood"): skip_blood = true if body.is_in_group("has_splash"): Game.alternate_death = Audio.a_die_splash var particles = SplashParticles.instance() particles.global_position = death_splatter_position.global_position particles.color = body.splash_color particles.emitting = true get_parent().add_child(particles) if body.is_in_group("death_zap"): Game.alternate_death = Audio.a_die_zap die() func _on_Ducking_event_received(event): if event == "jump": position.y -= 1 func _on_OxygenTimer_timeout(): if underwater: state_chart.send_event("drown") #Reset low oxygen effect when leaving level func _on_Player_tree_exited(): set_underwater_audio(false)