class_name Player extends CharacterBody2D signal hp_changed signal inhale_finished const MAX_SPEED = 75.0 const ACCELERATION = MAX_SPEED * 0.1 const DECELERATION = ACCELERATION const JUMP_VELOCITY = -200.0 const MAX_AIR_SPEED = MAX_SPEED * 0.75 const FLY_VELOCITY = -80 const HURT_VELOCITY = MAX_SPEED * 5 const MAX_WATER_SPEED = MAX_SPEED * 0.5 const MAX_GRAVITY = 300.0 const MAX_GRAVITY_AIR = 100.0 const MAX_GRAVITY_WATER = 50.0 var previous_velocity: Vector2 = Vector2.ZERO var jump_height_modifier: float var last_direction: float = 1.0 var max_gravity: float = MAX_GRAVITY var max_up_velocity: float = MAX_GRAVITY * 2 var previous_state: Dictionary = state_idle() var current_state: Dictionary = state_idle(): set(new_state): if current_state.id != "knockout": # final state if current_state.id != new_state.id: previous_state = current_state current_state.exit.call() new_state.enter.call() current_state = new_state var max_hp = 6 var hp = max_hp : set(value): hp = clamp(value, 0, max_hp) hp_changed.emit() Game.player_state.hp = value var life = 4 var sprite_small_star_base: PackedScene var star_shot_base: Area2D var is_inhaling = false var is_flying = false : set(value): is_flying = value set_collision_mask_value(8, value) var is_inflated = false var inhale_min_duration = 0.5 var does_inhale_transition = false var invincibility_duration = 0.6 var is_invincible = false var did_wall_bounce = false var entered_door: Node2D var blink_tween: Tween var visual_position: Vector2 : get(): return Vector2(0, 0) set(value): position = value + Vector2(0, 0) var visual_global_position: Vector2 : get(): return global_position + Vector2(0, 0) set(value): global_position = value - Vector2(0, 0) @onready var navigation_shape: CollisionShape2D = $NavigationShape func _ready(): if Game.player_state.hp: hp = Game.player_state.hp if Game.player_state.life: life = Game.player_state.life current_state = state_idle() sprite_small_star_base = preload("res://effect_star.tscn") star_shot_base = $StarShot.duplicate() $StarShot.queue_free() $InvincibilityTimer.wait_time = invincibility_duration func _physics_process(delta: float) -> void: # Add the gravity. if not is_on_floor(): velocity += get_gravity() * delta if is_flying: velocity -= (get_gravity() * delta) / 1.5 velocity.y = clamp(velocity.y, -max_up_velocity, max_gravity) previous_velocity = velocity current_state.process.call(delta) func apply_directional_movement(max_speed = MAX_SPEED): var direction = Input.get_axis("ui_left", "ui_right") if direction: if direction < 0: $AnimatedSprite2D.flip_h = true else: $AnimatedSprite2D.flip_h = false if sign(direction) != sign(last_direction): # no slidey-ness velocity.x = 0 velocity.x = move_toward(velocity.x, direction * max_speed, ACCELERATION) last_direction = direction return direction func apply_jump_height_modification(): if Input.is_action_pressed("jump"): jump_height_modifier += 10 if jump_height_modifier > 0: jump_height_modifier = 0 if Input.is_action_just_released("jump"): velocity.y -= jump_height_modifier func spawn_small_star(): var star := sprite_small_star_base.instantiate() star.global_position = visual_global_position get_tree().current_scene.add_child(star) var tween = get_tree().create_tween() var target_x = randi_range(-1, 1) * 16 var target_y = (randi_range(-1, 1) if target_x != 0 else 1) * 16 tween.tween_property( star, "global_position", star.global_position + Vector2(target_x, target_y), 0.1 ) tween.tween_callback(func(): await get_tree().create_timer(0.2).timeout star.queue_free() ) func air_shot(direction: float): var shot = preload("res://air_shot.tscn").instantiate() as Area2D shot.global_position = visual_global_position shot.direction = direction get_tree().current_scene.add_child(shot) shot.hit.connect(func(node): Game.score += 400 %ShotObstacleHit.play() if node.has_method("hit_by_projectile"): node.hit_by_projectile(shot.global_position) else: Game.hit_enemy(node, shot.global_position) shot.queue_free() ) shot.end_of_life.connect(func(): await get_tree().create_timer(0.2).timeout if is_instance_valid(shot): shot.queue_free() ) func star_shot(direction: float): var shot = star_shot_base.duplicate() as Area2D shot.global_position = visual_global_position get_tree().current_scene.add_child(shot) %SoundStarShot.play() shot.get_node("AnimatedSprite2D").play() var tween = get_tree().create_tween() var shot_hit = func(node): Game.score += 400 %ShotObstacleHit.play() tween.stop() if node.has_method("hit_by_projectile"): node.hit_by_projectile(shot.global_position) else: Game.hit_enemy(node, shot.global_position) shot.queue_free() shot.body_entered.connect(shot_hit) shot.area_entered.connect(shot_hit) shot.get_node("StarShotVisibleNotifier").screen_exited.connect(func(): if is_instance_valid(shot): tween.stop() shot.queue_free() ) tween.tween_property( shot, "global_position", #shot.global_position + Vector2(sign(direction) * abs(shot.global_position.x - get_viewport_rect().end.x), 0), shot.global_position + Vector2(sign(direction) * get_viewport_rect().size.x, 0), 0.5 ) tween.tween_callback(func(): if is_instance_valid(shot): shot.queue_free() ) func check_door_enter(): var areas = $CollisionArea.get_overlapping_areas() if areas.size() > 0 and areas[0].is_in_group("door"): if Input.is_action_just_pressed("up"): entered_door = areas[0].get_parent() return areas[0].get_parent() return null func start_blink(): blink_tween = get_tree().create_tween() blink_tween.tween_property($AnimatedSprite2D, "self_modulate", Color(1, 1, 1, 0.2), 0.1) blink_tween.tween_property($AnimatedSprite2D, "self_modulate", Color(1, 1, 1, 0.8), 0.1) await blink_tween.finished start_blink() func stop_blink(): blink_tween.stop() $AnimatedSprite2D.self_modulate = Color(1, 1, 1, 1) func inhale(node: CollisionObject2D): node.collision_layer = 0 var position_difference = node.global_position - global_position if position_difference.length() > 25: var tween = get_tree().create_tween() tween.tween_property( node, "global_position", global_position + (Vector2(1, 0) * 16 * sign(last_direction)), (node.global_position - global_position).normalized().length() * 0.25 ) await tween.finished else: await get_tree().create_timer(0.1).timeout node.queue_free() Game.score += 200 inhale_finished.emit() func wall_bounce_check(): return abs(previous_velocity.x) > 0 and is_on_wall() #if not did_wall_bounce and abs(previous_velocity.x) > 0 and is_on_wall(): #%SoundFall.play() #spawn_small_star() #$AnimationPlayer.play("wall_bounce") #did_wall_bounce = true #get_tree().create_timer(0.1).timeout.connect(func(): #did_wall_bounce = false #) # #return true # #return false func on_hurt(node: Node2D): if node.is_in_group("enemy"): if is_invincible: %ShotObstacleHit.play() node.queue_free() else: #current_state.id = "" # to force trigger state change into hurt # not needed anymore because of no double-hurt current_state = state_hurt({ "collider": node, "is_inflated": is_inflated, "is_flying": is_flying, }) elif node is TileMapLayer: current_state = state_knockout() func _on_hurt_area_body_entered(body: Node2D) -> void: on_hurt(body) func _on_hurt_area_area_entered(area: Area2D) -> void: on_hurt(area) func _on_collision_area_area_entered(area: Area2D) -> void: if area.is_in_group("object_heal"): %SoundPickup.play() area.visible = false if hp == max_hp: Game.score += 50 else: for n in range(clamp(max_hp - hp, 0, area.heal_power)): hp += 1 SoundManager.play_sound("BossHpFill") await get_tree().create_timer(0.1).timeout area.queue_free() func _on_invincibility_timer_timeout() -> void: stop_blink() is_invincible = false func state_idle(): return { "id": "idle", "enter": func(): is_inflated = false $AnimationPlayer.play("idle") , "process": func(_delta): velocity.x = move_toward(velocity.x, 0, DECELERATION) move_and_slide() var direction = Input.get_axis("ui_left", "ui_right") if (direction and ((direction == -1 and not $WallDetection/Left.is_colliding()) or (direction == 1 and not $WallDetection/Right.is_colliding()))): current_state = state_walk() if check_door_enter(): current_state = state_enter_door() elif Input.is_action_just_pressed("jump"): current_state = state_jump() if not is_on_floor(): current_state = state_fall() if Input.is_action_pressed("duck"): current_state = state_duck() if Input.is_action_just_pressed("inhale_exhale"): current_state = state_inhale() , "exit": func(): pass , } func state_walk(): return { "id": "walk", "enter": func(): $AnimationPlayer.play("walk") , "process": func(_delta): var direction = apply_directional_movement() if direction: move_and_slide() else: velocity.x = move_toward(velocity.x, 0, DECELERATION) move_and_slide() if velocity.x == 0: current_state = state_idle() if wall_bounce_check(): current_state = state_wall_bounce(state_idle()) if check_door_enter(): current_state = state_enter_door() elif Input.is_action_just_pressed("jump"): current_state = state_jump() if not is_on_floor(): $CoyoteTimer.start() current_state = state_fall() if Input.is_action_pressed("duck"): current_state = state_duck() if Input.is_action_just_pressed("inhale_exhale"): current_state = state_inhale() , "exit": func(): pass , } func state_jump(): return { "id": "jump", "enter": func(): %SoundJump.play() $AnimationPlayer.play("jump") velocity.y = JUMP_VELOCITY jump_height_modifier = JUMP_VELOCITY , "process": func(_delta): apply_jump_height_modification() apply_directional_movement() move_and_slide() if wall_bounce_check(): current_state = state_wall_bounce(state_fall()) if Input.is_action_just_pressed("jump"): velocity.y = FLY_VELOCITY %SoundFly.play() current_state = state_fly_idle() elif Input.is_action_just_pressed("inhale_exhale"): current_state = state_inhale() elif velocity.y >= 0: current_state = state_fall() , "exit": func(): pass , } func state_fall(): return { "id": "fall", "enter": func(): $AnimationPlayer.play("jump") , "process": func(_delta): if not $CoyoteTimer.is_stopped() and Input.is_action_just_pressed("jump"): current_state = state_jump() elif Input.is_action_just_pressed("jump"): velocity.y = FLY_VELOCITY %SoundFly.play() current_state = state_fly_idle() if Input.is_action_just_pressed("inhale_exhale"): current_state = state_inhale() if velocity.y >= MAX_GRAVITY * 0.9: $AnimatedSprite2D.play("fall") is_invincible = true apply_directional_movement(MAX_AIR_SPEED) move_and_slide() if wall_bounce_check(): current_state = state_wall_bounce(state_fall()) if is_on_floor(): spawn_small_star() if $AnimatedSprite2D.animation == "fall": %SoundFall2.play() current_state = state_bump() else: %SoundFall.play() current_state = state_idle() , "exit": func(): pass , } func state_bump(): return { "id": "bump", "enter": func(): $AnimatedSprite2D.play("fall") velocity = ( Vector2(sign(last_direction), -1) * clamp(velocity.x * 1.5, MAX_SPEED * 0.75, MAX_SPEED * 2) ) , "process": func(_delta): move_and_slide() if is_on_floor(): #velocity.x = 0 %SoundFall.play() spawn_small_star() current_state = state_idle() , "exit": func(): if $InvincibilityTimer.is_stopped(): is_invincible = false , } func state_duck(): return { "id": "duck", "enter": func(): $AnimationPlayer.play("duck") , "process": func(_delta): if not Input.is_action_pressed("duck"): current_state = state_idle() var collision := get_last_slide_collision() if collision: var collider = collision.get_collider() if collider.is_in_group("droppable_platforms"): global_position.y += 1 current_state = state_fall() , "exit": func(): pass , } func state_fly_idle(): return { "id": "fly_idle", "enter": func(): is_flying = true max_gravity = MAX_GRAVITY_AIR $AnimationPlayer.play("fly") , "process": func(_delta): if check_door_enter(): current_state = state_enter_door() elif Input.is_action_just_pressed("jump"): velocity.y = FLY_VELOCITY $AnimationPlayer.play("fly") %SoundFly.play() velocity.x = move_toward(velocity.x, 0, DECELERATION) move_and_slide() var direction = Input.get_axis("ui_left", "ui_right") if direction: current_state = state_fly_walk() if Input.is_action_just_pressed("inhale_exhale"): air_shot(last_direction) current_state = state_fly_exhale() , "exit": func(): $AnimationPlayer.play("RESET") is_flying = false max_gravity = MAX_GRAVITY , } func state_fly_walk(): return { "id": "fly_walk", "enter": func(): is_flying = true max_gravity = MAX_GRAVITY_AIR $AnimationPlayer.play("fly") , "process": func(_delta): #velocity -= (get_gravity() * delta) / 1.5 velocity.y = min(velocity.y, 100) if check_door_enter(): current_state = state_enter_door() elif Input.is_action_just_pressed("jump"): velocity.y = FLY_VELOCITY %SoundFly.play() var direction = apply_directional_movement(MAX_AIR_SPEED) if direction: $AnimationPlayer.play("fly") move_and_slide() else: current_state = state_fly_idle() if Input.is_action_just_pressed("inhale_exhale"): air_shot(last_direction) current_state = state_fly_exhale() , "exit": func(): is_flying = false max_gravity = MAX_GRAVITY , } func state_fly_exhale(): return { "id": "fly_exhale", "enter": func(): $AnimationPlayer.play("fly_exhale") await $AnimationPlayer.animation_finished current_state = state_idle() , "process": func(_delta): move_and_slide() , "exit": func(): pass , } func state_inhale(): return { "id": "inhale", "enter": func(): is_inhaling = true $InhaleMinDurationTimer.wait_time = inhale_min_duration $InhaleMinDurationTimer.start() $AnimationPlayer.play("inhale") %SoundInhale.play() %SoundInhale.finished.connect(func(): %SoundInhale.play() ) %InhaleArea.position.x = visual_position.x + (abs(visual_position.x - %InhaleArea.position.x) * sign(last_direction)) %InhaleParticles.process_material.gravity.x = - ( abs(%InhaleParticles.process_material.gravity.x) * sign(last_direction) ) %InhaleParticles.position.x = visual_position.x + (abs(visual_position.x - %InhaleParticles.position.x) * sign(last_direction)) %InhaleParticles.restart() %InhaleParticles.emitting = true , "process": func(_delta): velocity.x = move_toward(velocity.x, 0, DECELERATION) move_and_slide() var bodies = %InhaleArea.get_overlapping_bodies() if bodies.size() > 0: does_inhale_transition = true for body in bodies: inhale(body) await inhale_finished await get_tree().create_timer(inhale_min_duration / 2).timeout current_state = state_inflated_idle() return if ( $InhaleMinDurationTimer.is_stopped() and not does_inhale_transition and (not Input.is_action_pressed("inhale_exhale") or Input.is_action_just_released("inhale_exhale")) ): current_state = state_idle() , "exit": func(): #$AnimationTree.get("parameters/playback").travel("idle") $AnimationPlayer.play("RESET") is_inhaling = false %SoundInhale.stop() %InhaleParticles.emitting = false does_inhale_transition = false , } func state_inflated_idle(): return { "id": "inflated_idle", "enter": func(): is_inflated = true $AnimationPlayer.play("inflated_idle") , "process": func(_delta): velocity.x = move_toward(velocity.x, 0, DECELERATION) move_and_slide() var direction = Input.get_axis("ui_left", "ui_right") if direction: current_state = state_inflated_walk() if check_door_enter(): current_state = state_enter_door() elif Input.is_action_just_pressed("jump"): current_state = state_inflated_jump() if not is_on_floor(): current_state = state_inflated_fall() if Input.is_action_pressed("duck"): current_state = state_digest() if Input.is_action_just_pressed("inhale_exhale"): star_shot(last_direction) current_state = state_inflated_exhale() , "exit": func(): is_inflated = false , } func state_inflated_walk(): return { "id": "inflated_walk", "enter": func(): is_inflated = true $AnimationPlayer.play("inflated_walk") , "process": func(_delta): var direction = apply_directional_movement() if direction: move_and_slide() else: current_state = state_inflated_idle() if check_door_enter(): current_state = state_enter_door() elif Input.is_action_just_pressed("jump"): current_state = state_inflated_jump() if not is_on_floor(): $CoyoteTimer.start() current_state = state_inflated_fall() if Input.is_action_pressed("duck"): current_state = state_digest() if Input.is_action_just_pressed("inhale_exhale"): star_shot(last_direction) current_state = state_inflated_exhale() , "exit": func(): is_inflated = false , } func state_inflated_jump(): return { "id": "inflated_jump", "enter": func(): is_inflated = true %SoundJump.play() $AnimatedSprite2D.play("inflated_jump") velocity.y = JUMP_VELOCITY jump_height_modifier = JUMP_VELOCITY , "process": func(_delta): apply_jump_height_modification() apply_directional_movement() move_and_slide() if Input.is_action_just_pressed("inhale_exhale"): star_shot(last_direction) current_state = state_inflated_exhale() elif velocity.y >= 0: current_state = state_inflated_fall() , "exit": func(): is_inflated = false , } func state_inflated_fall(): return { "id": "fall", "enter": func(): is_inflated = true $AnimatedSprite2D.play("inflated_jump") , "process": func(_delta): if not $CoyoteTimer.is_stopped() and Input.is_action_just_pressed("jump"): current_state = state_inflated_jump() if Input.is_action_just_pressed("inhale_exhale"): star_shot(last_direction) current_state = state_inflated_exhale() apply_directional_movement(MAX_AIR_SPEED) move_and_slide() if is_on_floor(): %SoundFall.play() spawn_small_star() current_state = state_inflated_idle() , "exit": func(): is_inflated = false , } func state_inflated_exhale(): return { "id": "inflated_exhale", "enter": func(): $AnimationPlayer.play("inflated_exhale") await $AnimationPlayer.animation_finished current_state = state_idle() , "process": func(_delta): move_and_slide() , "exit": func(): pass , } func state_digest(): return { "id": "digest", "enter": func(): %SoundDigest.play() is_inflated = false hp += 1 Game.score += 100 $AnimationPlayer.play("digest") await $AnimationPlayer.animation_finished current_state = state_duck() , "process": func(_delta): move_and_slide() # animation position adjustment , "exit": func(): pass , } func state_hurt(data): return { "id": "hurt", "enter": func(): is_invincible = true $InvincibilityTimer.start() if data.collider.is_in_group("enemy"): data.collider.queue_free() hp -= 1 if hp == 0: current_state = state_knockout() return else: %SoundHurt.play() if previous_state.id.contains("exhale"): $AnimationPlayer.play("jump") elif previous_state.id.contains("inflated"): $AnimatedSprite2D.play("inflated_jump") elif previous_state.id.contains("fly"): $AnimatedSprite2D.play("fly") else: $AnimationPlayer.play("jump") var impulse = sign(visual_global_position - data.collider.global_position) * HURT_VELOCITY velocity.x = impulse.x velocity.y = -100 move_and_slide() velocity.x = 0 start_blink() await get_tree().create_timer(0.4).timeout if previous_state.id.contains("exhale"): current_state = state_idle() elif previous_state.id.contains("inflated"): current_state = state_inflated_idle() elif previous_state.id.contains("fly"): current_state = state_fly_idle() else: current_state = state_idle() , "process": func(_delta): move_and_slide() , "exit": func(): pass , } func state_wall_bounce(return_state): return { "id": "wall_bounce", "enter": func(): %SoundFall.play() spawn_small_star() $AnimatedSprite2D.position.x = 4 * last_direction # nudge into wall $AnimationPlayer.play("wall_bounce") await $AnimationPlayer.animation_finished current_state = return_state , "process": func(_delta): move_and_slide() if check_door_enter(): current_state = state_enter_door() elif Input.is_action_just_pressed("jump"): current_state = state_jump() if not is_on_floor(): current_state = state_fall() if Input.is_action_pressed("duck"): current_state = state_duck() if Input.is_action_just_pressed("inhale_exhale"): current_state = state_inhale() , "exit": func(): pass , } func state_water_idle(): return { "id": "water_idle", "enter": func(): max_gravity = MAX_GRAVITY_WATER $AnimationPlayer.play("walk") , "process": func(_delta): if check_door_enter(): current_state = state_enter_door() elif Input.is_action_just_pressed("jump"): %SoundJump.play() velocity.y -= MAX_GRAVITY_WATER * 2 velocity.x = move_toward(velocity.x, 0, DECELERATION) move_and_slide() if velocity.length() > 0: $AnimationPlayer.play("walk") else: $AnimationPlayer.play("idle") var direction = Input.get_axis("ui_left", "ui_right") if direction: current_state = state_water_swim() , "exit": func(): max_gravity = MAX_GRAVITY , } func state_water_swim(): return { "id": "water_swim", "enter": func(): max_gravity = MAX_GRAVITY_WATER $AnimationPlayer.play("walk") , "process": func(_delta): if check_door_enter(): current_state = state_enter_door() elif Input.is_action_just_pressed("jump"): %SoundJump.play() velocity.y -= MAX_GRAVITY_WATER * 2 var direction = apply_directional_movement() if direction: move_and_slide() else: velocity.x = move_toward(velocity.x, 0, DECELERATION) move_and_slide() if velocity.x == 0: current_state = state_water_idle() , "exit": func(): max_gravity = MAX_GRAVITY , } func state_enter_door(): return { "id": "enter_door", "enter": func(): SoundManager.play_sound("EnterDoor") $AnimationPlayer.play("enter_door") var tween = get_tree().create_tween() tween.tween_property($AnimatedSprite2D, "self_modulate", Color(0,0,0,1), 0.3) await get_tree().create_timer(0.1).timeout Game.transition_scene_with_door(entered_door) , "process": func(_delta): pass , "exit": func(): pass , } func state_exit_door(): return { "id": "exit_door", "enter": func(): $AnimationPlayer.play("exit_door") await get_tree().create_timer(0.3).timeout current_state = state_idle() , "process": func(_delta): pass , "exit": func(): pass , } func state_knockout(): return { "id": "knockout", "enter": func(): process_mode = PROCESS_MODE_ALWAYS get_tree().paused = true SoundManager.current_background.stop() $AnimatedSprite2D.play("knockout") $AnimatedSprite2D.stop() $AnimatedSprite2D.set_frame_and_progress(0, 0) %SoundNoHp.play() await %SoundNoHp.finished $AnimatedSprite2D.play("knockout") $Camera2D.drag_top_margin = 1.0 var tween = get_tree().create_tween() var to_up_difference = visual_global_position.y - 16 var to_down_difference = visual_global_position.y + get_viewport_rect().size.y tween.tween_property( self, "visual_global_position", Vector2( visual_global_position.x, to_up_difference ), (to_up_difference / to_down_difference) * 1.0 ) tween.tween_property( self, "visual_global_position", Vector2( visual_global_position.x, to_down_difference ), 1.0 ) %SoundKnockout.play() await %SoundKnockout.finished var scene_path = get_tree().current_scene.scene_file_path if scene_path.contains("Boss"): scene_path = scene_path.replace("Boss.tscn", "Intro.tscn") else: scene_path = scene_path.replace(".tscn", "Intro.tscn") Game.transition_to_scene(scene_path) process_mode = PROCESS_MODE_INHERIT get_tree().paused = false , "process": func(_delta): pass , "exit": func(): pass , } func state_win(): return { "id": "win", "enter": func(): process_mode = PROCESS_MODE_ALWAYS get_tree().paused = true SoundManager.current_background.stop() $AnimatedSprite2D.play("knockout") $AnimatedSprite2D.stop() $AnimatedSprite2D.set_frame_and_progress(0, 0) %SoundNoHp.play() await %SoundNoHp.finished $AnimatedSprite2D.play("knockout") $Camera2D.drag_top_margin = 1.0 var tween = get_tree().create_tween() var to_up_difference = visual_global_position.y - 16 var to_down_difference = visual_global_position.y + get_viewport_rect().size.y tween.tween_property( self, "visual_global_position", Vector2( visual_global_position.x, to_up_difference ), (to_up_difference / to_down_difference) * 1.0 ) tween.tween_property( self, "visual_global_position", Vector2( visual_global_position.x, to_down_difference ), 1.0 ) %SoundKnockout.play() await %SoundKnockout.finished var scene_path = get_tree().current_scene.scene_file_path if scene_path.contains("Boss"): scene_path = scene_path.replace("Boss.tscn", "Intro.tscn") else: scene_path = scene_path.replace(".tscn", "Intro.tscn") Game.transition_to_scene(scene_path) process_mode = PROCESS_MODE_INHERIT get_tree().paused = false , "process": func(_delta): pass , "exit": func(): pass , } func _on_collision_area_body_entered(body: Node2D) -> void: if body is TileMapLayer: current_state = state_water_idle() func _on_collision_area_body_exited(body: Node2D) -> void: if body is TileMapLayer: (func(): var bodies: Array = $CollisionArea.get_overlapping_bodies() bodies = bodies.filter(func(value): return value is TileMapLayer ) if bodies.is_empty(): current_state = state_jump() ).call_deferred()