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_directional_damage_bonuses(failures) _check_attack_updates_facing(failures) _check_move_cancel_restores_facing(failures) _check_scene_pixel_facing(failures) _check_scene_directional_attack_badges(failures) if failures.is_empty(): print("directional combat smoke ok") quit(0) return for failure in failures: push_error(failure) quit(1) func _check_directional_damage_bonuses(failures: Array[String]) -> void: var state = _loaded_state(failures, "directional damage") if state == null: return var attacker: Dictionary = state.get_unit("cao_cao") var target: Dictionary = state.get_unit("yellow_turban_1") if attacker.is_empty() or target.is_empty(): failures.append("missing units for directional damage") return _configure_duel_units(attacker, target) target["pos"] = Vector2i(5, 9) target["facing_x"] = 1 attacker["pos"] = Vector2i(6, 9) var front_damage: int = state.calculate_damage(attacker, target) var front_info: Dictionary = state.get_directional_attack_info(attacker, target) attacker["pos"] = Vector2i(5, 8) var side_damage: int = state.calculate_damage(attacker, target) var side_info: Dictionary = state.get_directional_attack_info(attacker, target) attacker["pos"] = Vector2i(4, 9) var back_damage: int = state.calculate_damage(attacker, target) var back_info: Dictionary = state.get_directional_attack_info(attacker, target) if str(front_info.get("kind", "")) != BattleStateScript.DIRECTIONAL_FRONT_ATTACK: failures.append("front attack should be classified as front: %s" % str(front_info)) if str(side_info.get("kind", "")) != BattleStateScript.DIRECTIONAL_SIDE_ATTACK: failures.append("same-file attack should be classified as side: %s" % str(side_info)) if str(back_info.get("kind", "")) != BattleStateScript.DIRECTIONAL_BACK_ATTACK: failures.append("rear attack should be classified as back: %s" % str(back_info)) if side_damage != front_damage + BattleStateScript.DIRECTIONAL_SIDE_DAMAGE_BONUS: failures.append("side damage should add side bonus: front %d side %d" % [front_damage, side_damage]) if back_damage != front_damage + BattleStateScript.DIRECTIONAL_BACK_DAMAGE_BONUS: failures.append("back damage should add back bonus: front %d back %d" % [front_damage, back_damage]) var preview: Dictionary = state.get_damage_preview("cao_cao", "yellow_turban_1") if str(preview.get("directional_attack", "")) != BattleStateScript.DIRECTIONAL_BACK_ATTACK: failures.append("damage preview should expose rear attack: %s" % str(preview)) if int(preview.get("directional_bonus", 0)) != BattleStateScript.DIRECTIONAL_BACK_DAMAGE_BONUS: failures.append("damage preview should expose rear bonus: %s" % str(preview)) func _check_attack_updates_facing(failures: Array[String]) -> void: var state = _loaded_state(failures, "attack facing") if state == null: return var attacker: Dictionary = state.get_unit("cao_cao") var target: Dictionary = state.get_unit("yellow_turban_1") if attacker.is_empty() or target.is_empty(): failures.append("missing units for attack facing") return _configure_duel_units(attacker, target) attacker["pos"] = Vector2i(4, 9) attacker["facing_x"] = -1 target["pos"] = Vector2i(5, 9) target["facing_x"] = 1 target["controllable"] = false var expected_damage: int = state.calculate_damage(attacker, target) var start_hp: int = int(target.get("hp", 0)) if not state.select_unit("cao_cao"): failures.append("could not select Cao Cao for attack facing") return if not state.try_attack_selected("yellow_turban_1"): failures.append("rear attack should execute") return if int(target.get("hp", 0)) != start_hp - expected_damage: failures.append("rear attack damage mismatch: expected %d got %d" % [expected_damage, start_hp - int(target.get("hp", 0))]) if state.unit_facing_x(attacker) != 1: failures.append("attacker should face target after attack") func _check_move_cancel_restores_facing(failures: Array[String]) -> void: var state = _loaded_state(failures, "move cancel facing") if state == null: return var unit: Dictionary = state.get_unit("cao_cao") if unit.is_empty(): failures.append("missing Cao Cao for move cancel facing") return unit["facing_x"] = -1 if not state.select_unit("cao_cao"): failures.append("could not select Cao Cao for move cancel facing") return if not state.try_move_selected(Vector2i(2, 3), true): failures.append("pending move should succeed for facing cancel") return if state.unit_facing_x(unit) != 1: failures.append("pending move to the east should face right") if not state.cancel_pending_move("cao_cao", Vector2i(1, 3), Vector2i(2, 3)): failures.append("pending move cancel should succeed for facing restore") return if state.unit_facing_x(unit) != -1: failures.append("canceling a pending move should restore previous facing") func _check_scene_pixel_facing(failures: Array[String]) -> void: var scene = BattleSceneScript.new() if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("could not load opening battle for pixel facing") scene.free() return var player: Dictionary = scene.state.get_unit("cao_cao") var enemy: Dictionary = scene.state.get_unit("yellow_turban_1") player["facing_x"] = -1 enemy["facing_x"] = 1 if scene._unit_pixel_facing(player) != -1: failures.append("pixel facing should use player facing_x") if scene._unit_pixel_facing(enemy) != 1: failures.append("pixel facing should use enemy facing_x") if scene._unit_facing_text(player) != "서쪽": failures.append("unit facing text should localize west-facing units") if scene._unit_facing_text(enemy) != "동쪽": failures.append("unit facing text should localize east-facing units") var marker_rect := Rect2(Vector2(12.0, 20.0), Vector2(scene.TILE_SIZE, scene.TILE_SIZE)) var left_points: PackedVector2Array = scene._unit_facing_marker_points(marker_rect, player) var right_points: PackedVector2Array = scene._unit_facing_marker_points(marker_rect, enemy) if left_points.size() != 3 or right_points.size() != 3: failures.append("unit facing marker should draw a compact triangular marker") elif left_points[0].x >= left_points[1].x or right_points[0].x <= right_points[1].x: failures.append("unit facing marker should point toward the unit facing: left=%s right=%s" % [str(left_points), str(right_points)]) var closed_points: PackedVector2Array = scene._closed_polyline_points(left_points) if closed_points.size() != 4 or closed_points[0] != closed_points[3]: failures.append("unit facing marker outline should close the triangle: %s" % str(closed_points)) var marker_color: Color = scene._unit_facing_marker_fill_color(player, Color(0.2, 0.4, 0.8, 1.0)) if marker_color.a <= 0.0: failures.append("unit facing marker fill should be visible") scene.unit_action_motion_by_unit["cao_cao"] = { "from": Vector2i(4, 3), "to": Vector2i(3, 3) } if scene._unit_pixel_facing(player) != -1: failures.append("pixel facing should prefer active action motion") scene.free() func _check_scene_directional_attack_badges(failures: Array[String]) -> void: var scene = BattleSceneScript.new() if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("could not load opening battle for directional attack badges") scene.free() return var attacker: Dictionary = scene.state.get_unit("cao_cao") var target: Dictionary = scene.state.get_unit("yellow_turban_1") if attacker.is_empty() or target.is_empty(): failures.append("missing units for directional attack badge") scene.free() return for unit in scene.state.units: if str(unit.get("id", "")) != "cao_cao" and str(unit.get("id", "")) != "yellow_turban_1": unit["alive"] = false _configure_duel_units(attacker, target) attacker["move"] = 4 attacker["pos"] = Vector2i(4, 9) target["pos"] = Vector2i(5, 9) target["facing_x"] = 1 target["controllable"] = false scene.battle_started = true if not scene.state.select_unit("cao_cao"): failures.append("could not select Cao Cao for directional attack badge") scene.free() return scene._refresh_ranges() scene.hover_cell = Vector2i(5, 9) var badge := scene._target_preview_badge() if not str(badge.get("text", "")).contains("후+%d" % BattleStateScript.DIRECTIONAL_BACK_DAMAGE_BONUS): failures.append("rear attack hover badge should expose compact rear bonus: %s" % str(badge)) var marker_text := "" for marker in scene._attack_target_marker_entries(): if str(marker.get("target_id", "")) == "yellow_turban_1": marker_text = str(marker.get("text", "")) if not marker_text.contains("후+%d" % BattleStateScript.DIRECTIONAL_BACK_DAMAGE_BONUS): failures.append("rear attack target marker should expose compact rear bonus: %s" % marker_text) attacker["pos"] = Vector2i(5, 8) scene._refresh_ranges() scene.hover_cell = Vector2i(5, 9) var side_badge: Dictionary = scene._target_preview_badge() if not str(side_badge.get("text", "")).contains("측+%d" % BattleStateScript.DIRECTIONAL_SIDE_DAMAGE_BONUS): failures.append("side attack hover badge should expose compact side bonus: %s" % str(side_badge)) attacker["pos"] = Vector2i(2, 9) attacker["facing_x"] = -1 target["pos"] = Vector2i(5, 9) target["facing_x"] = 1 scene._refresh_ranges() scene.hover_cell = Vector2i(5, 9) var auto_badge: Dictionary = scene._target_preview_badge() if str(auto_badge.get("text", "")).contains("행공") or not str(auto_badge.get("tooltip", "")).contains("이동 후 공격"): failures.append("auto-move attack hover badge should use a clear hint instead of jargon: %s" % str(auto_badge)) if not str(auto_badge.get("text", "")).contains("후+%d" % BattleStateScript.DIRECTIONAL_BACK_DAMAGE_BONUS): failures.append("auto-move attack hover badge should expose compact rear bonus: %s" % str(auto_badge)) if scene._compact_directional_badge_label(BattleStateScript.DIRECTIONAL_SIDE_ATTACK) != "측": failures.append("directional badge helper should compact side attacks") if scene._format_directional_badge_suffix({ "directional_attack": BattleStateScript.DIRECTIONAL_FRONT_ATTACK, "directional_bonus": 0, "directional_label": "정면" }) != "": failures.append("front/no-bonus directional badge suffix should stay empty") scene.free() func _configure_duel_units(attacker: Dictionary, target: Dictionary) -> void: attacker["atk"] = 30 attacker["agi"] = 50 attacker["hp"] = 80 attacker["max_hp"] = 80 attacker["range"] = 1 attacker["min_range"] = 1 attacker["acted"] = false attacker["moved"] = false target["def"] = 10 target["atk"] = 1 target["agi"] = 0 target["hp"] = 80 target["max_hp"] = 80 target["range"] = 1 target["min_range"] = 1 target["alive"] = true target["acted"] = false target["moved"] = false func _loaded_state(failures: Array[String], label: String): var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("%s could not load opening battle" % label) return null return state