Files
heros/tools/smoke_visual_assets.gd

6269 lines
363 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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_qhd_ui_import_settings(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_qhd_ui_import_settings(failures: Array[String]) -> void:
var import_paths := _collect_png_import_files("res://art/ui")
if import_paths.size() < 50:
failures.append("UI import check should cover the generated interface atlas, got %d files" % import_paths.size())
for import_path in import_paths:
var text := FileAccess.get_file_as_string(import_path)
if text.is_empty():
failures.append("UI import metadata should be readable: %s" % import_path)
continue
if not text.contains("mipmaps/generate=true"):
failures.append("QHD UI texture should generate mipmaps to avoid jagged fullscreen scaling: %s" % import_path)
if not text.contains("process/fix_alpha_border=true"):
failures.append("UI texture import should keep alpha border fix for clean transparent edges: %s" % import_path)
func _collect_png_import_files(root_path: String) -> Array[String]:
var result: Array[String] = []
var dir := DirAccess.open(root_path)
if dir == null:
return result
dir.list_dir_begin()
var file_name := dir.get_next()
while not file_name.is_empty():
if file_name.begins_with("."):
file_name = dir.get_next()
continue
var full_path := "%s/%s" % [root_path, file_name]
if dir.current_is_dir():
result.append_array(_collect_png_import_files(full_path))
elif file_name.ends_with(".png.import"):
result.append(full_path)
file_name = dir.get_next()
dir.list_dir_end()
return result
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])
if not camp_overview.contains("전장:") or not camp_overview.contains("전장"):
failures.append("camp overview should include a readable battlefield scale label: %s" % camp_overview)
if camp_overview.contains("칸 x") or camp_overview.contains("×") or camp_overview.contains("칸x"):
failures.append("camp overview should not expose raw map dimensions: %s" % camp_overview)
if camp_overview.contains("칸 +"):
failures.append("camp overview should keep recovery cell counts out of visible text: %s" % 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 formation_tooltips.contains("2,7") or formation_tooltips.contains("1,6"):
failures.append("formation row tooltip should describe placement by battlefield zone, not raw coordinates: %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 should keep story artwork out of the first menu")
_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._show_briefing()
_check_readable_text_control(failures, scene.briefing_title_label, "briefing title", 18)
_check_readable_text_control(failures, scene.briefing_objective_label, "briefing objective", 12)
_check_readable_text_control(failures, scene.briefing_label, "briefing body", 12)
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)
scene._show_command_notice("군령 갱신", "목표: 성채를 장악하라.\n주의: 조조가 퇴각하면 패한다.")
_check_readable_text_control(failures, scene.objective_notice_label, "command notice", 13)
_check_readable_text_control(failures, scene.auto_end_turn_prompt_label, "auto end turn prompt", 13)
_check_readable_text_control(failures, scene.auto_end_turn_yes_button, "auto end turn confirm", 12)
_check_readable_text_control(failures, scene.auto_end_turn_no_button, "auto end turn decline", 12)
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)
if font_size >= 13 and outline_size < 2:
failures.append("%s text should carry a stronger outline on generated panels: %d" % [context, outline_size])
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.32:
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 village_cell_detail := scene._format_cell_info_detail_text(Vector2i(6, 5))
if village_cell_detail.contains("7,6"):
failures.append("cell terrain detail should avoid raw coordinates: %s" % village_cell_detail)
if not village_cell_detail.contains("회복 +6/차례") or not village_cell_detail.contains("차례 시작"):
failures.append("cell terrain detail should explain recovery timing: %s" % village_cell_detail)
var village_summary := scene._format_cell_info_summary_text(Vector2i(6, 5))
if not village_summary.contains("회복 +6/차례"):
failures.append("cell terrain summary should show per-turn recovery: %s" % village_summary)
var village_badge_lines := scene._join_strings(scene._hover_info_badge_lines(Vector2i(6, 5)), "\n")
if not village_badge_lines.contains("회복 +6/차례"):
failures.append("hover badge should show per-turn recovery: %s" % village_badge_lines)
var recovery_overview := scene._format_briefing_recovery_terrain_text()
if not recovery_overview.contains("차례 시작"):
failures.append("briefing recovery overview should mention recovery timing: %s" % recovery_overview)
if not recovery_overview.contains("마을 회복 +6") or not recovery_overview.contains("성채 회복 +8"):
failures.append("briefing recovery overview should keep recovery amount but hide cell counts: %s" % recovery_overview)
if recovery_overview.contains("칸 +"):
failures.append("briefing recovery overview should not expose recovery cell counts: %s" % recovery_overview)
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")
elif scene.auto_end_turn_yes_button.icon == null or str(scene.auto_end_turn_yes_button.get_meta("command_icon", "")) != "end_turn":
failures.append("auto end turn confirm button should use a generated end-turn icon")
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")
elif scene.auto_end_turn_no_button.icon == null or str(scene.auto_end_turn_no_button.get_meta("command_icon", "")) != "cancel":
failures.append("auto end turn decline button should use a generated cancel icon")
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")
if scene.objective_notice_panel != null:
scene.objective_notice_panel.visible = false
scene.battle_started = false
scene._on_log_added("동쪽 들판에서 황건 잔병이 합류했다.")
if scene.objective_notice_panel != null and scene.objective_notice_panel.visible:
failures.append("pre-battle event-like logs should not create battlefield command notices")
scene.battle_started = true
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("lure-line event log should become a readable battlefield notice: %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 not _has_descendant_texture(scene.objective_notice_icon_strip):
failures.append("lure-line event notice should show generated visual icons")
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("reinforcement event log should become a battlefield notice: %s" % ("" if scene.objective_notice_label == null else scene.objective_notice_label.text))
if scene.objective_notice_icon_strip == null or scene.objective_notice_icon_strip.get_child_count() < 2:
failures.append("reinforcement event notice should include threat and warning icons")
if scene._floating_text_color("reinforcement") == scene._floating_text_color("priority"):
failures.append("reinforcement feedback should use a distinct floating text color")
if scene._combat_feedback_sfx_key("증원", "reinforcement") != "footstep":
failures.append("reinforcement feedback should use the footstep arrival SFX")
if not scene._combat_feedback_sfx_key("표적", "priority").is_empty():
failures.append("priority targeting feedback should not inherit reinforcement arrival SFX")
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("supply event log should become a battlefield notice: %s" % ("" if scene.objective_notice_label == null else scene.objective_notice_label.text))
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("castle event log should become a battlefield notice: %s" % ("" if scene.objective_notice_label == null else scene.objective_notice_label.text))
var localized_lines := scene._normalized_dialogue_lines([{"speaker": "Cao Cao", "display_speaker": "조조", "text": "군령을 전하라."}])
if localized_lines.is_empty() or str((localized_lines[0] as Dictionary).get("speaker", "")) != "조조":
failures.append("dialogue normalization should preserve display_speaker for localized names")
else:
scene.active_dialogue_lines = localized_lines
scene.active_dialogue_index = 0
scene._render_dialogue_line()
if scene.dialogue_speaker_label == null or scene.dialogue_speaker_label.text != "조조":
failures.append("dialogue speaker label should render localized display_speaker")
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 < 492.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 < 160.0 or caption_panel.custom_minimum_size.y > 188.0:
failures.append("opening caption should be large enough for readable Korean without covering the story art: %s" % str(caption_panel.custom_minimum_size))
if str(caption_panel.get_meta("panel_style_variant", "")) != "cinematic_caption" or bool(caption_panel.get_meta("generated_panel_texture", true)):
failures.append("opening caption should use a flat high-contrast cinematic surface so text stays visible")
if caption_panel.z_index < 20:
failures.append("opening caption should render above the generated story image")
if scene.opening_prologue_title_label != null and scene.opening_prologue_title_label.get_theme_color("font_color").get_luminance() < 0.78:
failures.append("opening title should use bright text over the story image")
if scene.opening_prologue_body_label != null:
if scene.opening_prologue_body_label.get_theme_color("font_color").get_luminance() < 0.86:
failures.append("opening body should use bright text over the story image")
if scene.opening_prologue_body_label.custom_minimum_size.x < 730.0:
failures.append("opening body should be wide enough for Korean opening text: %s" % str(scene.opening_prologue_body_label.custom_minimum_size))
if scene.opening_prologue_body_label.get_theme_constant("outline_size") < 3:
failures.append("opening body should have a strong outline over generated art")
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")
if scene.opening_prologue_next_button.expand_icon or scene.opening_prologue_next_button.get_theme_constant("icon_max_width") > 28:
failures.append("opening next icon should stay capped so it cannot cover story text")
_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")
if scene.opening_prologue_skip_button.expand_icon or scene.opening_prologue_skip_button.get_theme_constant("icon_max_width") > 28:
failures.append("opening skip icon should stay capped so it cannot cover story text")
_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 < 18 or body_size > 22:
failures.append("opening story body font should stay readable inside the 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 _count_descendants_by_meta(root: Node, meta_key: String, expected_value: Variant) -> int:
if root == null:
return 0
var count := 0
if root.has_meta(meta_key) and root.get_meta(meta_key) == expected_value:
count += 1
for child in root.get_children():
count += _count_descendants_by_meta(child, meta_key, expected_value)
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()])
_check_button_texture_slice_for_control(failures, button, style, context, 10, 10)
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()])
var min_top_margin := 8 if bool(button.get_meta("opening_story_button", false)) else 10
_check_button_texture_slice_for_control(failures, button, style, context, 16, min_top_margin)
var min_content_margin := 6 if bool(button.get_meta("opening_story_button", false)) else 8
if style.content_margin_left < min_content_margin or style.content_margin_top < 5:
failures.append("%s generated button texture should reserve readable content margins" % context)
func _check_button_texture_slice_for_control(
failures: Array[String],
button: Button,
style: StyleBoxTexture,
context: String,
min_x: int,
min_y: int
) -> void:
var size := button.custom_minimum_size
if size.x <= 0.0 or size.y <= 0.0:
size = button.get_combined_minimum_size()
var max_x := maxi(1, int(floor(size.x * 0.5)) - 2)
var max_y := maxi(1, int(floor(size.y * 0.5)) - 2)
if style.texture_margin_left < min_x or style.texture_margin_right < min_x:
failures.append("%s generated button texture should keep visible horizontal frame slices: %d/%d" % [context, style.texture_margin_left, style.texture_margin_right])
if style.texture_margin_top < min_y or style.texture_margin_bottom < min_y:
failures.append("%s generated button texture should keep visible vertical frame slices: %d/%d" % [context, style.texture_margin_top, style.texture_margin_bottom])
if style.texture_margin_left > max_x or style.texture_margin_right > max_x:
failures.append("%s generated button horizontal slices should fit the control width: margins %d/%d size %.1f" % [context, style.texture_margin_left, style.texture_margin_right, size.x])
if style.texture_margin_top > max_y or style.texture_margin_bottom > max_y:
failures.append("%s generated button vertical slices should fit the control height: margins %d/%d size %.1f" % [context, style.texture_margin_top, style.texture_margin_bottom, size.y])
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_status_label == null or scene.battle_unit_list_status_label.text.contains("행동") or scene.battle_unit_list_status_label.text.contains("완료"):
failures.append("ally battle unit list status should keep action counts in the tooltip: %s" % ("" if scene.battle_unit_list_status_label == null else scene.battle_unit_list_status_label.text))
elif not scene.battle_unit_list_status_label.tooltip_text.contains("명령 가능") or not scene.battle_unit_list_status_label.tooltip_text.contains("행동 완료"):
failures.append("ally battle unit list status tooltip should retain action counts: %s" % scene.battle_unit_list_status_label.tooltip_text)
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 ally_badge_text.contains("%"):
failures.append("ally battle unit list should move exact HP percent into the hover bar tooltip: %s" % ally_badge_text)
if not ally_badge_text.contains("서측"):
failures.append("ally battle unit list should show compact zone badges: %s" % ally_badge_text)
if _find_descendant_by_name(scene.battle_unit_list_rows, "BattleUnitListHpBar") == null:
failures.append("ally battle unit list should show a visual HP bar instead of inline percent text")
if _count_descendants_by_meta(scene.battle_unit_list_rows, "battle_unit_list_badge_kind", "action") < 2:
failures.append("ally battle unit list should show action state as icon badges")
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)
for action_label in ["행동", "완료", "호위", "퇴각"]:
if ally_badge_text.contains(action_label):
failures.append("ally battle unit list should move action labels 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 scene.battle_unit_list_status_label.text.contains("잔적") or not scene.battle_unit_list_status_label.text.contains("부대"):
failures.append("enemy battle unit list should summarize remaining enemies without old dense wording: %s" % ("" if scene.battle_unit_list_status_label == null else scene.battle_unit_list_status_label.text))
elif not scene.battle_unit_list_status_label.tooltip_text.contains("남은 적군") or not scene.battle_unit_list_status_label.tooltip_text.contains("행동 방침"):
failures.append("enemy battle unit list status tooltip should retain enemy count detail: %s" % scene.battle_unit_list_status_label.tooltip_text)
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 enemy_badge_text.contains("%"):
failures.append("enemy battle unit list should move exact HP percent into the hover bar tooltip: %s" % enemy_badge_text)
if _find_descendant_by_name(scene.battle_unit_list_rows, "BattleUnitListHpBar") == null:
failures.append("enemy battle unit list should show a visual HP bar instead of inline percent text")
if _count_descendants_by_meta(scene.battle_unit_list_rows, "battle_unit_list_badge_kind", "action") < 1:
failures.append("enemy battle unit list should show AI state as icon badges")
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)
for action_label in ["", "대기", "수비", "경계"]:
if enemy_badge_text.contains(action_label):
failures.append("enemy battle unit list should move AI state labels 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_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 enemy_tooltips.contains("가장 가까운 아군은 유인 밖") or not enemy_tooltips.contains("가장 가까운 아군은 거점 밖"):
failures.append("enemy battle unit list tooltips should expose current lure/guard distance: %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")
scene.state.select_unit("cao_cao")
scene._refresh_ranges()
var enemy_alive_states := {}
for foe in scene.state.get_living_units(BattleStateScript.TEAM_ENEMY):
enemy_alive_states[str(foe.get("id", ""))] = bool(foe.get("alive", false))
foe["alive"] = false
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")
for unit_id in enemy_alive_states.keys():
var restored_enemy: Dictionary = scene.state.get_unit(str(unit_id))
if not restored_enemy.is_empty():
restored_enemy["alive"] = bool(enemy_alive_states[unit_id])
scene._refresh_ranges()
enemy["pos"] = Vector2i(1, 5)
scene._refresh_ranges()
scene.hover_cell = Vector2i(2, 5)
var danger_move_badge := scene._target_preview_badge()
if str(danger_move_badge.get("text", "")) != "위험" or str(danger_move_badge.get("kind", "")) != "danger":
failures.append("moving into current enemy attack range should show DANGER badge: %s" % str(danger_move_badge))
if not str(danger_move_badge.get("tooltip", "")).contains("현재 적 공격권"):
failures.append("danger move badge should explain current enemy attack range: %s" % str(danger_move_badge))
var danger_detail_text := scene._target_preview_detail_badge_text(str(danger_move_badge.get("tooltip", "")))
if not danger_detail_text.contains("현재 적 공격권"):
failures.append("danger move badge detail text should be visible on the map: %s" % danger_detail_text)
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")
var edge_detail_rect: Rect2 = scene._target_preview_detail_badge_rect(
Vector2i(13, 9),
"현재 적 공격권: 장만성, 마을 방패병, +2",
edge_badge_rect
)
if edge_detail_rect.position.x < board_rect.position.x or edge_detail_rect.position.y < board_rect.position.y:
failures.append("edge target preview detail should stay inside visible board minimum: %s / %s" % [str(edge_detail_rect), str(board_rect)])
if edge_detail_rect.end.x > board_rect.end.x or edge_detail_rect.end.y > board_rect.end.y:
failures.append("edge target preview detail should stay inside visible board maximum: %s / %s" % [str(edge_detail_rect), str(board_rect)])
if edge_detail_rect.size.y > BattleSceneScript.TARGET_PREVIEW_DETAIL_BADGE_HEIGHT + 0.1:
failures.append("target preview detail should remain a compact one-line strip: %s" % str(edge_detail_rect))
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))
if not sentry_hover_text.contains("유인 밖 4칸"):
failures.append("sleeping sentry hover badge should expose current lure distance: %s" % str(sentry_hover_badge))
var lure_move_badge := scene._move_cell_preview_badge(Vector2i(5, 6))
if str(lure_move_badge.get("text", "")) != "유인" or str(lure_move_badge.get("kind", "")) != "advance":
failures.append("move preview should mark cells that wake sleeping sentries: %s" % str(lure_move_badge))
if not str(lure_move_badge.get("tooltip", "")).contains("초병 반응권"):
failures.append("lure move badge should explain sentry reaction zone: %s" % str(lure_move_badge))
if not scene._target_preview_detail_badge_text(str(lure_move_badge.get("tooltip", ""))).contains("초병 반응권"):
failures.append("lure move badge detail text should be drawable: %s" % str(lure_move_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))
var hovered_sentry: Dictionary = scene._hovered_ai_zone_unit()
if str(hovered_sentry.get("id", "")) != "yellow_turban_5":
failures.append("hovered sentry should expose a drawable AI zone: %s" % str(hovered_sentry))
var sentry_zone_cells: Array[Vector2i] = scene._ai_zone_cells_for_unit(central_sentry)
if sentry_zone_cells.size() != 61:
failures.append("central sentry lure zone should be a radius-5 diamond, got %d cells" % sentry_zone_cells.size())
if not sentry_zone_cells.has(Vector2i(10, 6)) or not sentry_zone_cells.has(Vector2i(5, 6)) or sentry_zone_cells.has(Vector2i(4, 6)):
failures.append("central sentry lure zone should include only cells within radius 5")
if scene._ai_zone_overlay_marker_key(central_sentry) != "attack":
failures.append("awake sentry AI zone should use attack-tinted marker artwork")
central_sentry["ai_awake"] = false
if scene._ai_zone_overlay_marker_key(central_sentry) != "move":
failures.append("sleeping sentry AI zone should use movement-tinted marker artwork")
var sentry_fill := scene._ai_zone_overlay_fill_color(central_sentry)
if sentry_fill.a > 0.24:
failures.append("sentry AI zone fill should stay subtle on top of terrain: %.2f" % sentry_fill.a)
var village_guard: Dictionary = scene.state.get_unit("yellow_turban_4")
scene.hover_cell = village_guard.get("pos", Vector2i(7, 5))
var guard_hover_badge := scene._hover_info_badge()
var guard_hover_text := str(guard_hover_badge.get("lines", []))
if not guard_hover_text.contains("수비") or not guard_hover_text.contains("거점 밖 4칸"):
failures.append("guard hover badge should expose current guard-zone distance: %s" % str(guard_hover_badge))
var guard_move_badge := scene._move_cell_preview_badge(Vector2i(4, 5))
if str(guard_move_badge.get("text", "")) != "경계" or str(guard_move_badge.get("kind", "")) != "select":
failures.append("move preview should mark guard reaction cells: %s" % str(guard_move_badge))
if not str(guard_move_badge.get("tooltip", "")).contains("거점 경계권"):
failures.append("guard move badge should explain guard reaction zone: %s" % str(guard_move_badge))
if not scene._target_preview_detail_badge_text(str(guard_move_badge.get("tooltip", ""))).contains("거점 경계권"):
failures.append("guard move badge detail text should be drawable: %s" % str(guard_move_badge))
var hovered_guard: Dictionary = scene._hovered_ai_zone_unit()
if str(hovered_guard.get("id", "")) != "yellow_turban_4":
failures.append("hovered guard should expose a drawable AI zone: %s" % str(hovered_guard))
var guard_zone_cells: Array[Vector2i] = scene._ai_zone_cells_for_unit(village_guard)
if guard_zone_cells.size() != 25:
failures.append("village guard zone should be a radius-3 diamond, got %d cells" % guard_zone_cells.size())
if not guard_zone_cells.has(Vector2i(7, 5)) or not guard_zone_cells.has(Vector2i(4, 5)) or guard_zone_cells.has(Vector2i(3, 5)):
failures.append("village guard zone should include only cells within radius 3")
if scene._ai_zone_overlay_marker_key(village_guard) != "select":
failures.append("guard AI zone should use select-tinted marker artwork")
var guard_fill := scene._ai_zone_overlay_fill_color(village_guard)
if guard_fill.a > 0.24:
failures.append("guard AI zone fill should stay subtle on top of terrain: %.2f" % guard_fill.a)
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("목표: 성채 장악"):
failures.append("castle objective cell info tooltip should explain the capture objective: %s" % scene.cell_info_label.tooltip_text)
if scene.cell_info_label != null and (not scene.cell_info_label.tooltip_text.contains("회복 +8/차례") or not scene.cell_info_label.tooltip_text.contains("차례 시작")):
failures.append("castle objective cell info tooltip should explain per-turn recovery timing: %s" % scene.cell_info_label.tooltip_text)
if scene.cell_info_label != null and scene.cell_info_label.tooltip_text.contains("21,2"):
failures.append("castle objective cell info tooltip should avoid raw coordinates: %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 := _load_smoke_image(path)
if image == null:
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 := _load_smoke_image(path)
if image == null:
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 := _load_smoke_image(path)
if image == null:
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 := _load_smoke_image(path)
if image == null:
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 := _load_smoke_image(path)
if image == null:
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 _load_smoke_image(path: String) -> Image:
var bytes := FileAccess.get_file_as_bytes(path)
if bytes.is_empty():
return null
var image := Image.new()
var lower_path := path.to_lower()
var err := OK
if lower_path.ends_with(".png"):
err = image.load_png_from_buffer(bytes)
elif lower_path.ends_with(".jpg") or lower_path.ends_with(".jpeg"):
err = image.load_jpg_from_buffer(bytes)
elif lower_path.ends_with(".webp"):
err = image.load_webp_from_buffer(bytes)
else:
var disk_path := ProjectSettings.globalize_path(path) if path.begins_with("res://") or path.begins_with("user://") else path
err = image.load(disk_path)
if err != OK:
return null
return image
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 := _load_smoke_image(path)
if image == null:
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 := _load_smoke_image(path)
if image == null:
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 := _load_smoke_image(path)
if image == null:
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 := _load_smoke_image(path)
if image == null:
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 := _load_smoke_image(path)
if image == null:
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 := _load_smoke_image(path)
if image == null:
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()])