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_core_unequip_contract(failures) _check_unequip_guards(failures) _check_armor_bonus_removal(failures) _check_snapshot_reload_and_sell_stock(failures) _check_unequip_ui_entries(failures) if failures.is_empty(): print("equipment unequip smoke ok") quit(0) return for failure in failures: push_error(failure) quit(1) func _check_core_unequip_contract(failures: Array[String]) -> void: var state = _loaded_state(failures, "core unequip") if state == null: return var cao_cao: Dictionary = state.get_unit("cao_cao") var before_atk := int(cao_cao.get("atk", 0)) var before_inventory: Dictionary = state.get_inventory_snapshot() var weapon_item: Dictionary = state.get_item_def("bronze_sword") var weapon_bonus := int(weapon_item.get("bonuses", {}).get("atk", 0)) if not state.get_equipped_equipment_slots("cao_cao").has("weapon"): failures.append("Cao Cao should expose an equipped weapon slot") if not state.try_unequip_item("cao_cao", "weapon"): failures.append("Cao Cao should be able to unequip Bronze Sword before acting") return var after_unit: Dictionary = state.get_unit("cao_cao") var after_equipment: Dictionary = state.get_equipment_snapshot("cao_cao") var after_inventory: Dictionary = state.get_inventory_snapshot() if after_equipment.has("weapon"): failures.append("weapon slot should be removed after unequip: %s" % str(after_equipment)) if int(after_inventory.get("bronze_sword", 0)) != int(before_inventory.get("bronze_sword", 0)) + 1: failures.append("unequip should return Bronze Sword to inventory") if int(after_unit.get("atk", 0)) != before_atk - weapon_bonus: failures.append("unequip should remove weapon ATK bonus: before %d after %d bonus %d" % [ before_atk, int(after_unit.get("atk", 0)), weapon_bonus ]) if int(after_unit.get("range", 0)) != 1 or int(after_unit.get("min_range", 0)) != 1: failures.append("unequipped Cao Cao should return to class attack range 1-1") var inventory_after_first: Dictionary = state.get_inventory_snapshot() if state.try_unequip_item("cao_cao", "weapon"): failures.append("unequipping an empty weapon slot should fail") var inventory_after_repeat: Dictionary = state.get_inventory_snapshot() if int(inventory_after_repeat.get("bronze_sword", 0)) != int(inventory_after_first.get("bronze_sword", 0)): failures.append("failed repeat unequip should not duplicate inventory") if not state.try_equip_item("cao_cao", "bronze_sword"): failures.append("returned Bronze Sword should be equippable again") var reequip_unit: Dictionary = state.get_unit("cao_cao") var reequip_inventory: Dictionary = state.get_inventory_snapshot() if int(reequip_unit.get("atk", 0)) != before_atk: failures.append("re-equipping should restore weapon ATK") if int(reequip_inventory.get("bronze_sword", 0)) != int(before_inventory.get("bronze_sword", 0)): failures.append("re-equipping should consume the returned weapon stock") func _check_unequip_guards(failures: Array[String]) -> void: var state = _loaded_state(failures, "unequip guards") if state == null: return var before_equipment: Dictionary = state.get_equipment_snapshot("cao_cao") var before_inventory: Dictionary = state.get_inventory_snapshot() var cao_cao: Dictionary = state.get_unit("cao_cao") cao_cao["moved"] = true if state.try_unequip_item("cao_cao", "weapon"): failures.append("moved units should not be able to unequip") cao_cao["moved"] = false cao_cao["acted"] = true if state.try_unequip_item("cao_cao", "weapon"): failures.append("acted units should not be able to unequip") cao_cao["acted"] = false var enemy: Dictionary = state.get_unit("yellow_turban_1") enemy["equipment"] = {"weapon": "bronze_sword"} if state.try_unequip_item("yellow_turban_1", "weapon"): failures.append("enemy units should not be able to unequip") var protected_state = BattleStateScript.new() if protected_state.load_battle("res://data/scenarios/007_xian_emperor_escort.json"): var envoy: Dictionary = protected_state.get_unit("protected_envoy") envoy["equipment"] = {"weapon": "bronze_sword"} if protected_state.try_unequip_item("protected_envoy", "weapon"): failures.append("non-controllable protected units should not be able to unequip") else: failures.append("could not load protected-unit scenario for unequip guard") if state.get_equipment_snapshot("cao_cao") != before_equipment: failures.append("failed guard unequips should not alter Cao Cao equipment") if state.get_inventory_snapshot() != before_inventory: failures.append("failed guard unequips should not alter inventory") func _check_armor_bonus_removal(failures: Array[String]) -> void: var state = _loaded_state(failures, "armor bonus removal") if state == null: return var cao_cao: Dictionary = state.get_unit("cao_cao") var base_max_mp := int(cao_cao.get("max_mp", 0)) var base_mp := int(cao_cao.get("mp", 0)) var base_def := int(cao_cao.get("def", 0)) var base_int := int(cao_cao.get("int", 0)) state.set_inventory_snapshot({"black_tortoise_robe": 1}) if not state.try_equip_item("cao_cao", "black_tortoise_robe"): failures.append("Cao Cao should be able to equip Black Tortoise Robe for armor unequip test") return var equipped: Dictionary = state.get_unit("cao_cao") if int(equipped.get("max_mp", 0)) != base_max_mp + 2: failures.append("Black Tortoise Robe should add max MP") if int(equipped.get("def", 0)) != base_def + 2: failures.append("Black Tortoise Robe should replace Cloth Robe with net DEF +2") if int(equipped.get("int", 0)) != base_int + 1: failures.append("Black Tortoise Robe should add INT") if not state.try_unequip_item("cao_cao", "armor"): failures.append("Cao Cao should be able to unequip armor") return var unequipped: Dictionary = state.get_unit("cao_cao") if int(unequipped.get("max_mp", 0)) != base_max_mp: failures.append("armor unequip should remove max MP bonus") if int(unequipped.get("mp", 0)) != mini(base_mp, base_max_mp): failures.append("armor unequip should clamp current MP to restored max") if int(unequipped.get("def", 0)) != base_def - 1: failures.append("empty armor slot should remove all armor DEF bonus") if int(unequipped.get("int", 0)) != base_int: failures.append("armor unequip should remove INT bonus") var inventory: Dictionary = state.get_inventory_snapshot() if int(inventory.get("black_tortoise_robe", 0)) != 1: failures.append("armor unequip should return Black Tortoise Robe to inventory") func _check_snapshot_reload_and_sell_stock(failures: Array[String]) -> void: var state = _loaded_state(failures, "snapshot reload") if state == null: return if not state.try_unequip_item("cao_cao", "weapon"): failures.append("could not unequip weapon before snapshot reload") return var roster_snapshot: Dictionary = state.get_player_roster_snapshot() var inventory_snapshot: Dictionary = state.get_inventory_snapshot() var reloaded = BattleStateScript.new() if not reloaded.load_battle("res://data/scenarios/001_yellow_turbans.json", roster_snapshot, inventory_snapshot): failures.append("could not reload battle from unequipped roster snapshot") return var reloaded_equipment: Dictionary = reloaded.get_equipment_snapshot("cao_cao") if reloaded_equipment.has("weapon"): failures.append("roster snapshot reload should keep weapon slot empty: %s" % str(reloaded_equipment)) if int(reloaded.get_inventory_snapshot().get("bronze_sword", 0)) != int(inventory_snapshot.get("bronze_sword", 0)): failures.append("inventory snapshot reload should keep returned weapon stock") var scene = BattleSceneScript.new() scene._create_hud() scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json", roster_snapshot, inventory_snapshot) scene.campaign_state.inventory = inventory_snapshot.duplicate(true) var sellable: Array = scene._get_sellable_item_ids() if not sellable.has("bronze_sword"): failures.append("returned unequipped weapon should appear in shop sell stock") scene.free() func _check_unequip_ui_entries(failures: Array[String]) -> void: var scene = BattleSceneScript.new() scene._create_hud() scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json") var selected: Dictionary = scene.state.get_unit("cao_cao") scene._rebuild_equip_menu(selected) if not _container_has_button_text(scene.equip_list, "Unequip Weapon:"): failures.append("battle equipment menu should include an Unequip Weapon button") scene.armory_unit_id = "cao_cao" scene._rebuild_armory_menu() if not _container_has_button_text(scene.armory_list, "Unequip Weapon:"): failures.append("pre-battle Armory should include an Unequip Weapon button") scene.free() func _container_has_button_text(container: Node, fragment: String) -> bool: for child in container.get_children(): if child is Button and str(child.text).contains(fragment): return true return false func _loaded_state(failures: Array[String], label: String): var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("%s could not load opening scenario" % label) return null return state