extends SceneTree const WARRIOR_TEXTURE := "res://assets/art/characters/warrior_man_sheet.png" const WARRIOR_WOMAN_TEXTURE := "res://assets/art/characters/warrior_woman_sheet.png" const CHARGE_EFFECT_TEXTURE := "res://assets/art/effects/effect_hp_mp_sheet.png" const EFFECT_TEXTURE := "res://assets/art/effects/effect_sheet.png" const WARRIOR_COLUMNS := 16 const WARRIOR_ROWS := 25 var failures: Array[String] = [] func _init() -> void: _run.call_deferred() func _run() -> void: var scene: PackedScene = load("res://scenes/characters/player.tscn") if scene == null: push_error("Could not load player.tscn") quit(1) return var player: Node = scene.instantiate() get_root().add_child(player) await process_frame var animation_player: AnimationPlayer = player.get_node("AnimationPlayer") as AnimationPlayer _expect_action_has_key("combo_w", KEY_W) _expect_action_has_key("combo_a", KEY_A) _expect_action_has_key("combo_d", KEY_D) _expect_action_has_key("combo_s", KEY_S) _expect_action_has_key("combo_space", KEY_SPACE) _expect_warrior_animation(animation_player, "warrior_idle", 1, 8) _expect_warrior_animation(animation_player, "warrior_w", 6, 6) _expect_warrior_animation(animation_player, "warrior_wa", 7, 5) _expect_warrior_animation(animation_player, "warrior_s", 9, 3) _expect_warrior_animation(animation_player, "warrior_a", 10, 7) _expect_warrior_animation(animation_player, "warrior_aa", 11, 5) _expect_warrior_animation(animation_player, "warrior_aaa", 12, 8) _expect_warrior_animation(animation_player, "warrior_s_projectile", 14, 13) _expect_warrior_animation(animation_player, "warrior_charge_intro", 13, 8, WARRIOR_WOMAN_TEXTURE) _expect_warrior_animation(animation_player, "warrior_charge_loop", 13, 4, WARRIOR_WOMAN_TEXTURE, 4) _expect_animation_loops(animation_player, "warrior_charge_loop") _expect_warrior_animation(animation_player, "warrior_charge_release", 13, 8, WARRIOR_WOMAN_TEXTURE, 8) _expect_warrior_animation(animation_player, "warrior_a_space_space", 15, 12) _expect_warrior_animation(animation_player, "warrior_a_space", 17, 10) _expect_charge_effect(player) if animation_player.has_animation("player_punch"): failures.append("Old player_punch animation should be removed") if animation_player.has_animation("挥砍"): failures.append("Old slash animation should be removed") player.call("submit_combo_input", "W", "perfect") _expect_string(str(player.get("last_requested_skill_id")), "skill_w", "W alone should request row 6 skill") _expect_string(str(player.get("current_skill_animation")), "warrior_w", "W alone should play warrior_w") player.call("submit_combo_input", "A", "perfect") await create_timer(0.55).timeout await physics_frame _expect_string(str(player.get("last_requested_skill_id")), "skill_wa", "W+A should request row 7 skill") _expect_string(str(player.get("current_skill_animation")), "warrior_wa", "W+A should play warrior_wa") var projectile := PlayerProjectile.new() get_root().add_child(projectile) _expect_projectile_animation(projectile) projectile.queue_free() player.queue_free() _finish() func _expect_action_has_key(action_name: String, key: Key) -> void: if not InputMap.has_action(action_name): failures.append("Missing input action: %s" % action_name) return for event: InputEvent in InputMap.action_get_events(action_name): var key_event := event as InputEventKey if key_event != null and (key_event.keycode == key or key_event.physical_keycode == key): return failures.append("Input action %s should be bound to key %s" % [action_name, OS.get_keycode_string(key)]) func _expect_warrior_animation(animation_player: AnimationPlayer, animation_name: String, row: int, expected_frames: int, texture_path := WARRIOR_TEXTURE, start_column := 0) -> void: if not animation_player.has_animation(animation_name): failures.append("Missing animation: %s" % animation_name) return var animation: Animation = animation_player.get_animation(animation_name) var hframes_found := false var vframes_found := false var texture_found := false var frame_values: Array[int] = [] for track_index: int in range(animation.get_track_count()): var track_path := animation.track_get_path(track_index) if track_path == NodePath("CharacterSprite:texture"): var texture: Texture2D = animation.track_get_key_value(track_index, 0) texture_found = texture != null and texture.resource_path == texture_path elif track_path == NodePath("CharacterSprite:hframes"): hframes_found = true var hframes: int = animation.track_get_key_value(track_index, 0) if hframes != WARRIOR_COLUMNS: failures.append("%s hframes expected %d, got %d" % [animation_name, WARRIOR_COLUMNS, hframes]) elif track_path == NodePath("CharacterSprite:vframes"): vframes_found = true var vframes: int = animation.track_get_key_value(track_index, 0) if vframes != WARRIOR_ROWS: failures.append("%s vframes expected %d, got %d" % [animation_name, WARRIOR_ROWS, vframes]) elif track_path == NodePath("CharacterSprite:frame"): for key_index: int in range(animation.track_get_key_count(track_index)): frame_values.append(animation.track_get_key_value(track_index, key_index)) if not texture_found: failures.append("%s should use %s" % [animation_name, texture_path]) if not hframes_found: failures.append("Missing hframes track: %s" % animation_name) if not vframes_found: failures.append("Missing vframes track: %s" % animation_name) if frame_values.size() != expected_frames: failures.append("%s should key %d frames, got %d" % [animation_name, expected_frames, frame_values.size()]) var first_frame := (row - 1) * WARRIOR_COLUMNS + start_column for index: int in range(frame_values.size()): var expected := first_frame + index if frame_values[index] != expected: failures.append("%s frame %d expected sheet frame %d, got %d" % [ animation_name, index, expected, frame_values[index], ]) func _expect_string(actual: String, expected: String, label: String) -> void: if actual != expected: failures.append("%s: expected %s, got %s" % [label, expected, actual]) func _expect_animation_loops(animation_player: AnimationPlayer, animation_name: String) -> void: if not animation_player.has_animation(animation_name): failures.append("Missing animation for loop check: %s" % animation_name) return var animation: Animation = animation_player.get_animation(animation_name) if int(animation.loop_mode) != 1: failures.append("%s should loop, got loop_mode %d" % [animation_name, int(animation.loop_mode)]) func _expect_projectile_animation(projectile: Node) -> void: if projectile.get_child_count() == 0: failures.append("Projectile should create a Sprite2D child") return var sprite := projectile.get_child(0) as Sprite2D if sprite == null: failures.append("Projectile child should be Sprite2D") return if sprite.texture == null or sprite.texture.resource_path != EFFECT_TEXTURE: failures.append("Projectile should use %s" % EFFECT_TEXTURE) if sprite.hframes != 6: failures.append("Projectile hframes expected 6, got %d" % sprite.hframes) if sprite.vframes != 2: failures.append("Projectile vframes expected 2, got %d" % sprite.vframes) projectile.call("_process", 0.0) if sprite.frame != 0: failures.append("Projectile first frame expected 0, got %d" % sprite.frame) projectile.call("_process", 0.18) if sprite.frame != 3: failures.append("Projectile should use first row frame 3 after 0.18s, got %d" % sprite.frame) func _expect_charge_effect(player: Node) -> void: var sprite := player.get_node_or_null("ChargeEffectSprite") as Sprite2D if sprite == null: failures.append("Player should include ChargeEffectSprite") return if sprite.texture == null or sprite.texture.resource_path != CHARGE_EFFECT_TEXTURE: failures.append("Charge effect should use %s" % CHARGE_EFFECT_TEXTURE) if sprite.hframes != 5: failures.append("Charge effect hframes expected 5, got %d" % sprite.hframes) if sprite.vframes != 2: failures.append("Charge effect vframes expected 2, got %d" % sprite.vframes) if sprite.z_index < 1: failures.append("Charge effect should draw above the player feet, got z_index %d" % sprite.z_index) if sprite.visible: failures.append("Charge effect should start hidden") func _finish() -> void: if failures.is_empty(): print("PASS player warrior actions") quit(0) else: for failure: String in failures: push_error(failure) quit(1)