class_name Player extends CharacterBody2D @export var double_jump_animation : PackedScene # allow taking away player control var handle_input : bool = true # Gravity var earth_center = Vector2.ZERO; var max_fall_speed = 700; var gravity = 100; # Movement var facing = -1; var base_hspeed = 150.0; var ground_jump_strength = 1400; var air_jump_strength = 1100; var air_jumps_max = 1; var air_jumps_current = 1: set(air_jumps_new): air_jumps_current = min(air_jumps_new, air_jumps_max) # HP and Iframes signal health_changed(new_health : int) signal max_hp_changed(new_max_hp : int) signal player_died var hit_iframes = 0.8 var current_hp = 5: set(new_hp): # HP can't be increased above Max HP or reduced below 0 # When reduced to 0, the player dies. new_hp = min(new_hp, max_hp) if new_hp <= 0: new_hp = 0 die() if new_hp != current_hp: current_hp = new_hp health_changed.emit(current_hp) @export var max_hp = 5: # When Max HP is reduced below current health, current health is adjusted. set(new_max_hp): max_hp = new_max_hp if max_hp <= 0: max_hp = 0 if current_hp > max_hp: current_hp = max_hp max_hp_changed.emit(max_hp) var dead = false; # Received Knockback var reset_to_velocity = Vector2.ZERO var knockback_strength = 1500 var damage_knockup = 500 @export var friction = 0.5 # Attack Handling var can_upslash = false signal attack # Active Item signal active_item_changed(newitem : Item) var active_item : ActiveItem = null: set(new_active_item): active_item = new_active_item active_item_changed.emit(active_item) func set_cooldown(cooldown): $ActiveItemCooldown.wait_time = cooldown func activate_cooldown(): $ActiveItemCooldown.start() func _ready() -> void: # Update the Health Bar initially max_hp_changed.emit(max_hp) health_changed.emit(current_hp) func _physics_process(delta: float) -> void: # Velocity management uses the physics framework, hence runs at fix 60FPS manage_velocity(delta) move_and_slide() func _process(_delta: float) -> void: # All non-velocity management can run without FPS cap update_vine_statuses() manage_movement_options() manage_interaction() manage_animation() if handle_input: manage_active() manage_attack() func manage_attack(): # If an attack is possible, a signal is sent which the weapon can connect to if(Input.is_action_just_pressed("attack") and $AttackCooldown.time_left <= 0): if Input.is_action_pressed("up") and can_upslash: attack.emit("up") else: attack.emit("horizontal") $Sprite.play("attack") $SwordSwingAudio.play() $AttackCooldown.start() func manage_active(): # Activate or remove items. Cooldown + use management is handled by the item. if(active_item != null and Input.is_action_just_pressed("item") and $ActiveItemCooldown.is_stopped()): active_item.trigger_activation() if(Input.is_action_just_pressed("drop_item") and active_item != null): active_item.remove() func manage_movement_options() -> void: # Reset Air Jumps when grounded if(is_on_floor()): air_jumps_current = air_jumps_max func manage_interaction(): # Interacts with all overlapping interactables on button press if Input.is_action_just_pressed("interact"): for area in $InteractBox.get_overlapping_areas(): if area.has_method("interact"): area.interact() func manage_animation() -> void: var walk_dir = 0 if(handle_input): if(Input.is_action_pressed("move_right")): walk_dir += 1 if(Input.is_action_pressed("move_left")): walk_dir -= 1 # Set the direction the player faces if walk_dir != 0: facing = walk_dir $Sprite.scale.x = - abs($Sprite.scale.x) * facing # Play the walk or idle animation when appropriate if(walk_dir != 0): if(is_on_floor() and not $Sprite.is_playing()): $Sprite.play("walk") else: if $Sprite.animation == "walk": $Sprite.stop() func manage_velocity(delta: float) -> void: up_direction = $EarthAligner.up # Convert the current velocity into local coordinates, then compute changes there. # This is important for capped values such as fall speed. var old_local_velocity = $EarthAligner.local_from_global(velocity) # Apply friction horizontally, exponentially. Factor 1 - friction per frame at 60FPS. var local_velocity = Vector2(old_local_velocity.x * pow(1 - friction,60*delta), old_local_velocity.y); local_velocity += Vector2(0, gravity) if handle_input: # Apply Slow Status if present. var hspeed = base_hspeed if not Status.affects("Slow", self) else base_hspeed * get_node("Slow").params.slow_factor # Change the local velocity by the movement speed, or more if moving in the opposite direction, # for more responsive turning around. if(Input.is_action_pressed("move_right")): if local_velocity.x > - 3 * hspeed: local_velocity += Vector2(hspeed, 0) else: local_velocity += Vector2(2 * hspeed, 0) if(Input.is_action_pressed("move_left")): if local_velocity.x < 3 * hspeed: local_velocity += Vector2(-hspeed, 0) else: local_velocity += Vector2(-2 * hspeed, 0) if(Input.is_action_just_pressed("jump") and (is_on_floor() or air_jumps_current > 0)): # If the player holds drop, just move through the platform if Input.is_action_pressed("drop"): self.position += 12 * $EarthAligner.down else: # Otherwise, either jump from the ground or perform a double jump. if is_on_floor(): local_velocity.y = -ground_jump_strength else: air_jumps_current -= 1; play_double_jump_animation() local_velocity.y = -air_jump_strength if(local_velocity.y > max_fall_speed): local_velocity.y = max_fall_speed # When knockback is applied, momentum is reset to the knockback value instead. if(reset_to_velocity.x != 0): local_velocity.x = reset_to_velocity.x if(reset_to_velocity.y != 0): local_velocity.y = reset_to_velocity.y reset_to_velocity = Vector2.ZERO # Return to world coordinates and apply the changes to velocity. velocity = $EarthAligner.global_from_local(local_velocity) func hurt(dmg: int, dir: Vector2 = Vector2.ZERO): # If the player has no iframes, apply knockback and damage and start iframes. if $IFrames.time_left <= 0: if Status.affects("Vulnerable", self): dmg += 1 $AudioStreamPlayer2D.play() current_hp -= dmg $IFrames.start(hit_iframes) reset_to_velocity = Vector2(-sign($EarthAligner.local_from_global(dir).x)*knockback_strength, -damage_knockup) return true return false func die(): if not dead: player_died.emit() dead = true # Connected to the signal marking the end of an attack. # Returns to default animation. func _on_attack_end(): if($Sprite.animation != "idle"): $Sprite.play("idle") $Sprite.stop() # Take away player control while the Death Screen is active. func _on_death_screen_visibility_changed() -> void: handle_input = !handle_input func play_double_jump_animation() -> void: # Instantiate a sprite which plays a double jump animation, then deletes itself. $AirJumpAudio.play() var node = double_jump_animation.instantiate() add_child(node) node.position = Vector2(0, 5) node.scale = .5 * Vector2.ONE node.reparent(get_parent()) # If there is any vine nearby, gain the corresponding statusses. func update_vine_statuses(): var location = Grid.get_location_from_world_pos(global_position) for vine : Vine in Grid.vines_per_node[location.x][location.y]: if vine.active_depth > 0: #TODO: Properly manage procedural activation Status.apply(vine.status_name, self, 0.1, vine.status_params)