extends SceneTree const BattleStateScript := preload("res://scripts/core/battle_state.gd") const BattleSceneScript := preload("res://scripts/scenes/battle_scene.gd") func _init() -> void: var failures: Array[String] = [] _check_deferred_move_events(failures) _check_scene_post_move_menu_flow(failures) if failures.is_empty(): print("post move action flow smoke ok") quit(0) return for failure in failures: push_error(failure) quit(1) func _check_deferred_move_events(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("could not load opening battle for pending move state") return var from_cell := Vector2i(1, 3) var to_cell := Vector2i(2, 3) state.battle_events.append({ "id": "pending_move_gold", "once": true, "when": { "type": "unit_reaches_tile", "unit_ids": ["cao_cao"], "pos": [to_cell.x, to_cell.y] }, "actions": [{"type": "grant_gold", "amount": 77}] }) if not state.select_unit("cao_cao"): failures.append("could not select Cao Cao for pending move state") return if not state.try_move_selected(to_cell, true): failures.append("tentative move should succeed") return var cao_cao: Dictionary = state.get_unit("cao_cao") if cao_cao.get("pos", Vector2i.ZERO) != to_cell: failures.append("tentative move should update position") if not bool(cao_cao.get("moved", false)) or bool(cao_cao.get("acted", false)): failures.append("tentative move should mark moved but not acted") if state.get_battle_gold_reward() != 0: failures.append("tentative move should defer unit_reaches_tile rewards") if not state.cancel_pending_move("cao_cao", from_cell, to_cell): failures.append("pending move cancel should succeed") cao_cao = state.get_unit("cao_cao") if cao_cao.get("pos", Vector2i.ZERO) != from_cell or bool(cao_cao.get("moved", false)) or bool(cao_cao.get("acted", false)): failures.append("pending move cancel should restore original action state") if state.get_battle_gold_reward() != 0: failures.append("canceled pending move should not fire movement event") if not state.try_move_selected(to_cell, true): failures.append("second tentative move should succeed") if not state.commit_pending_move_events("cao_cao"): failures.append("pending move commit should keep battle active") if state.get_battle_gold_reward() != 77: failures.append("pending move commit should fire deferred movement event") func _check_scene_post_move_menu_flow(failures: Array[String]) -> void: var scene = BattleSceneScript.new() scene._create_hud() if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("could not load opening battle for scene flow") scene.free() return scene.battle_started = true scene.campaign_complete_screen = false scene.briefing_panel.visible = false scene.result_panel.visible = false scene._clear_pending_move_state() if not scene.state.select_unit("cao_cao"): failures.append("could not select Cao Cao for scene flow") scene.free() return scene._refresh_ranges() scene._handle_board_click(_screen_for_cell(Vector2i(2, 3))) var cao_cao: Dictionary = scene.state.get_unit("cao_cao") if not scene._has_pending_move(): failures.append("scene should keep pending move metadata after move click") if scene.post_move_menu == null or not scene.post_move_menu.visible: failures.append("post-move action menu should be visible after moving") if cao_cao.get("pos", Vector2i.ZERO) != Vector2i(2, 3) or not bool(cao_cao.get("moved", false)) or bool(cao_cao.get("acted", false)): failures.append("scene move should be pending with action still available") if scene.state.get_selected_unit().is_empty(): failures.append("scene should keep moved unit selected for follow-up action") if not scene.attack_cells.is_empty(): failures.append("attack cells should wait until Attack is chosen from the post-move menu") scene._handle_cancel_input() cao_cao = scene.state.get_unit("cao_cao") if scene._has_pending_move() or (scene.post_move_menu != null and scene.post_move_menu.visible): failures.append("right-click cancel handler should clear pending move menu state") if cao_cao.get("pos", Vector2i.ZERO) != Vector2i(1, 3) or bool(cao_cao.get("moved", false)) or bool(cao_cao.get("acted", false)): failures.append("right-click cancel handler should restore the starting cell") scene._refresh_ranges() scene._handle_board_click(_screen_for_cell(Vector2i(2, 3))) scene._on_post_move_wait_pressed() cao_cao = scene.state.get_unit("cao_cao") if cao_cao.get("pos", Vector2i.ZERO) != Vector2i(2, 3) or not bool(cao_cao.get("moved", false)) or not bool(cao_cao.get("acted", false)): failures.append("post-move Wait should commit movement and end the unit action") if not scene.state.get_selected_unit().is_empty() or scene._has_pending_move(): failures.append("post-move Wait should clear selection and pending metadata") _check_scene_post_move_attack_targeting(failures) scene.free() func _check_scene_post_move_attack_targeting(failures: Array[String]) -> void: var scene = BattleSceneScript.new() scene._create_hud() if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("could not load opening battle for attack targeting flow") scene.free() return scene.battle_started = true scene.campaign_complete_screen = false scene.briefing_panel.visible = false scene.result_panel.visible = false scene._clear_pending_move_state() var enemy: Dictionary = scene.state.get_unit("yellow_turban_1") enemy["pos"] = Vector2i(3, 3) enemy["hp"] = 24 enemy["def"] = 1 var cao_cao: Dictionary = scene.state.get_unit("cao_cao") cao_cao["agi"] = 999 cao_cao["atk"] = 60 scene.state.rng.seed = 1 scene.state.battle_events.append({ "id": "pending_attack_gold", "once": true, "when": { "type": "unit_reaches_tile", "unit_ids": ["cao_cao"], "pos": [2, 3] }, "actions": [{"type": "grant_gold", "amount": 33}] }) if not scene.state.select_unit("cao_cao"): failures.append("could not select Cao Cao for attack targeting flow") scene.free() return scene._refresh_ranges() scene._handle_board_click(_screen_for_cell(Vector2i(2, 3))) if not scene._has_pending_move() or scene.post_move_menu == null or not scene.post_move_menu.visible: failures.append("attack targeting setup should leave the post-move menu visible") if scene.basic_attack_targeting: failures.append("attack targeting should be off before pressing Attack") if not scene.attack_cells.is_empty(): failures.append("attack range should stay hidden before pressing Attack") if scene.state.get_battle_gold_reward() != 0: failures.append("attack setup should not commit pending move events early") scene._on_post_move_attack_pressed() if not scene.basic_attack_targeting: failures.append("Attack should enter target selection mode") if scene.post_move_menu != null and scene.post_move_menu.visible: failures.append("post-move menu should hide while selecting an attack target") if scene.targeting_hint_panel == null or not scene.targeting_hint_panel.visible: failures.append("targeting hint panel should be visible during attack targeting") if not scene.attack_cells.has(Vector2i(3, 3)): failures.append("attack targeting should highlight the adjacent enemy cell") var markers := scene._attack_target_marker_entries() if markers.size() != 1 or markers[0].get("cell", Vector2i.ZERO) != Vector2i(3, 3): failures.append("attack target markers should include only the reachable enemy: %s" % str(markers)) scene._on_targeting_back_pressed() cao_cao = scene.state.get_unit("cao_cao") if scene.basic_attack_targeting or not scene._has_pending_move() or scene.post_move_menu == null or not scene.post_move_menu.visible: failures.append("Back should return to the post-move action menu") if cao_cao.get("pos", Vector2i.ZERO) != Vector2i(2, 3) or not bool(cao_cao.get("moved", false)) or bool(cao_cao.get("acted", false)): failures.append("Back should preserve the pending moved unit without acting") scene._on_post_move_attack_pressed() var enemy_hp_before := int(enemy.get("hp", 0)) scene._handle_board_click(_screen_for_cell(Vector2i(3, 3))) cao_cao = scene.state.get_unit("cao_cao") enemy = scene.state.get_unit("yellow_turban_1") if scene._has_pending_move() or scene.basic_attack_targeting: failures.append("attack click should clear pending move and targeting state") if not scene.state.get_selected_unit().is_empty(): failures.append("attack click should clear selected unit after acting") if cao_cao.get("pos", Vector2i.ZERO) != Vector2i(2, 3) or not bool(cao_cao.get("moved", false)) or not bool(cao_cao.get("acted", false)): failures.append("attack click should commit the moved attacker action") if int(enemy.get("hp", 0)) >= enemy_hp_before and bool(enemy.get("alive", true)): failures.append("attack click should damage or defeat the target") if scene.state.get_battle_gold_reward() != 33: failures.append("attack click should commit deferred movement event before resolving attack") scene.free() func _screen_for_cell(cell: Vector2i) -> Vector2: return BattleSceneScript.BOARD_OFFSET + Vector2(cell.x, cell.y) * BattleSceneScript.TILE_SIZE + Vector2.ONE * (BattleSceneScript.TILE_SIZE * 0.5)