extends SceneTree var failures: Array[String] = [] func _init() -> void: _run.call_deferred() func _run() -> void: await _check_player_death_aligns_and_disappears() await _check_stage_camera_survives_player_disappearance() await _check_minion_death_aligns_and_disappears() await _check_boss_death_aligns_and_disappears() _finish() func _check_player_death_aligns_and_disappears() -> void: var scene: PackedScene = load("res://scenes/characters/player.tscn") _expect(scene != null, "Player scene should load") if scene == null: return var player := scene.instantiate() root.add_child(player) await process_frame var sprite := player.get_node("Visual/CharacterSprite") as Sprite2D var baseline := _visible_sprite_bottom_global_y(sprite) player.get_node("StateMachine").call("set_life_state", &"Dead") player.call("handle_animations") _expect_float(_visible_sprite_bottom_global_y(sprite), baseline, "Player first death frame should keep the same visible ground line") player.call("_tick_manual_animation", 0.55) _expect_float(_visible_sprite_bottom_global_y(sprite), baseline, "Player fall-back death frame should keep the same visible ground line") player.call("_tick_manual_animation", 2.0) await process_frame _expect_bool(not is_instance_valid(player) or player.is_queued_for_deletion(), true, "Player corpse should disappear after death animation") func _check_stage_camera_survives_player_disappearance() -> void: var scene: PackedScene = load("res://scenes/stage/stage.tscn") _expect(scene != null, "Stage scene should load for player death cleanup") if scene == null: return var stage := scene.instantiate() root.add_child(stage) await process_frame var player := stage.get_node("ActorsContainer/Player") player.get_node("StateMachine").call("set_life_state", &"Dead") player.call("handle_animations") player.call("_tick_manual_animation", 3.0) await process_frame await process_frame stage.call("_update_camera_follow") stage.call("_update_wipe_center") _expect_bool(not is_instance_valid(player) or player.is_queued_for_deletion(), true, "Stage player corpse should be gone before camera follow updates") stage.queue_free() await process_frame func _check_minion_death_aligns_and_disappears() -> void: var scene: PackedScene = load("res://scenes/enemies/minion.tscn") _expect(scene != null, "Minion scene should load") if scene == null: return var minion := scene.instantiate() root.add_child(minion) await process_frame var sprite := minion.get_node("Visual/CharacterSprite") as Sprite2D var baseline := _visible_sprite_bottom_global_y(sprite) minion.get_node("StateMachine").call("set_life_state", &"Dead") minion.call("handle_animations") _expect_float(_visible_sprite_bottom_global_y(sprite), baseline, "Minion first death frame should keep the same visible ground line") var animation_player := minion.get_node("AnimationPlayer") as AnimationPlayer var animation_name := str(animation_player.current_animation) animation_player.advance(animation_player.get_animation(animation_name).length + 0.2) minion.call("handle_animations") await process_frame _expect_bool(not is_instance_valid(minion) or minion.is_queued_for_deletion(), true, "Minion corpse should disappear after death animation") func _check_boss_death_aligns_and_disappears() -> void: var scene: PackedScene = load("res://scenes/enemies/boss.tscn") _expect(scene != null, "Boss scene should load") if scene == null: return var boss := scene.instantiate() root.add_child(boss) await process_frame var sprite := boss.get_node("Visual/CharacterSprite") as Sprite2D var baseline := _visible_sprite_bottom_global_y(sprite) boss.get_node("StateMachine").call("set_life_state", &"Dead") boss.call("handle_animations") _expect_float(_visible_sprite_bottom_global_y(sprite), baseline, "Boss first death frame should keep the same visible ground line") var animation_player := boss.get_node("AnimationPlayer") as AnimationPlayer animation_player.advance(animation_player.get_animation("boss_die").length + 0.2) boss.call("handle_animations") await process_frame _expect_bool(not is_instance_valid(boss) or boss.is_queued_for_deletion(), true, "Boss corpse should disappear after death animation") func _visible_sprite_bottom_global_y(sprite: Sprite2D) -> float: if sprite == null or sprite.texture == null: return INF var image := sprite.texture.get_image() if image == null or image.is_empty(): return INF var hframes := maxi(1, sprite.hframes) var vframes := maxi(1, sprite.vframes) var frame_width := image.get_width() / hframes var frame_height := image.get_height() / vframes var frame_index := clampi(sprite.frame, 0, hframes * vframes - 1) var column := frame_index % hframes var row := int(frame_index / hframes) var bottom := -1 for y: int in range(frame_height): for x: int in range(frame_width): var pixel := image.get_pixel(column * frame_width + x, row * frame_height + y) if pixel.a > 0.01: bottom = y if bottom < 0: return INF return sprite.to_global(sprite.offset + Vector2(0.0, bottom)).y func _expect(condition: bool, label: String) -> void: if not condition: failures.append(label) func _expect_bool(actual: bool, expected: bool, label: String) -> void: if actual != expected: failures.append("%s: expected %s, got %s" % [label, expected, actual]) func _expect_float(actual: float, expected: float, label: String) -> void: if absf(actual - expected) > 0.03: failures.append("%s: expected %.3f, got %.3f" % [label, expected, actual]) func _finish() -> void: if failures.is_empty(): print("PASS test_death_presentation") else: printerr("FAIL test_death_presentation") for failure: String in failures: printerr(" - %s" % failure) quit(failures.size())