extends SceneTree const BattleStateScript := preload("res://scripts/core/battle_state.gd") const BattleSceneScript := preload("res://scripts/scenes/battle_scene.gd") const SCENARIO_BACKGROUNDS := [ { "scenario_path": "res://data/scenarios/001_yellow_turbans.json", "background_path": "res://art/backgrounds/battlefield_frontier.png", "label": "001 opening field background", "allow_default": true }, { "scenario_path": "res://data/scenarios/002_sishui_gate.json", "background_path": "res://art/backgrounds/sishui_gate_pass.png", "label": "002 Sishui Gate background" }, { "scenario_path": "res://data/scenarios/003_xingyang_ambush.json", "background_path": "res://art/backgrounds/xingyang_forest_ambush.png", "label": "003 Xingyang forest ambush background" }, { "scenario_path": "res://data/scenarios/004_qingzhou_campaign.json", "background_path": "res://art/backgrounds/qingzhou_village_road.png", "label": "004 Qingzhou village road background" }, { "scenario_path": "res://data/scenarios/005_puyang_raid.json", "background_path": "res://art/backgrounds/puyang_raid_camp.png", "label": "005 Puyang raid camp background" }, { "scenario_path": "res://data/scenarios/006_dingtao_counterattack.json", "background_path": "res://art/backgrounds/qingzhou_village_road.png", "label": "006 Dingtao field background" }, { "scenario_path": "res://data/scenarios/007_xian_emperor_escort.json", "background_path": "res://art/backgrounds/luoyang_escort_road.png", "label": "007 Luoyang escort road background" }, { "scenario_path": "res://data/scenarios/008_wan_castle_escape.json", "background_path": "res://art/backgrounds/wan_castle_escape.png", "label": "008 Wan Castle background" }, { "scenario_path": "res://data/scenarios/009_xiapi_siege.json", "background_path": "res://art/backgrounds/xiapi_flooded_siege.png", "label": "009 Xiapi flooded siege background" }, { "scenario_path": "res://data/scenarios/010_white_horse_relief.json", "background_path": "res://art/backgrounds/white_horse_relief.png", "label": "010 White Horse relief background" }, { "scenario_path": "res://data/scenarios/012_guandu_showdown.json", "background_path": "res://art/backgrounds/guandu_defensive_camps.png", "label": "012 Guandu defensive camps background" }, { "scenario_path": "res://data/scenarios/013_wuchao_raid.json", "background_path": "res://art/backgrounds/wuchao_night_granary.png", "label": "013 Wuchao night granary background" } ] func _init() -> void: var failures: Array[String] = [] _check_battle_visual_data(failures) _check_scene_texture_loading(failures) _check_hud_focus_text(failures) _check_ancient_ui_theme(failures) _check_hover_intent_badges(failures) _check_action_button_disabled_reasons(failures) _check_terrain_and_unit_presentation(failures) _check_attack_motion_profiles(failures) _check_attack_motion_signal(failures) if failures.is_empty(): print("visual assets smoke ok") quit(0) return for failure in failures: push_error(failure) quit(1) func _check_battle_visual_data(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") return var briefing := state.get_briefing() if (briefing.get("camp_dialogue", []) as Array).is_empty(): failures.append("001 briefing should expose camp dialogue") var conversations: Array = briefing.get("camp_conversations", []) if conversations.size() < 3: failures.append("001 briefing should expose at least three camp conversations") else: _check_camp_conversation(failures, conversations[0], "cao_cao_strategy", "officer", "cao_cao") _check_camp_conversation(failures, conversations[1], "xiahou_dun_vanguard", "officer", "xiahou_dun") _check_camp_conversation(failures, conversations[2], "northern_woods_cache", "topic", "") _check_camp_conversation_effect(failures, conversations[2], "bean", 1) var merchant := state.get_shop_merchant() if merchant.is_empty() or (merchant.get("lines", []) as Array).is_empty(): failures.append("001 shop should expose merchant lines") _check_image_path(failures, state.get_map_background_path(), "001 map background") _check_scenario_backgrounds_data(failures) for unit_id in ["cao_cao", "xiahou_dun", "yellow_turban_1", "yellow_turban_2", "yellow_turban_3"]: var unit := state.get_unit(unit_id) if unit.is_empty(): failures.append("missing expected unit: %s" % unit_id) continue _check_image_path(failures, str(unit.get("sprite", "")), "unit %s sprite" % unit_id) _check_unit_sprite_resolution(failures, state, "cao_cao", "res://art/units/strategist.png", false) _check_unit_sprite_resolution(failures, state, "yellow_turban_1", "res://art/units/enemies/enemy_warrior.png", true) _check_unit_sprite_resolution(failures, state, "yellow_turban_2", "res://art/units/enemies/enemy_infantry.png", true) _check_unit_sprite_resolution(failures, state, "yellow_turban_3", "res://art/units/enemies/enemy_archer.png", true) for path in [ "res://art/units/archer.png", "res://art/units/cavalry.png", "res://art/units/infantry.png", "res://art/units/strategist.png", "res://art/units/warrior.png", "res://art/units/enemies/enemy_archer.png", "res://art/units/enemies/enemy_cavalry.png", "res://art/units/enemies/enemy_infantry.png", "res://art/units/enemies/enemy_strategist.png", "res://art/units/enemies/enemy_warrior.png" ]: _check_alpha_cutout_path(failures, path, "unit cutout %s" % path) for path in [ "res://art/portraits/cao_cao.png", "res://art/portraits/cao_ren.png", "res://art/portraits/dian_wei.png", "res://art/portraits/guo_jia.png", "res://art/portraits/xiahou_dun.png", "res://art/portraits/xiahou_yuan.png", "res://art/portraits/zhang_he.png" ]: _check_alpha_cutout_path(failures, path, "officer portrait cutout %s" % path) var gate_state = BattleStateScript.new() if not gate_state.load_battle("res://data/scenarios/002_sishui_gate.json"): failures.append("could not load Sishui Gate for enemy cavalry sprite") else: _check_unit_sprite_resolution(failures, gate_state, "hua_xiong_vanguard", "res://art/units/enemies/enemy_cavalry.png", true) var gate_briefing := gate_state.get_briefing() var gate_conversations: Array = gate_briefing.get("camp_conversations", []) if gate_conversations.size() < 3: failures.append("002 briefing should expose at least three camp conversations") else: _check_camp_conversation(failures, gate_conversations[0], "sishui_coalition_reading", "officer", "cao_cao") _check_camp_conversation(failures, gate_conversations[1], "sishui_xiahou_yuan_ridge", "officer", "xiahou_yuan") _check_camp_conversation(failures, gate_conversations[2], "sishui_quartermaster_antidote", "topic", "") _check_camp_conversation_effect(failures, gate_conversations[2], "antidote", 1) var gate_merchant := gate_state.get_shop_merchant() if str(gate_merchant.get("name", "")) != "연합 상인": failures.append("002 shop should expose coalition merchant name") if (gate_merchant.get("lines", []) as Array).size() < 2: failures.append("002 shop should expose merchant flavor lines") var qingzhou_state = BattleStateScript.new() if not qingzhou_state.load_battle("res://data/scenarios/004_qingzhou_campaign.json"): failures.append("could not load 청주 평정전 camp data") else: var qingzhou_conversations: Array = qingzhou_state.get_briefing().get("camp_conversations", []) if qingzhou_conversations.size() != 2: failures.append("004 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, qingzhou_conversations[0], "qingzhou_settlement_council", "officer", "cao_cao") _check_camp_conversation(failures, qingzhou_conversations[1], "qingzhou_village_screen", "officer", "cao_ren") var qingzhou_merchant := qingzhou_state.get_shop_merchant() if str(qingzhou_merchant.get("name", "")) != "청주 길상인": failures.append("004 shop should expose Qingzhou merchant name") if (qingzhou_merchant.get("lines", []) as Array).size() < 2: failures.append("004 shop should expose merchant flavor lines") var qingzhou_pursuit_state = BattleStateScript.new() if not qingzhou_pursuit_state.load_battle("res://data/scenarios/004_qingzhou_campaign.json", {}, {}, {"pursued_dong_zhuo": true}): failures.append("could not load Qingzhou pursuit camp data") else: var veterans: Dictionary = _find_camp_conversation( qingzhou_pursuit_state.get_briefing().get("camp_conversations", []), "qingzhou_hardened_veterans" ) if veterans.is_empty(): failures.append("004 pursuit flag should expose hardened veterans conversation") else: _check_camp_conversation_effect(failures, veterans, "wine", 1) if not _find_camp_conversation( qingzhou_pursuit_state.get_briefing().get("camp_conversations", []), "qingzhou_sishui_stores" ).is_empty(): failures.append("004 pursuit flag should not expose Sishui stores conversation") var qingzhou_regroup_state = BattleStateScript.new() if not qingzhou_regroup_state.load_battle("res://data/scenarios/004_qingzhou_campaign.json", {}, {}, {"regrouped_after_sishui": true}): failures.append("could not load Qingzhou regroup camp data") else: var stores: Dictionary = _find_camp_conversation( qingzhou_regroup_state.get_briefing().get("camp_conversations", []), "qingzhou_sishui_stores" ) if stores.is_empty(): failures.append("004 regroup flag should expose Sishui stores conversation") else: _check_camp_conversation_effect(failures, stores, "bean", 1) if not _find_camp_conversation( qingzhou_regroup_state.get_briefing().get("camp_conversations", []), "qingzhou_hardened_veterans" ).is_empty(): failures.append("004 regroup flag should not expose hardened veterans conversation") var puyang_state = BattleStateScript.new() if not puyang_state.load_battle("res://data/scenarios/005_puyang_raid.json"): failures.append("could not load Puyang Raid camp data") else: var puyang_conversations: Array = puyang_state.get_briefing().get("camp_conversations", []) if puyang_conversations.size() != 3: failures.append("005 base briefing should expose exactly three camp conversations") else: _check_camp_conversation(failures, puyang_conversations[0], "puyang_night_raid_council", "officer", "cao_cao") _check_camp_conversation(failures, puyang_conversations[1], "puyang_dian_wei_shield", "officer", "dian_wei") _check_camp_conversation(failures, puyang_conversations[2], "puyang_raider_stores", "topic", "") _check_camp_conversation_effect(failures, puyang_conversations[2], "bean", 1) var puyang_merchant := puyang_state.get_shop_merchant() if str(puyang_merchant.get("name", "")) != "복양 야상": failures.append("005 shop should expose Puyang merchant name") if (puyang_merchant.get("lines", []) as Array).size() < 2: failures.append("005 shop should expose merchant flavor lines") var puyang_pursuit_state = BattleStateScript.new() if not puyang_pursuit_state.load_battle("res://data/scenarios/005_puyang_raid.json", {}, {}, {"pursued_dong_zhuo": true}): failures.append("could not load Puyang pursuit camp data") else: var vanguard: Dictionary = _find_camp_conversation( puyang_pursuit_state.get_briefing().get("camp_conversations", []), "puyang_hardened_vanguard" ) if vanguard.is_empty(): failures.append("005 pursuit flag should expose hardened vanguard conversation") else: _check_camp_conversation_effect(failures, vanguard, "wine", 1) if not _find_camp_conversation( puyang_pursuit_state.get_briefing().get("camp_conversations", []), "puyang_qingzhou_reserve_wagon" ).is_empty(): failures.append("005 pursuit flag should not expose Qingzhou reserve wagon conversation") var puyang_regroup_state = BattleStateScript.new() if not puyang_regroup_state.load_battle("res://data/scenarios/005_puyang_raid.json", {}, {}, {"regrouped_after_sishui": true}): failures.append("could not load Puyang regroup camp data") else: var reserve: Dictionary = _find_camp_conversation( puyang_regroup_state.get_briefing().get("camp_conversations", []), "puyang_qingzhou_reserve_wagon" ) if reserve.is_empty(): failures.append("005 regroup flag should expose Qingzhou reserve wagon conversation") else: _check_camp_conversation_effect(failures, reserve, "panacea", 1) if not _find_camp_conversation( puyang_regroup_state.get_briefing().get("camp_conversations", []), "puyang_hardened_vanguard" ).is_empty(): failures.append("005 regroup flag should not expose hardened vanguard conversation") var dingtao_state = BattleStateScript.new() if not dingtao_state.load_battle("res://data/scenarios/006_dingtao_counterattack.json"): failures.append("could not load Dingtao Counterattack camp data") else: var dingtao_conversations: Array = dingtao_state.get_briefing().get("camp_conversations", []) if dingtao_conversations.size() != 2: failures.append("006 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, dingtao_conversations[0], "dingtao_countercharge_council", "officer", "cao_cao") _check_camp_conversation(failures, dingtao_conversations[1], "dingtao_dian_wei_roadblock", "officer", "dian_wei") var dingtao_merchant := dingtao_state.get_shop_merchant() if str(dingtao_merchant.get("name", "")) != "정도 야상": failures.append("006 shop should expose Dingtao merchant name") if (dingtao_merchant.get("lines", []) as Array).size() < 2: failures.append("006 shop should expose merchant flavor lines") _check_shop_item_visibility(failures, dingtao_state, "iron_armor", false, "006 base") _check_shop_item_visibility(failures, dingtao_state, "war_drum", false, "006 base") var dingtao_fortified_state = BattleStateScript.new() if not dingtao_fortified_state.load_battle("res://data/scenarios/006_dingtao_counterattack.json", {}, {}, {"fortified_yan_province": true}): failures.append("could not load Dingtao fortified camp data") else: var fortified: Dictionary = _find_camp_conversation( dingtao_fortified_state.get_briefing().get("camp_conversations", []), "dingtao_fortified_reserves" ) if fortified.is_empty(): failures.append("006 fortified flag should expose fortified reserves conversation") else: _check_camp_conversation_effect(failures, fortified, "panacea", 1) if not _find_camp_conversation( dingtao_fortified_state.get_briefing().get("camp_conversations", []), "dingtao_pressed_vanguard" ).is_empty(): failures.append("006 fortified flag should not expose pressed vanguard conversation") _check_shop_item_visibility(failures, dingtao_fortified_state, "iron_armor", true, "006 fortified") _check_shop_item_visibility(failures, dingtao_fortified_state, "war_drum", false, "006 fortified") var dingtao_pressed_state = BattleStateScript.new() if not dingtao_pressed_state.load_battle("res://data/scenarios/006_dingtao_counterattack.json", {}, {}, {"pressed_lu_bu": true}): failures.append("could not load Dingtao pressed camp data") else: var pressed: Dictionary = _find_camp_conversation( dingtao_pressed_state.get_briefing().get("camp_conversations", []), "dingtao_pressed_vanguard" ) if pressed.is_empty(): failures.append("006 pressed flag should expose pressed vanguard conversation") else: _check_camp_conversation_effect(failures, pressed, "wine", 1) if not _find_camp_conversation( dingtao_pressed_state.get_briefing().get("camp_conversations", []), "dingtao_fortified_reserves" ).is_empty(): failures.append("006 pressed flag should not expose fortified reserves conversation") _check_shop_item_visibility(failures, dingtao_pressed_state, "iron_armor", false, "006 pressed") _check_shop_item_visibility(failures, dingtao_pressed_state, "war_drum", true, "006 pressed") var xian_state = BattleStateScript.new() if not xian_state.load_battle("res://data/scenarios/007_xian_emperor_escort.json"): failures.append("could not load Xian Emperor Escort camp data") else: var xian_conversations: Array = xian_state.get_briefing().get("camp_conversations", []) if xian_conversations.size() != 2: failures.append("007 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, xian_conversations[0], "xian_escort_council", "officer", "cao_cao") _check_camp_conversation(failures, xian_conversations[1], "xian_dian_wei_envoy_screen", "officer", "dian_wei") var xian_merchant := xian_state.get_shop_merchant() if str(xian_merchant.get("name", "")) != "호송 야상": failures.append("007 shop should expose escort merchant name") if (xian_merchant.get("lines", []) as Array).size() < 2: failures.append("007 shop should expose merchant flavor lines") _check_shop_item_visibility(failures, xian_state, "iron_armor", false, "007 base") _check_shop_item_visibility(failures, xian_state, "war_axe", false, "007 base") var xian_fortified_state = BattleStateScript.new() if not xian_fortified_state.load_battle("res://data/scenarios/007_xian_emperor_escort.json", {}, {}, {"fortified_yan_province": true}): failures.append("could not load Xian fortified camp data") else: var road_supplies: Dictionary = _find_camp_conversation( xian_fortified_state.get_briefing().get("camp_conversations", []), "xian_fortified_road_supplies" ) if road_supplies.is_empty(): failures.append("007 fortified flag should expose fortified road supplies conversation") else: _check_camp_conversation_effect(failures, road_supplies, "bean", 1) if not _find_camp_conversation( xian_fortified_state.get_briefing().get("camp_conversations", []), "xian_forced_march_canteens" ).is_empty(): failures.append("007 fortified flag should not expose forced march canteens conversation") _check_shop_item_visibility(failures, xian_fortified_state, "iron_armor", true, "007 fortified") _check_shop_item_visibility(failures, xian_fortified_state, "war_axe", false, "007 fortified") var xian_pressed_state = BattleStateScript.new() if not xian_pressed_state.load_battle("res://data/scenarios/007_xian_emperor_escort.json", {}, {}, {"pressed_lu_bu": true}): failures.append("could not load Xian pressed camp data") else: var canteens: Dictionary = _find_camp_conversation( xian_pressed_state.get_briefing().get("camp_conversations", []), "xian_forced_march_canteens" ) if canteens.is_empty(): failures.append("007 pressed flag should expose forced march canteens conversation") else: _check_camp_conversation_effect(failures, canteens, "wine", 1) if not _find_camp_conversation( xian_pressed_state.get_briefing().get("camp_conversations", []), "xian_fortified_road_supplies" ).is_empty(): failures.append("007 pressed flag should not expose fortified road supplies conversation") _check_shop_item_visibility(failures, xian_pressed_state, "iron_armor", false, "007 pressed") _check_shop_item_visibility(failures, xian_pressed_state, "war_axe", true, "007 pressed") var wan_state = BattleStateScript.new() if not wan_state.load_battle("res://data/scenarios/008_wan_castle_escape.json"): failures.append("could not load Wan Castle Escape camp data") else: var wan_conversations: Array = wan_state.get_briefing().get("camp_conversations", []) if wan_conversations.size() != 2: failures.append("008 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, wan_conversations[0], "wan_escape_route_council", "officer", "cao_cao") _check_camp_conversation(failures, wan_conversations[1], "wan_dian_wei_rear_guard", "officer", "dian_wei") var wan_merchant := wan_state.get_shop_merchant() if str(wan_merchant.get("name", "")) != "완성 야상": failures.append("008 shop should expose Wan Castle merchant name") if (wan_merchant.get("lines", []) as Array).size() < 2: failures.append("008 shop should expose merchant flavor lines") _check_shop_item_visibility(failures, wan_state, "imperial_seal", false, "008 base") _check_shop_item_visibility(failures, wan_state, "war_drum", false, "008 base") var wan_court_state = BattleStateScript.new() if not wan_court_state.load_battle("res://data/scenarios/008_wan_castle_escape.json", {}, {}, {"secured_imperial_court": true}): failures.append("could not load Wan court seal camp data") else: var postern: Dictionary = _find_camp_conversation( wan_court_state.get_briefing().get("camp_conversations", []), "wan_court_seal_postern" ) if postern.is_empty(): failures.append("008 secured court flag should expose court seal postern conversation") else: _check_camp_conversation_effect(failures, postern, "bean", 1) if not _find_camp_conversation( wan_court_state.get_briefing().get("camp_conversations", []), "wan_hard_pursuit_drums" ).is_empty(): failures.append("008 secured court flag should not expose hard pursuit drums conversation") _check_shop_item_visibility(failures, wan_court_state, "imperial_seal", true, "008 secured court") _check_shop_item_visibility(failures, wan_court_state, "war_drum", false, "008 secured court") var wan_pursuit_state = BattleStateScript.new() if not wan_pursuit_state.load_battle("res://data/scenarios/008_wan_castle_escape.json", {}, {}, {"pursued_li_jue_remnants": true}): failures.append("could not load Wan hard pursuit camp data") else: var drums: Dictionary = _find_camp_conversation( wan_pursuit_state.get_briefing().get("camp_conversations", []), "wan_hard_pursuit_drums" ) if drums.is_empty(): failures.append("008 hard pursuit flag should expose hard pursuit drums conversation") else: _check_camp_conversation_effect(failures, drums, "wine", 1) if not _find_camp_conversation( wan_pursuit_state.get_briefing().get("camp_conversations", []), "wan_court_seal_postern" ).is_empty(): failures.append("008 hard pursuit flag should not expose court seal postern conversation") _check_shop_item_visibility(failures, wan_pursuit_state, "imperial_seal", false, "008 hard pursuit") _check_shop_item_visibility(failures, wan_pursuit_state, "war_drum", true, "008 hard pursuit") var xiapi_state = BattleStateScript.new() if not xiapi_state.load_battle("res://data/scenarios/009_xiapi_siege.json"): failures.append("could not load Xiapi Siege camp data") else: var xiapi_conversations: Array = xiapi_state.get_briefing().get("camp_conversations", []) if xiapi_conversations.size() != 2: failures.append("009 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, xiapi_conversations[0], "xiapi_floodgate_council", "officer", "cao_cao") _check_camp_conversation(failures, xiapi_conversations[1], "xiapi_dian_wei_siege_road", "officer", "dian_wei") var xiapi_merchant := xiapi_state.get_shop_merchant() if str(xiapi_merchant.get("name", "")) != "하비 수문 야상": failures.append("009 shop should expose Xiapi merchant name") if (xiapi_merchant.get("lines", []) as Array).size() < 2: failures.append("009 shop should expose merchant flavor lines") _check_shop_item_visibility(failures, xiapi_state, "war_drum", false, "009 base") _check_shop_item_visibility(failures, xiapi_state, "war_axe", false, "009 base") var xiapi_held_state = BattleStateScript.new() if not xiapi_held_state.load_battle("res://data/scenarios/009_xiapi_siege.json", {}, {}, {"held_wan_gate": true}): failures.append("could not load Xiapi held Wan gate camp data") else: var medicine: Dictionary = _find_camp_conversation( xiapi_held_state.get_briefing().get("camp_conversations", []), "xiapi_wan_baggage_dressings" ) if medicine.is_empty(): failures.append("009 held Wan gate flag should expose Wan baggage dressings conversation") else: _check_camp_conversation_effect(failures, medicine, "panacea", 1) if not _find_camp_conversation( xiapi_held_state.get_briefing().get("camp_conversations", []), "xiapi_forced_march_rations" ).is_empty(): failures.append("009 held Wan gate flag should not expose forced march rations conversation") _check_shop_item_visibility(failures, xiapi_held_state, "war_drum", true, "009 held Wan gate") _check_shop_item_visibility(failures, xiapi_held_state, "war_axe", false, "009 held Wan gate") var xiapi_swift_state = BattleStateScript.new() if not xiapi_swift_state.load_battle("res://data/scenarios/009_xiapi_siege.json", {}, {}, {"swift_wan_escape": true}): failures.append("could not load Xiapi swift Wan escape camp data") else: var canteens: Dictionary = _find_camp_conversation( xiapi_swift_state.get_briefing().get("camp_conversations", []), "xiapi_forced_march_rations" ) if canteens.is_empty(): failures.append("009 swift Wan escape flag should expose forced march rations conversation") else: _check_camp_conversation_effect(failures, canteens, "bean", 1) if not _find_camp_conversation( xiapi_swift_state.get_briefing().get("camp_conversations", []), "xiapi_wan_baggage_dressings" ).is_empty(): failures.append("009 swift Wan escape flag should not expose Wan baggage dressings conversation") _check_shop_item_visibility(failures, xiapi_swift_state, "war_drum", false, "009 swift Wan escape") _check_shop_item_visibility(failures, xiapi_swift_state, "war_axe", true, "009 swift Wan escape") var white_horse_state = BattleStateScript.new() if not white_horse_state.load_battle("res://data/scenarios/010_white_horse_relief.json"): failures.append("could not load White Horse Relief shop data") else: var white_horse_conversations: Array = white_horse_state.get_briefing().get("camp_conversations", []) if white_horse_conversations.size() != 2: failures.append("010 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, white_horse_conversations[0], "white_horse_decoy_council", "officer", "guo_jia") _check_camp_conversation(failures, white_horse_conversations[1], "white_horse_xiahou_dun_false_retreat", "officer", "xiahou_dun") var white_horse_merchant := white_horse_state.get_shop_merchant() if str(white_horse_merchant.get("name", "")) != "백마 야상": failures.append("010 shop should expose White Horse merchant name") if (white_horse_merchant.get("lines", []) as Array).size() < 2: failures.append("010 shop should expose merchant flavor lines") _check_shop_item_visibility(failures, white_horse_state, "imperial_seal", false, "010 base") _check_shop_item_visibility(failures, white_horse_state, "war_drum", false, "010 base") var white_horse_xu_state = BattleStateScript.new() if not white_horse_xu_state.load_battle("res://data/scenarios/010_white_horse_relief.json", {}, {}, {"integrated_xu_province": true}): failures.append("could not load White Horse integrated Xu shop data") else: var medicine: Dictionary = _find_camp_conversation( white_horse_xu_state.get_briefing().get("camp_conversations", []), "white_horse_xu_reserve_medicine" ) if medicine.is_empty(): failures.append("010 integrated Xu flag should expose Xu reserve medicine conversation") else: _check_camp_conversation_effect(failures, medicine, "panacea", 1) if not _find_camp_conversation( white_horse_xu_state.get_briefing().get("camp_conversations", []), "white_horse_fast_march_beans" ).is_empty(): failures.append("010 integrated Xu flag should not expose fast march beans conversation") _check_shop_item_visibility(failures, white_horse_xu_state, "imperial_seal", true, "010 integrated Xu") _check_shop_item_visibility(failures, white_horse_xu_state, "war_drum", false, "010 integrated Xu") var white_horse_pressed_state = BattleStateScript.new() if not white_horse_pressed_state.load_battle("res://data/scenarios/010_white_horse_relief.json", {}, {}, {"pressed_northern_campaign": true}): failures.append("could not load White Horse pressed north shop data") else: var beans: Dictionary = _find_camp_conversation( white_horse_pressed_state.get_briefing().get("camp_conversations", []), "white_horse_fast_march_beans" ) if beans.is_empty(): failures.append("010 pressed north flag should expose fast march beans conversation") else: _check_camp_conversation_effect(failures, beans, "bean", 1) if not _find_camp_conversation( white_horse_pressed_state.get_briefing().get("camp_conversations", []), "white_horse_xu_reserve_medicine" ).is_empty(): failures.append("010 pressed north flag should not expose Xu reserve medicine conversation") _check_shop_item_visibility(failures, white_horse_pressed_state, "imperial_seal", false, "010 pressed north") _check_shop_item_visibility(failures, white_horse_pressed_state, "war_drum", true, "010 pressed north") var yan_ford_state = BattleStateScript.new() if not yan_ford_state.load_battle("res://data/scenarios/011_yan_ford_pursuit.json"): failures.append("could not load Yan Ford Pursuit shop data") else: _check_shop_items_unique(failures, yan_ford_state, "011 base") var yan_ford_conversations: Array = yan_ford_state.get_briefing().get("camp_conversations", []) if yan_ford_conversations.size() != 2: failures.append("011 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, yan_ford_conversations[0], "yan_ford_pursuit_council", "officer", "guo_jia") _check_camp_conversation(failures, yan_ford_conversations[1], "yan_ford_xiahou_yuan_banks", "officer", "xiahou_yuan") var yan_ford_merchant := yan_ford_state.get_shop_merchant() if str(yan_ford_merchant.get("name", "")) != "연진 강둑 야상": failures.append("011 shop should expose Yan Ford merchant name") if (yan_ford_merchant.get("lines", []) as Array).size() < 2: failures.append("011 shop should expose merchant flavor lines") _check_shop_item_visibility(failures, yan_ford_state, "imperial_seal", false, "011 base") _check_shop_item_visibility(failures, yan_ford_state, "war_axe", false, "011 base") var yan_ford_fortified_state = BattleStateScript.new() if not yan_ford_fortified_state.load_battle("res://data/scenarios/011_yan_ford_pursuit.json", {}, {}, {"fortified_white_horse": true}): failures.append("could not load Yan Ford fortified White Horse shop data") else: _check_shop_items_unique(failures, yan_ford_fortified_state, "011 fortified White Horse") var crossing_medicine: Dictionary = _find_camp_conversation( yan_ford_fortified_state.get_briefing().get("camp_conversations", []), "yan_ford_fortified_crossing_medicine" ) if crossing_medicine.is_empty(): failures.append("011 fortified White Horse flag should expose crossing medicine conversation") else: _check_camp_conversation_effect(failures, crossing_medicine, "panacea", 1) if not _find_camp_conversation( yan_ford_fortified_state.get_briefing().get("camp_conversations", []), "yan_ford_raided_supply_beans" ).is_empty(): failures.append("011 fortified White Horse flag should not expose raided supply beans conversation") _check_shop_item_visibility(failures, yan_ford_fortified_state, "imperial_seal", true, "011 fortified White Horse") _check_shop_item_visibility(failures, yan_ford_fortified_state, "war_axe", false, "011 fortified White Horse") var yan_ford_raided_state = BattleStateScript.new() if not yan_ford_raided_state.load_battle("res://data/scenarios/011_yan_ford_pursuit.json", {}, {}, {"raided_yuan_supplies": true}): failures.append("could not load Yan Ford raided Yuan supplies shop data") else: _check_shop_items_unique(failures, yan_ford_raided_state, "011 raided Yuan supplies") var supply_beans: Dictionary = _find_camp_conversation( yan_ford_raided_state.get_briefing().get("camp_conversations", []), "yan_ford_raided_supply_beans" ) if supply_beans.is_empty(): failures.append("011 raided Yuan supplies flag should expose raided supply beans conversation") else: _check_camp_conversation_effect(failures, supply_beans, "bean", 1) if not _find_camp_conversation( yan_ford_raided_state.get_briefing().get("camp_conversations", []), "yan_ford_fortified_crossing_medicine" ).is_empty(): failures.append("011 raided Yuan supplies flag should not expose crossing medicine conversation") _check_shop_item_visibility(failures, yan_ford_raided_state, "imperial_seal", false, "011 raided Yuan supplies") _check_shop_item_visibility(failures, yan_ford_raided_state, "war_axe", true, "011 raided Yuan supplies") var guandu_state = BattleStateScript.new() if not guandu_state.load_battle("res://data/scenarios/012_guandu_showdown.json"): failures.append("could not load Guandu Showdown shop data") else: _check_shop_items_unique(failures, guandu_state, "012 base") var guandu_conversations: Array = guandu_state.get_briefing().get("camp_conversations", []) if guandu_conversations.size() != 2: failures.append("012 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, guandu_conversations[0], "guandu_main_line_council", "officer", "cao_cao") _check_camp_conversation(failures, guandu_conversations[1], "guandu_guo_jia_supply_wound", "officer", "guo_jia") var guandu_merchant := guandu_state.get_shop_merchant() if str(guandu_merchant.get("name", "")) != "관도 영채 야상": failures.append("012 shop should expose Guandu merchant name") if (guandu_merchant.get("lines", []) as Array).size() < 2: failures.append("012 shop should expose merchant flavor lines") _check_shop_item_visibility(failures, guandu_state, "imperial_seal", false, "012 base") _check_shop_item_visibility(failures, guandu_state, "war_drum", false, "012 base") _check_shop_item_visibility(failures, guandu_state, "yitian_sword", false, "012 base") _check_shop_item_visibility(failures, guandu_state, "panacea", false, "012 base") _check_shop_item_visibility(failures, guandu_state, "antidote", false, "012 base") var guandu_secured_state = BattleStateScript.new() if not guandu_secured_state.load_battle("res://data/scenarios/012_guandu_showdown.json", {}, {}, {"secured_guandu_line": true}): failures.append("could not load Guandu secured line shop data") else: _check_shop_items_unique(failures, guandu_secured_state, "012 secured Guandu line") var line_medicine: Dictionary = _find_camp_conversation( guandu_secured_state.get_briefing().get("camp_conversations", []), "guandu_secured_line_medicine" ) if line_medicine.is_empty(): failures.append("012 secured Guandu line flag should expose secured line medicine conversation") else: _check_camp_conversation_effect(failures, line_medicine, "panacea", 1) if not _find_camp_conversation( guandu_secured_state.get_briefing().get("camp_conversations", []), "guandu_wuchao_scout_wine" ).is_empty(): failures.append("012 secured Guandu line flag should not expose Wuchao scout wine conversation") _check_shop_item_visibility(failures, guandu_secured_state, "imperial_seal", false, "012 secured Guandu line") _check_shop_item_visibility(failures, guandu_secured_state, "war_drum", false, "012 secured Guandu line") _check_shop_item_visibility(failures, guandu_secured_state, "yitian_sword", false, "012 secured Guandu line") _check_shop_item_visibility(failures, guandu_secured_state, "panacea", true, "012 secured Guandu line") _check_shop_item_visibility(failures, guandu_secured_state, "antidote", false, "012 secured Guandu line") var guandu_scouted_state = BattleStateScript.new() if not guandu_scouted_state.load_battle("res://data/scenarios/012_guandu_showdown.json", {}, {}, {"scouted_wuchao_depots": true}): failures.append("could not load Guandu scouted Wuchao shop data") else: _check_shop_items_unique(failures, guandu_scouted_state, "012 scouted Wuchao") var scout_wine: Dictionary = _find_camp_conversation( guandu_scouted_state.get_briefing().get("camp_conversations", []), "guandu_wuchao_scout_wine" ) if scout_wine.is_empty(): failures.append("012 scouted Wuchao flag should expose Wuchao scout wine conversation") else: _check_camp_conversation_effect(failures, scout_wine, "wine", 1) if not _find_camp_conversation( guandu_scouted_state.get_briefing().get("camp_conversations", []), "guandu_secured_line_medicine" ).is_empty(): failures.append("012 scouted Wuchao flag should not expose secured line medicine conversation") _check_shop_item_visibility(failures, guandu_scouted_state, "imperial_seal", false, "012 scouted Wuchao") _check_shop_item_visibility(failures, guandu_scouted_state, "war_drum", false, "012 scouted Wuchao") _check_shop_item_visibility(failures, guandu_scouted_state, "yitian_sword", false, "012 scouted Wuchao") _check_shop_item_visibility(failures, guandu_scouted_state, "panacea", false, "012 scouted Wuchao") _check_shop_item_visibility(failures, guandu_scouted_state, "antidote", true, "012 scouted Wuchao") var wuchao_state = BattleStateScript.new() if not wuchao_state.load_battle("res://data/scenarios/013_wuchao_raid.json"): failures.append("could not load Wuchao Raid shop data") else: _check_shop_items_unique(failures, wuchao_state, "013 base") _check_shop_item_visibility(failures, wuchao_state, "war_drum", false, "013 base") _check_shop_item_visibility(failures, wuchao_state, "imperial_seal", false, "013 base") var wuchao_prepared_state = BattleStateScript.new() if not wuchao_prepared_state.load_battle("res://data/scenarios/013_wuchao_raid.json", {}, {}, {"prepared_wuchao_raid": true}): failures.append("could not load Wuchao prepared raid shop data") else: _check_shop_items_unique(failures, wuchao_prepared_state, "013 prepared Wuchao raid") _check_shop_item_visibility(failures, wuchao_prepared_state, "war_drum", true, "013 prepared Wuchao raid") _check_shop_item_visibility(failures, wuchao_prepared_state, "imperial_seal", false, "013 prepared Wuchao raid") var wuchao_strengthened_state = BattleStateScript.new() if not wuchao_strengthened_state.load_battle("res://data/scenarios/013_wuchao_raid.json", {}, {}, {"strengthened_guandu_defense": true}): failures.append("could not load Wuchao strengthened Guandu shop data") else: _check_shop_items_unique(failures, wuchao_strengthened_state, "013 strengthened Guandu defense") _check_shop_item_visibility(failures, wuchao_strengthened_state, "war_drum", false, "013 strengthened Guandu defense") _check_shop_item_visibility(failures, wuchao_strengthened_state, "imperial_seal", true, "013 strengthened Guandu defense") var wuchao_old_flags_state = BattleStateScript.new() if not wuchao_old_flags_state.load_battle("res://data/scenarios/013_wuchao_raid.json", {}, {}, {"secured_guandu_line": true, "scouted_wuchao_depots": true}): failures.append("could not load Wuchao old Guandu flag shop data") else: _check_shop_items_unique(failures, wuchao_old_flags_state, "013 old Guandu flags") _check_shop_item_visibility(failures, wuchao_old_flags_state, "war_drum", false, "013 old Guandu flags") _check_shop_item_visibility(failures, wuchao_old_flags_state, "imperial_seal", false, "013 old Guandu flags") if not _find_camp_conversation( wuchao_old_flags_state.get_briefing().get("camp_conversations", []), "wuchao_fast_raid_supplies" ).is_empty(): failures.append("013 old Guandu flags should not expose fast raid supplies") if not _find_camp_conversation( wuchao_old_flags_state.get_briefing().get("camp_conversations", []), "wuchao_guandu_reserve_wagon" ).is_empty(): failures.append("013 old Guandu flags should not expose reserve wagon supplies") var cangting_state = BattleStateScript.new() if not cangting_state.load_battle("res://data/scenarios/014_cangting_pursuit.json"): failures.append("could not load Cangting Pursuit camp data") else: _check_shop_items_unique(failures, cangting_state, "014 base") var cangting_conversations: Array = cangting_state.get_briefing().get("camp_conversations", []) if cangting_conversations.size() != 2: failures.append("014 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, cangting_conversations[0], "cangting_pursuit_council", "topic", "") _check_camp_conversation(failures, cangting_conversations[1], "cangting_guo_jia_rearguard", "officer", "guo_jia") var cangting_merchant := cangting_state.get_shop_merchant() if str(cangting_merchant.get("name", "")) != "Cangting Road Sutler": failures.append("014 shop should expose Cangting merchant name") if (cangting_merchant.get("lines", []) as Array).size() < 2: failures.append("014 shop should expose merchant flavor lines") _check_shop_item_visibility(failures, cangting_state, "war_drum", false, "014 base") _check_shop_item_visibility(failures, cangting_state, "imperial_seal", false, "014 base") _check_shop_item_visibility(failures, cangting_state, "dragon_spear", false, "014 base") var cangting_pressed_state = BattleStateScript.new() if not cangting_pressed_state.load_battle("res://data/scenarios/014_cangting_pursuit.json", {}, {}, {"pressed_yuan_rout": true}): failures.append("could not load Cangting pressed rout camp data") else: _check_shop_items_unique(failures, cangting_pressed_state, "014 pressed Yuan rout") var fast_rations: Dictionary = _find_camp_conversation( cangting_pressed_state.get_briefing().get("camp_conversations", []), "cangting_fast_pursuit_beans" ) if fast_rations.is_empty(): failures.append("014 pressed Yuan rout flag should expose fast pursuit rations") else: _check_camp_conversation_effect(failures, fast_rations, "bean", 1) if not _find_camp_conversation( cangting_pressed_state.get_briefing().get("camp_conversations", []), "cangting_wuchao_store_medicine" ).is_empty(): failures.append("014 pressed Yuan rout flag should not expose Wuchao store medicine") _check_shop_item_visibility(failures, cangting_pressed_state, "war_drum", true, "014 pressed Yuan rout") _check_shop_item_visibility(failures, cangting_pressed_state, "imperial_seal", false, "014 pressed Yuan rout") _check_shop_item_visibility(failures, cangting_pressed_state, "dragon_spear", false, "014 pressed Yuan rout") var cangting_stores_state = BattleStateScript.new() if not cangting_stores_state.load_battle("res://data/scenarios/014_cangting_pursuit.json", {}, {}, {"secured_wuchao_stores": true}): failures.append("could not load Cangting secured Wuchao stores camp data") else: _check_shop_items_unique(failures, cangting_stores_state, "014 secured Wuchao stores") var store_medicine: Dictionary = _find_camp_conversation( cangting_stores_state.get_briefing().get("camp_conversations", []), "cangting_wuchao_store_medicine" ) if store_medicine.is_empty(): failures.append("014 secured Wuchao stores flag should expose store medicine") else: _check_camp_conversation_effect(failures, store_medicine, "panacea", 1) if not _find_camp_conversation( cangting_stores_state.get_briefing().get("camp_conversations", []), "cangting_fast_pursuit_beans" ).is_empty(): failures.append("014 secured Wuchao stores flag should not expose fast pursuit rations") _check_shop_item_visibility(failures, cangting_stores_state, "war_drum", false, "014 secured Wuchao stores") _check_shop_item_visibility(failures, cangting_stores_state, "imperial_seal", true, "014 secured Wuchao stores") _check_shop_item_visibility(failures, cangting_stores_state, "dragon_spear", false, "014 secured Wuchao stores") var cangting_old_flags_state = BattleStateScript.new() if not cangting_old_flags_state.load_battle("res://data/scenarios/014_cangting_pursuit.json", {}, {}, {"prepared_wuchao_raid": true, "strengthened_guandu_defense": true}): failures.append("could not load Cangting old Wuchao flag camp data") else: _check_shop_item_visibility(failures, cangting_old_flags_state, "war_drum", false, "014 old Wuchao flags") _check_shop_item_visibility(failures, cangting_old_flags_state, "imperial_seal", false, "014 old Wuchao flags") _check_shop_item_visibility(failures, cangting_old_flags_state, "dragon_spear", false, "014 old Wuchao flags") if not _find_camp_conversation( cangting_old_flags_state.get_briefing().get("camp_conversations", []), "cangting_fast_pursuit_beans" ).is_empty(): failures.append("014 old Wuchao flags should not expose fast pursuit rations") if not _find_camp_conversation( cangting_old_flags_state.get_briefing().get("camp_conversations", []), "cangting_wuchao_store_medicine" ).is_empty(): failures.append("014 old Wuchao flags should not expose Wuchao store medicine") var ye_state = BattleStateScript.new() if not ye_state.load_battle("res://data/scenarios/015_ye_campaign.json"): failures.append("could not load Ye Campaign camp data") else: _check_shop_items_unique(failures, ye_state, "015 base") var ye_conversations: Array = ye_state.get_briefing().get("camp_conversations", []) if ye_conversations.size() != 2: failures.append("015 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, ye_conversations[0], "ye_outer_council", "topic", "") _check_camp_conversation(failures, ye_conversations[1], "ye_zhang_he_city_map", "officer", "zhang_he") var ye_merchant := ye_state.get_shop_merchant() if str(ye_merchant.get("name", "")) != "Ye Outskirts Sutler": failures.append("015 shop should expose Ye merchant name") if (ye_merchant.get("lines", []) as Array).size() < 2: failures.append("015 shop should expose merchant flavor lines") _check_shop_item_visibility(failures, ye_state, "war_drum", false, "015 base") _check_shop_item_visibility(failures, ye_state, "imperial_seal", false, "015 base") _check_shop_item_visibility(failures, ye_state, "wind_chaser_bow", false, "015 base") var ye_advanced_state = BattleStateScript.new() if not ye_advanced_state.load_battle("res://data/scenarios/015_ye_campaign.json", {}, {}, {"advanced_to_ye": true}): failures.append("could not load Ye advanced camp data") else: _check_shop_items_unique(failures, ye_advanced_state, "015 advanced to Ye") var rapid_rations: Dictionary = _find_camp_conversation( ye_advanced_state.get_briefing().get("camp_conversations", []), "ye_rapid_march_rations" ) if rapid_rations.is_empty(): failures.append("015 advanced to Ye flag should expose rapid march rations") else: _check_camp_conversation_effect(failures, rapid_rations, "bean", 1) if not _find_camp_conversation( ye_advanced_state.get_briefing().get("camp_conversations", []), "ye_crossing_medicine" ).is_empty(): failures.append("015 advanced to Ye flag should not expose crossing medicine") _check_shop_item_visibility(failures, ye_advanced_state, "war_drum", true, "015 advanced to Ye") _check_shop_item_visibility(failures, ye_advanced_state, "imperial_seal", false, "015 advanced to Ye") _check_shop_item_visibility(failures, ye_advanced_state, "wind_chaser_bow", false, "015 advanced to Ye") var ye_crossing_state = BattleStateScript.new() if not ye_crossing_state.load_battle("res://data/scenarios/015_ye_campaign.json", {}, {}, {"secured_hebei_crossings": true}): failures.append("could not load Ye secured crossings camp data") else: _check_shop_items_unique(failures, ye_crossing_state, "015 secured Hebei crossings") var crossing_medicine: Dictionary = _find_camp_conversation( ye_crossing_state.get_briefing().get("camp_conversations", []), "ye_crossing_medicine" ) if crossing_medicine.is_empty(): failures.append("015 secured Hebei crossings flag should expose crossing medicine") else: _check_camp_conversation_effect(failures, crossing_medicine, "panacea", 1) if not _find_camp_conversation( ye_crossing_state.get_briefing().get("camp_conversations", []), "ye_rapid_march_rations" ).is_empty(): failures.append("015 secured Hebei crossings flag should not expose rapid march rations") _check_shop_item_visibility(failures, ye_crossing_state, "war_drum", false, "015 secured Hebei crossings") _check_shop_item_visibility(failures, ye_crossing_state, "imperial_seal", true, "015 secured Hebei crossings") _check_shop_item_visibility(failures, ye_crossing_state, "wind_chaser_bow", false, "015 secured Hebei crossings") var ye_old_flags_state = BattleStateScript.new() if not ye_old_flags_state.load_battle("res://data/scenarios/015_ye_campaign.json", {}, {}, {"pressed_yuan_rout": true, "secured_wuchao_stores": true}): failures.append("could not load Ye old Cangting flag camp data") else: _check_shop_item_visibility(failures, ye_old_flags_state, "war_drum", false, "015 old Cangting flags") _check_shop_item_visibility(failures, ye_old_flags_state, "imperial_seal", false, "015 old Cangting flags") if not _find_camp_conversation( ye_old_flags_state.get_briefing().get("camp_conversations", []), "ye_rapid_march_rations" ).is_empty(): failures.append("015 old Cangting flags should not expose rapid march rations") if not _find_camp_conversation( ye_old_flags_state.get_briefing().get("camp_conversations", []), "ye_crossing_medicine" ).is_empty(): failures.append("015 old Cangting flags should not expose crossing medicine") var ye_siege_state = BattleStateScript.new() if not ye_siege_state.load_battle("res://data/scenarios/016_ye_siege.json"): failures.append("could not load Ye Siege camp data") else: _check_shop_items_unique(failures, ye_siege_state, "016 base") var ye_siege_conversations: Array = ye_siege_state.get_briefing().get("camp_conversations", []) if ye_siege_conversations.size() != 2: failures.append("016 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, ye_siege_conversations[0], "ye_siege_inner_gate_council", "topic", "") _check_camp_conversation(failures, ye_siege_conversations[1], "ye_siege_zhang_he_streets", "officer", "zhang_he") var ye_siege_merchant := ye_siege_state.get_shop_merchant() if str(ye_siege_merchant.get("name", "")) != "Ye Gate Sutler": failures.append("016 shop should expose Ye Gate merchant name") if (ye_siege_merchant.get("lines", []) as Array).size() < 2: failures.append("016 shop should expose merchant flavor lines") _check_shop_item_visibility(failures, ye_siege_state, "war_drum", false, "016 base") _check_shop_item_visibility(failures, ye_siege_state, "imperial_seal", false, "016 base") _check_shop_item_visibility(failures, ye_siege_state, "iron_armor", true, "016 base") var ye_siege_pressed_state = BattleStateScript.new() if not ye_siege_pressed_state.load_battle("res://data/scenarios/016_ye_siege.json", {}, {}, {"pressed_ye_siege": true}): failures.append("could not load Ye Siege pressed camp data") else: _check_shop_items_unique(failures, ye_siege_pressed_state, "016 pressed Ye siege") var ladder_rations: Dictionary = _find_camp_conversation( ye_siege_pressed_state.get_briefing().get("camp_conversations", []), "ye_siege_ladder_rations" ) if ladder_rations.is_empty(): failures.append("016 pressed Ye siege flag should expose ladder rations") else: _check_camp_conversation_effect(failures, ladder_rations, "bean", 1) if not _find_camp_conversation( ye_siege_pressed_state.get_briefing().get("camp_conversations", []), "ye_siege_defector_medicine" ).is_empty(): failures.append("016 pressed Ye siege flag should not expose defector medicine") _check_shop_item_visibility(failures, ye_siege_pressed_state, "war_drum", true, "016 pressed Ye siege") _check_shop_item_visibility(failures, ye_siege_pressed_state, "imperial_seal", false, "016 pressed Ye siege") var ye_siege_split_state = BattleStateScript.new() if not ye_siege_split_state.load_battle("res://data/scenarios/016_ye_siege.json", {}, {}, {"split_yuan_heirs": true}): failures.append("could not load Ye Siege split heirs camp data") else: _check_shop_items_unique(failures, ye_siege_split_state, "016 split Yuan heirs") var defector_medicine: Dictionary = _find_camp_conversation( ye_siege_split_state.get_briefing().get("camp_conversations", []), "ye_siege_defector_medicine" ) if defector_medicine.is_empty(): failures.append("016 split Yuan heirs flag should expose defector medicine") else: _check_camp_conversation_effect(failures, defector_medicine, "panacea", 1) if not _find_camp_conversation( ye_siege_split_state.get_briefing().get("camp_conversations", []), "ye_siege_ladder_rations" ).is_empty(): failures.append("016 split Yuan heirs flag should not expose ladder rations") _check_shop_item_visibility(failures, ye_siege_split_state, "war_drum", false, "016 split Yuan heirs") _check_shop_item_visibility(failures, ye_siege_split_state, "imperial_seal", true, "016 split Yuan heirs") var ye_siege_old_flags_state = BattleStateScript.new() if not ye_siege_old_flags_state.load_battle("res://data/scenarios/016_ye_siege.json", {}, {}, {"advanced_to_ye": true, "secured_hebei_crossings": true}): failures.append("could not load Ye Siege old Ye flag camp data") else: _check_shop_item_visibility(failures, ye_siege_old_flags_state, "war_drum", false, "016 old Ye flags") _check_shop_item_visibility(failures, ye_siege_old_flags_state, "imperial_seal", false, "016 old Ye flags") if not _find_camp_conversation( ye_siege_old_flags_state.get_briefing().get("camp_conversations", []), "ye_siege_ladder_rations" ).is_empty(): failures.append("016 old Ye flags should not expose ladder rations") if not _find_camp_conversation( ye_siege_old_flags_state.get_briefing().get("camp_conversations", []), "ye_siege_defector_medicine" ).is_empty(): failures.append("016 old Ye flags should not expose defector medicine") var ye_surrender_state = BattleStateScript.new() if not ye_surrender_state.load_battle("res://data/scenarios/017_ye_surrender.json"): failures.append("could not load Ye Surrender camp data") else: _check_shop_items_unique(failures, ye_surrender_state, "017 base") var ye_surrender_conversations: Array = ye_surrender_state.get_briefing().get("camp_conversations", []) if ye_surrender_conversations.size() != 2: failures.append("017 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, ye_surrender_conversations[0], "ye_surrender_palace_road_council", "topic", "") _check_camp_conversation(failures, ye_surrender_conversations[1], "ye_surrender_zhang_he_last_loyalists", "officer", "zhang_he") var ye_surrender_merchant := ye_surrender_state.get_shop_merchant() if str(ye_surrender_merchant.get("name", "")) != "Ye Palace Road Sutler": failures.append("017 shop should expose Ye Palace Road merchant name") if (ye_surrender_merchant.get("lines", []) as Array).size() < 2: failures.append("017 shop should expose merchant flavor lines") _check_shop_item_visibility(failures, ye_surrender_state, "war_drum", false, "017 base") _check_shop_item_visibility(failures, ye_surrender_state, "imperial_seal", false, "017 base") _check_shop_item_visibility(failures, ye_surrender_state, "iron_armor", true, "017 base") var ye_surrender_forced_state = BattleStateScript.new() if not ye_surrender_forced_state.load_battle("res://data/scenarios/017_ye_surrender.json", {}, {}, {"forced_ye_surrender": true}): failures.append("could not load Ye Surrender forced camp data") else: _check_shop_items_unique(failures, ye_surrender_forced_state, "017 forced Ye surrender") var pressure_rations: Dictionary = _find_camp_conversation( ye_surrender_forced_state.get_briefing().get("camp_conversations", []), "ye_surrender_pressure_rations" ) if pressure_rations.is_empty(): failures.append("017 forced Ye surrender flag should expose pressure rations") else: _check_camp_conversation_effect(failures, pressure_rations, "bean", 1) if not _find_camp_conversation( ye_surrender_forced_state.get_briefing().get("camp_conversations", []), "ye_surrender_defector_medicine" ).is_empty(): failures.append("017 forced Ye surrender flag should not expose defector medicine") _check_shop_item_visibility(failures, ye_surrender_forced_state, "war_drum", true, "017 forced Ye surrender") _check_shop_item_visibility(failures, ye_surrender_forced_state, "imperial_seal", false, "017 forced Ye surrender") var ye_surrender_negotiated_state = BattleStateScript.new() if not ye_surrender_negotiated_state.load_battle("res://data/scenarios/017_ye_surrender.json", {}, {}, {"negotiated_yuan_defections": true}): failures.append("could not load Ye Surrender negotiated camp data") else: _check_shop_items_unique(failures, ye_surrender_negotiated_state, "017 negotiated Yuan defections") var surrender_defector_medicine: Dictionary = _find_camp_conversation( ye_surrender_negotiated_state.get_briefing().get("camp_conversations", []), "ye_surrender_defector_medicine" ) if surrender_defector_medicine.is_empty(): failures.append("017 negotiated Yuan defections flag should expose defector medicine") else: _check_camp_conversation_effect(failures, surrender_defector_medicine, "panacea", 1) if not _find_camp_conversation( ye_surrender_negotiated_state.get_briefing().get("camp_conversations", []), "ye_surrender_pressure_rations" ).is_empty(): failures.append("017 negotiated Yuan defections flag should not expose pressure rations") _check_shop_item_visibility(failures, ye_surrender_negotiated_state, "war_drum", false, "017 negotiated Yuan defections") _check_shop_item_visibility(failures, ye_surrender_negotiated_state, "imperial_seal", true, "017 negotiated Yuan defections") var ye_surrender_old_flags_state = BattleStateScript.new() if not ye_surrender_old_flags_state.load_battle("res://data/scenarios/017_ye_surrender.json", {}, {}, {"pressed_ye_siege": true, "split_yuan_heirs": true}): failures.append("could not load Ye Surrender old Ye Siege flag camp data") else: _check_shop_item_visibility(failures, ye_surrender_old_flags_state, "war_drum", false, "017 old Ye Siege flags") _check_shop_item_visibility(failures, ye_surrender_old_flags_state, "imperial_seal", false, "017 old Ye Siege flags") if not _find_camp_conversation( ye_surrender_old_flags_state.get_briefing().get("camp_conversations", []), "ye_surrender_pressure_rations" ).is_empty(): failures.append("017 old Ye Siege flags should not expose pressure rations") if not _find_camp_conversation( ye_surrender_old_flags_state.get_briefing().get("camp_conversations", []), "ye_surrender_defector_medicine" ).is_empty(): failures.append("017 old Ye Siege flags should not expose defector medicine") var northern_pursuit_state = BattleStateScript.new() if not northern_pursuit_state.load_battle("res://data/scenarios/018_northern_pursuit.json"): failures.append("could not load Northern Pursuit camp data") else: _check_shop_items_unique(failures, northern_pursuit_state, "018 base") var northern_pursuit_conversations: Array = northern_pursuit_state.get_briefing().get("camp_conversations", []) if northern_pursuit_conversations.size() != 2: failures.append("018 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, northern_pursuit_conversations[0], "northern_pursuit_road_council", "topic", "") _check_camp_conversation(failures, northern_pursuit_conversations[1], "northern_pursuit_zhang_he_retreat_road", "officer", "zhang_he") var northern_pursuit_merchant := northern_pursuit_state.get_shop_merchant() if str(northern_pursuit_merchant.get("name", "")) != "Northern Road Sutler": failures.append("018 shop should expose Northern Road merchant name") if (northern_pursuit_merchant.get("lines", []) as Array).size() < 2: failures.append("018 shop should expose merchant flavor lines") _check_shop_item_visibility(failures, northern_pursuit_state, "war_drum", false, "018 base") _check_shop_item_visibility(failures, northern_pursuit_state, "imperial_seal", false, "018 base") _check_shop_item_visibility(failures, northern_pursuit_state, "iron_armor", true, "018 base") var northern_pursuit_hard_state = BattleStateScript.new() if not northern_pursuit_hard_state.load_battle("res://data/scenarios/018_northern_pursuit.json", {}, {}, {"pursued_yuan_shang": true}): failures.append("could not load Northern Pursuit hard march camp data") else: _check_shop_items_unique(failures, northern_pursuit_hard_state, "018 pursued Yuan Shang") var hard_march_beans: Dictionary = _find_camp_conversation( northern_pursuit_hard_state.get_briefing().get("camp_conversations", []), "northern_pursuit_hard_march_beans" ) if hard_march_beans.is_empty(): failures.append("018 pursued Yuan Shang flag should expose hard march beans") else: _check_camp_conversation_effect(failures, hard_march_beans, "bean", 1) if not _find_camp_conversation( northern_pursuit_hard_state.get_briefing().get("camp_conversations", []), "northern_pursuit_ji_supply_medicine" ).is_empty(): failures.append("018 pursued Yuan Shang flag should not expose Ji supply medicine") _check_shop_item_visibility(failures, northern_pursuit_hard_state, "war_drum", true, "018 pursued Yuan Shang") _check_shop_item_visibility(failures, northern_pursuit_hard_state, "imperial_seal", false, "018 pursued Yuan Shang") var northern_pursuit_stabilized_state = BattleStateScript.new() if not northern_pursuit_stabilized_state.load_battle("res://data/scenarios/018_northern_pursuit.json", {}, {}, {"stabilized_ye": true}): failures.append("could not load Northern Pursuit stabilized camp data") else: _check_shop_items_unique(failures, northern_pursuit_stabilized_state, "018 stabilized Ye") var ji_supply_medicine: Dictionary = _find_camp_conversation( northern_pursuit_stabilized_state.get_briefing().get("camp_conversations", []), "northern_pursuit_ji_supply_medicine" ) if ji_supply_medicine.is_empty(): failures.append("018 stabilized Ye flag should expose Ji supply medicine") else: _check_camp_conversation_effect(failures, ji_supply_medicine, "panacea", 1) if not _find_camp_conversation( northern_pursuit_stabilized_state.get_briefing().get("camp_conversations", []), "northern_pursuit_hard_march_beans" ).is_empty(): failures.append("018 stabilized Ye flag should not expose hard march beans") _check_shop_item_visibility(failures, northern_pursuit_stabilized_state, "war_drum", false, "018 stabilized Ye") _check_shop_item_visibility(failures, northern_pursuit_stabilized_state, "imperial_seal", true, "018 stabilized Ye") var northern_pursuit_old_flags_state = BattleStateScript.new() if not northern_pursuit_old_flags_state.load_battle("res://data/scenarios/018_northern_pursuit.json", {}, {}, {"forced_ye_surrender": true, "negotiated_yuan_defections": true}): failures.append("could not load Northern Pursuit old Ye Surrender flag camp data") else: _check_shop_item_visibility(failures, northern_pursuit_old_flags_state, "war_drum", false, "018 old Ye Surrender flags") _check_shop_item_visibility(failures, northern_pursuit_old_flags_state, "imperial_seal", false, "018 old Ye Surrender flags") if not _find_camp_conversation( northern_pursuit_old_flags_state.get_briefing().get("camp_conversations", []), "northern_pursuit_hard_march_beans" ).is_empty(): failures.append("018 old Ye Surrender flags should not expose hard march beans") if not _find_camp_conversation( northern_pursuit_old_flags_state.get_briefing().get("camp_conversations", []), "northern_pursuit_ji_supply_medicine" ).is_empty(): failures.append("018 old Ye Surrender flags should not expose Ji supply medicine") var nanpi_state = BattleStateScript.new() if not nanpi_state.load_battle("res://data/scenarios/019_nanpi_pressure.json"): failures.append("could not load Nanpi camp data") else: _check_shop_items_unique(failures, nanpi_state, "019 base") var nanpi_conversations: Array = nanpi_state.get_briefing().get("camp_conversations", []) if nanpi_conversations.size() != 2: failures.append("019 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, nanpi_conversations[0], "nanpi_pressure_gate_council", "topic", "") _check_camp_conversation(failures, nanpi_conversations[1], "nanpi_pressure_zhang_he_gate_names", "officer", "zhang_he") var nanpi_merchant := nanpi_state.get_shop_merchant() if str(nanpi_merchant.get("name", "")) != "Nanpi Gate Sutler": failures.append("019 shop should expose Nanpi Gate merchant name") if (nanpi_merchant.get("lines", []) as Array).size() < 2: failures.append("019 shop should expose merchant flavor lines") _check_shop_item_visibility(failures, nanpi_state, "war_drum", false, "019 base") _check_shop_item_visibility(failures, nanpi_state, "imperial_seal", false, "019 base") _check_shop_item_visibility(failures, nanpi_state, "iron_armor", true, "019 base") var nanpi_pressed_state = BattleStateScript.new() if not nanpi_pressed_state.load_battle("res://data/scenarios/019_nanpi_pressure.json", {}, {}, {"pressed_north_to_nanpi": true}): failures.append("could not load Nanpi pressed north camp data") else: _check_shop_items_unique(failures, nanpi_pressed_state, "019 pressed north") var hard_push_beans: Dictionary = _find_camp_conversation( nanpi_pressed_state.get_briefing().get("camp_conversations", []), "nanpi_pressure_hard_push_beans" ) if hard_push_beans.is_empty(): failures.append("019 pressed north flag should expose hard push beans") else: _check_camp_conversation_effect(failures, hard_push_beans, "bean", 1) if not _find_camp_conversation( nanpi_pressed_state.get_briefing().get("camp_conversations", []), "nanpi_pressure_admin_medicine" ).is_empty(): failures.append("019 pressed north flag should not expose admin medicine") _check_shop_item_visibility(failures, nanpi_pressed_state, "war_drum", true, "019 pressed north") _check_shop_item_visibility(failures, nanpi_pressed_state, "imperial_seal", false, "019 pressed north") var nanpi_secured_state = BattleStateScript.new() if not nanpi_secured_state.load_battle("res://data/scenarios/019_nanpi_pressure.json", {}, {}, {"secured_ji_province": true}): failures.append("could not load Nanpi secured Ji camp data") else: _check_shop_items_unique(failures, nanpi_secured_state, "019 secured Ji") var admin_medicine: Dictionary = _find_camp_conversation( nanpi_secured_state.get_briefing().get("camp_conversations", []), "nanpi_pressure_admin_medicine" ) if admin_medicine.is_empty(): failures.append("019 secured Ji flag should expose admin medicine") else: _check_camp_conversation_effect(failures, admin_medicine, "panacea", 1) if not _find_camp_conversation( nanpi_secured_state.get_briefing().get("camp_conversations", []), "nanpi_pressure_hard_push_beans" ).is_empty(): failures.append("019 secured Ji flag should not expose hard push beans") _check_shop_item_visibility(failures, nanpi_secured_state, "war_drum", false, "019 secured Ji") _check_shop_item_visibility(failures, nanpi_secured_state, "imperial_seal", true, "019 secured Ji") var nanpi_old_flags_state = BattleStateScript.new() if not nanpi_old_flags_state.load_battle("res://data/scenarios/019_nanpi_pressure.json", {}, {}, {"pursued_yuan_shang": true, "stabilized_ye": true}): failures.append("could not load Nanpi old Northern Pursuit flag camp data") else: _check_shop_item_visibility(failures, nanpi_old_flags_state, "war_drum", false, "019 old Northern Pursuit flags") _check_shop_item_visibility(failures, nanpi_old_flags_state, "imperial_seal", false, "019 old Northern Pursuit flags") if not _find_camp_conversation( nanpi_old_flags_state.get_briefing().get("camp_conversations", []), "nanpi_pressure_hard_push_beans" ).is_empty(): failures.append("019 old Northern Pursuit flags should not expose hard push beans") if not _find_camp_conversation( nanpi_old_flags_state.get_briefing().get("camp_conversations", []), "nanpi_pressure_admin_medicine" ).is_empty(): failures.append("019 old Northern Pursuit flags should not expose admin medicine") var nanpi_surrender_state = BattleStateScript.new() if not nanpi_surrender_state.load_battle("res://data/scenarios/020_nanpi_surrender.json"): failures.append("could not load Nanpi Surrender camp data") else: _check_shop_items_unique(failures, nanpi_surrender_state, "020 base") var nanpi_surrender_conversations: Array = nanpi_surrender_state.get_briefing().get("camp_conversations", []) if nanpi_surrender_conversations.size() != 2: failures.append("020 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, nanpi_surrender_conversations[0], "nanpi_surrender_gate_terms", "topic", "") _check_camp_conversation(failures, nanpi_surrender_conversations[1], "nanpi_surrender_zhang_he_loyalists", "officer", "zhang_he") var nanpi_surrender_merchant := nanpi_surrender_state.get_shop_merchant() if str(nanpi_surrender_merchant.get("name", "")) != "Nanpi Surrender Sutler": failures.append("020 shop should expose Nanpi Surrender merchant name") if (nanpi_surrender_merchant.get("lines", []) as Array).size() < 2: failures.append("020 shop should expose merchant flavor lines") _check_shop_item_visibility(failures, nanpi_surrender_state, "war_drum", false, "020 base") _check_shop_item_visibility(failures, nanpi_surrender_state, "imperial_seal", false, "020 base") _check_shop_item_visibility(failures, nanpi_surrender_state, "iron_armor", true, "020 base") var nanpi_surrender_pressure_state = BattleStateScript.new() if not nanpi_surrender_pressure_state.load_battle("res://data/scenarios/020_nanpi_surrender.json", {}, {}, {"pressed_yuan_tan_surrender": true}): failures.append("could not load Nanpi Surrender pressure camp data") else: _check_shop_items_unique(failures, nanpi_surrender_pressure_state, "020 pressed Yuan Tan surrender") var pressure_wine: Dictionary = _find_camp_conversation( nanpi_surrender_pressure_state.get_briefing().get("camp_conversations", []), "nanpi_surrender_pressure_wine" ) if pressure_wine.is_empty(): failures.append("020 pressed Yuan Tan surrender flag should expose pressure wine") else: _check_camp_conversation_effect(failures, pressure_wine, "wine", 1) if not _find_camp_conversation( nanpi_surrender_pressure_state.get_briefing().get("camp_conversations", []), "nanpi_surrender_admin_medicine" ).is_empty(): failures.append("020 pressed Yuan Tan surrender flag should not expose admin medicine") _check_shop_item_visibility(failures, nanpi_surrender_pressure_state, "war_drum", true, "020 pressed Yuan Tan surrender") _check_shop_item_visibility(failures, nanpi_surrender_pressure_state, "imperial_seal", false, "020 pressed Yuan Tan surrender") var nanpi_surrender_admin_state = BattleStateScript.new() if not nanpi_surrender_admin_state.load_battle("res://data/scenarios/020_nanpi_surrender.json", {}, {}, {"secured_nanpi_admin": true}): failures.append("could not load Nanpi Surrender admin camp data") else: _check_shop_items_unique(failures, nanpi_surrender_admin_state, "020 secured Nanpi admin") var surrender_admin_medicine: Dictionary = _find_camp_conversation( nanpi_surrender_admin_state.get_briefing().get("camp_conversations", []), "nanpi_surrender_admin_medicine" ) if surrender_admin_medicine.is_empty(): failures.append("020 secured Nanpi admin flag should expose admin medicine") else: _check_camp_conversation_effect(failures, surrender_admin_medicine, "panacea", 1) if not _find_camp_conversation( nanpi_surrender_admin_state.get_briefing().get("camp_conversations", []), "nanpi_surrender_pressure_wine" ).is_empty(): failures.append("020 secured Nanpi admin flag should not expose pressure wine") _check_shop_item_visibility(failures, nanpi_surrender_admin_state, "war_drum", false, "020 secured Nanpi admin") _check_shop_item_visibility(failures, nanpi_surrender_admin_state, "imperial_seal", true, "020 secured Nanpi admin") var nanpi_surrender_old_flags_state = BattleStateScript.new() if not nanpi_surrender_old_flags_state.load_battle("res://data/scenarios/020_nanpi_surrender.json", {}, {}, {"pressed_north_to_nanpi": true, "secured_ji_province": true}): failures.append("could not load Nanpi Surrender old Nanpi Pressure flag camp data") else: _check_shop_item_visibility(failures, nanpi_surrender_old_flags_state, "war_drum", false, "020 old Nanpi Pressure flags") _check_shop_item_visibility(failures, nanpi_surrender_old_flags_state, "imperial_seal", false, "020 old Nanpi Pressure flags") if not _find_camp_conversation( nanpi_surrender_old_flags_state.get_briefing().get("camp_conversations", []), "nanpi_surrender_pressure_wine" ).is_empty(): failures.append("020 old Nanpi Pressure flags should not expose pressure wine") if not _find_camp_conversation( nanpi_surrender_old_flags_state.get_briefing().get("camp_conversations", []), "nanpi_surrender_admin_medicine" ).is_empty(): failures.append("020 old Nanpi Pressure flags should not expose admin medicine") var bohai_state = BattleStateScript.new() if not bohai_state.load_battle("res://data/scenarios/021_bohai_remnants.json"): failures.append("could not load Bohai camp data") else: _check_shop_items_unique(failures, bohai_state, "021 base") var bohai_conversations: Array = bohai_state.get_briefing().get("camp_conversations", []) if bohai_conversations.size() != 2: failures.append("021 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, bohai_conversations[0], "bohai_remnants_exile_council", "topic", "") _check_camp_conversation(failures, bohai_conversations[1], "bohai_remnants_zhang_he_escape_road", "officer", "zhang_he") var bohai_merchant := bohai_state.get_shop_merchant() if str(bohai_merchant.get("name", "")) != "Bohai Road Sutler": failures.append("021 shop should expose Bohai Road merchant name") if (bohai_merchant.get("lines", []) as Array).size() < 2: failures.append("021 shop should expose merchant flavor lines") _check_shop_item_visibility(failures, bohai_state, "war_drum", false, "021 base") _check_shop_item_visibility(failures, bohai_state, "imperial_seal", false, "021 base") _check_shop_item_visibility(failures, bohai_state, "iron_armor", true, "021 base") var bohai_pursued_state = BattleStateScript.new() if not bohai_pursued_state.load_battle("res://data/scenarios/021_bohai_remnants.json", {}, {}, {"pursued_yuan_brothers": true}): failures.append("could not load Bohai pursued Yuan brothers camp data") else: _check_shop_items_unique(failures, bohai_pursued_state, "021 pursued Yuan brothers") var fast_wine: Dictionary = _find_camp_conversation( bohai_pursued_state.get_briefing().get("camp_conversations", []), "bohai_remnants_fast_column_wine" ) if fast_wine.is_empty(): failures.append("021 pursued Yuan brothers flag should expose fast column wine") else: _check_camp_conversation_effect(failures, fast_wine, "wine", 1) if not _find_camp_conversation( bohai_pursued_state.get_briefing().get("camp_conversations", []), "bohai_remnants_commandery_medicine" ).is_empty(): failures.append("021 pursued Yuan brothers flag should not expose commandery medicine") _check_shop_item_visibility(failures, bohai_pursued_state, "war_drum", true, "021 pursued Yuan brothers") _check_shop_item_visibility(failures, bohai_pursued_state, "imperial_seal", false, "021 pursued Yuan brothers") var bohai_secured_state = BattleStateScript.new() if not bohai_secured_state.load_battle("res://data/scenarios/021_bohai_remnants.json", {}, {}, {"secured_bohai_commandery": true}): failures.append("could not load Bohai secured commandery camp data") else: _check_shop_items_unique(failures, bohai_secured_state, "021 secured Bohai") var commandery_medicine: Dictionary = _find_camp_conversation( bohai_secured_state.get_briefing().get("camp_conversations", []), "bohai_remnants_commandery_medicine" ) if commandery_medicine.is_empty(): failures.append("021 secured Bohai flag should expose commandery medicine") else: _check_camp_conversation_effect(failures, commandery_medicine, "panacea", 1) if not _find_camp_conversation( bohai_secured_state.get_briefing().get("camp_conversations", []), "bohai_remnants_fast_column_wine" ).is_empty(): failures.append("021 secured Bohai flag should not expose fast column wine") _check_shop_item_visibility(failures, bohai_secured_state, "war_drum", false, "021 secured Bohai") _check_shop_item_visibility(failures, bohai_secured_state, "imperial_seal", true, "021 secured Bohai") var bohai_old_flags_state = BattleStateScript.new() if not bohai_old_flags_state.load_battle("res://data/scenarios/021_bohai_remnants.json", {}, {}, {"pressed_yuan_tan_surrender": true, "secured_nanpi_admin": true}): failures.append("could not load Bohai old Nanpi Surrender flag camp data") else: _check_shop_item_visibility(failures, bohai_old_flags_state, "war_drum", false, "021 old Nanpi Surrender flags") _check_shop_item_visibility(failures, bohai_old_flags_state, "imperial_seal", false, "021 old Nanpi Surrender flags") if not _find_camp_conversation( bohai_old_flags_state.get_briefing().get("camp_conversations", []), "bohai_remnants_fast_column_wine" ).is_empty(): failures.append("021 old Nanpi Surrender flags should not expose fast column wine") if not _find_camp_conversation( bohai_old_flags_state.get_briefing().get("camp_conversations", []), "bohai_remnants_commandery_medicine" ).is_empty(): failures.append("021 old Nanpi Surrender flags should not expose commandery medicine") var liaoxi_state = BattleStateScript.new() if not liaoxi_state.load_battle("res://data/scenarios/022_liaoxi_pursuit.json", {}, {}, {}, ["zhang_he"]): failures.append("could not load Liaoxi Pursuit camp data") else: _check_shop_items_unique(failures, liaoxi_state, "022 base") var liaoxi_conversations: Array = liaoxi_state.get_briefing().get("camp_conversations", []) if liaoxi_conversations.size() != 2: failures.append("022 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, liaoxi_conversations[0], "liaoxi_frontier_council", "topic", "") _check_camp_conversation(failures, liaoxi_conversations[1], "liaoxi_zhang_he_hill_roads", "officer", "zhang_he") var liaoxi_merchant := liaoxi_state.get_shop_merchant() if str(liaoxi_merchant.get("name", "")) != "Liaoxi Frontier Sutler": failures.append("022 shop should expose Liaoxi Frontier merchant name") if (liaoxi_merchant.get("lines", []) as Array).size() < 2: failures.append("022 shop should expose merchant flavor lines") if liaoxi_state.get_unit("zhang_he_ch22").is_empty() or not liaoxi_state.is_required_deployment("zhang_he_ch22"): failures.append("022 should deploy Zhang He as a required officer") _check_shop_item_visibility(failures, liaoxi_state, "war_drum", false, "022 base") _check_shop_item_visibility(failures, liaoxi_state, "imperial_seal", false, "022 base") _check_shop_item_visibility(failures, liaoxi_state, "iron_armor", true, "022 base") var liaoxi_pressed_state = BattleStateScript.new() if not liaoxi_pressed_state.load_battle("res://data/scenarios/022_liaoxi_pursuit.json", {}, {}, {"pressed_liaoxi_pursuit": true}, ["zhang_he"]): failures.append("could not load Liaoxi pressed pursuit camp data") else: _check_shop_items_unique(failures, liaoxi_pressed_state, "022 pressed Liaoxi") var fast_drums: Dictionary = _find_camp_conversation( liaoxi_pressed_state.get_briefing().get("camp_conversations", []), "liaoxi_fast_pursuit_drums" ) if fast_drums.is_empty(): failures.append("022 pressed Liaoxi flag should expose fast pursuit drums") else: _check_camp_conversation_effect(failures, fast_drums, "war_drum", 1) if not _find_camp_conversation( liaoxi_pressed_state.get_briefing().get("camp_conversations", []), "liaoxi_approach_medicine" ).is_empty(): failures.append("022 pressed Liaoxi flag should not expose approach medicine") _check_shop_item_visibility(failures, liaoxi_pressed_state, "war_drum", true, "022 pressed Liaoxi") _check_shop_item_visibility(failures, liaoxi_pressed_state, "imperial_seal", false, "022 pressed Liaoxi") var liaoxi_secured_state = BattleStateScript.new() if not liaoxi_secured_state.load_battle("res://data/scenarios/022_liaoxi_pursuit.json", {}, {}, {"secured_liaoxi_approaches": true}, ["zhang_he"]): failures.append("could not load Liaoxi secured approaches camp data") else: _check_shop_items_unique(failures, liaoxi_secured_state, "022 secured Liaoxi") var approach_medicine: Dictionary = _find_camp_conversation( liaoxi_secured_state.get_briefing().get("camp_conversations", []), "liaoxi_approach_medicine" ) if approach_medicine.is_empty(): failures.append("022 secured Liaoxi flag should expose approach medicine") else: _check_camp_conversation_effect(failures, approach_medicine, "panacea", 1) if not _find_camp_conversation( liaoxi_secured_state.get_briefing().get("camp_conversations", []), "liaoxi_fast_pursuit_drums" ).is_empty(): failures.append("022 secured Liaoxi flag should not expose fast pursuit drums") _check_shop_item_visibility(failures, liaoxi_secured_state, "war_drum", false, "022 secured Liaoxi") _check_shop_item_visibility(failures, liaoxi_secured_state, "imperial_seal", true, "022 secured Liaoxi") var liaoxi_old_flags_state = BattleStateScript.new() if not liaoxi_old_flags_state.load_battle("res://data/scenarios/022_liaoxi_pursuit.json", {}, {}, {"pursued_yuan_brothers": true, "secured_bohai_commandery": true}, ["zhang_he"]): failures.append("could not load Liaoxi old Bohai flag camp data") else: _check_shop_item_visibility(failures, liaoxi_old_flags_state, "war_drum", false, "022 old Bohai flags") _check_shop_item_visibility(failures, liaoxi_old_flags_state, "imperial_seal", false, "022 old Bohai flags") if not _find_camp_conversation( liaoxi_old_flags_state.get_briefing().get("camp_conversations", []), "liaoxi_fast_pursuit_drums" ).is_empty(): failures.append("022 old Bohai flags should not expose fast pursuit drums") if not _find_camp_conversation( liaoxi_old_flags_state.get_briefing().get("camp_conversations", []), "liaoxi_approach_medicine" ).is_empty(): failures.append("022 old Bohai flags should not expose approach medicine") var white_wolf_state = BattleStateScript.new() if not white_wolf_state.load_battle("res://data/scenarios/023_white_wolf_mountain.json", {}, {}, {}, ["zhang_he"]): failures.append("could not load White Wolf Mountain data") else: _check_shop_items_unique(failures, white_wolf_state, "023 base") var white_wolf_conversations: Array = white_wolf_state.get_briefing().get("camp_conversations", []) if white_wolf_conversations.size() != 2: failures.append("023 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, white_wolf_conversations[0], "white_wolf_ridge_council", "topic", "") _check_camp_conversation(failures, white_wolf_conversations[1], "white_wolf_zhang_he_ridge_wind", "officer", "zhang_he") var white_wolf_merchant := white_wolf_state.get_shop_merchant() if str(white_wolf_merchant.get("name", "")) != "White Wolf Ridge Sutler": failures.append("023 shop should expose White Wolf Ridge merchant name") if (white_wolf_merchant.get("lines", []) as Array).size() < 2: failures.append("023 shop should expose merchant flavor lines") if white_wolf_state.get_unit("zhang_he_ch23").is_empty() or not white_wolf_state.is_required_deployment("zhang_he_ch23"): failures.append("023 should deploy Zhang He as a required officer") _check_shop_item_visibility(failures, white_wolf_state, "war_drum", false, "023 base") _check_shop_item_visibility(failures, white_wolf_state, "imperial_seal", false, "023 base") _check_shop_item_visibility(failures, white_wolf_state, "iron_armor", true, "023 base") var white_wolf_struck_state = BattleStateScript.new() if not white_wolf_struck_state.load_battle("res://data/scenarios/023_white_wolf_mountain.json", {}, {}, {"struck_wuhuan_vanguard": true}, ["zhang_he"]): failures.append("could not load White Wolf struck vanguard camp data") else: _check_shop_items_unique(failures, white_wolf_struck_state, "023 struck Wuhuan") var canteens: Dictionary = _find_camp_conversation( white_wolf_struck_state.get_briefing().get("camp_conversations", []), "white_wolf_scattered_canteens" ) if canteens.is_empty(): failures.append("023 struck Wuhuan flag should expose scattered canteens") else: _check_camp_conversation_effect(failures, canteens, "wine", 1) if not _find_camp_conversation( white_wolf_struck_state.get_briefing().get("camp_conversations", []), "white_wolf_route_medicine" ).is_empty(): failures.append("023 struck Wuhuan flag should not expose route medicine") _check_shop_item_visibility(failures, white_wolf_struck_state, "war_drum", true, "023 struck Wuhuan") _check_shop_item_visibility(failures, white_wolf_struck_state, "imperial_seal", false, "023 struck Wuhuan") var white_wolf_fortified_state = BattleStateScript.new() if not white_wolf_fortified_state.load_battle("res://data/scenarios/023_white_wolf_mountain.json", {}, {}, {"fortified_white_wolf_route": true}, ["zhang_he"]): failures.append("could not load White Wolf fortified route camp data") else: _check_shop_items_unique(failures, white_wolf_fortified_state, "023 fortified White Wolf") var route_medicine: Dictionary = _find_camp_conversation( white_wolf_fortified_state.get_briefing().get("camp_conversations", []), "white_wolf_route_medicine" ) if route_medicine.is_empty(): failures.append("023 fortified White Wolf flag should expose route medicine") else: _check_camp_conversation_effect(failures, route_medicine, "panacea", 1) if not _find_camp_conversation( white_wolf_fortified_state.get_briefing().get("camp_conversations", []), "white_wolf_scattered_canteens" ).is_empty(): failures.append("023 fortified White Wolf flag should not expose scattered canteens") _check_shop_item_visibility(failures, white_wolf_fortified_state, "war_drum", false, "023 fortified White Wolf") _check_shop_item_visibility(failures, white_wolf_fortified_state, "imperial_seal", true, "023 fortified White Wolf") var white_wolf_old_flags_state = BattleStateScript.new() if not white_wolf_old_flags_state.load_battle("res://data/scenarios/023_white_wolf_mountain.json", {}, {}, {"pressed_liaoxi_pursuit": true, "secured_liaoxi_approaches": true}, ["zhang_he"]): failures.append("could not load White Wolf old Liaoxi flag camp data") else: _check_shop_item_visibility(failures, white_wolf_old_flags_state, "war_drum", false, "023 old Liaoxi flags") _check_shop_item_visibility(failures, white_wolf_old_flags_state, "imperial_seal", false, "023 old Liaoxi flags") if not _find_camp_conversation( white_wolf_old_flags_state.get_briefing().get("camp_conversations", []), "white_wolf_scattered_canteens" ).is_empty(): failures.append("023 old Liaoxi flags should not expose scattered canteens") if not _find_camp_conversation( white_wolf_old_flags_state.get_briefing().get("camp_conversations", []), "white_wolf_route_medicine" ).is_empty(): failures.append("023 old Liaoxi flags should not expose route medicine") var liaodong_state = BattleStateScript.new() if not liaodong_state.load_battle("res://data/scenarios/024_liaodong_pursuit.json", {}, {}, {}, ["zhang_he"]): failures.append("could not load Liaodong Pursuit data") else: _check_shop_items_unique(failures, liaodong_state, "024 base") var liaodong_conversations: Array = liaodong_state.get_briefing().get("camp_conversations", []) if liaodong_conversations.size() != 2: failures.append("024 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, liaodong_conversations[0], "liaodong_border_council", "topic", "") _check_camp_conversation(failures, liaodong_conversations[1], "liaodong_zhang_he_border_price", "officer", "zhang_he") var liaodong_merchant := liaodong_state.get_shop_merchant() if str(liaodong_merchant.get("name", "")) != "Liaodong Border Sutler": failures.append("024 shop should expose Liaodong Border merchant name") if (liaodong_merchant.get("lines", []) as Array).size() < 2: failures.append("024 shop should expose merchant flavor lines") if liaodong_state.get_unit("zhang_he_ch24").is_empty() or not liaodong_state.is_required_deployment("zhang_he_ch24"): failures.append("024 should deploy Zhang He as a required officer") _check_shop_item_visibility(failures, liaodong_state, "war_drum", false, "024 base") _check_shop_item_visibility(failures, liaodong_state, "imperial_seal", false, "024 base") _check_shop_item_visibility(failures, liaodong_state, "iron_armor", true, "024 base") var liaodong_pressed_state = BattleStateScript.new() if not liaodong_pressed_state.load_battle("res://data/scenarios/024_liaodong_pursuit.json", {}, {}, {"pressed_liaodong_pursuit": true}, ["zhang_he"]): failures.append("could not load Liaodong pressed pursuit data") else: _check_shop_items_unique(failures, liaodong_pressed_state, "024 pressed Liaodong") var hard_wine: Dictionary = _find_camp_conversation( liaodong_pressed_state.get_briefing().get("camp_conversations", []), "liaodong_hard_pursuit_wine" ) if hard_wine.is_empty(): failures.append("024 pressed Liaodong flag should expose hard pursuit wine") else: _check_camp_conversation_effect(failures, hard_wine, "wine", 1) if not _find_camp_conversation( liaodong_pressed_state.get_briefing().get("camp_conversations", []), "liaodong_wuhuan_guides_medicine" ).is_empty(): failures.append("024 pressed Liaodong flag should not expose Wuhuan guide medicine") _check_shop_item_visibility(failures, liaodong_pressed_state, "war_drum", true, "024 pressed Liaodong") _check_shop_item_visibility(failures, liaodong_pressed_state, "imperial_seal", false, "024 pressed Liaodong") var liaodong_secured_state = BattleStateScript.new() if not liaodong_secured_state.load_battle("res://data/scenarios/024_liaodong_pursuit.json", {}, {}, {"secured_wuhuan_submission": true}, ["zhang_he"]): failures.append("could not load Liaodong secured Wuhuan data") else: _check_shop_items_unique(failures, liaodong_secured_state, "024 secured Wuhuan") var guide_medicine: Dictionary = _find_camp_conversation( liaodong_secured_state.get_briefing().get("camp_conversations", []), "liaodong_wuhuan_guides_medicine" ) if guide_medicine.is_empty(): failures.append("024 secured Wuhuan flag should expose guide medicine") else: _check_camp_conversation_effect(failures, guide_medicine, "panacea", 1) if not _find_camp_conversation( liaodong_secured_state.get_briefing().get("camp_conversations", []), "liaodong_hard_pursuit_wine" ).is_empty(): failures.append("024 secured Wuhuan flag should not expose hard pursuit wine") _check_shop_item_visibility(failures, liaodong_secured_state, "war_drum", false, "024 secured Wuhuan") _check_shop_item_visibility(failures, liaodong_secured_state, "imperial_seal", true, "024 secured Wuhuan") var liaodong_old_flags_state = BattleStateScript.new() if not liaodong_old_flags_state.load_battle("res://data/scenarios/024_liaodong_pursuit.json", {}, {}, {"struck_wuhuan_vanguard": true, "fortified_white_wolf_route": true}, ["zhang_he"]): failures.append("could not load Liaodong old White Wolf flag camp data") else: _check_shop_item_visibility(failures, liaodong_old_flags_state, "war_drum", false, "024 old White Wolf flags") _check_shop_item_visibility(failures, liaodong_old_flags_state, "imperial_seal", false, "024 old White Wolf flags") if not _find_camp_conversation( liaodong_old_flags_state.get_briefing().get("camp_conversations", []), "liaodong_hard_pursuit_wine" ).is_empty(): failures.append("024 old White Wolf flags should not expose hard pursuit wine") if not _find_camp_conversation( liaodong_old_flags_state.get_briefing().get("camp_conversations", []), "liaodong_wuhuan_guides_medicine" ).is_empty(): failures.append("024 old White Wolf flags should not expose guide medicine") var xinye_state = BattleStateScript.new() if not xinye_state.load_battle("res://data/scenarios/025_xinye_advance.json", {}, {}, {}, ["zhang_he"]): failures.append("could not load Xinye Advance data") else: _check_shop_items_unique(failures, xinye_state, "025 base") var xinye_conversations: Array = xinye_state.get_briefing().get("camp_conversations", []) if xinye_conversations.size() != 2: failures.append("025 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, xinye_conversations[0], "xinye_southern_war_council", "topic", "") _check_camp_conversation(failures, xinye_conversations[1], "xinye_zhang_he_south_road", "officer", "zhang_he") var xinye_merchant := xinye_state.get_shop_merchant() if str(xinye_merchant.get("name", "")) != "Xinye Road Sutler": failures.append("025 shop should expose Xinye Road merchant name") if (xinye_merchant.get("lines", []) as Array).size() < 2: failures.append("025 shop should expose merchant flavor lines") if xinye_state.get_unit("zhang_he_ch25").is_empty() or not xinye_state.is_required_deployment("zhang_he_ch25"): failures.append("025 should deploy Zhang He as a required officer") _check_shop_item_visibility(failures, xinye_state, "war_drum", false, "025 base") _check_shop_item_visibility(failures, xinye_state, "imperial_seal", false, "025 base") var xinye_terms_state = BattleStateScript.new() if not xinye_terms_state.load_battle("res://data/scenarios/025_xinye_advance.json", {}, {}, {"accepted_liaodong_terms": true}, ["zhang_he"]): failures.append("could not load Xinye accepted Liaodong terms data") else: var ordered_medicine: Dictionary = _find_camp_conversation( xinye_terms_state.get_briefing().get("camp_conversations", []), "xinye_ordered_medicine" ) if ordered_medicine.is_empty(): failures.append("025 accepted Liaodong terms should expose ordered medicine") else: _check_camp_conversation_effect(failures, ordered_medicine, "panacea", 1) if not _find_camp_conversation( xinye_terms_state.get_briefing().get("camp_conversations", []), "xinye_authority_wine" ).is_empty(): failures.append("025 accepted Liaodong terms should not expose authority wine") _check_shop_item_visibility(failures, xinye_terms_state, "imperial_seal", true, "025 accepted Liaodong terms") _check_shop_item_visibility(failures, xinye_terms_state, "war_drum", false, "025 accepted Liaodong terms") var xinye_authority_state = BattleStateScript.new() if not xinye_authority_state.load_battle("res://data/scenarios/025_xinye_advance.json", {}, {}, {"pressed_liaodong_authority": true}, ["zhang_he"]): failures.append("could not load Xinye pressed Liaodong authority data") else: var authority_wine: Dictionary = _find_camp_conversation( xinye_authority_state.get_briefing().get("camp_conversations", []), "xinye_authority_wine" ) if authority_wine.is_empty(): failures.append("025 pressed Liaodong authority should expose authority wine") else: _check_camp_conversation_effect(failures, authority_wine, "wine", 1) if not _find_camp_conversation( xinye_authority_state.get_briefing().get("camp_conversations", []), "xinye_ordered_medicine" ).is_empty(): failures.append("025 pressed Liaodong authority should not expose ordered medicine") _check_shop_item_visibility(failures, xinye_authority_state, "war_drum", true, "025 pressed Liaodong authority") _check_shop_item_visibility(failures, xinye_authority_state, "imperial_seal", false, "025 pressed Liaodong authority") var xinye_old_flags_state = BattleStateScript.new() if not xinye_old_flags_state.load_battle("res://data/scenarios/025_xinye_advance.json", {}, {}, {"pressed_liaodong_pursuit": true, "secured_wuhuan_submission": true}, ["zhang_he"]): failures.append("could not load Xinye old Liaodong Pursuit flag data") else: _check_shop_items_unique(failures, xinye_old_flags_state, "025 old Liaodong Pursuit flags") _check_shop_item_visibility(failures, xinye_old_flags_state, "war_drum", false, "025 old Liaodong Pursuit flags") _check_shop_item_visibility(failures, xinye_old_flags_state, "imperial_seal", false, "025 old Liaodong Pursuit flags") if not _find_camp_conversation( xinye_old_flags_state.get_briefing().get("camp_conversations", []), "xinye_ordered_medicine" ).is_empty(): failures.append("025 old Liaodong Pursuit flags should not expose ordered medicine") if not _find_camp_conversation( xinye_old_flags_state.get_briefing().get("camp_conversations", []), "xinye_authority_wine" ).is_empty(): failures.append("025 old Liaodong Pursuit flags should not expose authority wine") var changban_state = BattleStateScript.new() if not changban_state.load_battle("res://data/scenarios/026_changban_pursuit.json", {}, {}, {}, ["zhang_he"]): failures.append("could not load Changban Pursuit data") else: _check_shop_items_unique(failures, changban_state, "026 base") var changban_conversations: Array = changban_state.get_briefing().get("camp_conversations", []) if changban_conversations.size() != 2: failures.append("026 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, changban_conversations[0], "changban_bridge_council", "topic", "") _check_camp_conversation(failures, changban_conversations[1], "changban_zhang_he_bridge_shadow", "officer", "zhang_he") var changban_merchant := changban_state.get_shop_merchant() if str(changban_merchant.get("name", "")) != "Changban Bridge Sutler": failures.append("026 shop should expose Changban Bridge merchant name") if (changban_merchant.get("lines", []) as Array).size() < 2: failures.append("026 shop should expose merchant flavor lines") if changban_state.get_unit("zhang_he_ch26").is_empty() or not changban_state.is_required_deployment("zhang_he_ch26"): failures.append("026 should deploy Zhang He as a required officer") _check_shop_item_visibility(failures, changban_state, "war_drum", false, "026 base") _check_shop_item_visibility(failures, changban_state, "imperial_seal", false, "026 base") var changban_vanguard_state = BattleStateScript.new() if not changban_vanguard_state.load_battle("res://data/scenarios/026_changban_pursuit.json", {}, {}, {"pressed_xinye_vanguard": true}, ["zhang_he"]): failures.append("could not load Changban pressed Xinye vanguard data") else: var vanguard_rations: Dictionary = _find_camp_conversation( changban_vanguard_state.get_briefing().get("camp_conversations", []), "changban_vanguard_rations" ) if vanguard_rations.is_empty(): failures.append("026 pressed Xinye vanguard should expose vanguard rations") else: _check_camp_conversation_effect(failures, vanguard_rations, "bean", 1) if not _find_camp_conversation( changban_vanguard_state.get_briefing().get("camp_conversations", []), "changban_han_supply_medicine" ).is_empty(): failures.append("026 pressed Xinye vanguard should not expose Han supply medicine") _check_shop_item_visibility(failures, changban_vanguard_state, "war_drum", true, "026 pressed Xinye vanguard") _check_shop_item_visibility(failures, changban_vanguard_state, "imperial_seal", false, "026 pressed Xinye vanguard") var changban_supply_state = BattleStateScript.new() if not changban_supply_state.load_battle("res://data/scenarios/026_changban_pursuit.json", {}, {}, {"secured_han_river_supply": true}, ["zhang_he"]): failures.append("could not load Changban secured Han supply data") else: var han_medicine: Dictionary = _find_camp_conversation( changban_supply_state.get_briefing().get("camp_conversations", []), "changban_han_supply_medicine" ) if han_medicine.is_empty(): failures.append("026 secured Han supply should expose Han supply medicine") else: _check_camp_conversation_effect(failures, han_medicine, "panacea", 1) if not _find_camp_conversation( changban_supply_state.get_briefing().get("camp_conversations", []), "changban_vanguard_rations" ).is_empty(): failures.append("026 secured Han supply should not expose vanguard rations") _check_shop_item_visibility(failures, changban_supply_state, "imperial_seal", true, "026 secured Han supply") _check_shop_item_visibility(failures, changban_supply_state, "war_drum", false, "026 secured Han supply") var changban_old_flags_state = BattleStateScript.new() if not changban_old_flags_state.load_battle("res://data/scenarios/026_changban_pursuit.json", {}, {}, {"accepted_liaodong_terms": true, "pressed_liaodong_authority": true}, ["zhang_he"]): failures.append("could not load Changban old Xinye setup flag data") else: _check_shop_items_unique(failures, changban_old_flags_state, "026 old Xinye setup flags") _check_shop_item_visibility(failures, changban_old_flags_state, "war_drum", false, "026 old Xinye setup flags") _check_shop_item_visibility(failures, changban_old_flags_state, "imperial_seal", false, "026 old Xinye setup flags") if not _find_camp_conversation( changban_old_flags_state.get_briefing().get("camp_conversations", []), "changban_vanguard_rations" ).is_empty(): failures.append("026 old Xinye setup flags should not expose vanguard rations") if not _find_camp_conversation( changban_old_flags_state.get_briefing().get("camp_conversations", []), "changban_han_supply_medicine" ).is_empty(): failures.append("026 old Xinye setup flags should not expose Han supply medicine") var jiangling_state = BattleStateScript.new() if not jiangling_state.load_battle("res://data/scenarios/027_jiangling_chase.json", {}, {}, {}, ["zhang_he"]): failures.append("could not load Jiangling Chase data") else: _check_shop_items_unique(failures, jiangling_state, "027 base") var jiangling_conversations: Array = jiangling_state.get_briefing().get("camp_conversations", []) if jiangling_conversations.size() != 2: failures.append("027 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, jiangling_conversations[0], "jiangling_storehouse_council", "topic", "") _check_camp_conversation(failures, jiangling_conversations[1], "jiangling_zhang_he_granary_road", "officer", "zhang_he") var jiangling_merchant := jiangling_state.get_shop_merchant() if str(jiangling_merchant.get("name", "")) != "Jiangling Storehouse Sutler": failures.append("027 shop should expose Jiangling Storehouse merchant name") if (jiangling_merchant.get("lines", []) as Array).size() < 2: failures.append("027 shop should expose merchant flavor lines") if jiangling_state.get_unit("zhang_he_ch27").is_empty() or not jiangling_state.is_required_deployment("zhang_he_ch27"): failures.append("027 should deploy Zhang He as a required officer") _check_shop_item_visibility(failures, jiangling_state, "war_drum", false, "027 base") _check_shop_item_visibility(failures, jiangling_state, "imperial_seal", false, "027 base") var jiangling_chase_state = BattleStateScript.new() if not jiangling_chase_state.load_battle("res://data/scenarios/027_jiangling_chase.json", {}, {}, {"pressed_jiangling_chase": true}, ["zhang_he"]): failures.append("could not load Jiangling pressed chase data") else: var fast_beans: Dictionary = _find_camp_conversation( jiangling_chase_state.get_briefing().get("camp_conversations", []), "jiangling_fast_column_beans" ) if fast_beans.is_empty(): failures.append("027 pressed Jiangling chase should expose fast column beans") else: _check_camp_conversation_effect(failures, fast_beans, "bean", 1) if not _find_camp_conversation( jiangling_chase_state.get_briefing().get("camp_conversations", []), "jiangling_crossing_medicine" ).is_empty(): failures.append("027 pressed Jiangling chase should not expose crossing medicine") _check_shop_item_visibility(failures, jiangling_chase_state, "war_drum", true, "027 pressed Jiangling chase") _check_shop_item_visibility(failures, jiangling_chase_state, "imperial_seal", false, "027 pressed Jiangling chase") var jiangling_crossings_state = BattleStateScript.new() if not jiangling_crossings_state.load_battle("res://data/scenarios/027_jiangling_chase.json", {}, {}, {"secured_changban_crossings": true}, ["zhang_he"]): failures.append("could not load Jiangling secured crossings data") else: var crossing_medicine: Dictionary = _find_camp_conversation( jiangling_crossings_state.get_briefing().get("camp_conversations", []), "jiangling_crossing_medicine" ) if crossing_medicine.is_empty(): failures.append("027 secured Changban crossings should expose crossing medicine") else: _check_camp_conversation_effect(failures, crossing_medicine, "panacea", 1) if not _find_camp_conversation( jiangling_crossings_state.get_briefing().get("camp_conversations", []), "jiangling_fast_column_beans" ).is_empty(): failures.append("027 secured Changban crossings should not expose fast column beans") _check_shop_item_visibility(failures, jiangling_crossings_state, "imperial_seal", true, "027 secured Changban crossings") _check_shop_item_visibility(failures, jiangling_crossings_state, "war_drum", false, "027 secured Changban crossings") var jiangling_old_flags_state = BattleStateScript.new() if not jiangling_old_flags_state.load_battle("res://data/scenarios/027_jiangling_chase.json", {}, {}, {"pressed_xinye_vanguard": true, "secured_han_river_supply": true}, ["zhang_he"]): failures.append("could not load Jiangling old Changban setup flag data") else: _check_shop_items_unique(failures, jiangling_old_flags_state, "027 old Changban setup flags") _check_shop_item_visibility(failures, jiangling_old_flags_state, "war_drum", false, "027 old Changban setup flags") _check_shop_item_visibility(failures, jiangling_old_flags_state, "imperial_seal", false, "027 old Changban setup flags") if not _find_camp_conversation( jiangling_old_flags_state.get_briefing().get("camp_conversations", []), "jiangling_fast_column_beans" ).is_empty(): failures.append("027 old Changban setup flags should not expose fast column beans") if not _find_camp_conversation( jiangling_old_flags_state.get_briefing().get("camp_conversations", []), "jiangling_crossing_medicine" ).is_empty(): failures.append("027 old Changban setup flags should not expose crossing medicine") var xiakou_state = BattleStateScript.new() if not xiakou_state.load_battle("res://data/scenarios/028_xiakou_pursuit.json", {}, {}, {}, ["zhang_he"]): failures.append("could not load Xiakou Pursuit data") else: _check_shop_items_unique(failures, xiakou_state, "028 base") var xiakou_conversations: Array = xiakou_state.get_briefing().get("camp_conversations", []) if xiakou_conversations.size() != 2: failures.append("028 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, xiakou_conversations[0], "xiakou_harbor_council", "topic", "") _check_camp_conversation(failures, xiakou_conversations[1], "xiakou_zhang_he_river_mouth", "officer", "zhang_he") var xiakou_merchant := xiakou_state.get_shop_merchant() if str(xiakou_merchant.get("name", "")) != "Xiakou Harbor Sutler": failures.append("028 shop should expose Xiakou Harbor merchant name") if (xiakou_merchant.get("lines", []) as Array).size() < 2: failures.append("028 shop should expose merchant flavor lines") if xiakou_state.get_unit("zhang_he_ch28").is_empty() or not xiakou_state.is_required_deployment("zhang_he_ch28"): failures.append("028 should deploy Zhang He as a required officer") _check_shop_item_visibility(failures, xiakou_state, "war_drum", false, "028 base") _check_shop_item_visibility(failures, xiakou_state, "imperial_seal", false, "028 base") var xiakou_pursuit_state = BattleStateScript.new() if not xiakou_pursuit_state.load_battle("res://data/scenarios/028_xiakou_pursuit.json", {}, {}, {"pressed_xiakou_pursuit": true}, ["zhang_he"]): failures.append("could not load Xiakou pressed pursuit data") else: var envoy_beans: Dictionary = _find_camp_conversation( xiakou_pursuit_state.get_briefing().get("camp_conversations", []), "xiakou_envoy_chase_beans" ) if envoy_beans.is_empty(): failures.append("028 pressed Xiakou pursuit should expose envoy chase beans") else: _check_camp_conversation_effect(failures, envoy_beans, "bean", 1) if not _find_camp_conversation( xiakou_pursuit_state.get_briefing().get("camp_conversations", []), "xiakou_storehouse_medicine" ).is_empty(): failures.append("028 pressed Xiakou pursuit should not expose storehouse medicine") _check_shop_item_visibility(failures, xiakou_pursuit_state, "war_drum", true, "028 pressed Xiakou pursuit") _check_shop_item_visibility(failures, xiakou_pursuit_state, "imperial_seal", false, "028 pressed Xiakou pursuit") var xiakou_store_state = BattleStateScript.new() if not xiakou_store_state.load_battle("res://data/scenarios/028_xiakou_pursuit.json", {}, {}, {"secured_jiangling_storehouses": true}, ["zhang_he"]): failures.append("could not load Xiakou secured Jiangling stores data") else: var storehouse_medicine: Dictionary = _find_camp_conversation( xiakou_store_state.get_briefing().get("camp_conversations", []), "xiakou_storehouse_medicine" ) if storehouse_medicine.is_empty(): failures.append("028 secured Jiangling stores should expose storehouse medicine") else: _check_camp_conversation_effect(failures, storehouse_medicine, "panacea", 1) if not _find_camp_conversation( xiakou_store_state.get_briefing().get("camp_conversations", []), "xiakou_envoy_chase_beans" ).is_empty(): failures.append("028 secured Jiangling stores should not expose envoy chase beans") _check_shop_item_visibility(failures, xiakou_store_state, "imperial_seal", true, "028 secured Jiangling stores") _check_shop_item_visibility(failures, xiakou_store_state, "war_drum", false, "028 secured Jiangling stores") var xiakou_old_flags_state = BattleStateScript.new() if not xiakou_old_flags_state.load_battle("res://data/scenarios/028_xiakou_pursuit.json", {}, {}, {"pressed_jiangling_chase": true, "secured_changban_crossings": true}, ["zhang_he"]): failures.append("could not load Xiakou old Jiangling setup flag data") else: _check_shop_items_unique(failures, xiakou_old_flags_state, "028 old Jiangling setup flags") _check_shop_item_visibility(failures, xiakou_old_flags_state, "war_drum", false, "028 old Jiangling setup flags") _check_shop_item_visibility(failures, xiakou_old_flags_state, "imperial_seal", false, "028 old Jiangling setup flags") if not _find_camp_conversation( xiakou_old_flags_state.get_briefing().get("camp_conversations", []), "xiakou_envoy_chase_beans" ).is_empty(): failures.append("028 old Jiangling setup flags should not expose envoy chase beans") if not _find_camp_conversation( xiakou_old_flags_state.get_briefing().get("camp_conversations", []), "xiakou_storehouse_medicine" ).is_empty(): failures.append("028 old Jiangling setup flags should not expose storehouse medicine") var red_cliffs_state = BattleStateScript.new() if not red_cliffs_state.load_battle("res://data/scenarios/029_red_cliffs_fleet.json", {}, {}, {}, ["zhang_he"]): failures.append("could not load Red Cliffs Fleet data") else: _check_shop_items_unique(failures, red_cliffs_state, "029 base") var red_cliffs_conversations: Array = red_cliffs_state.get_briefing().get("camp_conversations", []) if red_cliffs_conversations.size() != 2: failures.append("029 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, red_cliffs_conversations[0], "red_cliffs_fleet_council", "topic", "") _check_camp_conversation(failures, red_cliffs_conversations[1], "red_cliffs_zhang_he_chain_line", "officer", "zhang_he") var red_cliffs_merchant := red_cliffs_state.get_shop_merchant() if str(red_cliffs_merchant.get("name", "")) != "Red Cliffs Deck Sutler": failures.append("029 shop should expose Red Cliffs Deck merchant name") if (red_cliffs_merchant.get("lines", []) as Array).size() < 2: failures.append("029 shop should expose merchant flavor lines") if red_cliffs_state.get_unit("zhang_he_ch29").is_empty() or not red_cliffs_state.is_required_deployment("zhang_he_ch29"): failures.append("029 should deploy Zhang He as a required officer") _check_shop_item_visibility(failures, red_cliffs_state, "war_drum", false, "029 base") _check_shop_item_visibility(failures, red_cliffs_state, "imperial_seal", false, "029 base") var red_cliffs_pressed_state = BattleStateScript.new() if not red_cliffs_pressed_state.load_battle("res://data/scenarios/029_red_cliffs_fleet.json", {}, {}, {"pressed_red_cliffs_fleet": true}, ["zhang_he"]): failures.append("could not load Red Cliffs pressed fleet data") else: var forward_beans: Dictionary = _find_camp_conversation( red_cliffs_pressed_state.get_briefing().get("camp_conversations", []), "red_cliffs_forward_deck_beans" ) if forward_beans.is_empty(): failures.append("029 pressed Red Cliffs fleet should expose forward deck beans") else: _check_camp_conversation_effect(failures, forward_beans, "bean", 1) if not _find_camp_conversation( red_cliffs_pressed_state.get_briefing().get("camp_conversations", []), "red_cliffs_harbor_medicine" ).is_empty(): failures.append("029 pressed Red Cliffs fleet should not expose harbor medicine") _check_shop_item_visibility(failures, red_cliffs_pressed_state, "war_drum", true, "029 pressed Red Cliffs fleet") _check_shop_item_visibility(failures, red_cliffs_pressed_state, "imperial_seal", false, "029 pressed Red Cliffs fleet") var red_cliffs_harbor_state = BattleStateScript.new() if not red_cliffs_harbor_state.load_battle("res://data/scenarios/029_red_cliffs_fleet.json", {}, {}, {"secured_xiakou_harbor": true}, ["zhang_he"]): failures.append("could not load Red Cliffs secured Xiakou harbor data") else: var harbor_medicine: Dictionary = _find_camp_conversation( red_cliffs_harbor_state.get_briefing().get("camp_conversations", []), "red_cliffs_harbor_medicine" ) if harbor_medicine.is_empty(): failures.append("029 secured Xiakou harbor should expose harbor medicine") else: _check_camp_conversation_effect(failures, harbor_medicine, "panacea", 1) if not _find_camp_conversation( red_cliffs_harbor_state.get_briefing().get("camp_conversations", []), "red_cliffs_forward_deck_beans" ).is_empty(): failures.append("029 secured Xiakou harbor should not expose forward deck beans") _check_shop_item_visibility(failures, red_cliffs_harbor_state, "imperial_seal", true, "029 secured Xiakou harbor") _check_shop_item_visibility(failures, red_cliffs_harbor_state, "war_drum", false, "029 secured Xiakou harbor") var red_cliffs_old_flags_state = BattleStateScript.new() if not red_cliffs_old_flags_state.load_battle("res://data/scenarios/029_red_cliffs_fleet.json", {}, {}, {"pressed_xiakou_pursuit": true, "secured_jiangling_storehouses": true}, ["zhang_he"]): failures.append("could not load Red Cliffs old Xiakou setup flag data") else: _check_shop_items_unique(failures, red_cliffs_old_flags_state, "029 old Xiakou setup flags") _check_shop_item_visibility(failures, red_cliffs_old_flags_state, "war_drum", false, "029 old Xiakou setup flags") _check_shop_item_visibility(failures, red_cliffs_old_flags_state, "imperial_seal", false, "029 old Xiakou setup flags") if not _find_camp_conversation( red_cliffs_old_flags_state.get_briefing().get("camp_conversations", []), "red_cliffs_forward_deck_beans" ).is_empty(): failures.append("029 old Xiakou setup flags should not expose forward deck beans") if not _find_camp_conversation( red_cliffs_old_flags_state.get_briefing().get("camp_conversations", []), "red_cliffs_harbor_medicine" ).is_empty(): failures.append("029 old Xiakou setup flags should not expose harbor medicine") var red_fire_state = BattleStateScript.new() if not red_fire_state.load_battle("res://data/scenarios/030_red_cliffs_fire.json", {}, {}, {}, ["zhang_he"]): failures.append("could not load Red Cliffs Fire data") else: _check_shop_items_unique(failures, red_fire_state, "030 base") var red_fire_conversations: Array = red_fire_state.get_briefing().get("camp_conversations", []) if red_fire_conversations.size() != 2: failures.append("030 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, red_fire_conversations[0], "red_cliffs_fire_smoke_council", "topic", "") _check_camp_conversation(failures, red_fire_conversations[1], "red_cliffs_zhang_he_fire_boats", "officer", "zhang_he") var red_fire_merchant := red_fire_state.get_shop_merchant() if str(red_fire_merchant.get("name", "")) != "Red Cliffs Fire Sutler": failures.append("030 shop should expose Red Cliffs Fire merchant name") if (red_fire_merchant.get("lines", []) as Array).size() < 2: failures.append("030 shop should expose merchant flavor lines") if red_fire_state.get_unit("zhang_he_ch30").is_empty() or not red_fire_state.is_required_deployment("zhang_he_ch30"): failures.append("030 should deploy Zhang He as a required officer") _check_shop_item_visibility(failures, red_fire_state, "war_drum", false, "030 base") _check_shop_item_visibility(failures, red_fire_state, "imperial_seal", false, "030 base") var red_fire_chain_state = BattleStateScript.new() if not red_fire_chain_state.load_battle("res://data/scenarios/030_red_cliffs_fire.json", {}, {}, {"reinforced_chain_line": true}, ["zhang_he"]): failures.append("could not load Red Cliffs Fire reinforced chain data") else: var chain_medicine: Dictionary = _find_camp_conversation( red_fire_chain_state.get_briefing().get("camp_conversations", []), "red_cliffs_reinforced_chain_medicine" ) if chain_medicine.is_empty(): failures.append("030 reinforced chain should expose chain medicine") else: _check_camp_conversation_effect(failures, chain_medicine, "panacea", 1) if not _find_camp_conversation( red_fire_chain_state.get_briefing().get("camp_conversations", []), "red_cliffs_night_raid_beans" ).is_empty(): failures.append("030 reinforced chain should not expose night raid beans") _check_shop_item_visibility(failures, red_fire_chain_state, "imperial_seal", true, "030 reinforced chain") _check_shop_item_visibility(failures, red_fire_chain_state, "war_drum", false, "030 reinforced chain") var red_fire_raid_state = BattleStateScript.new() if not red_fire_raid_state.load_battle("res://data/scenarios/030_red_cliffs_fire.json", {}, {}, {"ordered_night_raid": true}, ["zhang_he"]): failures.append("could not load Red Cliffs Fire night raid data") else: var night_beans: Dictionary = _find_camp_conversation( red_fire_raid_state.get_briefing().get("camp_conversations", []), "red_cliffs_night_raid_beans" ) if night_beans.is_empty(): failures.append("030 ordered night raid should expose night raid beans") else: _check_camp_conversation_effect(failures, night_beans, "bean", 1) if not _find_camp_conversation( red_fire_raid_state.get_briefing().get("camp_conversations", []), "red_cliffs_reinforced_chain_medicine" ).is_empty(): failures.append("030 ordered night raid should not expose chain medicine") _check_shop_item_visibility(failures, red_fire_raid_state, "war_drum", true, "030 ordered night raid") _check_shop_item_visibility(failures, red_fire_raid_state, "imperial_seal", false, "030 ordered night raid") var red_fire_old_flags_state = BattleStateScript.new() if not red_fire_old_flags_state.load_battle("res://data/scenarios/030_red_cliffs_fire.json", {}, {}, {"pressed_red_cliffs_fleet": true, "secured_xiakou_harbor": true}, ["zhang_he"]): failures.append("could not load Red Cliffs Fire old fleet setup flag data") else: _check_shop_items_unique(failures, red_fire_old_flags_state, "030 old Red Cliffs setup flags") _check_shop_item_visibility(failures, red_fire_old_flags_state, "war_drum", false, "030 old Red Cliffs setup flags") _check_shop_item_visibility(failures, red_fire_old_flags_state, "imperial_seal", false, "030 old Red Cliffs setup flags") if not _find_camp_conversation( red_fire_old_flags_state.get_briefing().get("camp_conversations", []), "red_cliffs_reinforced_chain_medicine" ).is_empty(): failures.append("030 old Red Cliffs setup flags should not expose chain medicine") if not _find_camp_conversation( red_fire_old_flags_state.get_briefing().get("camp_conversations", []), "red_cliffs_night_raid_beans" ).is_empty(): failures.append("030 old Red Cliffs setup flags should not expose night raid beans") var huarong_state = BattleStateScript.new() if not huarong_state.load_battle("res://data/scenarios/031_huarong_retreat.json", {}, {}, {}, ["zhang_he"]): failures.append("could not load Huarong Retreat data") else: _check_shop_items_unique(failures, huarong_state, "031 base") var huarong_conversations: Array = huarong_state.get_briefing().get("camp_conversations", []) if huarong_conversations.size() != 2: failures.append("031 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, _find_camp_conversation(huarong_conversations, "huarong_retreat_council"), "huarong_retreat_council", "topic", "") _check_camp_conversation(failures, _find_camp_conversation(huarong_conversations, "huarong_zhang_he_mud_road"), "huarong_zhang_he_mud_road", "officer", "zhang_he") var huarong_merchant := huarong_state.get_shop_merchant() if str(huarong_merchant.get("name", "")) != "Huarong Road Sutler": failures.append("031 shop should expose Huarong Road merchant name") if (huarong_merchant.get("lines", []) as Array).size() < 2: failures.append("031 shop should expose merchant flavor lines") if huarong_state.get_unit("zhang_he_ch31").is_empty() or not huarong_state.is_required_deployment("zhang_he_ch31"): failures.append("031 should deploy Zhang He as a required officer") _check_shop_item_visibility(failures, huarong_state, "war_drum", false, "031 base") _check_shop_item_visibility(failures, huarong_state, "imperial_seal", false, "031 base") var huarong_held_state = BattleStateScript.new() if not huarong_held_state.load_battle("res://data/scenarios/031_huarong_retreat.json", {}, {}, {"held_red_cliffs_line": true}, ["zhang_he"]): failures.append("could not load Huarong held Red Cliffs line data") else: var held_medicine: Dictionary = _find_camp_conversation( huarong_held_state.get_briefing().get("camp_conversations", []), "huarong_held_line_medicine" ) if held_medicine.is_empty(): failures.append("031 held Red Cliffs line should expose held line medicine") else: _check_camp_conversation_effect(failures, held_medicine, "panacea", 1) if not _find_camp_conversation( huarong_held_state.get_briefing().get("camp_conversations", []), "huarong_open_retreat_beans" ).is_empty(): failures.append("031 held Red Cliffs line should not expose opened retreat beans") _check_shop_item_visibility(failures, huarong_held_state, "imperial_seal", true, "031 held Red Cliffs line") _check_shop_item_visibility(failures, huarong_held_state, "war_drum", false, "031 held Red Cliffs line") var huarong_opened_state = BattleStateScript.new() if not huarong_opened_state.load_battle("res://data/scenarios/031_huarong_retreat.json", {}, {}, {"opened_huarong_retreat": true}, ["zhang_he"]): failures.append("could not load Huarong opened retreat data") else: var open_retreat_beans: Dictionary = _find_camp_conversation( huarong_opened_state.get_briefing().get("camp_conversations", []), "huarong_open_retreat_beans" ) if open_retreat_beans.is_empty(): failures.append("031 opened Huarong retreat should expose retreat beans") else: _check_camp_conversation_effect(failures, open_retreat_beans, "bean", 1) if not _find_camp_conversation( huarong_opened_state.get_briefing().get("camp_conversations", []), "huarong_held_line_medicine" ).is_empty(): failures.append("031 opened Huarong retreat should not expose held line medicine") _check_shop_item_visibility(failures, huarong_opened_state, "war_drum", true, "031 opened Huarong retreat") _check_shop_item_visibility(failures, huarong_opened_state, "imperial_seal", false, "031 opened Huarong retreat") var huarong_old_flags_state = BattleStateScript.new() if not huarong_old_flags_state.load_battle("res://data/scenarios/031_huarong_retreat.json", {}, {}, {"reinforced_chain_line": true, "ordered_night_raid": true}, ["zhang_he"]): failures.append("could not load Huarong old Red Cliffs Fire setup flag data") else: _check_shop_items_unique(failures, huarong_old_flags_state, "031 old Red Cliffs Fire setup flags") _check_shop_item_visibility(failures, huarong_old_flags_state, "war_drum", false, "031 old Red Cliffs Fire setup flags") _check_shop_item_visibility(failures, huarong_old_flags_state, "imperial_seal", false, "031 old Red Cliffs Fire setup flags") if not _find_camp_conversation( huarong_old_flags_state.get_briefing().get("camp_conversations", []), "huarong_held_line_medicine" ).is_empty(): failures.append("031 old Red Cliffs Fire setup flags should not expose held line medicine") if not _find_camp_conversation( huarong_old_flags_state.get_briefing().get("camp_conversations", []), "huarong_open_retreat_beans" ).is_empty(): failures.append("031 old Red Cliffs Fire setup flags should not expose opened retreat beans") var jiangling_rearguard_state = BattleStateScript.new() if not jiangling_rearguard_state.load_battle("res://data/scenarios/032_jiangling_rearguard.json", {}, {}, {}, ["zhang_he"]): failures.append("could not load Jiangling Rearguard data") else: _check_shop_items_unique(failures, jiangling_rearguard_state, "032 base") var jiangling_rearguard_conversations: Array = jiangling_rearguard_state.get_briefing().get("camp_conversations", []) if jiangling_rearguard_conversations.size() != 2: failures.append("032 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, _find_camp_conversation(jiangling_rearguard_conversations, "jiangling_rearguard_council"), "jiangling_rearguard_council", "topic", "") _check_camp_conversation(failures, _find_camp_conversation(jiangling_rearguard_conversations, "jiangling_zhang_he_rear_line"), "jiangling_zhang_he_rear_line", "officer", "zhang_he") var jiangling_rearguard_merchant := jiangling_rearguard_state.get_shop_merchant() if str(jiangling_rearguard_merchant.get("name", "")) != "Jiangling Rear Road Sutler": failures.append("032 shop should expose Jiangling rear road merchant name") if (jiangling_rearguard_merchant.get("lines", []) as Array).size() < 2: failures.append("032 shop should expose merchant flavor lines") if jiangling_rearguard_state.get_unit("zhang_he_ch32").is_empty() or not jiangling_rearguard_state.is_required_deployment("zhang_he_ch32"): failures.append("032 should deploy Zhang He as a required officer") _check_shop_item_visibility(failures, jiangling_rearguard_state, "war_drum", false, "032 base") _check_shop_item_visibility(failures, jiangling_rearguard_state, "imperial_seal", false, "032 base") var jiangling_rearguard_secured_state = BattleStateScript.new() if not jiangling_rearguard_secured_state.load_battle("res://data/scenarios/032_jiangling_rearguard.json", {}, {}, {"secured_huarong_rearguard": true}, ["zhang_he"]): failures.append("could not load Jiangling Rearguard secured Huarong data") else: _check_shop_items_unique(failures, jiangling_rearguard_secured_state, "032 secured Huarong rearguard") var stability_medicine: Dictionary = _find_camp_conversation( jiangling_rearguard_secured_state.get_briefing().get("camp_conversations", []), "jiangling_rearguard_stability_medicine" ) if stability_medicine.is_empty(): failures.append("032 secured Huarong rearguard should expose stability medicine") else: _check_camp_conversation_effect(failures, stability_medicine, "panacea", 1) if not _find_camp_conversation( jiangling_rearguard_secured_state.get_briefing().get("camp_conversations", []), "jiangling_rearguard_forced_march_beans" ).is_empty(): failures.append("032 secured Huarong rearguard should not expose forced march beans") _check_shop_item_visibility(failures, jiangling_rearguard_secured_state, "imperial_seal", true, "032 secured Huarong rearguard") _check_shop_item_visibility(failures, jiangling_rearguard_secured_state, "war_drum", false, "032 secured Huarong rearguard") var jiangling_rearguard_rushed_state = BattleStateScript.new() if not jiangling_rearguard_rushed_state.load_battle("res://data/scenarios/032_jiangling_rearguard.json", {}, {}, {"rushed_xuchang_return": true}, ["zhang_he"]): failures.append("could not load Jiangling Rearguard rushed Xuchang data") else: _check_shop_items_unique(failures, jiangling_rearguard_rushed_state, "032 rushed Xuchang return") var forced_march_beans: Dictionary = _find_camp_conversation( jiangling_rearguard_rushed_state.get_briefing().get("camp_conversations", []), "jiangling_rearguard_forced_march_beans" ) if forced_march_beans.is_empty(): failures.append("032 rushed Xuchang return should expose forced march beans") else: _check_camp_conversation_effect(failures, forced_march_beans, "bean", 1) if not _find_camp_conversation( jiangling_rearguard_rushed_state.get_briefing().get("camp_conversations", []), "jiangling_rearguard_stability_medicine" ).is_empty(): failures.append("032 rushed Xuchang return should not expose stability medicine") _check_shop_item_visibility(failures, jiangling_rearguard_rushed_state, "war_drum", true, "032 rushed Xuchang return") _check_shop_item_visibility(failures, jiangling_rearguard_rushed_state, "imperial_seal", false, "032 rushed Xuchang return") var jiangling_rearguard_old_flags_state = BattleStateScript.new() if not jiangling_rearguard_old_flags_state.load_battle("res://data/scenarios/032_jiangling_rearguard.json", {}, {}, {"held_red_cliffs_line": true, "opened_huarong_retreat": true}, ["zhang_he"]): failures.append("could not load Jiangling Rearguard old Huarong setup flag data") else: _check_shop_items_unique(failures, jiangling_rearguard_old_flags_state, "032 old Huarong setup flags") _check_shop_item_visibility(failures, jiangling_rearguard_old_flags_state, "war_drum", false, "032 old Huarong setup flags") _check_shop_item_visibility(failures, jiangling_rearguard_old_flags_state, "imperial_seal", false, "032 old Huarong setup flags") if not _find_camp_conversation( jiangling_rearguard_old_flags_state.get_briefing().get("camp_conversations", []), "jiangling_rearguard_stability_medicine" ).is_empty(): failures.append("032 old Huarong setup flags should not expose stability medicine") if not _find_camp_conversation( jiangling_rearguard_old_flags_state.get_briefing().get("camp_conversations", []), "jiangling_rearguard_forced_march_beans" ).is_empty(): failures.append("032 old Huarong setup flags should not expose forced march beans") var tong_pass_state = BattleStateScript.new() if not tong_pass_state.load_battle("res://data/scenarios/033_tong_pass_vanguard.json", {}, {}, {}, ["zhang_he"]): failures.append("could not load Tong Pass Vanguard data") else: _check_shop_items_unique(failures, tong_pass_state, "033 base") var tong_pass_conversations: Array = tong_pass_state.get_briefing().get("camp_conversations", []) if tong_pass_conversations.size() != 2: failures.append("033 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, _find_camp_conversation(tong_pass_conversations, "tong_pass_vanguard_council"), "tong_pass_vanguard_council", "topic", "") _check_camp_conversation(failures, _find_camp_conversation(tong_pass_conversations, "tong_pass_zhang_he_pass_road"), "tong_pass_zhang_he_pass_road", "officer", "zhang_he") var tong_pass_merchant := tong_pass_state.get_shop_merchant() if str(tong_pass_merchant.get("name", "")) != "Tong Pass Sutler": failures.append("033 shop should expose Tong Pass merchant name") if (tong_pass_merchant.get("lines", []) as Array).size() < 2: failures.append("033 shop should expose merchant flavor lines") if tong_pass_state.get_unit("zhang_he_ch33").is_empty() or not tong_pass_state.is_required_deployment("zhang_he_ch33"): failures.append("033 should deploy Zhang He as a required officer") _check_shop_item_visibility(failures, tong_pass_state, "war_drum", false, "033 base") _check_shop_item_visibility(failures, tong_pass_state, "imperial_seal", false, "033 base") var tong_pass_stabilized_state = BattleStateScript.new() if not tong_pass_stabilized_state.load_battle("res://data/scenarios/033_tong_pass_vanguard.json", {}, {}, {"stabilized_jiangling_rearguard": true}, ["zhang_he"]): failures.append("could not load Tong Pass stabilized Jiangling data") else: _check_shop_items_unique(failures, tong_pass_stabilized_state, "033 stabilized Jiangling rearguard") var jiangling_supply_medicine: Dictionary = _find_camp_conversation( tong_pass_stabilized_state.get_briefing().get("camp_conversations", []), "tong_pass_jiangling_supply_medicine" ) if jiangling_supply_medicine.is_empty(): failures.append("033 stabilized Jiangling rearguard should expose Jiangling supply medicine") else: _check_camp_conversation_effect(failures, jiangling_supply_medicine, "panacea", 1) if not _find_camp_conversation( tong_pass_stabilized_state.get_briefing().get("camp_conversations", []), "tong_pass_vanguard_rations" ).is_empty(): failures.append("033 stabilized Jiangling rearguard should not expose vanguard rations") _check_shop_item_visibility(failures, tong_pass_stabilized_state, "imperial_seal", true, "033 stabilized Jiangling rearguard") _check_shop_item_visibility(failures, tong_pass_stabilized_state, "war_drum", false, "033 stabilized Jiangling rearguard") var tong_pass_mobilized_state = BattleStateScript.new() if not tong_pass_mobilized_state.load_battle("res://data/scenarios/033_tong_pass_vanguard.json", {}, {}, {"mobilized_tong_pass_vanguard": true}, ["zhang_he"]): failures.append("could not load Tong Pass mobilized vanguard data") else: _check_shop_items_unique(failures, tong_pass_mobilized_state, "033 mobilized Tong Pass vanguard") var vanguard_rations: Dictionary = _find_camp_conversation( tong_pass_mobilized_state.get_briefing().get("camp_conversations", []), "tong_pass_vanguard_rations" ) if vanguard_rations.is_empty(): failures.append("033 mobilized Tong Pass vanguard should expose vanguard rations") else: _check_camp_conversation_effect(failures, vanguard_rations, "bean", 1) if not _find_camp_conversation( tong_pass_mobilized_state.get_briefing().get("camp_conversations", []), "tong_pass_jiangling_supply_medicine" ).is_empty(): failures.append("033 mobilized Tong Pass vanguard should not expose Jiangling supply medicine") _check_shop_item_visibility(failures, tong_pass_mobilized_state, "war_drum", true, "033 mobilized Tong Pass vanguard") _check_shop_item_visibility(failures, tong_pass_mobilized_state, "imperial_seal", false, "033 mobilized Tong Pass vanguard") var tong_pass_old_flags_state = BattleStateScript.new() if not tong_pass_old_flags_state.load_battle("res://data/scenarios/033_tong_pass_vanguard.json", {}, {}, {"secured_huarong_rearguard": true, "rushed_xuchang_return": true}, ["zhang_he"]): failures.append("could not load Tong Pass old Jiangling setup flag data") else: _check_shop_items_unique(failures, tong_pass_old_flags_state, "033 old Jiangling setup flags") _check_shop_item_visibility(failures, tong_pass_old_flags_state, "war_drum", false, "033 old Jiangling setup flags") _check_shop_item_visibility(failures, tong_pass_old_flags_state, "imperial_seal", false, "033 old Jiangling setup flags") if not _find_camp_conversation( tong_pass_old_flags_state.get_briefing().get("camp_conversations", []), "tong_pass_jiangling_supply_medicine" ).is_empty(): failures.append("033 old Jiangling setup flags should not expose Jiangling supply medicine") if not _find_camp_conversation( tong_pass_old_flags_state.get_briefing().get("camp_conversations", []), "tong_pass_vanguard_rations" ).is_empty(): failures.append("033 old Jiangling setup flags should not expose vanguard rations") var ma_chao_counterstroke_state = BattleStateScript.new() if not ma_chao_counterstroke_state.load_battle("res://data/scenarios/034_ma_chao_counterstroke.json", {}, {}, {}, ["zhang_he"]): failures.append("could not load Ma Chao Counterstroke data") else: _check_shop_items_unique(failures, ma_chao_counterstroke_state, "034 base") var ma_chao_conversations: Array = ma_chao_counterstroke_state.get_briefing().get("camp_conversations", []) if ma_chao_conversations.size() != 2: failures.append("034 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, _find_camp_conversation(ma_chao_conversations, "ma_chao_counterstroke_council"), "ma_chao_counterstroke_council", "topic", "") _check_camp_conversation(failures, _find_camp_conversation(ma_chao_conversations, "ma_chao_zhang_he_charge_line"), "ma_chao_zhang_he_charge_line", "officer", "zhang_he") var ma_chao_merchant := ma_chao_counterstroke_state.get_shop_merchant() if str(ma_chao_merchant.get("name", "")) != "Counterstroke Sutler": failures.append("034 shop should expose counterstroke merchant name") if (ma_chao_merchant.get("lines", []) as Array).size() < 2: failures.append("034 shop should expose merchant flavor lines") if ma_chao_counterstroke_state.get_unit("zhang_he_ch34").is_empty() or not ma_chao_counterstroke_state.is_required_deployment("zhang_he_ch34"): failures.append("034 should deploy Zhang He as a required officer") _check_shop_item_visibility(failures, ma_chao_counterstroke_state, "war_drum", false, "034 base") _check_shop_item_visibility(failures, ma_chao_counterstroke_state, "imperial_seal", false, "034 base") var ma_chao_fortified_state = BattleStateScript.new() if not ma_chao_fortified_state.load_battle("res://data/scenarios/034_ma_chao_counterstroke.json", {}, {}, {"fortified_tong_pass": true}, ["zhang_he"]): failures.append("could not load Ma Chao Counterstroke fortified Tong Pass data") else: _check_shop_items_unique(failures, ma_chao_fortified_state, "034 fortified Tong Pass") var fortified_medicine: Dictionary = _find_camp_conversation( ma_chao_fortified_state.get_briefing().get("camp_conversations", []), "ma_chao_counterstroke_fortified_medicine" ) if fortified_medicine.is_empty(): failures.append("034 fortified Tong Pass should expose fortified medicine") else: _check_camp_conversation_effect(failures, fortified_medicine, "panacea", 1) if not _find_camp_conversation( ma_chao_fortified_state.get_briefing().get("camp_conversations", []), "ma_chao_counterstroke_pressed_beans" ).is_empty(): failures.append("034 fortified Tong Pass should not expose pressed beans") _check_shop_item_visibility(failures, ma_chao_fortified_state, "imperial_seal", true, "034 fortified Tong Pass") _check_shop_item_visibility(failures, ma_chao_fortified_state, "war_drum", false, "034 fortified Tong Pass") var ma_chao_pressed_state = BattleStateScript.new() if not ma_chao_pressed_state.load_battle("res://data/scenarios/034_ma_chao_counterstroke.json", {}, {}, {"pressed_ma_chao_vanguard": true}, ["zhang_he"]): failures.append("could not load Ma Chao Counterstroke pressed vanguard data") else: _check_shop_items_unique(failures, ma_chao_pressed_state, "034 pressed Ma Chao vanguard") var pressed_beans: Dictionary = _find_camp_conversation( ma_chao_pressed_state.get_briefing().get("camp_conversations", []), "ma_chao_counterstroke_pressed_beans" ) if pressed_beans.is_empty(): failures.append("034 pressed Ma Chao vanguard should expose pressed beans") else: _check_camp_conversation_effect(failures, pressed_beans, "bean", 1) if not _find_camp_conversation( ma_chao_pressed_state.get_briefing().get("camp_conversations", []), "ma_chao_counterstroke_fortified_medicine" ).is_empty(): failures.append("034 pressed Ma Chao vanguard should not expose fortified medicine") _check_shop_item_visibility(failures, ma_chao_pressed_state, "war_drum", true, "034 pressed Ma Chao vanguard") _check_shop_item_visibility(failures, ma_chao_pressed_state, "imperial_seal", false, "034 pressed Ma Chao vanguard") var ma_chao_old_flags_state = BattleStateScript.new() if not ma_chao_old_flags_state.load_battle("res://data/scenarios/034_ma_chao_counterstroke.json", {}, {}, {"stabilized_jiangling_rearguard": true, "mobilized_tong_pass_vanguard": true}, ["zhang_he"]): failures.append("could not load Ma Chao Counterstroke old Tong Pass setup flag data") else: _check_shop_items_unique(failures, ma_chao_old_flags_state, "034 old Tong Pass setup flags") _check_shop_item_visibility(failures, ma_chao_old_flags_state, "war_drum", false, "034 old Tong Pass setup flags") _check_shop_item_visibility(failures, ma_chao_old_flags_state, "imperial_seal", false, "034 old Tong Pass setup flags") if not _find_camp_conversation( ma_chao_old_flags_state.get_briefing().get("camp_conversations", []), "ma_chao_counterstroke_fortified_medicine" ).is_empty(): failures.append("034 old Tong Pass setup flags should not expose fortified medicine") if not _find_camp_conversation( ma_chao_old_flags_state.get_briefing().get("camp_conversations", []), "ma_chao_counterstroke_pressed_beans" ).is_empty(): failures.append("034 old Tong Pass setup flags should not expose pressed beans") var weishui_camps_state = BattleStateScript.new() if not weishui_camps_state.load_battle("res://data/scenarios/035_weishui_camps.json", {}, {}, {}, ["zhang_he"]): failures.append("could not load Weishui Camps data") else: _check_shop_items_unique(failures, weishui_camps_state, "035 base") var weishui_conversations: Array = weishui_camps_state.get_briefing().get("camp_conversations", []) if weishui_conversations.size() != 2: failures.append("035 base briefing should expose exactly two camp conversations") else: _check_camp_conversation(failures, _find_camp_conversation(weishui_conversations, "weishui_camps_council"), "weishui_camps_council", "topic", "") _check_camp_conversation(failures, _find_camp_conversation(weishui_conversations, "weishui_zhang_he_river_line"), "weishui_zhang_he_river_line", "officer", "zhang_he") var weishui_merchant := weishui_camps_state.get_shop_merchant() if str(weishui_merchant.get("name", "")) != "Weishui Camp Sutler": failures.append("035 shop should expose Weishui Camp merchant name") if (weishui_merchant.get("lines", []) as Array).size() < 2: failures.append("035 shop should expose merchant flavor lines") if weishui_camps_state.get_unit("zhang_he_ch35").is_empty() or not weishui_camps_state.is_required_deployment("zhang_he_ch35"): failures.append("035 should deploy Zhang He as a required officer") _check_shop_item_visibility(failures, weishui_camps_state, "war_drum", false, "035 base") _check_shop_item_visibility(failures, weishui_camps_state, "imperial_seal", false, "035 base") var weishui_anchored_state = BattleStateScript.new() if not weishui_anchored_state.load_battle("res://data/scenarios/035_weishui_camps.json", {}, {}, {"anchored_weishui_camps": true}, ["zhang_he"]): failures.append("could not load Weishui Camps anchored data") else: _check_shop_items_unique(failures, weishui_anchored_state, "035 anchored Weishui camps") var anchor_medicine: Dictionary = _find_camp_conversation( weishui_anchored_state.get_briefing().get("camp_conversations", []), "weishui_anchor_medicine" ) if anchor_medicine.is_empty(): failures.append("035 anchored Weishui camps should expose anchor medicine") else: _check_camp_conversation_effect(failures, anchor_medicine, "panacea", 1) if not _find_camp_conversation( weishui_anchored_state.get_briefing().get("camp_conversations", []), "weishui_doubt_rations" ).is_empty(): failures.append("035 anchored Weishui camps should not expose doubt rations") _check_shop_item_visibility(failures, weishui_anchored_state, "imperial_seal", true, "035 anchored Weishui camps") _check_shop_item_visibility(failures, weishui_anchored_state, "war_drum", false, "035 anchored Weishui camps") var weishui_doubt_state = BattleStateScript.new() if not weishui_doubt_state.load_battle("res://data/scenarios/035_weishui_camps.json", {}, {}, {"sowed_han_sui_doubt": true}, ["zhang_he"]): failures.append("could not load Weishui Camps Han Sui doubt data") else: _check_shop_items_unique(failures, weishui_doubt_state, "035 sowed Han Sui doubt") var doubt_rations: Dictionary = _find_camp_conversation( weishui_doubt_state.get_briefing().get("camp_conversations", []), "weishui_doubt_rations" ) if doubt_rations.is_empty(): failures.append("035 sowed Han Sui doubt should expose doubt rations") else: _check_camp_conversation_effect(failures, doubt_rations, "bean", 1) if not _find_camp_conversation( weishui_doubt_state.get_briefing().get("camp_conversations", []), "weishui_anchor_medicine" ).is_empty(): failures.append("035 sowed Han Sui doubt should not expose anchor medicine") _check_shop_item_visibility(failures, weishui_doubt_state, "war_drum", true, "035 sowed Han Sui doubt") _check_shop_item_visibility(failures, weishui_doubt_state, "imperial_seal", false, "035 sowed Han Sui doubt") var weishui_old_flags_state = BattleStateScript.new() if not weishui_old_flags_state.load_battle("res://data/scenarios/035_weishui_camps.json", {}, {}, {"fortified_tong_pass": true, "pressed_ma_chao_vanguard": true}, ["zhang_he"]): failures.append("could not load Weishui Camps old Counterstroke setup flag data") else: _check_shop_items_unique(failures, weishui_old_flags_state, "035 old Counterstroke setup flags") _check_shop_item_visibility(failures, weishui_old_flags_state, "war_drum", false, "035 old Counterstroke setup flags") _check_shop_item_visibility(failures, weishui_old_flags_state, "imperial_seal", false, "035 old Counterstroke setup flags") if not _find_camp_conversation( weishui_old_flags_state.get_briefing().get("camp_conversations", []), "weishui_anchor_medicine" ).is_empty(): failures.append("035 old Counterstroke setup flags should not expose anchor medicine") if not _find_camp_conversation( weishui_old_flags_state.get_briefing().get("camp_conversations", []), "weishui_doubt_rations" ).is_empty(): failures.append("035 old Counterstroke setup flags should not expose doubt rations") for item_id in ["bronze_sword", "training_spear", "short_bow", "hand_axe", "iron_armor", "war_drum", "imperial_seal", "panacea"]: var item := state.get_item_def(item_id) if item.is_empty(): failures.append("missing expected item: %s" % item_id) continue var icon_path := str(item.get("icon", "")) _check_image_path(failures, icon_path, "item %s icon" % item_id) _check_alpha_cutout_path(failures, icon_path, "item %s icon cutout" % item_id) func _check_scenario_backgrounds_data(failures: Array[String]) -> void: for expectation in SCENARIO_BACKGROUNDS: var entry: Dictionary = expectation var scenario_path := str(entry.get("scenario_path", "")) var expected_path := str(entry.get("background_path", "")) var label := str(entry.get("label", scenario_path)) var state = BattleStateScript.new() if not state.load_battle(scenario_path): failures.append("could not load scenario for background: %s" % label) continue var background_path: String = state.get_map_background_path() if background_path != expected_path: failures.append("%s should use %s, got %s" % [label, expected_path, background_path]) continue _check_image_path(failures, background_path, label) func _check_scene_texture_loading(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 battle scene state") scene.free() return if scene._current_battle_background_texture() == null: failures.append("battle scene should load a battlefield background texture") var archer: Dictionary = scene.state.get_unit("yellow_turban_3") if scene._load_art_texture(str(archer.get("sprite", ""))) == null: failures.append("battle scene should load archer sprite texture") var panacea: Dictionary = scene.state.get_item_def("panacea") if scene._load_art_texture(str(panacea.get("icon", ""))) == null: failures.append("battle scene should load panacea icon texture") var conversations: Array = scene.state.get_briefing().get("camp_conversations", []) if not conversations.is_empty(): var first_lines: Array = (conversations[0] as Dictionary).get("lines", []) if not first_lines.is_empty(): var first_line: Dictionary = first_lines[0] if scene._load_art_texture(str(first_line.get("portrait", ""))) == null: failures.append("battle scene should load camp conversation speaker portrait") var scene_conversations: Array = scene._camp_conversation_entries() if scene_conversations.size() < 3: failures.append("battle scene should expose camp conversation entries") var strategy := scene._camp_conversation_by_id("cao_cao_strategy") if strategy.is_empty(): failures.append("battle scene should find Cao Cao camp conversation by id") elif not scene._format_camp_conversation_button_text(strategy).contains("조조"): failures.append("camp conversation button text should include label/speaker") elif scene._camp_conversation_preview_portrait_path(strategy).is_empty(): failures.append("camp conversation preview should resolve Cao Cao portrait") elif scene._load_art_texture(scene._camp_conversation_preview_portrait_path(strategy)) == null: failures.append("camp conversation preview portrait should load") elif not scene._format_camp_conversation_row_title(strategy).contains("장수"): failures.append("camp conversation row title should include group") elif not scene._format_camp_conversation_preview_text(strategy).contains("영천"): failures.append("camp conversation preview should include first dialogue text") var cache := scene._camp_conversation_by_id("northern_woods_cache") if cache.is_empty(): failures.append("battle scene should find cache camp conversation by id") elif not scene._format_camp_conversation_button_text(cache).contains("보급 콩"): failures.append("camp conversation button text should preview supply effect") var merchant_notice := scene._format_shop_merchant_notice_text(scene.state.get_shop_merchant()) if not merchant_notice.contains("군막 상인") or not merchant_notice.contains("길가의 물자"): failures.append("shop merchant notice should include name and flavor lines") var camp_overview := scene._format_briefing_camp_overview_text(scene.state.get_briefing(), false) for expected in ["영천, 중평 원년", "군자금 0냥", "군막 회의 · 3건", "보급 대기 1건", "군상 물품 · 2종", "출진 명부 2명 중 2명", "진형 배치 4칸", "병장고 비축"]: if not camp_overview.contains(expected): failures.append("camp overview should include %s: %s" % [expected, camp_overview]) var bean: Dictionary = scene.state.get_item_def("bean") var bean_buy_detail := scene._format_shop_buy_item_detail_text("bean", bean, int(bean.get("price", 0)), 0) if not bean_buy_detail.contains("병력 +20") or not bean_buy_detail.contains("보유 0점"): failures.append("shop buy detail should summarize item effect and owned count") scene.campaign_state.gold = 10 var need_gold_detail := scene._format_shop_buy_item_detail_text("bean", bean, int(bean.get("price", 0)), 0) if not need_gold_detail.contains("부족 40냥"): failures.append("shop buy detail should explain missing gold") var sold_out_detail := scene._format_shop_buy_item_detail_text("bean", bean, int(bean.get("price", 0)), 2, 3, 0) if not sold_out_detail.contains("잔량 0/3") or not sold_out_detail.contains("매진"): failures.append("shop buy detail should explain exhausted finite stock") var sell_detail := scene._format_shop_sell_item_detail_text("bean", bean, 25, 2) if not sell_detail.contains("반값으로 넘김") or not sell_detail.contains("보유 2점"): failures.append("shop sell detail should summarize sale rule and owned count") var bean_action := scene._format_shop_buy_action_text("bean", bean, int(bean.get("price", 0))) if not bean_action.contains("매입 콩") or not bean_action.contains("50냥"): failures.append("shop buy action should be concise and priced") var bean_tooltip := scene._format_shop_item_tooltip_text( scene._format_shop_item_button_text("bean", bean, int(bean.get("price", 0)), 0), bean_buy_detail ) if not bean_tooltip.contains("\n") or not bean_tooltip.contains("보유 0점"): failures.append("shop item tooltip should preserve full item detail") scene.free() _check_scenario_background_texture_loading(failures) func _check_scenario_background_texture_loading(failures: Array[String]) -> void: for expectation in SCENARIO_BACKGROUNDS: var entry: Dictionary = expectation var scenario_path := str(entry.get("scenario_path", "")) var expected_path := str(entry.get("background_path", "")) var label := str(entry.get("label", scenario_path)) var scene = BattleSceneScript.new() if not scene.state.load_battle(scenario_path): failures.append("could not load scene state for background: %s" % label) scene.free() continue var actual_path: String = scene.state.get_map_background_path() if actual_path != expected_path: failures.append("%s scene loaded %s instead of %s" % [label, actual_path, expected_path]) var allow_default := bool(entry.get("allow_default", false)) if not allow_default and actual_path == BattleSceneScript.DEFAULT_BATTLE_BACKGROUND_PATH: failures.append("%s should not rely on the default battlefield background" % label) if scene._current_battle_background_texture() == null: failures.append("battle scene should load texture for %s" % label) scene.free() func _check_hud_focus_text(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 battle scene state for HUD focus text") scene.free() return var cao_cao: Dictionary = scene.state.get_unit("cao_cao") var cao_text := scene._format_unit_focus_text(cao_cao, "Selected") if not cao_text.contains("군령 지휘"): failures.append("Cao Cao HUD focus should describe command role: %s" % cao_text) if not cao_text.contains("행군 4 (보병)") or not cao_text.contains("타격 1-1"): failures.append("Cao Cao HUD focus should clarify move/attack affordance: %s" % cao_text) if not cao_text.contains("군령:") or not cao_text.contains("행군") or not cao_text.contains("타격") or not cao_text.contains("책략") or not cao_text.contains("병장") or not cao_text.contains("대기"): failures.append("Cao Cao HUD focus should summarize available actions: %s" % cao_text) if not cao_text.contains("지형 2,4"): failures.append("Cao Cao HUD focus should include current tile: %s" % cao_text) if not cao_text.contains("행군 1"): failures.append("Cao Cao HUD focus should include terrain move cost: %s" % cao_text) if not cao_text.contains("방비 +0"): failures.append("Cao Cao HUD focus should include terrain defense: %s" % cao_text) var xiahou_dun: Dictionary = scene.state.get_unit("xiahou_dun") xiahou_dun["pos"] = Vector2i(3, 1) var forest_text := scene._unit_current_terrain_text(xiahou_dun) if not forest_text.contains("숲") or not forest_text.contains("행군 3"): failures.append("Cavalry movement should use mounted move_type cost on forest: %s" % forest_text) var archer: Dictionary = scene.state.get_unit("yellow_turban_3") var archer_text := scene._format_unit_focus_text(archer, "Hover") if not archer_text.contains("원거리 견제"): failures.append("Archer HUD focus should describe ranged role: %s" % archer_text) if not archer_text.contains("행군 4 (궁병)") or not archer_text.contains("타격 2-2"): failures.append("Archer HUD focus should clarify ranged attack affordance: %s" % archer_text) if not archer_text.contains("군령: 적군 군기"): failures.append("Enemy archer HUD focus should describe enemy control: %s" % archer_text) if not scene._unit_current_terrain_text(archer).contains("평원"): failures.append("Archer terrain text should reflect its current plain tile") _check_unit_class_mark(failures, scene, "cao_cao", "tactic", "策") _check_unit_class_mark(failures, scene, "xiahou_dun", "cavalry", "騎") _check_unit_class_mark(failures, scene, "yellow_turban_2", "infantry", "步") _check_unit_class_mark(failures, scene, "yellow_turban_3", "ranged", "弓") _check_unit_class_mark(failures, scene, "yellow_turban_1", "heavy", "武") scene.free() func _check_unit_class_mark(failures: Array[String], scene, unit_id: String, expected_family: String, expected_mark: String) -> void: var unit: Dictionary = scene.state.get_unit(unit_id) if unit.is_empty(): failures.append("missing unit for class mark check: %s" % unit_id) return var family := str(scene._unit_class_family(unit)) var mark := str(scene._unit_class_abbrev(unit)) if family != expected_family: failures.append("%s class family should be %s, got %s" % [unit_id, expected_family, family]) if mark != expected_mark: failures.append("%s class mark should be %s, got %s" % [unit_id, expected_mark, mark]) var color: Color = scene._unit_class_badge_color(unit, Color(0.20, 0.42, 0.82)) if color.a <= 0.0: failures.append("%s class badge color should be visible" % unit_id) func _check_ancient_ui_theme(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 battle scene state for ancient UI theme") scene.free() return var briefing_text := scene._format_briefing_objectives() for expected in ["勝令:", "敗兆:", "戰功 죽간:", "凶兆 징후:", "戰利 봉인:"]: if not briefing_text.contains(expected): failures.append("briefing objective parchment should use ancient heading `%s`: %s" % [expected, briefing_text]) var hud_objective := scene._format_objective_hud_text() if not hud_objective.contains("勝令:") or not hud_objective.contains("敗兆:"): failures.append("HUD objective should read as paired edict and omen text: %s" % hud_objective) var mission_text := scene._format_mission_panel_text() for expected in ["勝令", "敗兆", "戰功 죽간", "凶兆 징후"]: if not mission_text.contains(expected): failures.append("mission panel should use old campaign wording `%s`: %s" % [expected, mission_text]) _check_panel_style_fill(failures, scene.briefing_panel, "briefing panel", Color(0.68, 0.50, 0.25, 0.997)) _check_panel_style_frame(failures, scene.briefing_panel, "briefing panel", Color(0.20, 0.11, 0.045, 1.0), 10) _check_panel_style_fill(failures, scene.briefing_objective_panel, "briefing objective edict panel", Color(0.66, 0.52, 0.31, 0.995)) _check_panel_style_frame(failures, scene.briefing_objective_panel, "briefing objective edict panel", Color(0.20, 0.11, 0.045, 1.0), 6) _check_panel_style_fill(failures, scene.briefing_camp_overview_panel, "briefing silk map panel", Color(0.54, 0.38, 0.18, 0.992)) _check_panel_style_frame(failures, scene.briefing_camp_overview_panel, "briefing silk map panel", Color(0.43, 0.25, 0.095, 0.996), 4) _check_panel_style_fill(failures, scene.dialogue_panel, "dialogue panel", Color(0.070, 0.018, 0.010, 0.997)) _check_panel_style_frame(failures, scene.dialogue_panel, "dialogue panel", Color(0.78, 0.58, 0.27, 1.0), 9) _check_panel_style_fill(failures, scene.dialogue_portrait_panel, "dialogue portrait panel", Color(0.035, 0.014, 0.008, 1.0)) _check_panel_style_fill(failures, scene.dialogue_speaker_panel, "dialogue speaker seal panel", Color(0.28, 0.020, 0.014, 1.0)) _check_panel_style_fill(failures, scene.dialogue_text_panel, "dialogue text scroll panel", Color(0.66, 0.52, 0.31, 0.995)) _check_seal_ribbon(failures, scene.briefing_seal_ribbon, "briefing seal ribbon") _check_seal_ribbon(failures, scene.dialogue_seal_ribbon, "dialogue seal ribbon") _check_seal_ribbon(failures, scene.result_seal_ribbon, "result seal ribbon") _check_bamboo_gutter_row(failures, scene.briefing_objective_panel, "briefing objective bamboo gutters") _check_bamboo_gutter_row(failures, scene.dialogue_text_panel, "dialogue scroll bamboo gutters") _check_edict_marker_stack(failures, scene.briefing_objective_panel, "briefing objective edict markers") _check_hanging_tassel(failures, scene.dialogue_left_tassel, "dialogue left tassel") _check_hanging_tassel(failures, scene.dialogue_right_tassel, "dialogue right tassel") _check_dialogue_column_budget(failures, scene) if scene.dialogue_continue_button == null or scene.dialogue_continue_button.text != "뒤 죽간": failures.append("dialogue continue button should use ancient slip text") if scene.dialogue_previous_button == null or scene.dialogue_previous_button.text != "앞 죽간": failures.append("dialogue previous button should use ancient slip text") var localized_lines := scene._normalized_dialogue_lines([{"speaker": "Cao Cao", "display_speaker": "조조", "text": "군령을 전하라."}]) if localized_lines.is_empty() or str((localized_lines[0] as Dictionary).get("speaker", "")) != "조조": failures.append("dialogue normalization should preserve display_speaker for localized names") else: scene.active_dialogue_lines = localized_lines scene.active_dialogue_index = 0 scene._render_dialogue_line() if scene.dialogue_speaker_label == null or scene.dialogue_speaker_label.text != "조조": failures.append("dialogue speaker label should render localized display_speaker") scene._hide_dialogue_panel() var enemy_lines := scene._normalized_dialogue_lines([{"speaker": "Wen Chou", "text": "전진!"}]) if enemy_lines.is_empty() or str((enemy_lines[0] as Dictionary).get("speaker", "")) != "문추": failures.append("dialogue normalization should localize enemy speaker names") var right_lines := scene._normalized_dialogue_lines([{"speaker": "Xiahou Dun", "display_speaker": "하후돈", "side": "right", "text": "우익에서 응하겠습니다."}]) if right_lines.is_empty(): failures.append("dialogue normalization should keep right-side lines") else: scene.active_dialogue_lines = right_lines scene.active_dialogue_index = 0 scene._render_dialogue_line() _check_right_side_dialogue_layout(failures, scene) scene._hide_dialogue_panel() if scene._ui_font(false) == null: failures.append("ancient UI regular font should load from project assets") if scene._ui_font(true) == null: failures.append("ancient UI bold font should load from project assets") if scene.ui_root_control == null or scene.ui_root_control.theme == null or scene.ui_root_control.theme.default_font == null: failures.append("ancient UI root should install the project serif font as its default theme font") if scene._draw_ui_font(false) != scene._ui_font(false): failures.append("map labels should draw with the project serif font") if scene._draw_ui_font(true) != scene._ui_font(true): failures.append("map emphasis labels should draw with the bold project serif font") if scene.dialogue_text_label == null or not scene.dialogue_text_label.has_theme_font_override("font"): failures.append("dialogue text should use the project serif font override") if scene.dialogue_continue_button == null or not scene.dialogue_continue_button.has_theme_font_override("font"): failures.append("dialogue continue button should use the project serif font override") if scene.mission_title_label == null or scene.mission_title_label.text != "軍令 목패": failures.append("mission panel title should use bronze tablet wording") if scene.shop_button == null or scene.shop_button.text != "군상": failures.append("briefing shop button should use sutler wording") if scene.talk_button == null or scene.talk_button.text != "군막 회의": failures.append("briefing talk button should use war council wording") if scene.formation_button == null or scene.formation_button.text != "진형 배치": failures.append("briefing formation button should use battle array wording") if scene.post_move_title_label == null or scene.post_move_title_label.text != "군령 선택": failures.append("post-move command title should use order wording") if scene.post_move_cancel_button == null or scene.post_move_cancel_button.text != "발걸음 거두기": failures.append("post-move cancel button should use recall step wording") if scene.post_move_picker_back_button == null or scene.post_move_picker_back_button.text != "죽간 거두기": failures.append("post-move picker back button should use bamboo-slip wording") if scene.targeting_hint_title_label == null or scene.targeting_hint_title_label.text != "적장 지목": failures.append("targeting hint title should use mark foe wording") if scene.targeting_hint_back_button == null or scene.targeting_hint_back_button.text != "죽간 거두기": failures.append("targeting hint back button should use bamboo-slip wording") scene._update_briefing_camp_overview(scene.state.get_briefing(), false) if scene.briefing_camp_overview_fallback_label == null or scene.briefing_camp_overview_fallback_label.text != "비단 전장도": failures.append("briefing map fallback should read as an old campaign map") _check_panel_style_fill(failures, scene.result_panel, "result panel", Color(0.62, 0.44, 0.20, 0.996)) if not scene._format_victory_result_text().contains("승전 조서"): failures.append("victory result should read like a bronze proclamation") if not scene._format_defeat_result_text().contains("패전 비문"): failures.append("defeat result should read like a memorial tablet") var scroll_rod := scene._make_scroll_rod(120, 4) if scroll_rod.get_child_count() != 5: failures.append("scroll rod ornament should use five visual strips") scroll_rod.free() var seal_ribbon := scene._make_seal_ribbon(120, 18) _check_seal_ribbon(failures, seal_ribbon, "generated seal ribbon") seal_ribbon.free() scene.free() func _check_seal_ribbon(failures: Array[String], ribbon: HBoxContainer, label: String) -> void: if ribbon == null: failures.append("%s missing" % label) return if ribbon.get_child_count() != 5: failures.append("%s should use five clasp/ribbon/cluster segments" % label) return var cluster := ribbon.get_child(2) as HBoxContainer if cluster == null or cluster.get_child_count() != 3: failures.append("%s should include a three-seal center cluster" % label) func _check_bamboo_gutter_row(failures: Array[String], panel: PanelContainer, label: String) -> void: if panel == null or panel.get_child_count() <= 0: failures.append("%s missing row" % label) return var row := panel.get_child(0) as HBoxContainer if row == null or row.get_child_count() < 3: failures.append("%s should use a row with bamboo side gutters" % label) return _check_bamboo_gutter(failures, row.get_child(0), "%s left gutter" % label) _check_bamboo_gutter(failures, row.get_child(row.get_child_count() - 1), "%s right gutter" % label) func _check_bamboo_gutter(failures: Array[String], node: Node, label: String) -> void: var gutter := node as HBoxContainer if gutter == null: failures.append("%s should be a bamboo gutter container" % label) return if gutter.get_child_count() != 3: failures.append("%s should use three bamboo slips" % label) func _check_edict_marker_stack(failures: Array[String], panel: PanelContainer, label: String) -> void: if panel == null or panel.get_child_count() <= 0: failures.append("%s missing row" % label) return var row := panel.get_child(0) as HBoxContainer if row == null or row.get_child_count() < 5: failures.append("%s should include side marker stacks" % label) return _check_marker_stack(failures, row.get_child(1), "%s left stack" % label) _check_marker_stack(failures, row.get_child(row.get_child_count() - 2), "%s right stack" % label) func _check_marker_stack(failures: Array[String], node: Node, label: String) -> void: var stack := node as VBoxContainer if stack == null: failures.append("%s should be a vertical seal stack" % label) return if stack.get_child_count() < 2: failures.append("%s should include multiple seal plaques" % label) func _check_hanging_tassel(failures: Array[String], tassel: VBoxContainer, label: String) -> void: if tassel == null: failures.append("%s missing" % label) return if tassel.get_child_count() != 3: failures.append("%s should use top seal, hanging cords, and bottom seal" % label) return var cords := tassel.get_child(1) as HBoxContainer if cords == null or cords.get_child_count() != 2: failures.append("%s should include two hanging cord strips" % label) func _check_right_side_dialogue_layout(failures: Array[String], scene) -> void: if scene.dialogue_row == null or scene.dialogue_row.get_child_count() != 4: failures.append("right-side dialogue should keep four major columns") return if scene.dialogue_row.get_child(0) != scene.dialogue_left_tassel: failures.append("right-side dialogue should keep the left tassel at the outer edge") if scene.dialogue_row.get_child(1) != scene.dialogue_column: failures.append("right-side dialogue should place speech scroll before the portrait") if scene.dialogue_row.get_child(2) != scene.dialogue_portrait_panel: failures.append("right-side dialogue should place portrait near the right tassel") if scene.dialogue_row.get_child(3) != scene.dialogue_right_tassel: failures.append("right-side dialogue should keep the right tassel at the outer edge") if scene.dialogue_speaker_label == null or scene.dialogue_speaker_label.horizontal_alignment != HORIZONTAL_ALIGNMENT_RIGHT: failures.append("right-side dialogue speaker label should align right") if scene.dialogue_text_label == null or scene.dialogue_text_label.horizontal_alignment != HORIZONTAL_ALIGNMENT_RIGHT: failures.append("right-side dialogue text should align right") func _check_dialogue_column_budget(failures: Array[String], scene) -> void: if scene.dialogue_column == null: failures.append("dialogue column missing") return var total_height := 0.0 var visible_children := 0 for child in scene.dialogue_column.get_children(): var control := child as Control if control == null or not control.visible: continue visible_children += 1 total_height += control.custom_minimum_size.y if visible_children > 1: total_height += float(visible_children - 1) * float(scene.dialogue_column.get_theme_constant("separation")) if total_height > scene.DIALOGUE_COLUMN_SIZE.y: failures.append("dialogue column decorations exceed fixed height: %.1f > %.1f" % [total_height, scene.DIALOGUE_COLUMN_SIZE.y]) func _check_panel_style_fill(failures: Array[String], panel: PanelContainer, label: String, expected: Color) -> void: if panel == null: failures.append("%s missing" % label) return var style := panel.get_theme_stylebox("panel") as StyleBoxFlat if style == null: failures.append("%s should use a flat styled panel" % label) return if not _colors_nearly_equal(style.bg_color, expected): failures.append("%s fill mismatch: %s" % [label, str(style.bg_color)]) func _check_panel_style_frame(failures: Array[String], panel: PanelContainer, label: String, expected_border: Color, expected_width: int) -> void: if panel == null: failures.append("%s missing" % label) return var style := panel.get_theme_stylebox("panel") as StyleBoxFlat if style == null: failures.append("%s should use a flat styled panel" % label) return if not _colors_nearly_equal(style.border_color, expected_border): failures.append("%s border mismatch: %s" % [label, str(style.border_color)]) if style.get_border_width(SIDE_LEFT) != expected_width: failures.append("%s border width mismatch: %d" % [label, style.get_border_width(SIDE_LEFT)]) if style.get_corner_radius(CORNER_TOP_LEFT) != 0: failures.append("%s should use square old-frame corners" % label) func _colors_nearly_equal(left: Color, right: Color) -> bool: return ( absf(left.r - right.r) < 0.01 and absf(left.g - right.g) < 0.01 and absf(left.b - right.b) < 0.01 and absf(left.a - right.a) < 0.01 ) func _check_action_button_disabled_reasons(failures: Array[String]) -> void: var scene = BattleSceneScript.new() scene.wait_button = Button.new() scene.tactic_button = Button.new() scene.item_button = Button.new() scene.equip_button = Button.new() scene.end_turn_button = Button.new() scene.threat_button = Button.new() if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("could not load battle scene state for action button reasons") _free_action_button_test_scene(scene) return scene.battle_started = true var cao_cao: Dictionary = scene.state.get_unit("cao_cao") cao_cao["status_effects"] = [{ "type": "action_lock", "status": "seal", "action": "skill", "remaining_phases": 2 }] scene._update_tactic_button(cao_cao) if scene.tactic_button.text != "책략 - 봉인됨" or not scene.tactic_button.tooltip_text.contains("책략"): failures.append("sealed unit should explain disabled tactic button: %s | %s" % [scene.tactic_button.text, scene.tactic_button.tooltip_text]) cao_cao["status_effects"] = [] cao_cao["acted"] = true scene._update_wait_button(cao_cao) scene._update_tactic_button(cao_cao) scene._update_item_button(cao_cao) scene._update_equip_button(cao_cao) if not scene.wait_button.text.contains("봉인됨"): failures.append("acted unit hold button should show Sealed: %s" % scene.wait_button.text) if not scene.tactic_button.text.contains("봉인됨"): failures.append("acted unit stratagem button should show Sealed: %s" % scene.tactic_button.text) if not scene.item_button.text.contains("봉인됨"): failures.append("acted unit supply button should show Sealed: %s" % scene.item_button.text) if not scene.equip_button.text.contains("봉인됨"): failures.append("acted unit arms button should show Sealed: %s" % scene.equip_button.text) cao_cao["acted"] = false cao_cao["moved"] = true scene._update_equip_button(cao_cao) if scene.equip_button.text != "병장 - 행군 후" or not scene.equip_button.tooltip_text.contains("행군 전"): failures.append("moved unit should explain disabled equip button: %s | %s" % [scene.equip_button.text, scene.equip_button.tooltip_text]) cao_cao["moved"] = false scene.state.set_inventory_snapshot({}) scene._update_item_button(cao_cao) if scene.item_button.text != "도구 - 비어 있음" or not scene.item_button.tooltip_text.contains("도구"): failures.append("empty inventory should explain disabled item button: %s | %s" % [scene.item_button.text, scene.item_button.tooltip_text]) scene.state.current_team = BattleState.TEAM_ENEMY scene._update_end_turn_button() if scene.end_turn_button.text != "군령 종료 - 적군 차례" or not scene.end_turn_button.tooltip_text.contains("아군 군령"): failures.append("enemy phase should explain disabled end turn button: %s | %s" % [scene.end_turn_button.text, scene.end_turn_button.tooltip_text]) scene.state.current_team = BattleState.TEAM_PLAYER scene.battle_started = false scene._update_threat_button() if scene.threat_button.text != "적진 - 전장도 미개봉" or not scene.threat_button.tooltip_text.contains("전장도"): failures.append("inactive battle should explain disabled threat button: %s | %s" % [scene.threat_button.text, scene.threat_button.tooltip_text]) _free_action_button_test_scene(scene) func _free_action_button_test_scene(scene) -> void: for button in [ scene.wait_button, scene.tactic_button, scene.item_button, scene.equip_button, scene.end_turn_button, scene.threat_button ]: if button != null and is_instance_valid(button): button.free() scene.free() func _check_hover_intent_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 battle scene state for hover intent badges") scene.free() return scene.battle_started = true scene.state.select_unit("cao_cao") scene._refresh_ranges() scene.hover_cell = Vector2i(2, 3) var move_badge := scene._target_preview_badge() if str(move_badge.get("text", "")) != "행군" or str(move_badge.get("kind", "")) != "move": failures.append("reachable empty tile should show MOVE badge: %s" % str(move_badge)) scene.hover_cell = Vector2i(1, 4) var select_badge := scene._target_preview_badge() if str(select_badge.get("text", "")) != "선택" or str(select_badge.get("kind", "")) != "select": failures.append("friendly selectable unit should show SELECT badge: %s" % str(select_badge)) scene.hover_cell = Vector2i(7, 2) var attack_badge := scene._target_preview_badge() if attack_badge.is_empty() or str(attack_badge.get("kind", "")) == "move" or str(attack_badge.get("text", "")) == "행군": failures.append("enemy hover should keep attack badge priority: %s" % str(attack_badge)) var cao_cao: Dictionary = scene.state.get_unit("cao_cao") cao_cao["moved"] = true scene._refresh_ranges() scene.hover_cell = Vector2i(2, 3) var moved_badge := scene._target_preview_badge() if str(moved_badge.get("kind", "")) == "move" or str(moved_badge.get("text", "")) == "행군": failures.append("moved unit should not show MOVE badge: %s" % str(moved_badge)) var default_color := scene._target_preview_badge_color("") if scene._target_preview_badge_color("move") == default_color: failures.append("MOVE badge should use a distinct color") if scene._target_preview_badge_color("select") == default_color: failures.append("SELECT badge should use a distinct color") scene.free() func _check_terrain_and_unit_presentation(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 battle scene state for presentation helpers") scene.free() return if scene._terrain_key_at(Vector2i(3, 1)) != "F": failures.append("terrain helper should read forest cells") if not scene._terrain_key_at(Vector2i(-1, 0)).is_empty(): failures.append("terrain helper should return empty outside the board") var background_fill: Color = scene._terrain_fill_color(Vector2i(0, 0), "G", true) var fallback_fill: Color = scene._terrain_fill_color(Vector2i(0, 0), "G", false) if background_fill.a >= 0.20: failures.append("background terrain fill should stay translucent over high-res art") if fallback_fill.a <= background_fill.a: failures.append("fallback terrain fill should be stronger than background fill") if scene._map_grid_color(true).a >= scene._map_grid_color(false).a: failures.append("background grid should be lighter than fallback grid") if scene._terrain_edge_color("F", "G").a <= 0.0: failures.append("forest edge blend should be visible") if not scene._should_draw_terrain_edge("F", "G", Vector2i(1, 0)): failures.append("forest should own forest/plain edge drawing") if scene._should_draw_terrain_edge("G", "F", Vector2i(-1, 0)): failures.append("plain should not redraw forest-owned edge") if scene._should_draw_terrain_edge("F", "F", Vector2i(1, 0)): failures.append("matching terrain should not draw edge blends") if scene._terrain_edge_width("W", "G") <= scene._terrain_edge_width("G", "F"): failures.append("water shoreline edge should be wider than grass blend") if not scene.state.load_battle("res://data/scenarios/002_sishui_gate.json"): failures.append("could not load Sishui Gate for road presentation helpers") else: var road_flags: Dictionary = scene._terrain_connection_flags(Vector2i(2, 4), "R") if not bool(road_flags.get("west", false)) or not bool(road_flags.get("east", false)): failures.append("Sishui Gate road should connect west/east at 3,5: %s" % str(road_flags)) if bool(road_flags.get("north", false)) or bool(road_flags.get("south", false)): failures.append("Sishui Gate road should not connect north/south at 3,5: %s" % str(road_flags)) if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("could not reload opening battle for unit presentation helpers") scene.free() return var generic_enemy: Dictionary = scene.state.get_unit("yellow_turban_2") var named_officer: Dictionary = scene.state.get_unit("cao_cao") if not scene._is_generic_enemy_unit(generic_enemy): failures.append("generic enemy unit should be marked as generic enemy") if scene._is_generic_enemy_unit(named_officer): failures.append("named officer should not be marked as generic enemy") if scene._is_generic_enemy_unit({"team": "enemy", "officer_id": "named_enemy"}): failures.append("enemy with officer_id should not be marked as generic") if scene._is_generic_enemy_unit({"team": "player", "officer_id": ""}): failures.append("player unit should not be marked as generic enemy") var enemy_modulate: Color = scene._unit_sprite_modulate(generic_enemy, false) if enemy_modulate.r < 0.99 or enemy_modulate.g < 0.99 or enemy_modulate.b < 0.99 or enemy_modulate.a < 0.97: failures.append("transparent enemy class sprite should render near its original colors") var fallback_enemy_modulate: Color = scene._unit_sprite_modulate({"team": "enemy", "officer_id": ""}, false) if fallback_enemy_modulate == Color.WHITE or fallback_enemy_modulate.a >= 1.0: failures.append("generic enemy fallback sprite should still receive a distinct semi-real tint") var acted_modulate: Color = scene._unit_sprite_modulate(generic_enemy, true) if acted_modulate.a >= enemy_modulate.a: failures.append("acted generic enemy sprite should still dim below active tint") scene.free() func _check_attack_motion_profiles(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 battle scene state for attack profiles") scene.free() return _check_attack_profile(failures, scene, "cao_cao", Vector2i(1, 3), Vector2i(2, 3), "command_strike") _check_attack_profile(failures, scene, "xiahou_dun", Vector2i(1, 4), Vector2i(2, 4), "cavalry_charge") _check_attack_profile(failures, scene, "yellow_turban_1", Vector2i(7, 2), Vector2i(6, 2), "heavy_strike") _check_attack_profile(failures, scene, "yellow_turban_2", Vector2i(8, 5), Vector2i(7, 5), "infantry_strike") _check_attack_profile(failures, scene, "yellow_turban_3", Vector2i(7, 6), Vector2i(7, 4), "arrow") _check_synthetic_attack_profile(failures, scene, "elite_cavalry", "cavalry_charge") _check_synthetic_attack_profile(failures, scene, "guard_captain", "infantry_strike") _check_synthetic_attack_profile(failures, scene, "marksman", "arrow", 2) _check_synthetic_attack_profile(failures, scene, "military_advisor", "command_strike") _check_synthetic_attack_profile(failures, scene, "commander", "command_strike") _check_synthetic_attack_profile(failures, scene, "champion", "heavy_strike") _check_synthetic_attack_profile(failures, scene, "bandit", "heavy_strike") _check_skill_action_profile(failures, scene, "skill_damage", "tactic_damage", "tactic_flare") _check_skill_action_profile(failures, scene, "skill_heal", "tactic_heal", "healing_sigil") _check_skill_action_profile(failures, scene, "skill_support", "tactic_support", "order_banner") scene._on_unit_action_motion_requested("xiahou_dun", Vector2i(1, 4), Vector2i(2, 4), "attack") _check_stored_attack_motion(failures, scene, "xiahou_dun", "cavalry_charge", "melee", "attack", "charge_dust") scene._on_unit_action_motion_requested("yellow_turban_3", Vector2i(7, 6), Vector2i(7, 4), "attack") _check_stored_attack_motion(failures, scene, "yellow_turban_3", "arrow", "ranged", "attack", "piercing_trace") scene._on_unit_action_motion_requested("cao_cao", Vector2i(1, 3), Vector2i(3, 3), "skill_damage") _check_stored_attack_motion(failures, scene, "cao_cao", "tactic_damage", "tactic", "skill_damage", "tactic_flare") scene._on_unit_action_motion_requested("cao_cao", Vector2i(1, 3), Vector2i(1, 3), "skill_heal") _check_stored_attack_motion(failures, scene, "cao_cao", "tactic_heal", "tactic", "skill_heal", "healing_sigil") _check_attack_vfx_signature(failures, scene, "arrow", "piercing_trace") _check_attack_vfx_signature(failures, scene, "cavalry_charge", "charge_dust") _check_attack_vfx_signature(failures, scene, "infantry_strike", "shield_spark") _check_attack_vfx_signature(failures, scene, "command_strike", "command_ring") _check_attack_vfx_signature(failures, scene, "heavy_strike", "shock_crack") _check_attack_vfx_signature(failures, scene, "tactic_damage", "tactic_flare") _check_attack_vfx_signature(failures, scene, "tactic_heal", "healing_sigil") _check_attack_vfx_signature(failures, scene, "tactic_support", "order_banner") _check_unique_attack_vfx_signatures(failures, scene) if scene._action_motion_duration("cavalry_charge") <= scene._action_motion_duration("slash"): failures.append("cavalry charge animation should last longer than default slash") if scene._action_motion_duration("tactic_damage") <= scene._action_motion_duration("command_strike"): failures.append("tactic cast animation should linger longer than command physical strike") if scene._action_motion_lunge_scale("arrow") >= scene._action_motion_lunge_scale("slash"): failures.append("arrow animation should keep the archer mostly planted") if scene._action_motion_lunge_scale("tactic_damage") >= scene._action_motion_lunge_scale("arrow"): failures.append("tactic animation should keep the caster planted like a spell cast") if scene._action_motion_impact_radius("heavy_strike") <= scene._action_motion_impact_radius("infantry_strike"): failures.append("heavy strike impact radius should exceed infantry strike") if scene._action_motion_impact_radius("cavalry_charge") <= scene._action_motion_impact_radius("arrow"): failures.append("cavalry charge impact radius should exceed arrow impact") if scene._action_motion_sfx("arrow") != "bow_release": failures.append("arrow animation should use bow SFX") if scene._action_motion_sfx("infantry_strike") != "slash": failures.append("infantry animation should use slash SFX") if scene._action_motion_sfx("heavy_strike") != "slash": failures.append("heavy animation should use swing SFX before hit resolution") if scene._action_motion_sfx("tactic_damage") != "skill_cast": failures.append("tactic animation should use skill cast SFX") _check_attack_motion_curves(failures, scene) scene.free() func _check_attack_profile(failures: Array[String], scene, unit_id: String, from_cell: Vector2i, to_cell: Vector2i, expected_profile: String) -> void: var unit: Dictionary = scene.state.get_unit(unit_id) if unit.is_empty(): failures.append("missing unit for attack profile: %s" % unit_id) return var profile: String = scene._unit_action_motion_profile(unit, from_cell, to_cell, "attack") if profile != expected_profile: failures.append("%s attack profile mismatch: expected %s got %s" % [unit_id, expected_profile, profile]) func _check_synthetic_attack_profile(failures: Array[String], scene, class_id: String, expected_profile: String, attack_range := 1) -> void: var unit := { "class_id": class_id, "range": attack_range } var profile: String = scene._unit_action_motion_profile(unit, Vector2i(0, 0), Vector2i(0, max(1, attack_range)), "attack") if profile != expected_profile: failures.append("%s synthetic attack profile mismatch: expected %s got %s" % [class_id, expected_profile, profile]) func _check_skill_action_profile(failures: Array[String], scene, action_kind: String, expected_profile: String, expected_signature: String) -> void: var profile: String = scene._unit_action_motion_profile({"class_id": "hero", "range": 1}, Vector2i(0, 0), Vector2i(0, 0), action_kind) if profile != expected_profile: failures.append("%s action profile mismatch: expected %s got %s" % [action_kind, expected_profile, profile]) if not scene._action_motion_allows_same_cell(profile): failures.append("%s should allow same-cell tactic motion" % profile) if scene._action_motion_vfx_signature(profile) != expected_signature: failures.append("%s action VFX mismatch: expected %s got %s" % [action_kind, expected_signature, scene._action_motion_vfx_signature(profile)]) func _check_attack_vfx_signature(failures: Array[String], scene, profile: String, expected_signature: String) -> void: var signature: String = scene._action_motion_vfx_signature(profile) if signature != expected_signature: failures.append("%s VFX signature mismatch: expected %s got %s" % [profile, expected_signature, signature]) if str(scene._action_motion_impact_cue(profile)).is_empty(): failures.append("%s should expose an impact cue" % profile) if float(scene._action_motion_impact_radius(profile)) <= 0.0: failures.append("%s should expose a positive impact radius" % profile) func _check_unique_attack_vfx_signatures(failures: Array[String], scene) -> void: var seen := {} for profile in ["arrow", "cavalry_charge", "infantry_strike", "command_strike", "heavy_strike"]: var signature: String = scene._action_motion_vfx_signature(profile) if seen.has(signature): failures.append("attack VFX signature should be unique: %s" % signature) seen[signature] = true if scene._action_motion_vfx_signature("slash") == scene._action_motion_vfx_signature("heavy_strike"): failures.append("default slash should not share heavy strike VFX signature") func _check_stored_attack_motion(failures: Array[String], scene, unit_id: String, expected_profile: String, expected_style: String, expected_kind: String, expected_vfx_signature: String) -> void: if not scene.unit_action_motion_by_unit.has(unit_id): failures.append("%s should store an action motion" % unit_id) return var motion: Dictionary = scene.unit_action_motion_by_unit[unit_id] var profile := str(motion.get("profile", "")) var family := str(motion.get("attack_family", "")) var style := str(motion.get("style", "")) var kind := str(motion.get("kind", "")) var signature := str(motion.get("vfx_signature", "")) if profile != expected_profile or family != expected_profile: failures.append("%s stored motion profile mismatch: %s" % [unit_id, str(motion)]) if style != expected_style: failures.append("%s stored motion style mismatch: %s" % [unit_id, str(motion)]) if kind != expected_kind: failures.append("%s stored motion kind mismatch: %s" % [unit_id, str(motion)]) if signature != expected_vfx_signature: failures.append("%s stored motion VFX signature mismatch: %s" % [unit_id, str(motion)]) if str(motion.get("impact_cue", "")).is_empty(): failures.append("%s stored motion should include an impact cue: %s" % [unit_id, str(motion)]) if float(motion.get("impact_radius", 0.0)) <= 0.0: failures.append("%s stored motion should include a positive impact radius: %s" % [unit_id, str(motion)]) if float(motion.get("duration", 0.0)) <= 0.0: failures.append("%s stored motion duration should be positive: %s" % [unit_id, str(motion)]) func _check_attack_motion_curves(failures: Array[String], scene) -> void: var profiles := ["arrow", "cavalry_charge", "infantry_strike", "command_strike", "heavy_strike", "tactic_damage", "tactic_heal", "tactic_support", "slash"] for profile in profiles: for sample in [0.0, 0.25, 0.5, 0.75, 1.0]: var lunge: float = scene._action_motion_lunge_curve(profile, sample) var peak: float = scene._action_motion_effect_peak(profile, sample) if lunge < -0.001 or lunge > 1.001: failures.append("%s lunge curve out of bounds at %.2f: %.3f" % [profile, sample, lunge]) if peak < -0.001 or peak > 1.001: failures.append("%s effect peak out of bounds at %.2f: %.3f" % [profile, sample, peak]) if absf(scene._action_motion_lunge_curve(profile, 0.0)) > 0.001: failures.append("%s lunge should start near zero" % profile) if absf(scene._action_motion_lunge_curve(profile, 1.0)) > 0.001: failures.append("%s lunge should recover near zero" % profile) var arrow_mid: float = scene._action_motion_lunge_curve("arrow", 0.5) * scene._action_motion_lunge_scale("arrow") var infantry_mid: float = scene._action_motion_lunge_curve("infantry_strike", 0.5) * scene._action_motion_lunge_scale("infantry_strike") var cavalry_mid: float = scene._action_motion_lunge_curve("cavalry_charge", 0.5) * scene._action_motion_lunge_scale("cavalry_charge") var heavy_early: float = scene._action_motion_lunge_curve("heavy_strike", 0.25) * scene._action_motion_lunge_scale("heavy_strike") var heavy_late: float = scene._action_motion_lunge_curve("heavy_strike", 0.72) * scene._action_motion_lunge_scale("heavy_strike") var tactic_mid: float = scene._action_motion_lunge_curve("tactic_damage", 0.5) * scene._action_motion_lunge_scale("tactic_damage") if not (arrow_mid < infantry_mid and tactic_mid < infantry_mid): failures.append("lunge curves should keep arrow and tactic planted below infantry: arrow %.3f tactic %.3f infantry %.3f" % [arrow_mid, tactic_mid, infantry_mid]) if cavalry_mid <= infantry_mid: failures.append("cavalry mid-lunge should exceed infantry: cavalry %.3f infantry %.3f" % [cavalry_mid, infantry_mid]) if heavy_late <= heavy_early: failures.append("heavy strike should build toward a later lunge: early %.3f late %.3f" % [heavy_early, heavy_late]) if scene._action_motion_effect_peak("arrow", 0.34) <= scene._action_motion_effect_peak("arrow", 0.82): failures.append("arrow effect should peak early and fade after flight") if scene._action_motion_effect_peak("heavy_strike", 0.62) <= scene._action_motion_effect_peak("heavy_strike", 0.25): failures.append("heavy effect should peak later than its wind-up") if scene._action_motion_effect_peak("heavy_strike", 1.0) > 0.001: failures.append("heavy effect should fade near zero by the end") if scene._action_motion_effect_peak("command_strike", 0.0) <= 0.0: failures.append("command effect should keep a visible ring during wind-up") if scene._action_motion_effect_peak("tactic_heal", 0.0) <= 0.0: failures.append("tactic heal effect should keep a visible sigil during wind-up") func _check_attack_motion_signal(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 battle for attack motion") return var attacker := state.get_unit("cao_cao") var target := state.get_unit("yellow_turban_1") if attacker.is_empty() or target.is_empty(): failures.append("missing attacker or target for attack motion") return attacker["pos"] = Vector2i(6, 2) target["pos"] = Vector2i(7, 2) var motions: Array[String] = [] state.unit_action_motion_requested.connect(func(unit_id: String, from_cell: Vector2i, to_cell: Vector2i, action_kind: String) -> void: motions.append("%s|%s|%d,%d|%d,%d" % [unit_id, action_kind, from_cell.x, from_cell.y, to_cell.x, to_cell.y]) ) if not state.select_unit("cao_cao"): failures.append("could not select Cao Cao for attack motion") return if not state.try_attack_selected("yellow_turban_1"): failures.append("Cao Cao should be able to attack adjacent target") return if not motions.has("cao_cao|attack|6,2|7,2"): failures.append("attack motion signal missing expected Cao Cao attack: %s" % str(motions)) _check_skill_motion_signal(failures, "spark", "skill_damage", Vector2i(3, 3), func(skill_state) -> void: var enemy: Dictionary = skill_state.get_unit("yellow_turban_1") enemy["pos"] = Vector2i(3, 3) ) _check_skill_motion_signal(failures, "mend", "skill_heal", Vector2i(1, 3), func(skill_state) -> void: var caster: Dictionary = skill_state.get_unit("cao_cao") caster["hp"] = max(1, int(caster.get("hp", 1)) - 10) ) _check_skill_motion_signal(failures, "guard_order", "skill_support", Vector2i(1, 3), func(_skill_state) -> void: pass ) func _check_skill_motion_signal(failures: Array[String], skill_id: String, expected_kind: String, target_cell: Vector2i, setup: Callable) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("could not load battle for %s motion" % skill_id) return setup.call(state) var motions: Array[String] = [] state.unit_action_motion_requested.connect(func(unit_id: String, from_cell: Vector2i, to_cell: Vector2i, action_kind: String) -> void: motions.append("%s|%s|%d,%d|%d,%d" % [unit_id, action_kind, from_cell.x, from_cell.y, to_cell.x, to_cell.y]) ) if not state.select_unit("cao_cao"): failures.append("could not select Cao Cao for %s motion" % skill_id) return if not state.try_cast_selected_skill(skill_id, target_cell): failures.append("%s should cast successfully for motion check" % skill_id) return var expected := "cao_cao|%s|1,3|%d,%d" % [expected_kind, target_cell.x, target_cell.y] if not motions.has(expected): failures.append("%s motion signal missing `%s`: %s" % [skill_id, expected, str(motions)]) func _find_camp_conversation(conversations: Array, conversation_id: String) -> Dictionary: for conversation in conversations: if typeof(conversation) == TYPE_DICTIONARY and str(conversation.get("id", "")) == conversation_id: return conversation return {} func _check_shop_item_visibility(failures: Array[String], state, item_id: String, expected_visible: bool, context: String) -> void: var visible: bool = state.get_shop_item_ids().has(item_id) if visible != expected_visible: failures.append("%s shop visibility for %s should be %s" % [context, item_id, str(expected_visible)]) func _check_shop_items_unique(failures: Array[String], state, context: String) -> void: var seen := {} for item_id in state.get_shop_item_ids(): var key := str(item_id) if seen.has(key): failures.append("%s shop should not expose duplicate item id: %s" % [context, key]) seen[key] = true func _check_camp_conversation_effect(failures: Array[String], conversation: Dictionary, expected_item_id: String, expected_count: int) -> void: var effects: Array = conversation.get("effects", []) if effects.is_empty(): failures.append("camp conversation %s should expose effects" % str(conversation.get("id", ""))) return var effect: Dictionary = effects[0] if str(effect.get("type", "")) != "grant_item": failures.append("camp conversation effect should be grant_item: %s" % str(effect)) if str(effect.get("item_id", "")) != expected_item_id: failures.append("camp conversation effect item mismatch: %s" % str(effect)) if int(effect.get("count", 0)) != expected_count: failures.append("camp conversation effect count mismatch: %s" % str(effect)) func _check_camp_conversation(failures: Array[String], conversation: Dictionary, expected_id: String, expected_group: String, expected_officer_id: String) -> void: if str(conversation.get("id", "")) != expected_id: failures.append("camp conversation id mismatch: expected %s got %s" % [expected_id, str(conversation.get("id", ""))]) if str(conversation.get("group", "")) != expected_group: failures.append("camp conversation group mismatch for %s" % expected_id) if str(conversation.get("officer_id", "")) != expected_officer_id: failures.append("camp conversation officer mismatch for %s" % expected_id) if str(conversation.get("label", "")).is_empty(): failures.append("camp conversation label missing for %s" % expected_id) var lines: Array = conversation.get("lines", []) if lines.is_empty(): failures.append("camp conversation lines missing for %s" % expected_id) return var first_line: Dictionary = lines[0] if str(first_line.get("text", "")).is_empty(): failures.append("camp conversation first text missing for %s" % expected_id) if expected_group == "officer" and str(first_line.get("portrait", "")).is_empty(): failures.append("camp conversation officer portrait missing for %s" % expected_id) func _check_unit_sprite_resolution(failures: Array[String], state, unit_id: String, expected_sprite: String, expected_enemy_sprite: bool) -> void: var unit: Dictionary = state.get_unit(unit_id) if unit.is_empty(): failures.append("missing unit for sprite resolution: %s" % unit_id) return var sprite := str(unit.get("sprite", "")) if sprite != expected_sprite: failures.append("%s sprite mismatch: expected %s got %s" % [unit_id, expected_sprite, sprite]) if bool(unit.get("uses_enemy_sprite", false)) != expected_enemy_sprite: failures.append("%s enemy sprite flag mismatch: expected %s got %s" % [unit_id, str(expected_enemy_sprite), str(unit.get("uses_enemy_sprite", false))]) _check_image_path(failures, sprite, "unit %s resolved sprite" % unit_id) func _check_alpha_cutout_path(failures: Array[String], path: String, context: String) -> void: if path.is_empty(): failures.append("%s has an empty image path" % context) return if not path.begins_with("res://"): failures.append("%s must use a res:// path: %s" % [context, path]) return if not FileAccess.file_exists(path): failures.append("%s references missing image: %s" % [context, path]) return var image := Image.new() var err := image.load(path) if err != OK: failures.append("%s references unreadable image: %s" % [context, path]) return if image.get_width() < 512 or image.get_height() < 512: failures.append("%s image should be at least 512x512: %s (%dx%d)" % [context, path, image.get_width(), image.get_height()]) return for corner in [Vector2i(0, 0), Vector2i(image.get_width() - 1, 0), Vector2i(0, image.get_height() - 1), Vector2i(image.get_width() - 1, image.get_height() - 1)]: if image.get_pixelv(corner).a > 0.01: failures.append("%s should have transparent corners: %s" % [context, path]) return var transparent_samples := 0 var total_samples := 0 for y in range(0, image.get_height(), 4): for x in range(0, image.get_width(), 4): total_samples += 1 if image.get_pixel(x, y).a <= 0.01: transparent_samples += 1 if transparent_samples < int(total_samples * 0.20): failures.append("%s should have substantial transparent background: %s (%d/%d sampled pixels)" % [context, path, transparent_samples, total_samples]) func _check_image_path(failures: Array[String], path: String, context: String) -> void: if path.is_empty(): failures.append("%s has an empty image path" % context) return if not path.begins_with("res://"): failures.append("%s must use a res:// path: %s" % [context, path]) return if not FileAccess.file_exists(path): failures.append("%s references missing image: %s" % [context, path]) return var image := Image.new() var err := image.load(path) if err != OK: failures.append("%s references unreadable image: %s" % [context, path]) return if image.get_width() < 512 or image.get_height() < 512: failures.append("%s image should be at least 512x512: %s (%dx%d)" % [context, path, image.get_width(), image.get_height()])