extends SceneTree const BattleStateScript := preload("res://scripts/core/battle_state.gd") const BattleSceneScript := preload("res://scripts/scenes/battle_scene.gd") func _init() -> void: var failures: Array[String] = [] _check_required_only_roster(failures) _check_optional_roster(failures) _check_roster_button_guard(failures) _check_roster_visual_rows(failures) if failures.is_empty(): print("roster choices smoke ok") quit(0) return for failure in failures: push_error(failure) quit(1) func _check_required_only_roster(failures: Array[String]) -> void: var state = _loaded_state(failures, "required-only roster", "res://data/scenarios/001_yellow_turbans.json") if state == null: return if not state.has_deployment_roster(): failures.append("001 should keep roster rules for required deployments") if state.has_deployment_roster_choices(): failures.append("001 should not expose roster choices when every player unit is required") if state.try_set_unit_deployed("cao_cao", false): failures.append("001 should not allow Cao Cao to be moved to reserve") if state.try_set_unit_deployed("xiahou_dun", false): failures.append("001 should not allow Xiahou Dun to be moved to reserve") func _check_optional_roster(failures: Array[String]) -> void: var state = _loaded_state(failures, "optional roster", "res://data/scenarios/007_xian_emperor_escort.json") if state == null: return if not state.has_deployment_roster_choices(): failures.append("007 should expose roster choices for optional deployed officers") func _check_roster_button_guard(failures: Array[String]) -> void: var scene = BattleSceneScript.new() scene._create_hud() scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json") scene.battle_started = false scene.briefing_panel.visible = true scene._on_roster_pressed() if scene.roster_menu.visible: failures.append("roster menu should not open when there are no roster choices") scene.free() func _check_roster_visual_rows(failures: Array[String]) -> void: var scene = BattleSceneScript.new() scene._create_hud() if not scene.state.load_battle( "res://data/scenarios/007_xian_emperor_escort.json", {}, {}, {}, ["dian_wei", "xiahou_yuan", "cao_ren"] ): failures.append("007 could not load for roster visual rows") scene.free() return scene._rebuild_roster_menu() if scene.roster_list == null or scene.roster_list.get_child_count() < 5: failures.append("roster visual rows should list required and optional officers") else: var visible_text := _collect_child_text(scene.roster_list) var tooltip_text := _collect_child_tooltips(scene.roster_list) if not visible_text.contains("조조") or not visible_text.contains("품계") or not visible_text.contains("출진"): failures.append("roster rows should show compact name/status badges: %s" % visible_text) if visible_text.contains("병장:") or visible_text.contains("청동검") or visible_text.contains("피갑"): failures.append("roster rows should leave equipment detail to tooltips: %s" % visible_text) if not tooltip_text.contains("병장:") or not tooltip_text.contains("필수 출진") or not tooltip_text.contains("호위 대상"): failures.append("roster row tooltips should preserve equipment and lock detail: %s" % tooltip_text) if not tooltip_text.contains("클릭하면 출진과 예비"): failures.append("roster row tooltips should explain optional toggle behavior: %s" % tooltip_text) if not _has_descendant_texture(scene.roster_list): failures.append("roster rows should reuse officer portrait artwork") var cao_cao: Dictionary = scene.state.get_unit("cao_cao_ch7") if scene._format_roster_unit_button_text(cao_cao).contains("품계"): failures.append("roster row button text should stay compact") scene.free() func _loaded_state(failures: Array[String], label: String, path: String): var state = BattleStateScript.new() if not state.load_battle(path): failures.append("%s could not load scenario: %s" % [label, path]) return null return state 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_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 _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 _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