Files
heros/tools/smoke_visual_assets.gd

5999 lines
343 KiB
GDScript

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/yingchuan_opening_battlefield.png",
"label": "001 opening field background",
"allow_default": false
},
{
"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"
},
{
"scenario_path": "res://data/scenarios/014_cangting_pursuit.json",
"background_path": "res://art/backgrounds/cangting_pursuit.png",
"label": "014 Cangting pursuit background"
},
{
"scenario_path": "res://data/scenarios/015_ye_campaign.json",
"background_path": "res://art/backgrounds/ye_outer_defense.png",
"label": "015 Ye outer defense background"
},
{
"scenario_path": "res://data/scenarios/016_ye_siege.json",
"background_path": "res://art/backgrounds/ye_inner_gate.png",
"label": "016 Ye inner gate background"
},
{
"scenario_path": "res://data/scenarios/017_ye_surrender.json",
"background_path": "res://art/backgrounds/ye_palace_road.png",
"label": "017 Ye palace road background"
}
]
func _init() -> void:
var failures: Array[String] = []
_check_battle_visual_data(failures)
_check_generated_toolbar_icons(failures)
_check_generated_panel_textures(failures)
_check_generated_button_textures(failures)
_check_generated_tile_marker_textures(failures)
_check_generated_class_icon_textures(failures)
_check_generated_map_badge_textures(failures)
_check_generated_terrain_textures(failures)
_check_generated_terrain_feature_textures(failures)
_check_scene_texture_loading(failures)
_check_map_overlay_softness(failures)
_check_core_text_visibility(failures)
_check_hud_focus_text(failures)
_check_ancient_ui_theme(failures)
_check_opening_prologue_presentation(failures)
_check_hover_intent_badges(failures)
_check_counter_attack_badge_variants(failures)
_check_hud_command_grid_layout(failures)
_check_battle_unit_list_panel(failures)
_check_action_button_disabled_reasons(failures)
_check_terrain_and_unit_presentation(failures)
_check_objective_and_status_marker_helpers(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() < 4:
failures.append("001 briefing should expose at least four 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")
var villager_report := _find_camp_conversation(conversations, "yingchuan_villager_report")
_check_camp_conversation(failures, villager_report, "yingchuan_villager_report", "topic", "")
if not str(villager_report).contains("영천 백성") or not str(villager_report).contains("마을 우물가"):
failures.append("001 villager report should explain the route and recovery point")
var cache := _find_camp_conversation(conversations, "northern_woods_cache")
_check_camp_conversation(failures, cache, "northern_woods_cache", "topic", "")
_check_camp_conversation_effect(failures, cache, "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")
if state.get_map_background_path() != "res://art/backgrounds/yingchuan_opening_battlefield.png":
failures.append("001 should use a dedicated Yingchuan battlefield background")
var opening_story_images: Array[String] = []
for page in BattleSceneScript.OPENING_PROLOGUE_PAGES:
var image_path := str((page as Dictionary).get("image", ""))
if image_path.is_empty():
failures.append("opening prologue pages should reference generated story artwork")
continue
if opening_story_images.has(image_path):
failures.append("opening prologue pages should use distinct story artwork: %s" % image_path)
opening_story_images.append(image_path)
_check_image_path(failures, image_path, "opening prologue story art")
if opening_story_images.size() != 4:
failures.append("opening prologue should expose four generated story panels")
_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_camp_dialogue: Array = gate_briefing.get("camp_dialogue", [])
if gate_camp_dialogue.size() < 3 or not str(gate_camp_dialogue).contains("사수관") or not str(gate_camp_dialogue).contains("화웅"):
failures.append("002 briefing should expose camp dialogue that bridges Sishui Gate context")
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", "")) != "창정 길목 야상":
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", "")) != "업성 외곽 야상":
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", "")) != "업성 문전 야상":
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", "")) != "업성 궁도 야상":
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", "")) != "북도 야상":
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", "")) != "남피 성문 야상":
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", "")) != "남피 항복 야상":
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", "")) != "발해 길목 야상":
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")
var western_hanzhong_expectations := [
{
"path": "res://data/scenarios/036_weishui_crossing.json",
"label": "036 Weishui crossing",
"merchant": "위수 도하 야상",
"conversations": [
["weishui_crossing_council", "topic", ""],
["weishui_guo_jia_alliance_rift", "officer", "guo_jia"]
],
"imperial_flag": {"secured_weishui_crossing": true},
"war_drum_flag": {"deepened_han_sui_rift": true}
},
{
"path": "res://data/scenarios/037_guanzhong_foothold.json",
"label": "037 Guanzhong foothold",
"merchant": "관중 교두보 야상",
"conversations": [
["guanzhong_foothold_council", "topic", ""],
["guanzhong_zhang_he_rearguard", "officer", "zhang_he"]
],
"imperial_flag": {"secured_guanzhong_foothold": true},
"war_drum_flag": {"split_western_alliance": true}
},
{
"path": "res://data/scenarios/038_ma_chao_retreat.json",
"label": "038 Ma Chao retreat",
"merchant": "서량 퇴로 야상",
"conversations": [
["ma_chao_retreat_council", "topic", ""],
["ma_chao_guo_jia_hanzhong_shadow", "officer", "guo_jia"]
],
"imperial_flag": {"secured_guanzhong_commandery": true},
"war_drum_flag": {"pressed_ma_chao_retreat": true}
},
{
"path": "res://data/scenarios/039_hanzhong_approach.json",
"label": "039 Hanzhong approach",
"merchant": "한중 산문 야상",
"conversations": [
["hanzhong_approach_council", "topic", ""],
["hanzhong_guo_jia_pass_faith", "officer", "guo_jia"]
],
"imperial_flag": {"pacified_guanzhong": true},
"war_drum_flag": {"prepared_hanzhong_front": true}
},
{
"path": "res://data/scenarios/040_hanzhong_gate.json",
"label": "040 Hanzhong gate",
"merchant": "한중 관문 야상",
"conversations": [
["hanzhong_gate_council", "topic", ""],
["hanzhong_xiahou_yuan_beacon", "officer", "xiahou_yuan"]
],
"imperial_flag": {"secured_hanzhong_approach": true},
"war_drum_flag": {"pressed_zhang_lu_gate": true}
},
{
"path": "res://data/scenarios/041_hanzhong_settlement.json",
"label": "041 Hanzhong settlement",
"merchant": "한중 창고 야상",
"conversations": [
["hanzhong_settlement_council", "topic", ""],
["hanzhong_zhang_he_holdouts", "officer", "zhang_he"]
],
"imperial_flag": {"accepted_zhang_lu_submission": true},
"war_drum_flag": {"pacified_hanzhong": true}
}
]
for expectation in western_hanzhong_expectations:
_check_campaign_entry_expectation(failures, expectation)
var ruxu_expectations := [
{
"path": "res://data/scenarios/042_ruxu_advance.json",
"label": "042 Ruxu advance",
"merchant": "유수 강안 야상",
"conversations": [
["ruxu_advance_council", "topic", ""],
["ruxu_guo_jia_tide", "officer", "guo_jia"]
],
"imperial_flag": {"garrisoned_hanzhong": true},
"war_drum_flag": {"pivoted_to_ruxu": true}
},
{
"path": "res://data/scenarios/043_ruxu_river_line.json",
"label": "043 Ruxu river line",
"merchant": "유수 수로 야상",
"conversations": [
["ruxu_river_line_council", "topic", ""],
["ruxu_guo_jia_two_roads", "officer", "guo_jia"]
],
"imperial_flag": {"secured_ruxu_bank": true},
"war_drum_flag": {"pressed_sun_quan_line": true}
},
{
"path": "res://data/scenarios/044_ruxu_crossing.json",
"label": "044 Ruxu crossing",
"merchant": "유수 도하 야상",
"conversations": [
["ruxu_crossing_council", "topic", ""],
["ruxu_guo_jia_crossing_fear", "officer", "guo_jia"]
],
"imperial_flag": {"anchored_ruxu_camps": true},
"war_drum_flag": {"forced_ruxu_crossing": true}
}
]
for expectation in ruxu_expectations:
_check_campaign_entry_expectation(failures, expectation)
var jing_expectations := [
{
"path": "res://data/scenarios/045_fancheng_relief.json",
"label": "045 Fancheng relief",
"merchant": "번성 구원 야상",
"conversations": [
["fancheng_relief_council", "topic", ""],
["fancheng_guo_jia_siege_clock", "officer", "guo_jia"]
],
"imperial_flag": {"secured_ruxu_crossing": true},
"war_drum_flag": {"prepared_fancheng_relief": true}
},
{
"path": "res://data/scenarios/046_jingzhou_supply_line.json",
"label": "046 Jingzhou supply line",
"merchant": "형주 보급로 야상",
"conversations": [
["jingzhou_supply_council", "topic", ""],
["jingzhou_guo_jia_hungry_courage", "officer", "guo_jia"]
],
"imperial_flag": {"reinforced_fancheng": true},
"war_drum_flag": {"struck_jingzhou_supply": true}
},
{
"path": "res://data/scenarios/047_maicheng_pursuit.json",
"label": "047 Maicheng pursuit",
"merchant": "맥성 추격 야상",
"conversations": [
["maicheng_pursuit_council", "topic", ""],
["maicheng_guo_jia_pride", "officer", "guo_jia"]
],
"imperial_flag": {"secured_fancheng_line": true},
"war_drum_flag": {"pursued_guan_yu_rearguard": true}
},
{
"path": "res://data/scenarios/048_jing_province_settlement.json",
"label": "048 Jing province settlement",
"merchant": "형주 평정 야상",
"conversations": [
["jing_settlement_council", "topic", ""],
["jing_guo_jia_bowstring", "officer", "guo_jia"]
],
"imperial_flag": {"coordinated_wu_encirclement": true},
"war_drum_flag": {"secured_xiangfan_front": true}
}
]
for expectation in jing_expectations:
_check_campaign_entry_expectation(failures, expectation)
var wei_succession_expectations := [
{
"path": "res://data/scenarios/049_luoyang_summons.json",
"label": "049 Luoyang summons",
"merchant": "낙양 외조 야상",
"conversations": [
["luoyang_summons_council", "topic", ""],
["luoyang_guo_jia_summons_shadow", "officer", "guo_jia"]
],
"imperial_flag": {"stabilized_jing_province": true},
"war_drum_flag": {"answered_luoyang_summons": true}
},
{
"path": "res://data/scenarios/050_wei_succession.json",
"label": "050 Wei succession",
"merchant": "위왕 계승 야상",
"conversations": [
["wei_succession_council", "topic", ""],
["wei_guo_jia_seal_hands", "officer", "guo_jia"]
],
"imperial_flag": {"secured_luoyang_court": true},
"war_drum_flag": {"prepared_wei_succession": true}
},
{
"path": "res://data/scenarios/051_eastern_front_alarm.json",
"label": "051 Eastern front alarm",
"merchant": "동부 강안 야상",
"conversations": [
["eastern_alarm_council", "topic", ""],
["eastern_guo_jia_river_test", "officer", "guo_jia"]
],
"imperial_flag": {"proclaimed_wei_authority": true},
"war_drum_flag": {"prepared_eastern_front": true}
}
]
for expectation in wei_succession_expectations:
_check_campaign_entry_expectation(failures, expectation)
var jianye_campaign_expectations := [
{
"path": "res://data/scenarios/052_wu_river_line.json",
"label": "052 Wu river line",
"merchant": "유수 수채 야상",
"conversations": [
["wu_river_line_council", "topic", ""],
["wu_river_line_guo_jia_current", "officer", "guo_jia"]
],
"imperial_flag": {"secured_eastern_fleet": true},
"war_drum_flag": {"pressed_wu_river_line": true}
},
{
"path": "res://data/scenarios/053_jianye_approach.json",
"label": "053 Jianye approach",
"merchant": "건업 길목 야상",
"conversations": [
["jianye_approach_council", "topic", ""],
["jianye_guo_jia_approach_road", "officer", "guo_jia"]
],
"imperial_flag": {"anchored_ruxu_fleet": true},
"war_drum_flag": {"prepared_jianye_pressure": true}
},
{
"path": "res://data/scenarios/054_wu_counterstroke.json",
"label": "054 Wu counterstroke",
"merchant": "건업 반격 야상",
"conversations": [
["wu_counterstroke_council", "topic", ""],
["wu_counterstroke_guo_jia_spent_courage", "officer", "guo_jia"]
],
"imperial_flag": {"secured_jianye_approach": true},
"war_drum_flag": {"forced_wu_counterstroke": true}
},
{
"path": "res://data/scenarios/055_jianye_gate.json",
"label": "055 Jianye gate",
"merchant": "건업 외문 야상",
"conversations": [
["jianye_gate_council", "topic", ""],
["jianye_gate_guo_jia_outer_line", "officer", "guo_jia"]
],
"imperial_flag": {"held_lower_yangtze_route": true},
"war_drum_flag": {"pressed_jianye_gate": true}
},
{
"path": "res://data/scenarios/056_jianye_sortie.json",
"label": "056 Jianye sortie",
"merchant": "건업 출격 야상",
"conversations": [
["jianye_sortie_council", "topic", ""],
["jianye_sortie_guo_jia_open_door", "officer", "guo_jia"]
],
"imperial_flag": {"secured_jianye_gate": true},
"war_drum_flag": {"forced_capital_sortie": true}
},
{
"path": "res://data/scenarios/057_inner_jianye.json",
"label": "057 Inner Jianye",
"merchant": "건업 내성 야상",
"conversations": [
["inner_jianye_council", "topic", ""],
["inner_jianye_guo_jia_doors", "officer", "guo_jia"]
],
"imperial_flag": {"contained_capital_sortie": true},
"war_drum_flag": {"pressed_inner_jianye": true}
},
{
"path": "res://data/scenarios/058_jianye_command.json",
"label": "058 Jianye command",
"merchant": "건업 지휘구 야상",
"conversations": [
["jianye_command_council", "topic", ""],
["jianye_command_guo_jia_orders", "officer", "guo_jia"]
],
"imperial_flag": {"secured_inner_jianye": true},
"war_drum_flag": {"forced_wu_last_stand": true}
},
{
"path": "res://data/scenarios/059_wu_surrender.json",
"label": "059 Wu surrender",
"merchant": "건업 항복로 야상",
"conversations": [
["wu_surrender_council", "topic", ""],
["wu_surrender_guo_jia_terms", "officer", "guo_jia"]
],
"imperial_flag": {"secured_jianye_command": true},
"war_drum_flag": {"forced_wu_surrender": true}
},
{
"path": "res://data/scenarios/060_southern_holdouts.json",
"label": "060 Southern holdouts",
"merchant": "남방 운하 야상",
"conversations": [
["southern_holdouts_council", "topic", ""],
["southern_holdouts_guo_jia_rumor", "officer", "guo_jia"]
],
"imperial_flag": {"accepted_wu_surrender": true},
"war_drum_flag": {"pursued_wu_holdouts": true}
},
{
"path": "res://data/scenarios/061_nanhai_route.json",
"label": "061 Nanhai route",
"merchant": "남해 길목 야상",
"conversations": [
["nanhai_route_council", "topic", ""],
["nanhai_route_guo_jia_passage", "officer", "guo_jia"]
],
"imperial_flag": {"settled_southern_holdouts": true},
"war_drum_flag": {"pressed_nanhai_route": true}
},
{
"path": "res://data/scenarios/062_southern_fleet.json",
"label": "062 Southern fleet",
"merchant": "남방 선단 야상",
"conversations": [
["southern_fleet_council", "topic", ""],
["southern_fleet_guo_jia_weather", "officer", "guo_jia"]
],
"imperial_flag": {"secured_nanhai_route": true},
"war_drum_flag": {"pressed_southern_fleet": true}
},
{
"path": "res://data/scenarios/063_hainan_coast.json",
"label": "063 Hainan coast",
"merchant": "해남 해안 야상",
"conversations": [
["hainan_coast_council", "topic", ""],
["hainan_coast_guo_jia_door", "officer", "guo_jia"]
],
"imperial_flag": {"secured_southern_fleet": true},
"war_drum_flag": {"pressed_hainan_coast": true}
},
{
"path": "res://data/scenarios/064_final_harbor.json",
"label": "064 Final harbor",
"merchant": "최종 항구 야상",
"conversations": [
["final_harbor_council", "topic", ""],
["final_harbor_guo_jia_last_shore", "officer", "guo_jia"]
],
"imperial_flag": {"secured_hainan_coast": true},
"war_drum_flag": {"pressed_final_harbor": true}
},
{
"path": "res://data/scenarios/065_last_captains.json",
"label": "065 Last captains",
"merchant": "마지막 대장 야상",
"conversations": [
["last_captains_council", "topic", ""],
["last_captains_guo_jia_names", "officer", "guo_jia"]
],
"imperial_flag": {"secured_final_harbor": true},
"war_drum_flag": {"pursued_last_captains": true}
},
{
"path": "res://data/scenarios/066_sea_exiles.json",
"label": "066 Sea exiles",
"merchant": "외해 망명로 야상",
"conversations": [
["sea_exiles_council", "topic", ""],
["sea_exiles_guo_jia_distance", "officer", "guo_jia"]
],
"imperial_flag": {"settled_last_captains": true},
"war_drum_flag": {"pursued_sea_exiles": true}
},
{
"path": "res://data/scenarios/067_outer_islands.json",
"label": "067 Outer islands",
"merchant": "외섬 상륙 야상",
"conversations": [
["outer_islands_council", "topic", ""],
["outer_islands_guo_jia_carried_road", "officer", "guo_jia"]
],
"imperial_flag": {"settled_sea_exiles": true},
"war_drum_flag": {"pressed_outer_islands": true}
}
]
for expectation in jianye_campaign_expectations:
_check_campaign_entry_expectation(failures, expectation)
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_generated_toolbar_icons(failures: Array[String]) -> void:
var icon_kinds := [
"attack",
"tactic",
"item",
"wait",
"end_turn",
"threat",
"unit_list",
"save",
"load",
"settings",
"campaign",
"shop",
"talk",
"equip",
"roster",
"formation",
"status",
"objective",
"terrain",
"forecast",
"move",
"cancel",
"restart",
"new"
]
for icon_kind in icon_kinds:
_check_toolbar_icon_path(failures, "res://art/ui/icons/toolbar_%s.png" % icon_kind, "toolbar icon %s" % icon_kind)
func _check_generated_panel_textures(failures: Array[String]) -> void:
var panel_keys := ["lacquer", "paper_scroll", "hud_jade", "command_seal", "title_command"]
for panel_key in panel_keys:
_check_panel_texture_path(failures, "res://art/ui/panels/panel_%s.png" % panel_key, "panel texture %s" % panel_key)
func _check_generated_button_textures(failures: Array[String]) -> void:
var button_keys := ["normal", "hover", "pressed", "disabled"]
for button_key in button_keys:
_check_button_texture_path(failures, "res://art/ui/buttons/button_%s.png" % button_key, "button texture %s" % button_key)
for button_key in button_keys:
_check_icon_button_texture_path(failures, "res://art/ui/buttons/button_icon_%s.png" % button_key, "icon button texture %s" % button_key)
func _check_generated_tile_marker_textures(failures: Array[String]) -> void:
var marker_keys := ["move", "attack", "select", "target", "objective", "recover"]
for marker_key in marker_keys:
_check_tile_marker_texture_path(failures, "res://art/ui/tiles/tile_marker_%s.png" % marker_key, "tile marker %s" % marker_key)
func _check_generated_class_icon_textures(failures: Array[String]) -> void:
var icon_keys := ["infantry", "cavalry", "ranged", "tactic", "heavy", "generic"]
for icon_key in icon_keys:
_check_class_icon_texture_path(failures, "res://art/ui/class_icons/class_%s.png" % icon_key, "class icon %s" % icon_key)
func _check_generated_map_badge_textures(failures: Array[String]) -> void:
var badge_keys := ["objective", "supply", "gold", "tactic", "recover", "event"]
for badge_key in badge_keys:
_check_map_badge_texture_path(failures, "res://art/ui/map_badges/map_badge_%s.png" % badge_key, "map badge %s" % badge_key)
func _check_generated_terrain_textures(failures: Array[String]) -> void:
var terrain_keys := ["g", "f", "h", "d", "r", "w", "t", "c"]
for terrain_key in terrain_keys:
_check_terrain_texture_path(failures, "res://art/terrain/terrain_%s.png" % terrain_key, "terrain texture %s" % terrain_key)
func _check_generated_terrain_feature_textures(failures: Array[String]) -> void:
var feature_keys := [
"road_node",
"road_n", "road_s", "road_e", "road_w",
"road_ns", "road_ew",
"road_ne", "road_es", "road_sw", "road_wn",
"road_nse", "road_esw", "road_nsw", "road_new", "road_nesw",
"town", "castle", "water", "wasteland"
]
for feature_key in feature_keys:
_check_terrain_feature_texture_path(failures, "res://art/terrain/features/terrain_feature_%s.png" % feature_key, "terrain feature %s" % feature_key)
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")
if scene._load_tile_marker_texture("move") == null or scene._load_tile_marker_texture("target") == null:
failures.append("battle scene should load generated tactical tile marker textures")
if scene._load_class_icon_texture("infantry") == null or scene._load_class_icon_texture("tactic") == null:
failures.append("battle scene should load generated class icon textures")
if scene._load_map_badge_texture("objective") == null or scene._load_map_badge_texture("recover") == null:
failures.append("battle scene should load generated map badge textures")
if scene._load_terrain_feature_texture("road_ew") == null or scene._load_terrain_feature_texture("town") == null or scene._load_terrain_feature_texture("castle") == null:
failures.append("battle scene should load generated terrain feature overlays")
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("조조 - 군략") or not scene._format_camp_conversation_row_title(strategy).contains("조조"):
failures.append("camp conversation row title should stay compact with label/speaker")
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냥", "군막 회의 · 4건", "보급 대기 1건", "군상 물품 · 2종", "출진 명부 2명 중 2명", "진형 배치 6칸", "비축 물자"]:
if not camp_overview.contains(expected):
failures.append("camp overview should include %s: %s" % [expected, camp_overview])
var briefing_marker_entries := scene._briefing_tactical_marker_entries()
if briefing_marker_entries.size() < 3:
failures.append("briefing should expose image-first tactical marker entries")
var briefing_marker_labels := []
for marker_entry in briefing_marker_entries:
briefing_marker_labels.append(str(marker_entry.get("label", "")))
if scene._load_map_badge_texture(str(marker_entry.get("badge", ""))) == null:
failures.append("briefing tactical marker should resolve generated badge art: %s" % str(marker_entry))
if scene._load_tile_marker_texture(str(marker_entry.get("tile", ""))) == null:
failures.append("briefing tactical marker should resolve generated tile art: %s" % str(marker_entry))
for expected_marker in ["유인선", "북숲 보급고", "마을 보급"]:
if not briefing_marker_labels.has(expected_marker):
failures.append("briefing tactical markers should include %s: %s" % [expected_marker, str(briefing_marker_labels)])
var briefing_allies := scene._briefing_force_preview_allies()
if briefing_allies.size() < 2:
failures.append("briefing force preview should expose Cao Cao and Xiahou Dun")
var briefing_enemy_types := scene._briefing_force_preview_enemy_types()
if briefing_enemy_types.size() < 3:
failures.append("briefing force preview should expose representative enemy unit art")
for preview_unit in briefing_allies + briefing_enemy_types:
var preview_side := "ally" if str(preview_unit.get("team", "")) == BattleStateScript.TEAM_PLAYER else "enemy"
if scene._briefing_force_preview_texture(preview_unit, preview_side) == null:
failures.append("briefing force preview unit should resolve portrait or unit art: %s" % str(preview_unit.get("name", "")))
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 bean_action.contains("매입") or not bean_action.contains("") or not bean_action.contains("50냥"):
failures.append("shop buy action should stay compact 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.campaign_state.load_campaign(BattleSceneScript.CAMPAIGN_PATH)
scene.campaign_state.start_new("001_yellow_turbans")
scene._create_hud()
scene.campaign_state.gold = 320
scene.shop_sell_mode = false
scene._rebuild_shop_menu()
var shop_visible_text := _collect_child_text(scene.shop_list)
var shop_tooltips := _collect_child_tooltips(scene.shop_list)
if shop_visible_text.contains("병력 +20") or shop_visible_text.contains("보유 0점") or shop_visible_text.contains("잔량 0/"):
failures.append("shop rows should keep effects and inventory details in hover text: %s" % shop_visible_text)
if not shop_visible_text.contains("") or not shop_visible_text.contains("50냥"):
failures.append("shop row should still show item name and price compactly: %s" % shop_visible_text)
if not shop_tooltips.contains("병력 +20") or not shop_tooltips.contains("보유 0점"):
failures.append("shop row tooltip should retain full item detail: %s" % shop_tooltips)
if not _has_descendant_texture(scene.shop_list):
failures.append("shop rows should use item icon artwork")
scene._rebuild_talk_menu()
var talk_visible_text := _collect_child_text(scene.talk_list)
var talk_tooltips := _collect_child_tooltips(scene.talk_list)
if not talk_visible_text.contains("조조 - 군략") or not talk_visible_text.contains("장수") or not talk_visible_text.contains("보급 대기"):
failures.append("talk rows should show compact labels and status badges: %s" % talk_visible_text)
if talk_visible_text.contains("첫 행군") or talk_visible_text.contains("보급 콩") or talk_visible_text.contains("수령 가능"):
failures.append("talk rows should keep preview and supply details in hover text: %s" % talk_visible_text)
if not talk_tooltips.contains("영천") or not talk_tooltips.contains("보급 콩") or not talk_tooltips.contains("수령 가능"):
failures.append("talk row tooltip should retain dialogue preview and supply detail: %s" % talk_tooltips)
if not _has_descendant_texture(scene.talk_list):
failures.append("talk rows should use speaker portrait artwork")
scene._rebuild_formation_menu()
var formation_visible_text := _collect_child_text(scene.formation_list)
var formation_tooltips := _collect_child_tooltips(scene.formation_list)
if not formation_visible_text.contains("조조") or not formation_visible_text.contains("배치") or not formation_visible_text.contains("선택"):
failures.append("formation rows should show officer name and compact placement badges: %s" % formation_visible_text)
if formation_visible_text.contains("좌표") or formation_visible_text.contains("전장도의") or formation_visible_text.contains("클릭하면"):
failures.append("formation rows should keep placement instructions in hover text: %s" % formation_visible_text)
if not formation_tooltips.contains("금빛 진형 칸") or not formation_tooltips.contains("현재 위치"):
failures.append("formation row tooltip should retain placement instructions: %s" % formation_tooltips)
if not _has_descendant_texture(scene.formation_list):
failures.append("formation rows should reuse officer portrait artwork")
if scene._format_formation_unit_button_text(scene.state.get_unit("cao_cao")).contains(","):
failures.append("formation unit button text should stay compact")
scene._rebuild_chapter_menu()
var chapter_status_text: String = scene.chapter_status_label.text if scene.chapter_status_label != null else ""
var chapter_visible_text := _collect_child_text(scene.chapter_list)
var chapter_tooltips := _collect_child_tooltips(scene.chapter_list)
if not chapter_status_text.contains("조조전") or not chapter_status_text.contains("진행"):
failures.append("chapter status should stay compact and campaign-aware: %s" % chapter_status_text)
if not chapter_visible_text.contains("난세의 기치") or not chapter_visible_text.contains("영천 소전") or not chapter_visible_text.contains("현 전장"):
failures.append("chapter rows should show compact titles and state badges: %s" % chapter_visible_text)
if chapter_visible_text.contains("[") or chapter_visible_text.contains("현 전장:") or chapter_visible_text.contains("앞 전장을 마치면"):
failures.append("chapter rows should keep dense state detail in hover text: %s" % chapter_visible_text)
if chapter_visible_text.contains("백마 구원전"):
failures.append("chapter rows should not dump unopened future chapter battles: %s" % chapter_visible_text)
if not chapter_tooltips.contains("현 전장: 영천 소전") or not chapter_tooltips.contains("클릭하면 이 전장의 군막으로 이동합니다.") or not chapter_tooltips.contains("앞 전장을 마치면 열립니다."):
failures.append("chapter row tooltips should retain current and locked battle details: %s" % chapter_tooltips)
if not _has_descendant_texture(scene.chapter_list):
failures.append("chapter scenario rows should use battlefield thumbnail artwork")
if not scene.campaign_state.save_manual_game():
failures.append("visual smoke should be able to create a manual checkpoint for save menu checks")
scene._rebuild_save_menu()
var save_status_text: String = scene.save_status_label.text if scene.save_status_label != null else ""
var save_visible_text := _collect_child_text(scene.save_slot_list)
var save_tooltips := _collect_child_tooltips(scene.save_slot_list)
if not save_status_text.contains("전기록") or not save_status_text.contains("군막"):
failures.append("save status should stay compact and mode-aware: %s" % save_status_text)
if not save_visible_text.contains("현재 군막") or not save_visible_text.contains("수동 봉인") or not save_visible_text.contains("진행") or not save_visible_text.contains("열기"):
failures.append("save rows should show compact slot titles, action, and badges: %s" % save_visible_text)
if save_visible_text.contains("현재 봉인:") or save_visible_text.contains("군자금"):
failures.append("save rows should keep checkpoint detail in hover text: %s" % save_visible_text)
if not save_tooltips.contains("현재 봉인:") or not save_tooltips.contains("클릭하면 이 군막 준비를 수동 봉인합니다.") or not save_tooltips.contains("클릭하면 이 수동 봉인을 열어 군막으로 돌아갑니다."):
failures.append("save row tooltips should retain checkpoint detail and action hints: %s" % save_tooltips)
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_map_overlay_softness(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 map overlay softness")
scene.free()
return
var grass_fill: Color = scene._terrain_fill_color(Vector2i(1, 1), "G", true)
if grass_fill.a > 0.070:
failures.append("background-backed grass fill should stay subtle, got %.3f" % grass_fill.a)
var water_fill: Color = scene._terrain_fill_color(Vector2i(1, 1), "W", true)
if water_fill.a > 0.086:
failures.append("background-backed water fill should avoid blocky square color, got %.3f" % water_fill.a)
var grid_color: Color = scene._map_grid_color(true)
if grid_color.a > 0.010:
failures.append("background-backed grid should be nearly invisible, got %.3f" % grid_color.a)
var fallback_grid: Color = scene._map_grid_color(false)
if fallback_grid.a > 0.24:
failures.append("fallback grid should not become a harsh square net, got %.3f" % fallback_grid.a)
var grass_texture: Color = scene._terrain_texture_modulate(Vector2i(3, 4), "G", true)
if grass_texture.a > 0.235:
failures.append("background-backed grass texture should blend into the high-res map, got %.3f" % grass_texture.a)
var town_texture: Color = scene._terrain_texture_modulate(Vector2i(7, 5), "T", true)
if town_texture.a > 0.310:
failures.append("background-backed town texture should signal terrain without tiling loudly, got %.3f" % town_texture.a)
var fallback_texture: Color = scene._terrain_texture_modulate(Vector2i(3, 4), "G", false)
if fallback_texture.a < 0.50:
failures.append("fallback terrain texture should remain readable without a background, got %.3f" % fallback_texture.a)
scene.free()
func _check_core_text_visibility(failures: Array[String]) -> void:
var scene = BattleSceneScript.new()
scene._ready()
_check_readable_text_control(failures, scene.title_caption_label, "title campaign name", 26)
_check_readable_text_control(failures, scene.title_subtitle_label, "title subtitle", 13)
var title_feature: Node = scene.title_screen_root.find_child("TitleStoryFeaturePanel", true, false) if scene.title_screen_root != null else null
if title_feature == null:
failures.append("title text visibility check could not find story feature panel")
else:
_check_readable_text_control(failures, title_feature.find_child("TitleStoryFeatureTitle", true, false) as Control, "title story feature title", 14)
_check_readable_text_control(failures, title_feature.find_child("TitleStoryFeatureBody", true, false) as Control, "title story feature body", 10)
_check_readable_text_control(failures, scene.title_start_button, "title start button", 12)
_check_readable_text_control(failures, scene.title_load_button, "title load button", 12)
_check_readable_text_control(failures, scene.title_settings_button, "title settings button", 12)
scene._show_opening_prologue()
_check_readable_text_control(failures, scene.opening_prologue_title_label, "opening title", 16)
_check_readable_text_control(failures, scene.opening_prologue_body_label, "opening body", 13)
_check_readable_text_control(failures, scene.opening_prologue_step_label, "opening step", 11)
_check_readable_text_control(failures, scene.opening_prologue_next_button, "opening next button", 12)
_check_readable_text_control(failures, scene.opening_prologue_skip_button, "opening skip button", 11)
if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
failures.append("could not load battle scene state for text visibility")
scene.free()
return
scene._hide_opening_prologue()
scene.battle_started = true
scene.briefing_panel.visible = false
scene.result_panel.visible = false
scene._update_hud()
_check_readable_text_control(failures, scene.status_label, "top turn status", 11)
_check_readable_text_control(failures, scene.objective_label, "top objective", 11)
_check_readable_text_control(failures, scene.campaign_status_label, "top campaign status", 11)
var lines := scene._normalized_dialogue_lines([{"speaker": "Cao Cao", "display_speaker": "조조", "text": "영천으로 향한다. 백성이 빠져나갈 길을 연다."}])
scene.active_dialogue_lines = lines
scene.active_dialogue_index = 0
scene._render_dialogue_line()
_check_readable_text_control(failures, scene.dialogue_speaker_label, "dialogue speaker", 14)
_check_readable_text_control(failures, scene.dialogue_text_label, "dialogue body", 13)
scene.free()
func _check_readable_text_control(failures: Array[String], control: Control, context: String, min_font_size: int) -> void:
if control == null:
failures.append("%s text control missing" % context)
return
var text := ""
if control is Label:
text = str((control as Label).text).strip_edges()
elif control is Button:
text = str((control as Button).text).strip_edges()
else:
failures.append("%s should be a label or button for text visibility checks" % context)
return
if text.is_empty():
failures.append("%s should not render empty text" % context)
var font_size := control.get_theme_font_size("font_size")
if font_size < min_font_size:
failures.append("%s text should stay readable: font size %d < %d" % [context, font_size, min_font_size])
var font_color := control.get_theme_color("font_color")
if font_color.a < 0.72:
failures.append("%s text should not be transparent: %s" % [context, str(font_color)])
var outline_size := control.get_theme_constant("outline_size")
if outline_size < 1:
failures.append("%s text should carry a thin outline for image-backed panels" % context)
var outline_color := control.get_theme_color("font_outline_color")
if outline_color.a < 0.25:
failures.append("%s outline should be visible enough: %s" % [context, str(outline_color)])
var shadow_color := control.get_theme_color("font_shadow_color")
if shadow_color.a < 0.20:
failures.append("%s text should keep a soft shadow: %s" % [context, str(shadow_color)])
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")
var cao_summary := scene._format_unit_focus_summary_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("시선 동쪽"):
failures.append("Cao Cao HUD focus should expose facing direction: %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("지형:") or cao_text.contains("2,7"):
failures.append("Cao Cao HUD focus should include terrain effects without raw coordinates: %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)
if not cao_summary.contains("군기: 조조") or cao_summary.contains("병력") or cao_summary.contains("기력") or cao_summary.contains("무위") or cao_summary.contains("지형"):
failures.append("Cao Cao visible HUD summary should leave resource/detail text to bars and tooltip: %s" % cao_summary)
scene._create_hud()
scene.battle_started = true
scene.state.selected_unit_id = "cao_cao"
scene.hover_cell = Vector2i(1, 6)
scene._update_hud()
if scene.hud_unit_hp_bar == null or int(scene.hud_unit_hp_bar.value) != int(cao_cao.get("hp", 0)) or int(scene.hud_unit_hp_bar.max_value) != int(cao_cao.get("max_hp", 1)):
failures.append("selected HUD should expose Cao Cao HP through a resource bar")
if scene.hud_unit_mp_bar == null or int(scene.hud_unit_mp_bar.value) != int(cao_cao.get("mp", 0)) or int(scene.hud_unit_mp_bar.max_value) != int(cao_cao.get("max_mp", 0)):
failures.append("selected HUD should expose Cao Cao MP through a resource bar")
if not scene.hud_unit_hp_bar.tooltip_text.contains("병력") or not scene.hud_unit_hp_bar.tooltip_text.contains("군기: 조조"):
failures.append("selected HP bar tooltip should preserve unit detail: %s" % scene.hud_unit_hp_bar.tooltip_text)
var portrait_team_seal := _find_descendant_by_name(scene.hud_unit_portrait_panel, "HudUnitTeamSeal")
if portrait_team_seal == null:
failures.append("selected unit portrait should overlay a generated team seal")
else:
_check_panel_uses_generated_texture(failures, portrait_team_seal, "selected portrait team seal")
if not _collect_child_text(portrait_team_seal).contains(""):
failures.append("selected portrait team seal should show ally mark: %s" % _collect_child_text(portrait_team_seal))
var portrait_class_icon := _find_descendant_by_name(scene.hud_unit_portrait_panel, "HudUnitClassIcon")
if portrait_class_icon == null or not portrait_class_icon is TextureRect or (portrait_class_icon as TextureRect).texture == null:
failures.append("selected unit portrait should overlay generated class crest artwork")
var portrait_class_panel := _find_descendant_by_name(scene.hud_unit_portrait_panel, "HudUnitClassCrest")
if portrait_class_panel == null:
failures.append("selected unit portrait class crest frame missing")
else:
_check_panel_uses_generated_texture(failures, portrait_class_panel, "selected portrait class crest frame")
if _count_descendants_by_name(scene.hud_unit_info_column, "HudUnitResourceIcon") < 2:
failures.append("selected unit resource rows should use generated HP/MP icons")
if scene.selected_label.text.contains("병력") or scene.selected_label.text.contains("기력"):
failures.append("selected label should not duplicate HP/MP bars: %s" % scene.selected_label.text)
if scene.selected_label.text.contains("군령:"):
failures.append("selected label should move command availability into visual badges: %s" % scene.selected_label.text)
if scene.inventory_label == null or scene.inventory_label.text.contains("군수:") or scene.inventory_label.text.contains("소모") or scene.inventory_label.text.contains("병장"):
failures.append("inventory HUD summary should stay compact and leave details to tooltip: %s" % ("" if scene.inventory_label == null else scene.inventory_label.text))
elif not scene.inventory_label.tooltip_text.contains("군수:"):
failures.append("inventory HUD tooltip should preserve full inventory detail: %s" % scene.inventory_label.tooltip_text)
if scene.hud_unit_badge_row == null or not scene.hud_unit_badge_row.visible:
failures.append("selected unit HUD should expose compact hover badges")
else:
var badge_tooltips := _collect_child_tooltips(scene.hud_unit_badge_row)
if scene.hud_unit_badge_row.get_child_count() < 6:
failures.append("selected unit HUD should show role/facing/action badges, got %d" % scene.hud_unit_badge_row.get_child_count())
if not badge_tooltips.contains("시선 동쪽") or not badge_tooltips.contains("등 뒤"):
failures.append("selected unit facing badge should explain directional defense: %s" % badge_tooltips)
if not badge_tooltips.contains("행군") or not badge_tooltips.contains("타격") or not badge_tooltips.contains("대기"):
failures.append("selected unit action badges should expose available commands through tooltips: %s" % badge_tooltips)
if not _has_descendant_texture(scene.hud_unit_badge_row):
failures.append("selected unit action badges should use icon imagery, not only text")
var badge_visible_text := _collect_child_text(scene.hud_unit_badge_row).strip_edges()
if not badge_visible_text.is_empty():
failures.append("selected unit compact badges should avoid one-letter class/action text and rely on icons: %s" % badge_visible_text)
var xiahou_dun: Dictionary = scene.state.get_unit("xiahou_dun")
xiahou_dun["pos"] = Vector2i(4, 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 expose facing direction: %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 hill tile")
var guard: Dictionary = scene.state.get_unit("yellow_turban_7")
var guard_text := scene._format_unit_focus_text(guard, "Hover")
if not guard_text.contains("수비: 거점 경계 반경 3") or guard_text.contains("20,3"):
failures.append("Guard HUD focus should describe the defended zone without raw coordinates: %s" % guard_text)
if scene._recovery_marker_text(Vector2i(6, 5)) != "+6":
failures.append("Village recovery marker should show healing amount")
if scene._recovery_marker_text(Vector2i(20, 1)) != "+8":
failures.append("Castle recovery marker should show healing amount")
if not scene._recovery_marker_text(Vector2i(1, 6)).is_empty():
failures.append("Plain terrain should not show a recovery marker")
var marker_rect: Rect2 = scene._recovery_marker_rect(Vector2i(6, 5))
if not scene._rect_for_cell(Vector2i(6, 5)).encloses(marker_rect):
failures.append("Recovery terrain marker should stay inside its tile: %s" % str(marker_rect))
if marker_rect.size.x < BattleSceneScript.RECOVERY_TERRAIN_MARKER_SIZE.x or marker_rect.size.y < BattleSceneScript.RECOVERY_TERRAIN_MARKER_SIZE.y:
failures.append("Recovery terrain marker should reserve readable generated badge space: %s" % str(marker_rect.size))
var recovery_amount_rect: Rect2 = scene._recovery_marker_amount_rect(marker_rect)
if not marker_rect.encloses(recovery_amount_rect):
failures.append("Recovery terrain amount badge should sit inside generated marker: %s in %s" % [str(recovery_amount_rect), str(marker_rect)])
var recovery_tile_modulate: Color = scene._recovery_tile_marker_modulate(Vector2i(6, 5))
if recovery_tile_modulate.a < 0.45:
failures.append("Recovery terrain tile marker should be visible over high-res map art: %.3f" % recovery_tile_modulate.a)
var facing_tile_rect: Rect2 = scene._rect_for_cell(Vector2i(1, 6))
var facing_marker_points: PackedVector2Array = scene._unit_facing_marker_points(facing_tile_rect, cao_cao)
var facing_points_inside := facing_marker_points.size() == 3
for point in facing_marker_points:
if not facing_tile_rect.has_point(point):
facing_points_inside = false
if not facing_points_inside:
failures.append("Unit facing marker should stay anchored inside its tile: %s" % str(facing_marker_points))
if scene._unit_board_panel_key() != "hud_jade":
failures.append("Board unit nameplate and HP bar should use generated jade panel art")
if scene._load_art_texture("res://art/ui/panels/panel_%s.png" % scene._unit_board_panel_key()) == null:
failures.append("Board unit nameplate and HP bar should load generated panel texture")
if scene._target_marker_panel_key() != "hud_jade":
failures.append("Battlefield target markers should use generated jade panel art")
if scene._load_art_texture("res://art/ui/panels/panel_%s.png" % scene._target_marker_panel_key()) == null:
failures.append("Battlefield target markers should load generated panel texture")
var board_unit_rect: Rect2 = scene._visual_rect_for_unit(cao_cao)
var nameplate_rect: Rect2 = scene._unit_nameplate_rect(board_unit_rect)
var hp_bar_rect: Rect2 = scene._unit_hp_bar_rect(board_unit_rect)
if not facing_tile_rect.encloses(nameplate_rect):
failures.append("Unit nameplate should stay inside its tile: %s within %s" % [str(nameplate_rect), str(facing_tile_rect)])
if not facing_tile_rect.encloses(hp_bar_rect):
failures.append("Unit HP bar should stay inside its tile: %s within %s" % [str(hp_bar_rect), str(facing_tile_rect)])
if nameplate_rect.intersects(hp_bar_rect):
failures.append("Unit nameplate should not collide with HP bar: %s / %s" % [str(nameplate_rect), str(hp_bar_rect)])
_check_unit_class_family(failures, scene, "cao_cao", "tactic")
_check_unit_class_family(failures, scene, "xiahou_dun", "cavalry")
_check_unit_class_family(failures, scene, "yellow_turban_2", "infantry")
_check_unit_class_family(failures, scene, "yellow_turban_3", "ranged")
_check_unit_class_family(failures, scene, "yellow_turban_1", "heavy")
scene.free()
func _check_unit_class_family(failures: Array[String], scene, unit_id: String, expected_family: String) -> void:
var unit: Dictionary = scene.state.get_unit(unit_id)
if unit.is_empty():
failures.append("missing unit for class family check: %s" % unit_id)
return
var family := str(scene._unit_class_family(unit))
if family != expected_family:
failures.append("%s class family should be %s, got %s" % [unit_id, expected_family, family])
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 silhouette color should be visible" % unit_id)
if scene._load_unit_class_icon_texture(unit) == null:
failures.append("%s should resolve a generated class icon texture" % 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()
scene.briefing_objective_label.text = briefing_text
scene._update_briefing_objective_visibility()
for expected in ["승리:", "패배:", "보상:"]:
if not briefing_text.contains(expected):
failures.append("briefing objective parchment should use Korean heading `%s`: %s" % [expected, briefing_text])
for clutter in ["승리 조건", "패배 조건", "진행", "위험"]:
if briefing_text.contains(clutter):
failures.append("briefing objective parchment should stay concise without `%s`: %s" % [clutter, briefing_text])
var hud_objective := scene._format_objective_hud_text()
var hud_objective_detail := scene._format_objective_hud_detail_text()
if hud_objective.begins_with("목표:") or hud_objective.contains("승리 조건") or hud_objective.contains("패배 조건"):
failures.append("HUD objective should hide labels and keep only a concise current-goal summary: %s" % hud_objective)
if not hud_objective_detail.contains("승리:") or not hud_objective_detail.contains("패배:"):
failures.append("HUD objective tooltip should retain win/loss details: %s" % hud_objective_detail)
scene.battle_started = true
scene.briefing_panel.visible = false
scene.result_panel.visible = false
scene._update_hud()
if scene.status_label == null or scene.status_label.text.contains("군령") or scene.status_label.text.contains(""):
failures.append("top turn status should stay compact and leave full wording to tooltip: %s" % ("" if scene.status_label == null else scene.status_label.text))
elif not scene.status_label.tooltip_text.contains("군령"):
failures.append("top turn status tooltip should retain full turn wording: %s" % scene.status_label.tooltip_text)
if scene.campaign_status_label == null or scene.campaign_status_label.text.contains("전기") or scene.campaign_status_label.text.contains("군자금"):
failures.append("top campaign status should use compact seal/gold counts: %s" % ("" if scene.campaign_status_label == null else scene.campaign_status_label.text))
elif not scene.campaign_status_label.tooltip_text.contains("군자금"):
failures.append("top campaign status tooltip should retain full campaign progress: %s" % scene.campaign_status_label.tooltip_text)
if scene.objective_label == null or scene.objective_label.text.begins_with("목표:"):
failures.append("top objective label should avoid repeated visible heading: %s" % ("" if scene.objective_label == null else scene.objective_label.text))
elif not scene.objective_label.tooltip_text.contains("승리:") or not scene.objective_label.tooltip_text.contains("패배:"):
failures.append("top objective tooltip should retain objective detail: %s" % ("" if scene.objective_label == null else scene.objective_label.tooltip_text))
_check_top_hud_chip(failures, scene, "TopStatusChip", scene.status_label, "군령")
_check_top_hud_chip(failures, scene, "TopObjectiveChip", scene.objective_label, "승리:")
_check_top_hud_chip(failures, scene, "TopCampaignChip", scene.campaign_status_label, "군자금")
var mission_text := scene._format_mission_panel_text()
for expected in ["승리:", "패배:", "진행:", "주의:"]:
if not mission_text.contains(expected):
failures.append("mission panel should use Korean campaign wording `%s`: %s" % [expected, mission_text])
for clutter in ["목표|", "주의|", "현황|", "경고|"]:
if mission_text.contains(clutter):
failures.append("mission panel should avoid old verbose marker `%s`: %s" % [clutter, mission_text])
_check_panel_style_fill(failures, scene.briefing_panel, "briefing panel", Color(0.34, 0.32, 0.27, 0.997))
_check_panel_style_frame(failures, scene.briefing_panel, "briefing panel", Color(0.025, 0.023, 0.020, 1.0), 16)
_check_panel_style_fill(failures, scene.briefing_objective_panel, "briefing objective edict panel", Color(0.56, 0.54, 0.47, 0.998))
_check_panel_style_frame(failures, scene.briefing_objective_panel, "briefing objective edict panel", Color(0.065, 0.060, 0.052, 0.98), 8)
_check_panel_style_fill(failures, scene.briefing_camp_overview_panel, "briefing silk map panel", Color(0.32, 0.31, 0.25, 0.995))
_check_panel_style_frame(failures, scene.briefing_camp_overview_panel, "briefing silk map panel", Color(0.065, 0.060, 0.052, 0.98), 5)
_check_panel_style_fill(failures, scene.dialogue_panel, "dialogue panel", Color(0.045, 0.045, 0.040, 0.982))
_check_panel_style_frame(failures, scene.dialogue_panel, "dialogue panel", Color(0.18, 0.17, 0.14, 1.0), 9)
_check_panel_style_fill(failures, scene.auto_end_turn_prompt_panel, "auto end turn prompt panel", Color(0.050, 0.045, 0.040, 0.992))
_check_panel_style_fill(failures, scene.dialogue_portrait_panel, "dialogue portrait panel", Color(0.025, 0.023, 0.020, 1.0))
_check_panel_style_fill(failures, scene.dialogue_speaker_panel, "dialogue speaker seal panel", Color(0.26, 0.035, 0.030, 0.995))
_check_panel_style_fill(failures, scene.dialogue_text_panel, "dialogue text scroll panel", Color(0.58, 0.55, 0.48, 0.996))
_check_panel_uses_generated_texture(failures, scene.briefing_panel, "briefing generated frame")
_check_panel_uses_generated_texture(failures, scene.briefing_objective_panel, "briefing objective generated frame")
_check_panel_uses_generated_texture(failures, scene.briefing_camp_overview_panel, "briefing map generated frame")
_check_panel_uses_generated_texture(failures, scene.dialogue_panel, "dialogue generated frame")
_check_panel_uses_generated_texture(failures, scene.dialogue_text_panel, "dialogue text generated frame")
_check_panel_uses_generated_texture(failures, scene.auto_end_turn_prompt_panel, "auto end turn generated frame")
if scene.screen_backdrop == null:
failures.append("ancient briefing screen should create an ink-wash backdrop")
else:
if scene.screen_backdrop.color.a < 0.70:
failures.append("ink-wash backdrop should darken modern screen edges: %s" % str(scene.screen_backdrop.color))
scene.briefing_panel.visible = true
scene.result_panel.visible = false
scene._refresh_screen_backdrop_visibility()
if not scene.screen_backdrop.visible:
failures.append("ink-wash backdrop should appear behind briefing documents")
scene.briefing_panel.visible = false
scene._refresh_screen_backdrop_visibility()
var map_lattice_overlay := _find_descendant_by_name(scene.briefing_camp_overview_panel, "MapLatticeOverlay")
if map_lattice_overlay == null:
failures.append("briefing map thumbnail should carry old silk-map lattice overlay")
elif map_lattice_overlay.get_child_count() < 10:
failures.append("briefing map thumbnail should carry wash, lattice, and four seal corners")
_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_bamboo_gutter_row(failures, scene.mission_detail_panel, "mission detail bamboo gutters")
_check_edict_marker_stack(failures, scene.briefing_objective_panel, "briefing objective edict markers")
_check_edict_marker_stack(failures, scene.mission_detail_panel, "mission detail edict markers")
_check_panel_style_fill(failures, scene.cell_info_panel, "HUD terrain info panel", Color(0.045, 0.050, 0.048, 0.90))
_check_panel_style_frame(failures, scene.cell_info_panel, "HUD terrain info panel", Color(0.20, 0.24, 0.21, 0.96), 2)
_check_panel_style_fill(failures, scene.forecast_panel, "HUD forecast info panel", Color(0.045, 0.050, 0.048, 0.90))
_check_panel_style_frame(failures, scene.forecast_panel, "HUD forecast info panel", Color(0.20, 0.24, 0.21, 0.96), 2)
_check_panel_uses_generated_texture(failures, scene.mission_detail_panel, "mission detail generated frame")
_check_panel_uses_generated_texture(failures, scene.cell_info_panel, "HUD terrain generated frame")
_check_panel_uses_generated_texture(failures, scene.forecast_panel, "HUD forecast generated frame")
if scene.cell_info_icon == null:
failures.append("HUD terrain info panel should expose an icon chip")
if scene.forecast_icon == null:
failures.append("HUD forecast info panel should expose an icon chip")
_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 Korean text")
if scene.dialogue_previous_button == null or scene.dialogue_previous_button.text != "이전":
failures.append("dialogue previous button should use Korean text")
if scene.auto_end_turn_prompt_label == null or not _collect_child_text(scene.auto_end_turn_prompt_panel).contains("군령 완료"):
failures.append("auto end turn prompt should show an old command-complete caption")
if scene.auto_end_turn_yes_button == null or scene.auto_end_turn_yes_button.text != "차례 넘김":
failures.append("auto end turn confirm button should use command wording")
if scene.auto_end_turn_no_button == null or scene.auto_end_turn_no_button.text != "머무름":
failures.append("auto end turn decline button should use command wording")
scene._on_log_added("적군 차례가 열렸습니다.")
if scene.objective_notice_panel == null or not scene.objective_notice_panel.visible:
failures.append("enemy turn log should show the command notice panel")
elif scene.objective_notice_label == null or not scene.objective_notice_label.text.contains("적군 군령") or not scene.objective_notice_label.text.contains("적군 차례"):
failures.append("enemy turn notice should use compact command wording: %s" % ("" if scene.objective_notice_label == null else scene.objective_notice_label.text))
if scene.objective_notice_icon_strip == null or not scene.objective_notice_icon_strip.visible or scene.objective_notice_icon_strip.get_child_count() < 2:
failures.append("enemy turn notice should show generated visual command icons")
elif not _has_descendant_texture(scene.objective_notice_icon_strip):
failures.append("enemy turn notice icons should use generated textures")
scene._on_log_added("아군 차례가 열렸습니다.")
if scene.objective_notice_label == null or not scene.objective_notice_label.text.contains("아군 군령") or not scene.objective_notice_label.text.contains("아군 차례"):
failures.append("player turn notice should use compact command wording: %s" % ("" if scene.objective_notice_label == null else scene.objective_notice_label.text))
if scene.objective_notice_icon_strip == null or not scene.objective_notice_icon_strip.visible or scene.objective_notice_icon_strip.get_child_count() < 3:
failures.append("player turn notice should show generated visual command icons")
elif not _has_descendant_texture(scene.objective_notice_icon_strip):
failures.append("player turn notice icons should use generated textures")
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")
if scene.dialogue_text_label == null or scene.dialogue_text_label.text != "「군령을 전하라.」":
failures.append("dialogue text should render as a concise quotation scroll")
scene._hide_dialogue_panel()
var narrator_lines := scene._normalized_dialogue_lines([{"text": "성채 위로 먼지가 올랐다."}])
if narrator_lines.is_empty():
failures.append("dialogue normalization should keep narration lines without a speaker")
else:
var portrait_index_before_narration: int = scene.dialogue_row.get_children().find(scene.dialogue_portrait_panel)
scene.active_dialogue_lines = narrator_lines
scene.active_dialogue_index = 0
scene._render_dialogue_line()
if scene.dialogue_portrait_panel == null or not scene.dialogue_portrait_panel.visible:
failures.append("speakerless narration should keep the dialogue portrait slot visible")
if scene.dialogue_portrait_label == null or scene.dialogue_portrait_label.text != "기록":
failures.append("speakerless narration should show a stable record placeholder")
var portrait_index_after_narration: int = scene.dialogue_row.get_children().find(scene.dialogue_portrait_panel)
if portrait_index_after_narration != portrait_index_before_narration:
failures.append("speakerless narration should not move the portrait slot")
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:
var portrait_index_before: int = scene.dialogue_row.get_children().find(scene.dialogue_portrait_panel)
scene.active_dialogue_lines = right_lines
scene.active_dialogue_index = 0
scene._render_dialogue_line()
_check_right_side_dialogue_layout(failures, scene)
var portrait_index_after: int = scene.dialogue_row.get_children().find(scene.dialogue_portrait_panel)
if portrait_index_after != portrait_index_before:
failures.append("right-side dialogue should not move the portrait panel between lines")
if scene.dialogue_continue_button == null or scene.dialogue_continue_button.text != "다음":
failures.append("dialogue continue button should stay stable even on the final line")
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 clear Korean wording")
if scene.mission_toggle_button == null or scene.mission_toggle_button.text != "" or scene.mission_toggle_button.icon == null or not bool(scene.mission_toggle_button.get_meta("icon_only", false)):
failures.append("mission objective panel should start compact with an icon-only open control")
else:
if not scene.mission_toggle_button.tooltip_text.contains("펼치기"):
failures.append("mission objective icon tooltip should explain opening the panel: %s" % scene.mission_toggle_button.tooltip_text)
scene._on_mission_toggle_pressed()
if scene.mission_detail_panel == null or not scene.mission_detail_panel.visible or not scene.mission_toggle_button.tooltip_text.contains("접기"):
failures.append("mission objective panel should reopen cleanly")
scene._on_mission_toggle_pressed()
if scene.mission_detail_panel == null or scene.mission_detail_panel.visible or not scene.mission_toggle_button.tooltip_text.contains("펼치기"):
failures.append("mission objective panel should collapse cleanly")
if scene.briefing_start_button == null or scene.briefing_start_button.text != "전투 시작":
failures.append("briefing start button should clearly start after briefing")
if scene.briefing_objective_toggle_button == null or scene.briefing_objective_toggle_button.text != "" or scene.briefing_objective_toggle_button.icon == null or not bool(scene.briefing_objective_toggle_button.get_meta("icon_only", false)):
failures.append("briefing objective panel should expose an icon-only close button")
else:
_check_button_uses_generated_toolbar_icon(failures, scene.briefing_objective_toggle_button, "briefing objective toggle")
_check_button_uses_generated_surface(failures, scene.briefing_objective_toggle_button, "briefing objective toggle")
if not scene.briefing_objective_toggle_button.tooltip_text.contains("접기"):
failures.append("briefing objective icon tooltip should explain closing the panel")
scene._set_briefing_objective_collapsed(true)
if scene.briefing_objective_panel == null or scene.briefing_objective_panel.visible or scene.briefing_objective_toggle_button.text != "" or not scene.briefing_objective_toggle_button.tooltip_text.contains("펼치기"):
failures.append("briefing objective panel should collapse cleanly")
scene._set_briefing_objective_collapsed(false)
if scene.briefing_objective_panel == null or not scene.briefing_objective_panel.visible or scene.briefing_objective_toggle_button.text != "" or not scene.briefing_objective_toggle_button.tooltip_text.contains("접기"):
failures.append("briefing objective panel should reopen cleanly")
scene.briefing_panel.visible = true
scene.battle_started = false
var escape_event := InputEventKey.new()
escape_event.keycode = KEY_ESCAPE
scene._handle_key(escape_event)
if not scene.briefing_objective_collapsed or scene.battle_started:
failures.append("briefing Escape should collapse conditions before starting battle")
scene.briefing_panel.visible = false
_check_prep_menu_icon_button(failures, scene.chapter_button, "군기록", "campaign")
_check_prep_menu_icon_button(failures, scene.shop_button, "군상", "shop")
_check_prep_menu_icon_button(failures, scene.talk_button, "군막 회의", "talk")
_check_prep_menu_icon_button(failures, scene.armory_button, "병장고", "equip")
_check_prep_menu_icon_button(failures, scene.roster_button, "출진 명부", "unit_list")
_check_prep_menu_icon_button(failures, scene.formation_button, "진형 배치", "formation")
_check_prep_menu_icon_button(failures, scene.save_button, "전기록", "save")
scene.briefing_panel.visible = true
scene.battle_started = false
_check_prep_menu_button_active_selection(failures, scene)
if scene.shop_buy_button == null or scene.shop_buy_button.text != "사들이기":
failures.append("briefing shop buy button should avoid modern purchase wording")
if scene.shop_sell_button == null or scene.shop_sell_button.text != "내다팔기":
failures.append("briefing shop sell button should avoid modern sale 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")
_check_panel_uses_generated_texture(failures, scene.post_move_menu, "post-move command generated frame")
var post_move_header: PanelContainer = null
var post_move_header_seal: PanelContainer = null
var post_move_header_icon: TextureRect = null
if scene.post_move_menu != null:
post_move_header = scene.post_move_menu.find_child("LocalCommandHeader", true, false) as PanelContainer
post_move_header_seal = scene.post_move_menu.find_child("LocalCommandHeaderSeal", true, false) as PanelContainer
post_move_header_icon = scene.post_move_menu.find_child("LocalCommandHeaderIcon", true, false) as TextureRect
_check_panel_uses_generated_texture(failures, post_move_header, "post-move command generated header")
_check_panel_uses_generated_texture(failures, post_move_header_seal, "post-move command generated header seal")
if post_move_header_icon == null or post_move_header_icon.texture == null:
failures.append("post-move command header should show a generated move icon")
elif post_move_header_icon.texture.get_width() < 128 or post_move_header_icon.texture.get_height() < 128:
failures.append("post-move command header should use high-resolution generated move icon art")
_check_local_command_icon_button(failures, scene.post_move_attack_button, "타격령", "attack")
_check_local_command_icon_button(failures, scene.post_move_tactic_button, "책략첩", "tactic")
_check_local_command_icon_button(failures, scene.post_move_item_button, "보급첩", "item")
_check_local_command_icon_button(failures, scene.post_move_wait_button, "대기령", "wait")
_check_local_command_icon_button(failures, scene.post_move_cancel_button, "발걸음 거두기", "cancel")
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._on_objective_updated("동문에 깃발을 세워라.", "조조가 퇴각하면 패전이다.")
if scene.objective_notice_label == null or not scene.objective_notice_label.text.contains("목표:") or not scene.objective_notice_label.text.contains("주의:"):
failures.append("objective update notice should use Korean annotation wording")
if scene.objective_notice_icon_strip == null or not scene.objective_notice_icon_strip.visible or scene.objective_notice_icon_strip.get_child_count() < 2:
failures.append("objective update notice should show objective and warning icons")
elif not _has_descendant_texture(scene.objective_notice_icon_strip):
failures.append("objective update notice icons should use generated textures")
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")
if scene.briefing_force_preview_overlay == null:
failures.append("briefing map thumbnail should include a force preview overlay")
else:
var ally_icons: Array = scene.briefing_force_preview_overlay.find_children("BriefingForcePreviewAllyIcon*", "", false, false)
var enemy_icons: Array = scene.briefing_force_preview_overlay.find_children("BriefingForcePreviewEnemyIcon*", "", false, false)
if ally_icons.size() < 2:
failures.append("briefing force preview should show allied portrait icons")
if enemy_icons.size() < 3:
failures.append("briefing force preview should show representative enemy unit icons")
if not _has_descendant_texture(scene.briefing_force_preview_overlay):
failures.append("briefing force preview should render generated portrait/unit textures")
if scene.briefing_tactical_marker_list == null:
failures.append("briefing should include a tactical marker card list")
elif scene.briefing_tactical_marker_list.get_child_count() < 3:
failures.append("briefing tactical marker card list should expose three image-first markers")
elif not _has_descendant_texture(scene.briefing_tactical_marker_list):
failures.append("briefing tactical marker cards should render generated badge/tile textures")
scene._apply_panel_style(scene.result_panel, "result_victory_tablet")
_check_panel_style_fill(failures, scene.result_panel, "victory result panel", Color(0.27, 0.34, 0.28, 0.997))
_check_panel_uses_generated_texture(failures, scene.result_panel, "victory result generated frame")
scene._apply_panel_style(scene.result_panel, "result_defeat_tablet")
_check_panel_style_fill(failures, scene.result_panel, "defeat result panel", Color(0.10, 0.035, 0.028, 0.997))
_check_panel_uses_generated_texture(failures, scene.result_panel, "defeat result generated frame")
if not scene._format_victory_result_text().contains("승리"):
failures.append("victory result should use Korean title")
if not scene._format_defeat_result_text().contains("패배"):
failures.append("defeat result should use Korean title")
var scroll_rod := scene._make_scroll_rod(120, 4)
_check_scroll_rod(failures, scroll_rod, "generated scroll rod")
scroll_rod.free()
var tablet_binding := scene._make_tablet_binding(120, 16)
_check_tablet_binding(failures, tablet_binding, "generated tablet binding")
tablet_binding.free()
var ink_rule := scene._make_ink_wash_rule(120, 6)
_check_ink_wash_rule(failures, ink_rule, "generated ink wash rule")
ink_rule.free()
var seal_ribbon := scene._make_seal_ribbon(120, 18)
_check_seal_ribbon(failures, seal_ribbon, "generated seal ribbon")
seal_ribbon.free()
var seal_tile := scene._make_seal_tile("1", 24)
_check_panel_uses_generated_texture(failures, seal_tile, "numbered seal tile")
seal_tile.free()
var seal_tick := scene._make_seal_tick()
_check_panel_uses_generated_texture(failures, seal_tick, "section seal tick")
seal_tick.free()
scene.free()
func _check_opening_prologue_presentation(failures: Array[String]) -> void:
var scene = BattleSceneScript.new()
scene._create_hud()
var caption_panel := _find_descendant_by_name(scene.opening_prologue_root, "OpeningCaptionScroll") as PanelContainer
if caption_panel == null:
failures.append("opening prologue should build a cinematic caption panel")
else:
if caption_panel.position.y < 500.0:
failures.append("opening caption should sit low enough to let generated story art dominate: %s" % str(caption_panel.position))
if caption_panel.custom_minimum_size.y > 150.0:
failures.append("opening caption should stay compact instead of covering the story art: %s" % str(caption_panel.custom_minimum_size))
_check_panel_uses_generated_texture(failures, caption_panel, "opening cinematic caption generated frame")
if str(caption_panel.get_meta("panel_texture_key", "")) != "story_scroll":
failures.append("opening caption should use the generated story scroll surface")
if scene.opening_prologue_next_button == null:
failures.append("opening prologue should expose a next button")
else:
if scene.opening_prologue_next_button.icon == null:
failures.append("opening next button should carry generated icon art")
_check_button_uses_generated_surface(failures, scene.opening_prologue_next_button, "opening next")
if scene.opening_prologue_skip_button == null:
failures.append("opening prologue should expose a skip button")
else:
if scene.opening_prologue_skip_button.icon == null:
failures.append("opening skip button should carry generated icon art")
_check_button_uses_generated_surface(failures, scene.opening_prologue_skip_button, "opening skip")
for index in range(BattleSceneScript.OPENING_PROLOGUE_PAGES.size()):
scene.opening_prologue_index = index
scene._update_opening_prologue_page()
var body_text: String = scene.opening_prologue_body_label.text if scene.opening_prologue_body_label != null else ""
var body_area: Vector2 = scene.opening_prologue_body_label.custom_minimum_size if scene.opening_prologue_body_label != null else Vector2.ZERO
var body_size: int = scene.opening_prologue_body_label.get_theme_font_size("font_size") if scene.opening_prologue_body_label != null else 0
if body_size < 14 or body_size > 18:
failures.append("opening story body font should fit inside its compact caption range, got %d" % body_size)
elif scene._estimated_wrapped_text_height(body_text, body_area.x, body_size) > body_area.y:
failures.append("opening story body should fit without clipping on page %d: size=%d area=%s" % [index + 1, body_size, str(body_area)])
if scene.opening_prologue_next_button != null:
scene.opening_prologue_index = BattleSceneScript.OPENING_PROLOGUE_PAGES.size() - 1
scene._update_opening_prologue_page()
if scene.opening_prologue_next_button.text != "군막으로" or scene.opening_prologue_next_button.icon == null:
failures.append("opening final page should switch the next control into camp-entry mode")
scene.free()
func _check_ink_wash_rule(failures: Array[String], rule: HBoxContainer, label: String) -> void:
if rule == null:
failures.append("%s missing" % label)
return
if rule.get_child_count() != 5:
failures.append("%s should use five ink-and-seal segments" % label)
return
for index in range(rule.get_child_count()):
_check_panel_uses_generated_ornament_texture(failures, rule.get_child(index) as PanelContainer, "%s segment %d" % [label, index])
func _check_scroll_rod(failures: Array[String], scroll_rod: HBoxContainer, label: String) -> void:
if scroll_rod == null:
failures.append("%s missing" % label)
return
if scroll_rod.get_child_count() != 5:
failures.append("%s should use five visual strips" % label)
return
for index in range(scroll_rod.get_child_count()):
_check_panel_uses_generated_ornament_texture(failures, scroll_rod.get_child(index) as PanelContainer, "%s segment %d" % [label, index])
func _check_tablet_binding(failures: Array[String], binding: HBoxContainer, label: String) -> void:
if binding == null:
failures.append("%s missing" % label)
return
if binding.get_child_count() != 7:
failures.append("%s should use seven clasp/strip/knot segments" % label)
return
for index in range(binding.get_child_count()):
if index == 3:
var knot := binding.get_child(index) as HBoxContainer
if knot == null or knot.get_child_count() != 2:
failures.append("%s should include a two-seal center knot" % label)
continue
for knot_index in range(knot.get_child_count()):
_check_panel_uses_generated_ornament_texture(failures, knot.get_child(knot_index) as PanelContainer, "%s knot seal %d" % [label, knot_index])
continue
_check_panel_uses_generated_ornament_texture(failures, binding.get_child(index) as PanelContainer, "%s segment %d" % [label, index])
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)
for index in range(ribbon.get_child_count()):
var panel := ribbon.get_child(index) as PanelContainer
if index == 2:
continue
_check_panel_uses_generated_texture(failures, panel, "%s segment %d" % [label, index])
if cluster != null:
for index in range(cluster.get_child_count()):
_check_panel_uses_generated_texture(failures, cluster.get_child(index) as PanelContainer, "%s center seal %d" % [label, index])
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)
return
for index in range(gutter.get_child_count()):
_check_panel_uses_generated_ornament_texture(failures, gutter.get_child(index) as PanelContainer, "%s slip %d" % [label, index])
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
_check_panel_uses_generated_ornament_texture(failures, tassel.get_child(0) as PanelContainer, "%s top seal" % label)
_check_panel_uses_generated_ornament_texture(failures, tassel.get_child(2) as PanelContainer, "%s bottom seal" % label)
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)
return
for index in range(cords.get_child_count()):
_check_panel_uses_generated_ornament_texture(failures, cords.get_child(index) as PanelContainer, "%s cord %d" % [label, index])
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_portrait_panel:
failures.append("right-side dialogue should keep the portrait fixed beside the left tassel")
if scene.dialogue_row.get_child(2) != scene.dialogue_column:
failures.append("right-side dialogue should keep the speech scroll in its fixed column")
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 stylebox := panel.get_theme_stylebox("panel")
if stylebox is StyleBoxTexture:
_check_panel_style_texture(failures, stylebox as StyleBoxTexture, label)
return
var style := stylebox as StyleBoxFlat
if style == null:
failures.append("%s should use a 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 stylebox := panel.get_theme_stylebox("panel")
if stylebox is StyleBoxTexture:
_check_panel_style_texture(failures, stylebox as StyleBoxTexture, label)
return
var style := stylebox as StyleBoxFlat
if style == null:
failures.append("%s should use a 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)])
var radius := style.get_corner_radius(CORNER_TOP_LEFT)
if radius < 8 or radius > 14:
failures.append("%s should use softened rounded corners, got %d" % [label, radius])
func _check_panel_style_texture(failures: Array[String], style: StyleBoxTexture, label: String) -> void:
if style == null:
failures.append("%s should use a generated texture style" % label)
return
if style.texture == null:
failures.append("%s generated texture style is missing its texture" % label)
return
if style.texture.get_width() < 512 or style.texture.get_height() < 512:
failures.append("%s generated panel texture should keep high-resolution source pixels: %dx%d" % [label, style.texture.get_width(), style.texture.get_height()])
if style.texture_margin_left < 24 or style.texture_margin_top < 24:
failures.append("%s generated panel texture should define a visible frame slice margin" % label)
if style.content_margin_left < 7 or style.content_margin_top < 7:
failures.append("%s generated panel texture should reserve readable content margins" % label)
func _check_panel_uses_generated_texture(failures: Array[String], panel: PanelContainer, label: String) -> void:
if panel == null:
failures.append("%s missing" % label)
return
var stylebox := panel.get_theme_stylebox("panel")
if not (stylebox is StyleBoxTexture):
failures.append("%s should use a generated panel texture" % label)
return
_check_panel_style_texture(failures, stylebox as StyleBoxTexture, label)
func _check_panel_uses_generated_ornament_texture(failures: Array[String], panel: PanelContainer, label: String) -> void:
if panel == null:
failures.append("%s missing" % label)
return
var stylebox := panel.get_theme_stylebox("panel")
if not (stylebox is StyleBoxTexture):
failures.append("%s should use a generated ornament texture" % label)
return
var style := stylebox as StyleBoxTexture
if style.texture == null:
failures.append("%s generated ornament texture is missing its texture" % label)
return
if style.texture.get_width() < 512 or style.texture.get_height() < 512:
failures.append("%s generated ornament texture should keep high-resolution source pixels: %dx%d" % [label, style.texture.get_width(), style.texture.get_height()])
if style.texture_margin_left < 24 or style.texture_margin_top < 24:
failures.append("%s generated ornament texture should define a visible frame slice margin" % 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 _find_descendant_by_name(root: Node, node_name: String) -> Node:
if root == null:
return null
if str(root.name).begins_with(node_name):
return root
for child in root.get_children():
var found := _find_descendant_by_name(child, node_name)
if found != null:
return found
return null
func _count_descendants_by_name(root: Node, node_name: String) -> int:
if root == null:
return 0
var count := 1 if str(root.name).begins_with(node_name) else 0
for child in root.get_children():
count += _count_descendants_by_name(child, node_name)
return count
func _is_descendant_of(node: Node, ancestor: Node) -> bool:
var current := node
while current != null:
if current == ancestor:
return true
current = current.get_parent()
return false
func _check_top_hud_chip(failures: Array[String], scene, chip_name: String, expected_label: Label, expected_tooltip_text: String) -> void:
var chip := _find_descendant_by_name(scene.ui_root_control, chip_name)
if chip == null:
failures.append("top HUD chip missing: %s" % chip_name)
return
if not chip is PanelContainer:
failures.append("top HUD chip should be a framed panel: %s" % chip_name)
if expected_label == null or not _is_descendant_of(expected_label, chip):
failures.append("top HUD chip should contain the existing label var: %s" % chip_name)
if not _has_descendant_texture(chip):
failures.append("top HUD chip should contain an icon texture: %s" % chip_name)
if (chip as Control).custom_minimum_size.x <= 0.0 or (chip as Control).custom_minimum_size.y <= 0.0:
failures.append("top HUD chip should reserve stable space: %s" % chip_name)
var tooltip_text := _collect_child_tooltips(chip)
if not tooltip_text.contains(expected_tooltip_text):
failures.append("top HUD chip tooltip should preserve detail `%s`: %s" % [expected_tooltip_text, tooltip_text])
func _collect_child_tooltips(root: Node) -> String:
if root == null:
return ""
var parts: Array[String] = []
_collect_child_tooltips_into(root, parts)
return "\n".join(parts)
func _collect_child_tooltips_into(root: Node, parts: Array[String]) -> void:
if root == null:
return
if root is Control:
var tooltip := str((root as Control).tooltip_text).strip_edges()
if not tooltip.is_empty():
parts.append(tooltip)
for child in root.get_children():
_collect_child_tooltips_into(child, parts)
func _collect_child_text(root: Node) -> String:
if root == null:
return ""
var parts: Array[String] = []
_collect_child_text_into(root, parts)
return "\n".join(parts)
func _collect_descendant_text_by_name(root: Node, node_name: String) -> String:
if root == null:
return ""
var parts: Array[String] = []
_collect_descendant_text_by_name_into(root, node_name, parts)
return "\n".join(parts)
func _collect_descendant_text_by_name_into(root: Node, node_name: String, parts: Array[String]) -> void:
if root == null:
return
if str(root.name).begins_with(node_name):
if root is Button:
var button_text := str((root as Button).text).strip_edges()
if not button_text.is_empty():
parts.append(button_text)
elif root is Label:
var label_text := str((root as Label).text).strip_edges()
if not label_text.is_empty():
parts.append(label_text)
for child in root.get_children():
_collect_descendant_text_by_name_into(child, node_name, parts)
func _collect_child_text_into(root: Node, parts: Array[String]) -> void:
if root == null:
return
if root is Button:
var button_text := str((root as Button).text).strip_edges()
if not button_text.is_empty():
parts.append(button_text)
elif root is Label:
var label_text := str((root as Label).text).strip_edges()
if not label_text.is_empty():
parts.append(label_text)
for child in root.get_children():
_collect_child_text_into(child, parts)
func _has_descendant_texture(root: Node) -> bool:
if root == null:
return false
if root is TextureRect and (root as TextureRect).texture != null:
return true
for child in root.get_children():
if _has_descendant_texture(child):
return true
return false
func _check_button_uses_generated_toolbar_icon(failures: Array[String], button: Button, context: String) -> void:
if button == null or button.icon == null:
return
if button.icon.get_width() < 128 or button.icon.get_height() < 128:
failures.append("%s should use generated high-resolution toolbar art, got %dx%d" % [context, button.icon.get_width(), button.icon.get_height()])
func _check_button_uses_generated_surface(failures: Array[String], button: Button, context: String) -> void:
if button == null:
failures.append("%s button missing" % context)
return
var stylebox := button.get_theme_stylebox("normal")
if not (stylebox is StyleBoxTexture):
failures.append("%s should use a generated button texture surface" % context)
return
var style := stylebox as StyleBoxTexture
if style.texture == null:
failures.append("%s generated button style is missing its texture" % context)
return
if bool(button.get_meta("icon_only", false)):
if not bool(button.get_meta("generated_icon_button_texture", false)):
failures.append("%s icon-only button should use the generated compact icon surface" % context)
if style.texture.get_width() < 256 or style.texture.get_height() < 256:
failures.append("%s generated icon button texture should keep high-resolution source pixels: %dx%d" % [context, style.texture.get_width(), style.texture.get_height()])
if abs(style.texture.get_width() - style.texture.get_height()) > 4:
failures.append("%s generated icon button texture should be square or near-square: %dx%d" % [context, style.texture.get_width(), style.texture.get_height()])
if style.texture_margin_left < 48 or style.texture_margin_top < 48:
failures.append("%s generated icon button texture should define visible frame slice margins" % context)
if style.content_margin_left < 5 or style.content_margin_top < 5:
failures.append("%s generated icon button texture should reserve icon content margins" % context)
else:
if bool(button.get_meta("generated_icon_button_texture", false)):
failures.append("%s text button should keep the wide generated button surface" % context)
if style.texture.get_width() < 512 or style.texture.get_height() < 192:
failures.append("%s generated button texture should keep high-resolution source pixels: %dx%d" % [context, style.texture.get_width(), style.texture.get_height()])
if style.texture_margin_left < 48 or style.texture_margin_top < 24:
failures.append("%s generated button texture should define visible frame slice margins" % context)
if style.content_margin_left < 8 or style.content_margin_top < 5:
failures.append("%s generated button texture should reserve readable content margins" % context)
func _check_local_command_icon_button(failures: Array[String], button: Button, expected_label: String, expected_icon: String) -> void:
if button == null:
failures.append("local command button missing: %s" % expected_label)
return
if not bool(button.get_meta("icon_only", false)) or button.text != "":
failures.append("local command should be icon-first without visible text: %s text=%s" % [expected_label, button.text])
if button.icon == null or str(button.get_meta("command_icon", "")) != expected_icon:
failures.append("local command should carry an immediate visual icon: %s icon=%s meta=%s" % [expected_label, str(button.icon), str(button.get_meta("command_icon", ""))])
_check_button_uses_generated_toolbar_icon(failures, button, expected_label)
_check_button_uses_generated_surface(failures, button, expected_label)
if str(button.get_meta("command_label", "")) != expected_label:
failures.append("local command should keep old wording in hover metadata: %s meta=%s" % [expected_label, str(button.get_meta("command_label", ""))])
if str(button.tooltip_text).strip_edges().is_empty() or not str(button.tooltip_text).contains(expected_label):
failures.append("local command should expose its function through hover tooltip: %s tooltip=%s" % [expected_label, str(button.tooltip_text)])
if button.custom_minimum_size.x <= 0.0 or button.custom_minimum_size.y <= 0.0:
failures.append("local command should reserve stable icon space: %s" % expected_label)
func _check_hud_command_grid_layout(failures: Array[String]) -> void:
var scene = BattleSceneScript.new()
scene._create_hud()
var command_grid := scene.wait_button.get_parent() as GridContainer
if command_grid == null:
failures.append("unit command buttons should live in a fixed grid")
else:
if command_grid.columns != BattleSceneScript.HUD_COMMAND_GRID_COLUMNS:
failures.append("unit command grid should use %d columns, got %d" % [BattleSceneScript.HUD_COMMAND_GRID_COLUMNS, command_grid.columns])
if command_grid.custom_minimum_size.x < BattleSceneScript.HUD_COMMAND_GRID_SIZE.x or command_grid.custom_minimum_size.y < BattleSceneScript.HUD_COMMAND_GRID_SIZE.y:
failures.append("unit command grid should reserve stable space: %s" % str(command_grid.custom_minimum_size))
var command_icon_expectations := [
{"button": scene.wait_button, "icon": "wait"},
{"button": scene.tactic_button, "icon": "tactic"},
{"button": scene.item_button, "icon": "item"},
{"button": scene.equip_button, "icon": "equip"}
]
for expectation in command_icon_expectations:
var button: Button = expectation["button"]
if button == null:
failures.append("unit command button missing")
continue
if button.get_parent() != command_grid:
failures.append("unit command button should stay in the side command grid: %s" % str(button.name))
if button.custom_minimum_size.x < BattleSceneScript.HUD_COMMAND_BUTTON_SIZE.x or button.custom_minimum_size.y < BattleSceneScript.HUD_COMMAND_BUTTON_SIZE.y:
failures.append("unit command button should reserve fixed icon space: %s" % str(button.custom_minimum_size))
if button.icon == null or str(button.get_meta("command_icon", "")) != str(expectation["icon"]):
failures.append("unit command button should carry an immediate visual icon: %s icon=%s meta=%s" % [button.text, str(button.icon), str(button.get_meta("command_icon", ""))])
_check_button_uses_generated_toolbar_icon(failures, button, str(expectation["icon"]))
_check_button_uses_generated_surface(failures, button, str(expectation["icon"]))
if not bool(button.get_meta("icon_only", false)) or button.text != "":
failures.append("unit command button should be icon-first without visible text: %s text=%s" % [str(button.name), button.text])
if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
failures.append("could not load opening battle for command icon tooltip check")
else:
scene.battle_started = true
scene.briefing_panel.visible = false
scene.result_panel.visible = false
scene._update_hud()
for expectation in command_icon_expectations:
var button: Button = expectation["button"]
if button == null:
continue
if button.tooltip_text.strip_edges().is_empty():
failures.append("unit command icon should expose its command through tooltip: %s" % str(button.name))
scene._on_command_hint_button_mouse_entered(scene.wait_button)
if scene.command_hint_panel == null or not scene.command_hint_panel.visible:
failures.append("hovering a unit command icon should show the command hint panel")
else:
if scene.command_hint_title_label == null or scene.command_hint_title_label.text != "대기":
failures.append("unit command hint should show command title: %s" % ("" if scene.command_hint_title_label == null else scene.command_hint_title_label.text))
if scene.command_hint_detail_label == null or not scene.command_hint_detail_label.text.contains("부대 미지정"):
failures.append("disabled unit command hint should show the blocked reason: %s" % ("" if scene.command_hint_detail_label == null else scene.command_hint_detail_label.text))
if scene.command_hint_icon == null or scene.command_hint_icon.texture == null:
failures.append("unit command hint should show generated command icon artwork")
scene._on_command_hint_button_mouse_exited()
if scene.command_hint_panel != null and scene.command_hint_panel.visible:
failures.append("unit command hint panel should hide when leaving the icon")
var top_toolbar := scene.end_turn_button.get_parent() as HBoxContainer
if top_toolbar == null or top_toolbar.name != "TopToolBar":
failures.append("global battle commands should move to the top icon toolbar")
for button in [
scene.end_turn_button,
scene.threat_button,
scene.battle_unit_list_button,
scene.top_save_button,
scene.top_load_button,
scene.restart_button,
scene.new_campaign_button
]:
if button == null:
failures.append("top tool button missing")
continue
if button.get_parent() != top_toolbar:
failures.append("top tool button should share the top toolbar: %s" % str(button.name))
if not bool(button.get_meta("icon_only", false)) or button.text != "" or button.icon == null:
failures.append("top tool button should be icon-only with tooltip detail: %s text=%s icon=%s" % [str(button.name), button.text, str(button.icon)])
_check_button_uses_generated_toolbar_icon(failures, button, str(button.name))
_check_button_uses_generated_surface(failures, button, str(button.name))
if button.custom_minimum_size.x < BattleSceneScript.TOP_TOOL_BUTTON_SIZE.x or button.custom_minimum_size.y < BattleSceneScript.TOP_TOOL_BUTTON_SIZE.y:
failures.append("top tool button should reserve stable icon space: %s" % str(button.custom_minimum_size))
scene._on_command_hint_button_mouse_entered(scene.end_turn_button)
if scene.command_hint_panel == null or not scene.command_hint_panel.visible:
failures.append("hovering a top command icon should show the command hint panel")
else:
if scene.command_hint_title_label == null or scene.command_hint_title_label.text != "차례 종료":
failures.append("top command hint should show command title: %s" % ("" if scene.command_hint_title_label == null else scene.command_hint_title_label.text))
if scene.command_hint_detail_label == null or not scene.command_hint_detail_label.text.contains("적군"):
failures.append("top command hint should keep command detail: %s" % ("" if scene.command_hint_detail_label == null else scene.command_hint_detail_label.text))
if scene.command_hint_icon == null or scene.command_hint_icon.texture == null or str(scene.end_turn_button.get_meta("command_icon", "")) != "end_turn":
failures.append("top command hint should use generated top command icon artwork")
scene.free()
func _check_battle_unit_list_panel(failures: Array[String]) -> void:
var scene = BattleSceneScript.new()
scene._create_hud()
if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
failures.append("could not load opening battle for battle unit list")
scene.free()
return
scene.battle_started = true
scene.campaign_complete_screen = false
scene.briefing_panel.visible = false
scene.result_panel.visible = false
scene._update_hud()
if scene.battle_unit_list_button == null:
failures.append("top toolbar should expose a battle unit list button")
scene.free()
return
if scene.battle_unit_list_button.disabled:
failures.append("battle unit list button should be enabled during active battle")
if scene.battle_unit_list_button.icon == null or not bool(scene.battle_unit_list_button.get_meta("icon_only", false)):
failures.append("battle unit list button should be an icon-only top command")
scene._on_battle_unit_list_pressed()
if scene.battle_unit_list_panel == null or not scene.battle_unit_list_panel.visible:
failures.append("battle unit list panel should open from the top toolbar")
else:
_check_panel_style_fill(failures, scene.battle_unit_list_panel, "battle unit list panel", Color(0.045, 0.048, 0.044, 0.93))
_check_panel_uses_generated_texture(failures, scene.battle_unit_list_panel, "battle unit list generated frame")
var header_icon := _find_descendant_by_name(scene.battle_unit_list_panel, "BattleUnitListHeaderIcon")
if header_icon == null or not header_icon is TextureRect or (header_icon as TextureRect).texture == null:
failures.append("battle unit list header should carry generated unit-list icon artwork")
var header_seal := _find_descendant_by_name(scene.battle_unit_list_panel, "BattleUnitListHeaderSeal")
if header_seal == null:
failures.append("battle unit list header should use a compact Korean seal mark")
if scene.battle_unit_list_title_label == null or not scene.battle_unit_list_title_label.text.contains("아군"):
failures.append("battle unit list should open on the ally tab")
if scene.battle_unit_list_rows == null or scene.battle_unit_list_rows.get_child_count() < 2:
failures.append("ally battle unit list should include deployed officers")
elif not _row_texts_contain(scene.battle_unit_list_rows, "조조") or not _row_texts_contain(scene.battle_unit_list_rows, "하후돈"):
failures.append("ally battle unit list should show Cao Cao and Xiahou Dun")
var ally_visible_text := _collect_child_text(scene.battle_unit_list_rows)
var ally_badge_text := _collect_descendant_text_by_name(scene.battle_unit_list_rows, "BattleUnitListBadgeLabel")
var ally_tooltips := _collect_child_tooltips(scene.battle_unit_list_rows)
if not ally_badge_text.contains("100%") or not ally_badge_text.contains("서측"):
failures.append("ally battle unit list should show compact HP percent and zone badges: %s" % ally_badge_text)
for class_label in ["지휘관", "기병", "보병", "궁병", "책사"]:
if ally_badge_text.contains(class_label):
failures.append("ally battle unit list should move class names into icon hover text: %s" % ally_badge_text)
if ally_visible_text.contains(""):
failures.append("ally battle unit list should keep raw coordinates out of visible badges: %s" % ally_visible_text)
if ally_visible_text.contains("군령:") or ally_visible_text.contains("행군 4") or ally_visible_text.contains("타격 1-1"):
failures.append("ally battle unit list rows should keep dense combat detail in hover text: %s" % ally_visible_text)
if not ally_tooltips.contains("군세") or not ally_tooltips.contains("클릭하면 이 장수를 선택") or not ally_tooltips.contains("지휘관") or not ally_tooltips.contains("병력"):
failures.append("ally battle unit list tooltips should retain selection detail: %s" % ally_tooltips)
if not _has_descendant_texture(scene.battle_unit_list_rows):
failures.append("ally battle unit list should show officer portrait artwork")
if _find_descendant_by_name(scene.battle_unit_list_rows, "BattleUnitListClassIcon") == null:
failures.append("ally battle unit list rows should overlay generated class crest artwork")
if _count_descendants_by_name(scene.battle_unit_list_rows, "BattleUnitListBadgeIcon") < 4:
failures.append("ally battle unit list compact badges should include icon artwork for class, HP, action, and zone")
scene._on_battle_unit_list_row_pressed("cao_cao")
var selected: Dictionary = scene.state.get_selected_unit()
if selected.is_empty() or str(selected.get("id", "")) != "cao_cao":
failures.append("clicking an available ally in the battle unit list should select that officer")
scene._on_battle_unit_list_tab_pressed("enemy")
if scene.battle_unit_list_title_label == null or not scene.battle_unit_list_title_label.text.contains("적군"):
failures.append("battle unit list should switch to the enemy tab")
if scene.battle_unit_list_status_label == null or not scene.battle_unit_list_status_label.text.contains("잔적"):
failures.append("enemy battle unit list should summarize remaining enemies")
if scene.battle_unit_list_rows == null or scene.battle_unit_list_rows.get_child_count() < 1:
failures.append("enemy battle unit list should show living enemies")
elif not _row_texts_contain(scene.battle_unit_list_rows, "장만성"):
failures.append("enemy battle unit list should include the enemy commander")
var enemy_visible_text := _collect_child_text(scene.battle_unit_list_rows)
var enemy_badge_text := _collect_descendant_text_by_name(scene.battle_unit_list_rows, "BattleUnitListBadgeLabel")
var enemy_tooltips := _collect_child_tooltips(scene.battle_unit_list_rows)
if not enemy_badge_text.contains("") or not enemy_badge_text.contains("%"):
failures.append("enemy battle unit list should show compact status and HP percent badges: %s" % enemy_badge_text)
if not enemy_badge_text.contains("성채") and not enemy_badge_text.contains("동측"):
failures.append("enemy battle unit list should show a readable zone badge instead of raw coordinates: %s" % enemy_badge_text)
for class_label in ["지휘관", "기병", "보병", "궁병", "책사"]:
if enemy_badge_text.contains(class_label):
failures.append("enemy battle unit list should move class names into icon hover text: %s" % enemy_badge_text)
if enemy_visible_text.contains(""):
failures.append("enemy battle unit list should keep raw coordinates out of visible badges: %s" % enemy_visible_text)
if enemy_visible_text.contains("군령:") or enemy_visible_text.contains("직접 명령"):
failures.append("enemy battle unit list rows should keep dense enemy detail in hover text: %s" % enemy_visible_text)
if not (enemy_badge_text.contains("대기") or enemy_badge_text.contains("수비") or enemy_badge_text.contains("경계")):
failures.append("enemy battle unit list should expose compact AI state badges: %s" % enemy_badge_text)
if not enemy_tooltips.contains("군령: 적군 군기") or not enemy_tooltips.contains("클릭하면 위치로 이동"):
failures.append("enemy battle unit list tooltips should retain enemy detail: %s" % enemy_tooltips)
if not enemy_tooltips.contains("유인 반경") or not enemy_tooltips.contains("거점 경계 반경"):
failures.append("enemy battle unit list tooltips should explain sentry and guard behavior: %s" % enemy_tooltips)
if not _has_descendant_texture(scene.battle_unit_list_rows):
failures.append("enemy battle unit list should show unit sprite artwork")
if _find_descendant_by_name(scene.battle_unit_list_rows, "BattleUnitListClassIcon") == null:
failures.append("enemy battle unit list rows should overlay generated class crest artwork")
if _count_descendants_by_name(scene.battle_unit_list_rows, "BattleUnitListBadgeIcon") < 4:
failures.append("enemy battle unit list compact badges should include icon artwork for class, HP, action, and zone")
scene._on_battle_unit_list_row_pressed("yellow_turban_1")
selected = scene.state.get_selected_unit()
if selected.is_empty() or str(selected.get("id", "")) != "cao_cao":
failures.append("clicking an enemy in the battle unit list should focus without stealing ally selection")
var escape_event := InputEventKey.new()
escape_event.keycode = KEY_ESCAPE
scene._handle_key(escape_event)
if scene.battle_unit_list_panel != null and scene.battle_unit_list_panel.visible:
failures.append("Escape should close the battle unit list panel")
scene.free()
func _row_texts_contain(container: VBoxContainer, needle: String) -> bool:
if container == null:
return false
return _collect_child_text(container).contains(needle)
func _check_prep_menu_icon_button(failures: Array[String], button: Button, expected_label: String, expected_icon: String) -> void:
if button == null:
failures.append("pre-battle prep button missing: %s" % expected_label)
return
if not button.toggle_mode:
failures.append("pre-battle prep button should support active selected state: %s" % expected_label)
if not bool(button.get_meta("icon_only", false)) or button.text != "":
failures.append("pre-battle prep button should be icon-first without visible text: %s text=%s" % [expected_label, button.text])
if button.icon == null or str(button.get_meta("command_icon", "")) != expected_icon:
failures.append("pre-battle prep button should carry a distinct icon: %s icon=%s meta=%s" % [expected_label, str(button.icon), str(button.get_meta("command_icon", ""))])
_check_button_uses_generated_toolbar_icon(failures, button, expected_label)
_check_button_uses_generated_surface(failures, button, expected_label)
if str(button.get_meta("command_label", "")) != expected_label:
failures.append("pre-battle prep button should preserve Korean label in metadata: %s meta=%s" % [expected_label, str(button.get_meta("command_label", ""))])
if str(button.tooltip_text).strip_edges().is_empty() or not str(button.tooltip_text).contains(expected_label):
failures.append("pre-battle prep button should expose its function through hover tooltip: %s tooltip=%s" % [expected_label, str(button.tooltip_text)])
func _check_prep_menu_button_active_selection(failures: Array[String], scene) -> void:
scene._on_shop_pressed()
if scene.shop_button == null or not scene.shop_button.button_pressed or not bool(scene.shop_button.get_meta("prep_menu_active", false)):
failures.append("opening the shop should mark only the shop prep icon active")
if scene.talk_button != null and scene.talk_button.button_pressed:
failures.append("opening the shop should leave the talk prep icon inactive")
scene._on_talk_pressed()
if scene.talk_button == null or not scene.talk_button.button_pressed or not bool(scene.talk_button.get_meta("prep_menu_active", false)):
failures.append("opening talk should mark the talk prep icon active")
if scene.shop_button != null and scene.shop_button.button_pressed:
failures.append("opening talk should clear the previous shop prep icon active state")
scene._hide_talk_menu()
scene._update_hud()
if scene.talk_button != null and scene.talk_button.button_pressed:
failures.append("hiding a prep menu should clear its active icon state")
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()
for button in [
scene.wait_button,
scene.tactic_button,
scene.item_button,
scene.equip_button,
scene.end_turn_button,
scene.threat_button
]:
button.set_meta("icon_only", true)
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("봉인됨") 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])
if str(scene.tactic_button.get_meta("command_label", "")) != "책략" or str(scene.tactic_button.get_meta("disabled_hint", "")) != "봉인됨" or not bool(scene.tactic_button.get_meta("command_disabled", false)):
failures.append("sealed tactic button should keep reusable command metadata: %s / %s / %s" % [
str(scene.tactic_button.get_meta("command_label", "")),
str(scene.tactic_button.get_meta("disabled_hint", "")),
str(scene.tactic_button.get_meta("command_disabled", false))
])
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 scene.wait_button.text != "" or not scene.wait_button.tooltip_text.contains("대기") or not scene.wait_button.tooltip_text.contains("이미 행동"):
failures.append("acted unit hold button should keep short text and explain in tooltip: %s | %s" % [scene.wait_button.text, scene.wait_button.tooltip_text])
if scene.tactic_button.text != "" or not scene.tactic_button.tooltip_text.contains("책략") or not scene.tactic_button.tooltip_text.contains("이미 행동"):
failures.append("acted unit stratagem button should keep short text and explain in tooltip: %s | %s" % [scene.tactic_button.text, scene.tactic_button.tooltip_text])
if scene.item_button.text != "" or not scene.item_button.tooltip_text.contains("도구") or not scene.item_button.tooltip_text.contains("이미 행동"):
failures.append("acted unit supply button should keep short text and explain in tooltip: %s | %s" % [scene.item_button.text, scene.item_button.tooltip_text])
if scene.equip_button.text != "" or not scene.equip_button.tooltip_text.contains("병장") or not scene.equip_button.tooltip_text.contains("이미 행동"):
failures.append("acted unit arms button should keep short text and explain in tooltip: %s | %s" % [scene.equip_button.text, scene.equip_button.tooltip_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("병장") or not scene.equip_button.tooltip_text.contains("행군 후") 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])
if str(scene.equip_button.get_meta("command_label", "")) != "병장" or str(scene.equip_button.get_meta("disabled_hint", "")) != "행군 후":
failures.append("moved equip button should keep a compact disabled reason for hover UI reuse: %s / %s" % [
str(scene.equip_button.get_meta("command_label", "")),
str(scene.equip_button.get_meta("disabled_hint", ""))
])
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("비어 있음") 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])
if str(scene.item_button.get_meta("disabled_hint", "")) != "비어 있음":
failures.append("empty inventory item button should keep disabled hint metadata: %s" % str(scene.item_button.get_meta("disabled_hint", "")))
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("차례 종료") or not scene.end_turn_button.tooltip_text.contains("적군 차례") 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])
if str(scene.end_turn_button.get_meta("command_label", "")) != "차례 종료" or str(scene.end_turn_button.get_meta("disabled_hint", "")) != "적군 차례":
failures.append("enemy phase end-turn button should preserve disabled metadata: %s / %s" % [
str(scene.end_turn_button.get_meta("command_label", "")),
str(scene.end_turn_button.get_meta("disabled_hint", ""))
])
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("적정") or not scene.threat_button.tooltip_text.contains("전장도 미개봉") 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])
if str(scene.threat_button.get_meta("disabled_hint", "")) != "전장도 미개봉":
failures.append("inactive threat button should retain disabled hint metadata: %s" % str(scene.threat_button.get_meta("disabled_hint", "")))
_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
var enemy: Dictionary = scene.state.get_unit("yellow_turban_1")
enemy["pos"] = Vector2i(1, 5)
scene.state.select_unit("cao_cao")
scene._refresh_ranges()
scene.hover_cell = Vector2i(2, 5)
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))
if str(move_badge.get("icon", "")) != "move":
failures.append("MOVE badge should carry a generated icon hint: %s" % str(move_badge))
if str(move_badge.get("panel", "")) != "hud_jade":
failures.append("MOVE badge should use generated jade panel metadata: %s" % str(move_badge))
if not scene._hover_info_badge().is_empty():
failures.append("hover info badge should yield to active move intent badge")
scene.hover_cell = Vector2i(1, 7)
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))
if str(select_badge.get("icon", "")).strip_edges().is_empty():
failures.append("SELECT badge should carry a generated icon hint: %s" % str(select_badge))
if str(select_badge.get("panel", "")) != "hud_jade":
failures.append("SELECT badge should use generated jade panel metadata: %s" % str(select_badge))
scene.hover_cell = Vector2i(1, 5)
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))
if not str(attack_badge.get("text", "")).contains(""):
failures.append("adjacent attack badge should expose counterattack risk: %s" % str(attack_badge))
if str(attack_badge.get("kind", "")) != "counter" and str(attack_badge.get("kind", "")) != "danger":
failures.append("counterable attack badge should use a counter danger color kind: %s" % str(attack_badge))
if str(attack_badge.get("icon", "")) != "threat":
failures.append("counterable attack badge should carry a threat icon hint: %s" % str(attack_badge))
if str(attack_badge.get("panel", "")) != "hud_jade":
failures.append("counterable attack badge should use generated jade panel metadata: %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, 5)
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")
if scene._target_preview_badge_color("counter") == default_color:
failures.append("COUNTER badge should use a distinct color")
if scene._target_preview_badge_color("danger") == default_color:
failures.append("DANGER badge should use a distinct color")
var board_rect: Rect2 = scene._board_rect().intersection(scene._map_view_rect())
var edge_badge_rect: Rect2 = scene._target_preview_badge_rect(Vector2i(13, 9))
if edge_badge_rect.position.x < board_rect.position.x or edge_badge_rect.position.y < board_rect.position.y:
failures.append("edge target preview badge should stay inside visible board minimum: %s / %s" % [str(edge_badge_rect), str(board_rect)])
if edge_badge_rect.end.x > board_rect.end.x or edge_badge_rect.end.y > board_rect.end.y:
failures.append("edge target preview badge should stay inside visible board maximum: %s / %s" % [str(edge_badge_rect), str(board_rect)])
if edge_badge_rect.size.x <= BattleSceneScript.TILE_SIZE:
failures.append("target preview badge should remain wide enough for Korean tactical text")
if edge_badge_rect.size.x < 120.0:
failures.append("target preview badge should reserve room for icon and Korean text")
scene.state.clear_selection()
scene.selected_skill_id = ""
scene.selected_item_id = ""
scene.basic_attack_targeting = false
scene._refresh_ranges()
scene.hover_cell = Vector2i(1, 5)
var enemy_hover_badge := scene._hover_info_badge()
if str(enemy_hover_badge.get("kind", "")) != "enemy" or not str(enemy_hover_badge.get("lines", [])).contains(str(enemy.get("name", ""))):
failures.append("enemy hover info badge should expose enemy identity: %s" % str(enemy_hover_badge))
if str(enemy_hover_badge.get("icon", "")) != "threat":
failures.append("enemy hover info badge should use generated threat icon metadata: %s" % str(enemy_hover_badge))
if str(enemy_hover_badge.get("panel", "")) != "hud_jade":
failures.append("enemy hover info badge should use generated jade panel metadata: %s" % str(enemy_hover_badge))
var central_sentry: Dictionary = scene.state.get_unit("yellow_turban_5")
central_sentry["ai_awake"] = false
scene.hover_cell = central_sentry.get("pos", Vector2i(10, 6))
var sentry_hover_badge := scene._hover_info_badge()
var sentry_hover_text := str(sentry_hover_badge.get("lines", []))
if not sentry_hover_text.contains("초병 대기") or not sentry_hover_text.contains("제4군령"):
failures.append("sleeping sentry hover badge should expose lure timing compactly: %s" % str(sentry_hover_badge))
central_sentry["ai_awake"] = true
var awake_sentry_hover_badge := scene._hover_info_badge()
if not str(awake_sentry_hover_badge.get("lines", [])).contains("초병 경계"):
failures.append("awake sentry hover badge should show the changed AI state: %s" % str(awake_sentry_hover_badge))
scene.hover_cell = Vector2i(4, 2)
var event_hover_badge := scene._hover_info_badge()
if str(event_hover_badge.get("kind", "")) != "event" or not str(event_hover_badge.get("lines", [])).contains("표식 북숲 보급고"):
failures.append("event hover info badge should expose marker label: %s" % str(event_hover_badge))
if str(event_hover_badge.get("icon", "")) != "item":
failures.append("event hover info badge should use generated supply icon metadata: %s" % str(event_hover_badge))
if str(event_hover_badge.get("lines", [])).contains("5,3"):
failures.append("event hover info badge should not lead with raw grid coordinates: %s" % str(event_hover_badge))
scene.hover_cell = Vector2i(4, 6)
var tactic_hover_badge := scene._hover_info_badge()
if str(tactic_hover_badge.get("kind", "")) != "event" or not str(tactic_hover_badge.get("lines", [])).contains("표식 유인선"):
failures.append("tactic hover info badge should expose lure-line marker label: %s" % str(tactic_hover_badge))
if str(tactic_hover_badge.get("icon", "")) != "tactic":
failures.append("tactic hover info badge should use generated tactic icon metadata: %s" % str(tactic_hover_badge))
if str(tactic_hover_badge.get("lines", [])).contains("5,7"):
failures.append("tactic hover info badge should hide raw grid coordinates: %s" % str(tactic_hover_badge))
scene.hover_cell = Vector2i(20, 1)
var objective_hover_badge := scene._hover_info_badge()
var objective_hover_text := str(objective_hover_badge.get("lines", []))
if str(objective_hover_badge.get("kind", "")) != "objective" or not objective_hover_text.contains("목표 성채 장악") or not objective_hover_text.contains("회복 +8"):
failures.append("objective hover info badge should expose capture and recovery: %s" % str(objective_hover_badge))
if str(objective_hover_badge.get("icon", "")) != "objective":
failures.append("objective hover info badge should use generated objective icon metadata: %s" % str(objective_hover_badge))
if objective_hover_text.contains("21,2"):
failures.append("objective hover info badge should not show raw coordinates in visible text: %s" % objective_hover_text)
var edge_hover_lines: Array[String] = ["긴 전장 배지", "둘째 줄"]
var edge_hover_rect: Rect2 = scene._hover_info_badge_rect(Vector2i(13, 9), edge_hover_lines)
if edge_hover_rect.position.x < board_rect.position.x or edge_hover_rect.position.y < board_rect.position.y:
failures.append("edge hover info badge should stay inside visible board minimum: %s / %s" % [str(edge_hover_rect), str(board_rect)])
if edge_hover_rect.end.x > board_rect.end.x or edge_hover_rect.end.y > board_rect.end.y:
failures.append("edge hover info badge should stay inside visible board maximum: %s / %s" % [str(edge_hover_rect), str(board_rect)])
scene.free()
func _check_counter_attack_badge_variants(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 counter badge variants")
scene.free()
return
scene.battle_started = true
scene.state.select_unit("cao_cao")
var cao_cao: Dictionary = scene.state.get_unit("cao_cao")
var infantry: Dictionary = scene.state.get_unit("yellow_turban_2")
var archer: Dictionary = scene.state.get_unit("yellow_turban_3")
if cao_cao.is_empty() or infantry.is_empty() or archer.is_empty():
failures.append("counter badge variant smoke missing units")
scene.free()
return
infantry["pos"] = Vector2i(2, 6)
infantry["hp"] = 99
infantry["max_hp"] = 99
archer["pos"] = Vector2i(10, 8)
scene.hover_cell = Vector2i(2, 6)
var counter_badge := scene._target_preview_badge()
if str(counter_badge.get("kind", "")) != "counter" or not str(counter_badge.get("text", "")).contains(""):
failures.append("melee target should expose counter badge: %s" % str(counter_badge))
scene.basic_attack_targeting = true
scene._refresh_ranges()
var marker_entries := scene._attack_target_marker_entries()
if marker_entries.is_empty() or str(marker_entries[0].get("kind", "")) != "counter":
failures.append("attack target marker should carry counter kind: %s" % str(marker_entries))
elif str(marker_entries[0].get("panel", "")) != "hud_jade":
failures.append("attack target marker should carry generated panel metadata: %s" % str(marker_entries))
scene.basic_attack_targeting = false
infantry["pos"] = Vector2i(10, 8)
archer["pos"] = Vector2i(2, 6)
archer["hp"] = 99
archer["max_hp"] = 99
scene.hover_cell = Vector2i(2, 6)
var no_counter_badge := scene._target_preview_badge()
if str(no_counter_badge.get("kind", "")) != "damage" or str(no_counter_badge.get("text", "")).contains(""):
failures.append("adjacent archer target should not expose counter badge: %s" % str(no_counter_badge))
archer["pos"] = Vector2i(10, 8)
infantry["pos"] = Vector2i(2, 6)
infantry["hp"] = 99
infantry["max_hp"] = 99
infantry["atk"] = 999
cao_cao["hp"] = 1
scene.hover_cell = Vector2i(2, 6)
var danger_badge := scene._target_preview_badge()
if str(danger_badge.get("kind", "")) != "danger" or not str(danger_badge.get("text", "")).contains("반격!"):
failures.append("lethal counter should expose danger badge: %s" % str(danger_badge))
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(4, 1)) != "F":
failures.append("terrain helper should read forest cells")
if scene._terrain_key_at(Vector2i(6, 5)) != "T":
failures.append("terrain helper should read village cells")
if not scene._terrain_key_at(Vector2i(-1, 0)).is_empty():
failures.append("terrain helper should return empty outside the board")
if scene.state.get_terrain_name(Vector2i(6, 5)) != "마을" or scene.state.get_terrain_heal(Vector2i(6, 5)) != 6:
failures.append("village terrain should expose Korean name and healing")
if scene.state.get_terrain_heal(Vector2i(20, 1)) != 8:
failures.append("castle terrain should expose stronger healing")
scene._create_hud()
scene._update_forecast()
scene._update_forecast_panel_visibility()
if scene.forecast_panel == null or scene.forecast_panel.visible:
failures.append("idle forecast panel should stay hidden instead of showing empty tactical text")
scene.hover_cell = Vector2i(20, 1)
scene._update_cell_info()
if scene.cell_info_label == null or not scene.cell_info_label.text.contains("목표 성채 장악") or not scene.cell_info_label.text.contains("회복 +8"):
failures.append("castle objective cell info should expose capture label and healing: %s" % scene.cell_info_label.text)
if scene.cell_info_label != null and (scene.cell_info_label.text.contains("") or scene.cell_info_label.text.contains("방+")):
failures.append("visible cell info should leave terrain numbers to the tooltip: %s" % scene.cell_info_label.text)
if scene.cell_info_label != null and not scene.cell_info_label.tooltip_text.contains("21,2"):
failures.append("castle objective cell info should keep coordinates in the hover tooltip: %s" % scene.cell_info_label.tooltip_text)
if scene.cell_info_panel == null or not scene.cell_info_panel.visible:
failures.append("cell info panel should be visible for a hovered battlefield cell")
if scene.cell_info_icon == null or str(scene.cell_info_icon.get_meta("icon_kind", "")) != "objective":
failures.append("castle objective cell info should switch the side chip to the objective icon")
if scene.cell_info_label.text.contains("20,2"):
failures.append("visible cell info should stay compact and leave coordinates to tooltip/badge: %s" % scene.cell_info_label.text)
scene.hover_cell = Vector2i(4, 2)
scene._update_cell_info()
if scene.cell_info_label == null or not scene.cell_info_label.text.contains("표식 북숲 보급고"):
failures.append("side event marker cell info should expose the supply label: %s" % scene.cell_info_label.text)
if scene.cell_info_icon == null or str(scene.cell_info_icon.get_meta("icon_kind", "")) != "item":
failures.append("supply marker cell info should switch the side chip to an item icon")
scene.hover_cell = Vector2i(4, 6)
scene._update_cell_info()
if scene.cell_info_label == null or not scene.cell_info_label.text.contains("표식 유인선"):
failures.append("tactical lure marker cell info should expose the marker label: %s" % scene.cell_info_label.text)
if scene.cell_info_icon == null or str(scene.cell_info_icon.get_meta("icon_kind", "")) != "tactic":
failures.append("tactical lure cell info should switch the side chip to a tactic icon")
if scene.state.get_event_marker_kind(Vector2i(4, 6)) != "tactic":
failures.append("tactical lure marker should expose tactic kind")
scene.hover_cell = Vector2i(-1, -1)
scene._update_cell_info()
if scene.cell_info_panel != null and scene.cell_info_panel.visible:
failures.append("cell info panel should hide when the cursor is outside the board")
scene.state.select_unit("cao_cao")
var forecast_enemy: Dictionary = scene.state.get_unit("yellow_turban_1")
forecast_enemy["pos"] = Vector2i(1, 5)
scene.hover_cell = Vector2i(1, 5)
scene._refresh_ranges()
scene._update_forecast()
scene._update_forecast_panel_visibility()
if scene.forecast_panel == null or not scene.forecast_panel.visible:
failures.append("forecast panel should return when hovering a real target")
scene.cell_info_label.free()
scene.cell_info_label = null
var event_marker_rect: Rect2 = scene._event_marker_rect(scene._rect_for_cell(Vector2i(4, 2)))
if not scene._rect_for_cell(Vector2i(4, 2)).encloses(event_marker_rect):
failures.append("Side event marker should stay inside its tile: %s" % str(event_marker_rect))
if scene._event_marker_abbrev("북숲 보급고", "supply") != "":
failures.append("Northern woods marker should use a compact forest glyph")
if scene._event_marker_abbrev("마을 보급", "supply") != "":
failures.append("Village supply marker should use a compact village glyph")
if scene._event_marker_abbrev("", "supply") != "":
failures.append("Supply marker fallback should stay concise")
if scene._event_marker_abbrev("군자금", "gold") != "":
failures.append("Gold marker should use a compact gold glyph")
if scene._event_marker_abbrev("유인선", "tactic") != "":
failures.append("Tactic lure marker should use a compact lure glyph")
if scene._event_marker_color("tactic") == scene._event_marker_color("supply"):
failures.append("Tactic marker should be visually distinct from supply markers")
if scene._event_marker_badge_kind("마을 보급", "supply") != "supply":
failures.append("Supply event markers should use the generated supply badge")
if scene._event_marker_badge_kind("군자금", "gold") != "gold":
failures.append("Gold event markers should use the generated gold badge")
if scene._event_marker_badge_kind("유인선", "tactic") != "tactic":
failures.append("Tactic event markers should use the generated tactic badge")
if scene._event_marker_tile_marker_key("마을 보급", "supply") != "recover":
failures.append("Supply event markers should lay generated recovery tile art under the badge")
if scene._event_marker_tile_marker_key("유인선", "tactic") != "move":
failures.append("Tactic lure markers should lay generated movement tile art under the badge")
if scene._event_marker_tile_marker_key("군자금", "gold") != "objective":
failures.append("Gold markers should use a generated objective tile frame")
if scene._event_marker_tile_marker_key("", "") != "select":
failures.append("Unknown event markers should still use a generated tile frame")
if scene._event_marker_badge_kind("군자금", "") != "gold":
failures.append("Event marker labels should infer generated badge kind when kind is absent")
if scene._event_marker_badge_kind("", "") != "event":
failures.append("Unknown event markers should use the generated event badge")
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")
var background_texture_modulate: Color = scene._terrain_texture_modulate(Vector2i(0, 0), "G", true)
var fallback_texture_modulate: Color = scene._terrain_texture_modulate(Vector2i(0, 0), "G", false)
if background_texture_modulate.a < 0.18:
failures.append("background terrain texture should still leave a visible terrain hint over high-res art: %.3f" % background_texture_modulate.a)
if background_texture_modulate.a >= fallback_texture_modulate.a:
failures.append("background terrain texture should stay lighter than fallback terrain texture")
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._map_grid_color(true).a > 0.025:
failures.append("high-resolution battle backgrounds should not be covered by visible square grid lines: %.3f" % scene._map_grid_color(true).a)
if not scene.has_method("_draw_tactical_tile_overlay") or not scene.has_method("_draw_tactical_corner_brackets"):
failures.append("battlefield tactical overlays should use generated tile art with corner brackets instead of full square UI borders")
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 scene._terrain_edge_priority("T") <= scene._terrain_edge_priority("F"):
failures.append("village should draw above forest/grass terrain blends")
var wasteland_fill: Color = scene._terrain_fill_color(Vector2i(0, 0), "D", true)
if wasteland_fill.a <= background_fill.a:
failures.append("wasteland fill should sit slightly above grass over background art")
if scene._load_terrain_texture("G") == null or scene._load_terrain_texture("T") == null or scene._load_terrain_texture("C") == null:
failures.append("battle map should load generated terrain texture assets")
var grass_texture_modulate := scene._terrain_texture_modulate(Vector2i(0, 0), "G", true)
var grass_fallback_modulate := scene._terrain_texture_modulate(Vector2i(0, 0), "G", false)
var castle_texture_modulate := scene._terrain_texture_modulate(Vector2i(20, 1), "C", true)
if grass_texture_modulate.a <= 0.0 or grass_texture_modulate.a >= 0.35:
failures.append("background terrain texture should stay subtle over high-res art: %s" % str(grass_texture_modulate))
if grass_fallback_modulate.a <= grass_texture_modulate.a:
failures.append("fallback terrain texture should be stronger without map background")
if castle_texture_modulate.a <= grass_texture_modulate.a:
failures.append("castle terrain texture should read stronger than plain grass")
if scene._terrain_edge_color("D", "G").a <= 0.0:
failures.append("wasteland edge blend should be visible")
if scene._terrain_edge_priority("D") <= scene._terrain_edge_priority("R"):
failures.append("wasteland should draw over roads at broken dirt edges")
if not scene.state.load_battle("res://data/scenarios/014_cangting_pursuit.json"):
failures.append("could not load Cangting Pursuit for wasteland presentation helpers")
elif scene._terrain_key_at(Vector2i(0, 0)) != "D" or scene.state.get_terrain_name(Vector2i(0, 0)) != "황무지":
failures.append("Cangting Pursuit should expose wasteland terrain at the dry outer edge")
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 scene._road_feature_key(Vector2i(2, 4)) != "road_ew":
failures.append("Sishui Gate road should use generated east-west road feature: %s" % scene._road_feature_key(Vector2i(2, 4)))
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")
scene.hover_cell = Vector2i(-1, -1)
if scene._should_draw_unit_nameplate(generic_enemy, false):
failures.append("generic enemy nameplates should stay hidden until hovered or selected")
var generic_enemy_pos: Vector2i = generic_enemy.get("pos", Vector2i(-1, -1))
scene.hover_cell = generic_enemy_pos
if not scene._should_draw_unit_nameplate(generic_enemy, false):
failures.append("generic enemy nameplate should appear on hover for inspection")
scene.hover_cell = Vector2i(-1, -1)
if not scene._should_draw_unit_nameplate(named_officer, false):
failures.append("named officers should keep visible nameplates for command clarity")
if not scene._should_draw_unit_nameplate(generic_enemy, true):
failures.append("selected generic enemy should expose a nameplate")
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")
var sprite_texture := scene._load_art_texture(str(named_officer.get("sprite", "")))
if sprite_texture == null:
failures.append("named officer high-res map sprite should load")
else:
var sprite_rect: Rect2 = scene._unit_map_sprite_rect(sprite_texture, scene._rect_for_cell(Vector2i(1, 6)), named_officer)
if sprite_rect.size.x < 24.0 or sprite_rect.size.y < 36.0:
failures.append("high-res map sprite should occupy a readable token area: %s" % str(sprite_rect))
if sprite_rect.position.x < scene._rect_for_cell(Vector2i(1, 6)).position.x or sprite_rect.end.x > scene._rect_for_cell(Vector2i(1, 6)).end.x:
failures.append("high-res map sprite should stay inside its tile horizontally: %s" % str(sprite_rect))
if scene.texture_filter != CanvasItem.TEXTURE_FILTER_LINEAR:
failures.append("battle scene should use linear texture filtering for smoother high-res map sprites")
scene.free()
func _check_objective_and_status_marker_helpers(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 objective/status markers")
scene.free()
return
if scene.state.get_objective_cells().is_empty():
failures.append("opening battle should expose objective cells for foreground markers")
var glyphs := {
"support": "",
"debuff": "",
"poison": "",
"seal": "",
"snare": "",
"disarm": ""
}
for marker_kind in glyphs.keys():
if scene._unit_status_marker_glyph(str(marker_kind)) != str(glyphs[marker_kind]):
failures.append("status marker glyph mismatch for %s" % str(marker_kind))
var unit := {
"status_effects": [
{"type": "stat_bonus", "stat": "def", "amount": 2, "remaining_phases": 2},
{"type": "stat_bonus", "stat": "def", "amount": -2, "remaining_phases": 2},
{"type": "damage_over_time", "status": "poison", "amount": 4, "remaining_phases": 2},
{"type": "action_lock", "status": "seal", "action": "skill", "remaining_phases": 2}
]
}
var marker_kinds := scene._unit_status_marker_kinds(unit)
for expected in ["support", "debuff", "poison", "seal"]:
if not marker_kinds.has(expected):
failures.append("status marker kinds should include %s: %s" % [expected, str(marker_kinds)])
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, 6), Vector2i(2, 6), "command_strike")
_check_attack_profile(failures, scene, "xiahou_dun", Vector2i(1, 7), Vector2i(2, 7), "cavalry_charge")
_check_attack_profile(failures, scene, "yellow_turban_1", Vector2i(20, 1), Vector2i(19, 1), "heavy_strike")
_check_attack_profile(failures, scene, "yellow_turban_2", Vector2i(12, 5), Vector2i(11, 5), "infantry_strike")
_check_attack_profile(failures, scene, "yellow_turban_3", Vector2i(17, 3), Vector2i(17, 1), "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, 7), Vector2i(2, 7), "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(17, 3), Vector2i(17, 1), "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, 6), Vector2i(3, 6), "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, 6), Vector2i(1, 6), "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")
if scene._movement_motion_sfx("cao_cao", Vector2i(1, 6), Vector2i(2, 6)) != "footstep":
failures.append("unit movement animation should use footstep SFX")
scene._on_unit_motion_requested("cao_cao", Vector2i(1, 6), Vector2i(2, 6))
var move_motion: Dictionary = scene.unit_motion_by_unit.get("cao_cao", {})
if str(move_motion.get("sfx", "")) != "footstep":
failures.append("stored movement motion should queue footstep SFX: %s" % str(move_motion))
if not bool(move_motion.get("sfx_played", false)):
failures.append("immediate movement motion should mark SFX as played: %s" % str(move_motion))
_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, 6), func(skill_state) -> void:
var enemy: Dictionary = skill_state.get_unit("yellow_turban_1")
enemy["pos"] = Vector2i(3, 6)
)
_check_skill_motion_signal(failures, "mend", "skill_heal", Vector2i(1, 6), 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, 6), 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 caster: Dictionary = state.get_unit("cao_cao")
var caster_pos: Vector2i = caster.get("pos", Vector2i(1, 6))
var expected := "cao_cao|%s|%d,%d|%d,%d" % [expected_kind, caster_pos.x, caster_pos.y, 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_campaign_entry_expectation(failures: Array[String], expectation: Dictionary) -> void:
var path := str(expectation.get("path", ""))
var label := str(expectation.get("label", path))
var state = BattleStateScript.new()
if not state.load_battle(path, {}, {}, {}, ["zhang_he"]):
failures.append("could not load %s data" % label)
return
_check_shop_items_unique(failures, state, "%s base" % label)
var conversations: Array = state.get_briefing().get("camp_conversations", [])
var expected_conversations: Array = expectation.get("conversations", [])
if conversations.size() != expected_conversations.size():
failures.append("%s briefing should expose exactly %d camp conversations" % [label, expected_conversations.size()])
for expected_variant in expected_conversations:
if typeof(expected_variant) != TYPE_ARRAY:
failures.append("%s conversation expectation should be an array" % label)
continue
var expected: Array = expected_variant
if expected.size() < 3:
failures.append("%s conversation expectation should include id, group, and officer id" % label)
continue
var expected_id := str(expected[0])
var expected_group := str(expected[1])
var expected_officer_id := str(expected[2])
_check_camp_conversation(
failures,
_find_camp_conversation(conversations, expected_id),
expected_id,
expected_group,
expected_officer_id
)
var merchant: Dictionary = state.get_shop_merchant()
if str(merchant.get("name", "")) != str(expectation.get("merchant", "")):
failures.append("%s shop merchant name should be present" % label)
var merchant_lines: Array = merchant.get("lines", [])
if merchant_lines.size() < 2:
failures.append("%s shop should expose merchant flavor lines" % label)
_check_shop_item_visibility(failures, state, "imperial_seal", false, "%s base" % label)
_check_shop_item_visibility(failures, state, "war_drum", false, "%s base" % label)
var imperial_state = BattleStateScript.new()
var imperial_flags: Dictionary = expectation.get("imperial_flag", {})
if not imperial_state.load_battle(path, {}, {}, imperial_flags, ["zhang_he"]):
failures.append("could not load %s imperial branch data" % label)
else:
_check_shop_items_unique(failures, imperial_state, "%s imperial branch" % label)
_check_shop_item_visibility(failures, imperial_state, "imperial_seal", true, "%s imperial branch" % label)
_check_shop_item_visibility(failures, imperial_state, "war_drum", false, "%s imperial branch" % label)
var drum_state = BattleStateScript.new()
var drum_flags: Dictionary = expectation.get("war_drum_flag", {})
if not drum_state.load_battle(path, {}, {}, drum_flags, ["zhang_he"]):
failures.append("could not load %s war drum branch data" % label)
else:
_check_shop_items_unique(failures, drum_state, "%s war drum branch" % label)
_check_shop_item_visibility(failures, drum_state, "war_drum", true, "%s war drum branch" % label)
_check_shop_item_visibility(failures, drum_state, "imperial_seal", false, "%s war drum branch" % label)
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_toolbar_icon_path(failures: Array[String], path: String, context: String) -> void:
if path.is_empty():
failures.append("%s has an empty icon 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 icon: %s" % [context, path])
return
var image := Image.new()
var err := image.load(path)
if err != OK:
failures.append("%s references unreadable icon: %s" % [context, path])
return
if image.get_width() < 128 or image.get_height() < 128:
failures.append("%s icon should keep high-resolution source pixels: %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 icon should have transparent corners: %s" % [context, path])
return
var opaque_samples := 0
var transparent_samples := 0
var green_fringe_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
var pixel := image.get_pixel(x, y)
if pixel.a <= 0.01:
transparent_samples += 1
elif pixel.a >= 0.80:
opaque_samples += 1
if pixel.g > 0.88 and pixel.r < 0.20 and pixel.b < 0.20:
green_fringe_samples += 1
if transparent_samples < int(total_samples * 0.12):
failures.append("%s icon should retain transparent UI padding: %s (%d/%d sampled pixels)" % [context, path, transparent_samples, total_samples])
if opaque_samples < int(total_samples * 0.18):
failures.append("%s icon should contain substantial artwork: %s (%d/%d sampled pixels)" % [context, path, opaque_samples, total_samples])
if green_fringe_samples > 0:
failures.append("%s icon should not keep chroma-key green pixels: %s (%d sampled)" % [context, path, green_fringe_samples])
func _check_panel_texture_path(failures: Array[String], path: String, context: String) -> void:
if path.is_empty():
failures.append("%s has an empty panel 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 panel texture: %s" % [context, path])
return
var image := Image.new()
var err := image.load(path)
if err != OK:
failures.append("%s references unreadable panel texture: %s" % [context, path])
return
if image.get_width() < 512 or image.get_height() < 512:
failures.append("%s should keep high-resolution source pixels: %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 opaque_samples := 0
var transparent_samples := 0
var green_fringe_samples := 0
var luma_min := 999.0
var luma_max := -999.0
var total_samples := 0
for y in range(0, image.get_height(), 8):
for x in range(0, image.get_width(), 8):
total_samples += 1
var pixel := image.get_pixel(x, y)
if pixel.a <= 0.01:
transparent_samples += 1
continue
if pixel.a >= 0.80:
opaque_samples += 1
if pixel.g > 0.80 and pixel.r < 0.18 and pixel.b < 0.18:
green_fringe_samples += 1
var luma := pixel.r * 0.299 + pixel.g * 0.587 + pixel.b * 0.114
luma_min = minf(luma_min, luma)
luma_max = maxf(luma_max, luma)
if transparent_samples < int(total_samples * 0.08):
failures.append("%s should retain transparent frame padding: %s (%d/%d sampled pixels)" % [context, path, transparent_samples, total_samples])
if opaque_samples < int(total_samples * 0.36):
failures.append("%s should contain substantial generated frame artwork: %s (%d/%d sampled pixels)" % [context, path, opaque_samples, total_samples])
if green_fringe_samples > 0:
failures.append("%s should not keep chroma-key green pixels: %s (%d sampled)" % [context, path, green_fringe_samples])
if luma_max - luma_min < 0.10:
failures.append("%s should contain visible material contrast: %s" % [context, path])
var warm_bias := _image_visible_warm_bias(image, 8)
if warm_bias > 0.12:
failures.append("%s should avoid a dominant yellow UI cast: %s (warm bias %.3f)" % [context, path, warm_bias])
func _check_button_texture_path(failures: Array[String], path: String, context: String) -> void:
if path.is_empty():
failures.append("%s has an empty button 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 button texture: %s" % [context, path])
return
var image := Image.new()
var err := image.load(path)
if err != OK:
failures.append("%s references unreadable button texture: %s" % [context, path])
return
if image.get_width() < 512 or image.get_height() < 192:
failures.append("%s should keep high-resolution source pixels: %s (%dx%d)" % [context, path, image.get_width(), image.get_height()])
return
if image.get_width() <= image.get_height():
failures.append("%s should be a wide button surface: %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 opaque_samples := 0
var transparent_samples := 0
var green_fringe_samples := 0
var luma_min := 999.0
var luma_max := -999.0
var total_samples := 0
for y in range(0, image.get_height(), 8):
for x in range(0, image.get_width(), 8):
total_samples += 1
var pixel := image.get_pixel(x, y)
if pixel.a <= 0.01:
transparent_samples += 1
continue
if pixel.a >= 0.80:
opaque_samples += 1
if pixel.g > 0.80 and pixel.r < 0.18 and pixel.b < 0.18:
green_fringe_samples += 1
var luma := pixel.r * 0.299 + pixel.g * 0.587 + pixel.b * 0.114
luma_min = minf(luma_min, luma)
luma_max = maxf(luma_max, luma)
if transparent_samples < int(total_samples * 0.08):
failures.append("%s should retain transparent button padding: %s (%d/%d sampled pixels)" % [context, path, transparent_samples, total_samples])
if opaque_samples < int(total_samples * 0.45):
failures.append("%s should contain substantial generated button artwork: %s (%d/%d sampled pixels)" % [context, path, opaque_samples, total_samples])
if green_fringe_samples > 0:
failures.append("%s should not keep chroma-key green pixels: %s (%d sampled)" % [context, path, green_fringe_samples])
if luma_max - luma_min < 0.08:
failures.append("%s should contain visible material contrast: %s" % [context, path])
var warm_bias := _image_visible_warm_bias(image, 8)
if warm_bias > 0.12:
failures.append("%s should avoid a dominant yellow UI cast: %s (warm bias %.3f)" % [context, path, warm_bias])
func _check_icon_button_texture_path(failures: Array[String], path: String, context: String) -> void:
if path.is_empty():
failures.append("%s has an empty icon button 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 icon button texture: %s" % [context, path])
return
var image := Image.new()
var err := image.load(path)
if err != OK:
failures.append("%s references unreadable icon button texture: %s" % [context, path])
return
if image.get_width() < 256 or image.get_height() < 256:
failures.append("%s should keep high-resolution compact source pixels: %s (%dx%d)" % [context, path, image.get_width(), image.get_height()])
return
if abs(image.get_width() - image.get_height()) > 4:
failures.append("%s should be a square compact icon surface: %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 opaque_samples := 0
var transparent_samples := 0
var green_fringe_samples := 0
var luma_min := 999.0
var luma_max := -999.0
var total_samples := 0
for y in range(0, image.get_height(), 8):
for x in range(0, image.get_width(), 8):
total_samples += 1
var pixel := image.get_pixel(x, y)
if pixel.a <= 0.01:
transparent_samples += 1
continue
if pixel.a >= 0.80:
opaque_samples += 1
if pixel.g > 0.80 and pixel.r < 0.18 and pixel.b < 0.18:
green_fringe_samples += 1
var luma := pixel.r * 0.299 + pixel.g * 0.587 + pixel.b * 0.114
luma_min = minf(luma_min, luma)
luma_max = maxf(luma_max, luma)
if transparent_samples < int(total_samples * 0.08):
failures.append("%s should retain transparent compact button padding: %s (%d/%d sampled pixels)" % [context, path, transparent_samples, total_samples])
if opaque_samples < int(total_samples * 0.42):
failures.append("%s should contain substantial generated compact button artwork: %s (%d/%d sampled pixels)" % [context, path, opaque_samples, total_samples])
if green_fringe_samples > 0:
failures.append("%s should not keep chroma-key green pixels: %s (%d sampled)" % [context, path, green_fringe_samples])
if luma_max - luma_min < 0.08:
failures.append("%s should contain visible material contrast: %s" % [context, path])
var warm_bias := _image_visible_warm_bias(image, 8)
if warm_bias > 0.12:
failures.append("%s should avoid a dominant yellow UI cast: %s (warm bias %.3f)" % [context, path, warm_bias])
func _image_visible_warm_bias(image: Image, sample_step: int) -> float:
var total_bias := 0.0
var visible_samples := 0
var step := maxi(1, sample_step)
for y in range(0, image.get_height(), step):
for x in range(0, image.get_width(), step):
var pixel := image.get_pixel(x, y)
if pixel.a <= 0.05:
continue
total_bias += maxf(0.0, ((pixel.r + pixel.g) * 0.5) - pixel.b)
visible_samples += 1
if visible_samples <= 0:
return 0.0
return total_bias / float(visible_samples)
func _check_tile_marker_texture_path(failures: Array[String], path: String, context: String) -> void:
if path.is_empty():
failures.append("%s has an empty marker 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 tile marker texture: %s" % [context, path])
return
var image := Image.new()
var err := image.load(path)
if err != OK:
failures.append("%s references unreadable tile marker texture: %s" % [context, path])
return
if image.get_width() < 512 or image.get_height() < 512:
failures.append("%s should keep high-resolution source pixels: %s (%dx%d)" % [context, path, image.get_width(), image.get_height()])
return
if image.get_width() != image.get_height():
failures.append("%s should be a square tactical marker: %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 opaque_samples := 0
var transparent_samples := 0
var green_fringe_samples := 0
var luma_min := 999.0
var luma_max := -999.0
var total_samples := 0
for y in range(0, image.get_height(), 8):
for x in range(0, image.get_width(), 8):
total_samples += 1
var pixel := image.get_pixel(x, y)
if pixel.a <= 0.01:
transparent_samples += 1
continue
if pixel.a >= 0.80:
opaque_samples += 1
if pixel.g > 0.80 and pixel.r < 0.18 and pixel.b < 0.18:
green_fringe_samples += 1
var luma := pixel.r * 0.299 + pixel.g * 0.587 + pixel.b * 0.114
luma_min = minf(luma_min, luma)
luma_max = maxf(luma_max, luma)
if transparent_samples < int(total_samples * 0.50):
failures.append("%s should keep the map visible through transparent center space: %s (%d/%d sampled pixels)" % [context, path, transparent_samples, total_samples])
if opaque_samples < int(total_samples * 0.16):
failures.append("%s should contain readable generated marker artwork: %s (%d/%d sampled pixels)" % [context, path, opaque_samples, total_samples])
if green_fringe_samples > 0:
failures.append("%s should not keep chroma-key green pixels: %s (%d sampled)" % [context, path, green_fringe_samples])
if luma_max - luma_min < 0.08:
failures.append("%s should contain visible generated material contrast: %s" % [context, path])
func _check_class_icon_texture_path(failures: Array[String], path: String, context: String) -> void:
if path.is_empty():
failures.append("%s has an empty icon 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 class icon texture: %s" % [context, path])
return
var image := Image.new()
var err := image.load(path)
if err != OK:
failures.append("%s references unreadable class icon texture: %s" % [context, path])
return
if image.get_width() < 512 or image.get_height() < 512:
failures.append("%s should keep high-resolution source pixels: %s (%dx%d)" % [context, path, image.get_width(), image.get_height()])
return
if image.get_width() != image.get_height():
failures.append("%s should be square: %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 opaque_samples := 0
var transparent_samples := 0
var green_fringe_samples := 0
var luma_min := 999.0
var luma_max := -999.0
var total_samples := 0
for y in range(0, image.get_height(), 8):
for x in range(0, image.get_width(), 8):
total_samples += 1
var pixel := image.get_pixel(x, y)
if pixel.a <= 0.01:
transparent_samples += 1
continue
if pixel.a >= 0.80:
opaque_samples += 1
if pixel.g > 0.82 and pixel.r < 0.16 and pixel.b < 0.16:
green_fringe_samples += 1
var luma := pixel.r * 0.299 + pixel.g * 0.587 + pixel.b * 0.114
luma_min = minf(luma_min, luma)
luma_max = maxf(luma_max, luma)
if transparent_samples < int(total_samples * 0.24):
failures.append("%s should keep transparent icon padding: %s (%d/%d sampled pixels)" % [context, path, transparent_samples, total_samples])
if opaque_samples < int(total_samples * 0.24):
failures.append("%s should contain substantial generated class artwork: %s (%d/%d sampled pixels)" % [context, path, opaque_samples, total_samples])
if green_fringe_samples > 0:
failures.append("%s should not keep chroma-key green pixels: %s (%d sampled)" % [context, path, green_fringe_samples])
if luma_max - luma_min < 0.08:
failures.append("%s should contain visible generated material contrast: %s" % [context, path])
func _check_map_badge_texture_path(failures: Array[String], path: String, context: String) -> void:
if path.is_empty():
failures.append("%s has an empty badge 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 map badge texture: %s" % [context, path])
return
var image := Image.new()
var err := image.load(path)
if err != OK:
failures.append("%s references unreadable map badge texture: %s" % [context, path])
return
if image.get_width() < 512 or image.get_height() < 512:
failures.append("%s should keep high-resolution source pixels: %s (%dx%d)" % [context, path, image.get_width(), image.get_height()])
return
if image.get_width() != image.get_height():
failures.append("%s should be square: %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 opaque_samples := 0
var transparent_samples := 0
var green_fringe_samples := 0
var luma_min := 999.0
var luma_max := -999.0
var total_samples := 0
for y in range(0, image.get_height(), 8):
for x in range(0, image.get_width(), 8):
total_samples += 1
var pixel := image.get_pixel(x, y)
if pixel.a <= 0.01:
transparent_samples += 1
continue
if pixel.a >= 0.80:
opaque_samples += 1
if pixel.a > 0.05 and pixel.g > 0.92 and pixel.r < 0.08 and pixel.b < 0.08:
green_fringe_samples += 1
var luma := pixel.r * 0.299 + pixel.g * 0.587 + pixel.b * 0.114
luma_min = minf(luma_min, luma)
luma_max = maxf(luma_max, luma)
if transparent_samples < int(total_samples * 0.44):
failures.append("%s should keep transparent padding for map readability: %s (%d/%d sampled pixels)" % [context, path, transparent_samples, total_samples])
if opaque_samples < int(total_samples * 0.18):
failures.append("%s should contain readable generated badge artwork: %s (%d/%d sampled pixels)" % [context, path, opaque_samples, total_samples])
if green_fringe_samples > 0:
failures.append("%s should not keep chroma-key green pixels: %s (%d sampled)" % [context, path, green_fringe_samples])
if luma_max - luma_min < 0.08:
failures.append("%s should contain visible generated material contrast: %s" % [context, path])
func _check_terrain_texture_path(failures: Array[String], path: String, context: String) -> void:
if path.is_empty():
failures.append("%s has an empty texture 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 texture: %s" % [context, path])
return
var image := Image.new()
var err := image.load(path)
if err != OK:
failures.append("%s references unreadable texture: %s" % [context, path])
return
if image.get_width() < 128 or image.get_height() < 128:
failures.append("%s should keep high-resolution terrain source pixels: %s (%dx%d)" % [context, path, image.get_width(), image.get_height()])
return
var min_luma := 999.0
var max_luma := -999.0
for y in range(0, image.get_height(), 16):
for x in range(0, image.get_width(), 16):
var pixel := image.get_pixel(x, y)
var luma := pixel.r * 0.299 + pixel.g * 0.587 + pixel.b * 0.114
min_luma = minf(min_luma, luma)
max_luma = maxf(max_luma, luma)
if max_luma - min_luma < 0.08:
failures.append("%s should contain visible generated terrain texture variation: %s" % [context, path])
func _check_terrain_feature_texture_path(failures: Array[String], path: String, context: String) -> void:
if path.is_empty():
failures.append("%s has an empty feature 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 terrain feature texture: %s" % [context, path])
return
var image := Image.new()
var err := image.load(path)
if err != OK:
failures.append("%s references unreadable terrain feature texture: %s" % [context, path])
return
if image.get_width() < 512 or image.get_height() < 512:
failures.append("%s should keep high-resolution overlay pixels: %s (%dx%d)" % [context, path, image.get_width(), image.get_height()])
return
if image.get_width() != image.get_height():
failures.append("%s should be square: %s (%dx%d)" % [context, path, image.get_width(), image.get_height()])
return
var transparent_samples := 0
var opaque_samples := 0
var green_fringe_samples := 0
var min_luma := 999.0
var max_luma := -999.0
var total_samples := 0
for y in range(0, image.get_height(), 8):
for x in range(0, image.get_width(), 8):
total_samples += 1
var pixel := image.get_pixel(x, y)
if pixel.a <= 0.01:
transparent_samples += 1
continue
if pixel.a >= 0.32:
opaque_samples += 1
if pixel.a > 0.05 and pixel.g > 0.92 and pixel.r < 0.08 and pixel.b < 0.08:
green_fringe_samples += 1
var luma := pixel.r * 0.299 + pixel.g * 0.587 + pixel.b * 0.114
min_luma = minf(min_luma, luma)
max_luma = maxf(max_luma, luma)
if transparent_samples < int(total_samples * 0.18):
failures.append("%s should keep transparent padding so the map art shows through: %s (%d/%d sampled pixels)" % [context, path, transparent_samples, total_samples])
if opaque_samples < int(total_samples * 0.04):
failures.append("%s should contain readable generated feature artwork: %s (%d/%d sampled pixels)" % [context, path, opaque_samples, total_samples])
if green_fringe_samples > 0:
failures.append("%s should not keep chroma-key green pixels: %s (%d sampled)" % [context, path, green_fringe_samples])
if max_luma - min_luma < 0.06:
failures.append("%s should contain visible generated feature variation: %s" % [context, path])
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()])