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 oxygen_origin = oxygen_timer.wait_time # 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") #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") # 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("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") # STATE PROCESSING # ## when on ground func _process_grounded(delta: float) -> void: # make sure is_on_floor detected still velocity.y = 1.0 ## 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()