Add equipment unequip flow
This commit is contained in:
@@ -17,10 +17,10 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr
|
||||
- Min/max attack ranges and counterattack forecasts.
|
||||
- MP tactics with low/high damage options, area damage/healing/status options, ally support buffs, enemy debuffs, movement and accuracy/evasion modifiers, poison damage-over-time, seal/snare/disarm action locks, a skill list menu, previews, and target overlays.
|
||||
- Campaign inventory consumables with an item menu, HP/MP recovery, poison/seal/snare/disarm cure previews, and target overlay.
|
||||
- Weapon, armor, and accessory rewards can be stored, displayed, compared, and equipped from the battle HUD; some weapons deal bonus physical damage against matching move types.
|
||||
- Weapon, armor, and accessory rewards can be stored, displayed, compared, equipped, and unequipped from the battle HUD; some weapons deal bonus physical damage against matching move types.
|
||||
- First named equipment rewards exist in the northern campaign arc and show a `[Named]` or `Named` tag in reward, inventory, shop, Armory, and equipment menus.
|
||||
- Scenario-specific pre-battle shop purchases, optional finite shop stock, and 50% sell-back save immediately and sync into the upcoming battle inventory.
|
||||
- Pre-battle Armory equipment changes save immediately and sync roster equipment plus inventory stock.
|
||||
- Pre-battle Armory equipment changes, including unequipping gear back into stock, save immediately and sync roster equipment plus inventory stock.
|
||||
- Pre-battle Roster can move optional officers between sortie and reserve within scenario limits.
|
||||
- Pre-battle Formation lets deployed officers swap starting positions inside scenario-defined cells.
|
||||
- Pre-battle briefing shows the campaign chapter, chapter battle number, location, victory/defeat summary, and first-clear reward preview.
|
||||
|
||||
@@ -344,7 +344,7 @@ Consumables use `kind: "consumable"` with effect records. `heal_hp` restores HP,
|
||||
|
||||
Scenario `shop.items` controls which positive-price items appear in the current pre-battle shop. `shop.conditional_items` can append extra stock when saved campaign flags match. Entries can be plain item ids or objects with `id` and optional non-negative `stock`; `shop.stock` and conditional block `stock` objects can also set finite limits by item id. Items omitted from stock data are unlimited. Finite stock is tracked as per-scenario purchase counts in the campaign save, so buying an item decrements that scenario's remaining stock and manual checkpoint saves preserve it. Purchases spend campaign gold, increment the campaign inventory count, save immediately, and then refresh the battle inventory copy if a scenario briefing is already loaded. The shop can also sell unequipped inventory stock back for 50% of base `price`, saving immediately and refreshing the loaded battle inventory copy. Later campaign data should add broader unlock flags.
|
||||
|
||||
The pre-battle Armory operates on the currently loaded player deployments and the copied battle inventory, then saves the changed roster entries and inventory counts back into `CampaignState`. Unlike in-battle equipment swaps, Armory changes persist immediately and survive defeat or restart. Completed-scenario replays keep Shop and Armory disabled so replay setup cannot alter the campaign save.
|
||||
The pre-battle Armory operates on the currently loaded player deployments and the copied battle inventory, then saves the changed roster entries and inventory counts back into `CampaignState`. Gear can also be unequipped back into inventory stock before combat, making it available for later Armory swaps or shop selling. Unlike in-battle equipment swaps, Armory changes persist immediately and survive defeat or restart. Completed-scenario replays keep Shop and Armory disabled so replay setup cannot alter the campaign save.
|
||||
|
||||
Scenario `roster.max_units`, `roster.required_officers`, and `roster.required_units` control the first pre-battle Roster selection pass. Required officers and required scenario units always deploy, optional player deployments can move between sortie and reserve, and reserve units do not participate in map occupancy, targeting, or living-unit victory checks. If all loaded player units are required, the rules still apply but the pre-battle Roster button stays disabled because there is no meaningful sortie choice. If `roster` is omitted, all player deployments sortie as before.
|
||||
|
||||
@@ -577,7 +577,7 @@ Event-spawned units use the same compact deployment shape as scenario starting u
|
||||
|
||||
After a first-time victory, `CampaignState` advances `current_scenario_id` to the next entry in `data/campaign/campaign.json`. Completed-scenario replays return a result summary without rewriting the save or moving `current_scenario_id`. The battle scene passes `CampaignState.get_roster_overrides()`, `CampaignState.get_inventory_snapshot()`, `CampaignState.get_flags_snapshot()`, and `CampaignState.get_joined_officers_snapshot()` into `BattleState.load_battle()`, so officers can keep level/EXP/stat progression, equipped gear, consumable/equipment stock counts, recruitment gates, and branch flags into the next scenario.
|
||||
|
||||
During a battle, item consumption, event item pickups, and equipment swaps affect `BattleState.battle_inventory`. `CampaignState.apply_battle_result()` commits that inventory snapshot only for a first-time victory, then adds the victory rewards. Equipped gear is stored in the player roster snapshot together with the current derived stats; a later stat-model refactor should split base stats, growth, and equipment bonuses.
|
||||
During a battle, item consumption, event item pickups, equipment swaps, and unequipped gear returned to stock affect `BattleState.battle_inventory`. `CampaignState.apply_battle_result()` commits that inventory snapshot only for a first-time victory, then adds the victory rewards. Equipped gear is stored in the player roster snapshot together with the current derived stats; a later stat-model refactor should split base stats, growth, and equipment bonuses.
|
||||
|
||||
`BattleState` records player-only `progression_events` when a unit levels up or promotes. `CampaignState.apply_battle_result()` includes those events in the victory summary only when the first-time victory save succeeds, so completed-scenario replays and failed saves do not present unsaved growth as campaign progress. The result panel presents saved rewards, roster changes, growth, campaign status, and pending choices as separate sections; failed saves explicitly show rewards as not applied.
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
- Unit classes and class growth tables. Basic version exists.
|
||||
- Automatic class promotion routes. Basic level-threshold promotion exists for core first-tier battle classes.
|
||||
- Weapon, armor, and accessory slots. Basic stat bonuses and first weapon-vs-move-type physical damage bonuses exist.
|
||||
- Equipment rewards can be stored, displayed, compared, and swapped from the battle HUD.
|
||||
- Equipment rewards can be stored, displayed, compared, equipped, unequipped, and swapped from the battle HUD.
|
||||
- Terrain movement, defense, and avoid modifiers affect combat.
|
||||
- Skills, tactics, and MP. Multiple damage/heal tactics, first area damage/healing/status tactics, ally support buffs, enemy debuffs, movement and accuracy/evasion modifiers, poison damage-over-time, seal/snare/disarm action locks, a skill list menu with range/effect hints, and enemy tactic AI exist.
|
||||
- Consumable item use. Basic global inventory, Bean HP recovery, Wine MP recovery, Antidote poison cure, Panacea poison/seal/snare/disarm cure, item menu, preview, and target overlay exist.
|
||||
@@ -59,7 +59,7 @@
|
||||
- Save reset exists.
|
||||
- Victory rewards, consumable counts, equipment stock, and equipped gear persist.
|
||||
- A basic pre-battle shop with scenario-specific stock, optional finite item limits, and 50% sell-back exists. Richer item management beyond stock limits is still planned.
|
||||
- A basic pre-battle Armory exists for saved equipment changes before combat.
|
||||
- A basic pre-battle Armory exists for saved equipment changes and unequipping before combat.
|
||||
- A basic pre-battle Roster exists for required officers, required scenario units, optional officers, and sortie limits.
|
||||
- A basic pre-battle Formation screen exists for scenario-defined starting cells.
|
||||
- Officer join/leave rewards, choice membership changes, and joined-officer deployment gates exist in a basic form.
|
||||
|
||||
@@ -1681,7 +1681,7 @@ func try_equip_item(unit_id: String, item_id: String) -> bool:
|
||||
return false
|
||||
|
||||
var equipment: Dictionary = unit.get("equipment", {}).duplicate(true)
|
||||
var old_item_id := str(equipment.get(slot, ""))
|
||||
var old_item_id := _equipment_item_id_for_slot(equipment, slot)
|
||||
if old_item_id == item_id:
|
||||
_emit_log("%s already has %s equipped." % [unit["name"], item.get("name", item_id)])
|
||||
return false
|
||||
@@ -1700,6 +1700,43 @@ func try_equip_item(unit_id: String, item_id: String) -> bool:
|
||||
return true
|
||||
|
||||
|
||||
func try_unequip_item(unit_id: String, slot: String) -> bool:
|
||||
var unit := get_unit(unit_id)
|
||||
if unit.is_empty() or battle_status != STATUS_ACTIVE:
|
||||
return false
|
||||
if unit.get("team", "") != TEAM_PLAYER or current_team != TEAM_PLAYER:
|
||||
return false
|
||||
if not bool(unit.get("controllable", true)):
|
||||
return false
|
||||
if unit.get("moved", false) or unit.get("acted", false):
|
||||
_emit_log("%s can change equipment before moving or acting." % unit["name"])
|
||||
return false
|
||||
|
||||
var normalized_slot := _normalized_equipment_slot(slot)
|
||||
if normalized_slot.is_empty():
|
||||
return false
|
||||
|
||||
var equipment: Dictionary = unit.get("equipment", {}).duplicate(true)
|
||||
var old_item_id := _equipment_item_id_for_slot(equipment, normalized_slot)
|
||||
if old_item_id.is_empty():
|
||||
_emit_log("%s has no %s equipped." % [unit["name"], normalized_slot])
|
||||
return false
|
||||
|
||||
var old_item := get_item_def(old_item_id)
|
||||
if old_item.is_empty():
|
||||
push_error("Cannot unequip unknown item %s from %s." % [old_item_id, unit["name"]])
|
||||
return false
|
||||
|
||||
equipment.erase(normalized_slot)
|
||||
unit["equipment"] = equipment
|
||||
_apply_equipment_stat_delta(unit, old_item, {})
|
||||
_refresh_attack_range_from_equipment(unit)
|
||||
battle_inventory[old_item_id] = int(battle_inventory.get(old_item_id, 0)) + 1
|
||||
_emit_log("%s unequips %s." % [unit["name"], old_item.get("name", old_item_id)])
|
||||
_notify_changed()
|
||||
return true
|
||||
|
||||
|
||||
func get_equippable_item_ids(unit_id: String) -> Array:
|
||||
var result := []
|
||||
var unit := get_unit(unit_id)
|
||||
@@ -1718,7 +1755,7 @@ func get_equippable_item_ids(unit_id: String) -> Array:
|
||||
if item.is_empty() or _equipment_slot_for_item(item).is_empty():
|
||||
continue
|
||||
var slot := _equipment_slot_for_item(item)
|
||||
if str(equipment.get(slot, "")) == normalized:
|
||||
if _equipment_item_id_for_slot(equipment, slot) == normalized:
|
||||
continue
|
||||
if not _unit_can_equip_item(unit, item):
|
||||
continue
|
||||
@@ -1727,6 +1764,24 @@ func get_equippable_item_ids(unit_id: String) -> Array:
|
||||
return result
|
||||
|
||||
|
||||
func get_equipped_equipment_slots(unit_id: String) -> Array:
|
||||
var result := []
|
||||
var unit := get_unit(unit_id)
|
||||
if unit.is_empty() or unit.get("team", "") != TEAM_PLAYER:
|
||||
return result
|
||||
if not bool(unit.get("controllable", true)):
|
||||
return result
|
||||
var equipment: Dictionary = unit.get("equipment", {})
|
||||
for slot in ["weapon", "armor", "accessory"]:
|
||||
var item_id := _equipment_item_id_for_slot(equipment, slot)
|
||||
if item_id.is_empty():
|
||||
continue
|
||||
if get_item_def(item_id).is_empty():
|
||||
continue
|
||||
result.append(slot)
|
||||
return result
|
||||
|
||||
|
||||
func get_equipment_snapshot(unit_id: String) -> Dictionary:
|
||||
var unit := get_unit(unit_id)
|
||||
if unit.is_empty():
|
||||
@@ -2420,6 +2475,20 @@ func _equipment_slot_for_item(item: Dictionary) -> String:
|
||||
return ""
|
||||
|
||||
|
||||
func _normalized_equipment_slot(slot: String) -> String:
|
||||
var normalized := slot.strip_edges().to_lower()
|
||||
if normalized == "weapon" or normalized == "armor" or normalized == "accessory":
|
||||
return normalized
|
||||
return ""
|
||||
|
||||
|
||||
func _equipment_item_id_for_slot(equipment: Dictionary, slot: String) -> String:
|
||||
var item_id = equipment.get(slot, "")
|
||||
if item_id == null:
|
||||
return ""
|
||||
return str(item_id)
|
||||
|
||||
|
||||
func _unit_can_equip_item(unit: Dictionary, item: Dictionary) -> bool:
|
||||
var slot := _equipment_slot_for_item(item)
|
||||
if slot.is_empty():
|
||||
|
||||
@@ -3139,6 +3139,21 @@ func _rebuild_armory_menu() -> void:
|
||||
equipment_label.text = "Equipped: %s" % _format_unit_equipment(selected)
|
||||
armory_list.add_child(equipment_label)
|
||||
|
||||
var equipped_slots := state.get_equipped_equipment_slots(armory_unit_id)
|
||||
if not equipped_slots.is_empty():
|
||||
var unequip_title := Label.new()
|
||||
unequip_title.text = "Unequip"
|
||||
armory_list.add_child(unequip_title)
|
||||
var equipment := state.get_equipment_snapshot(armory_unit_id)
|
||||
for slot in equipped_slots:
|
||||
var normalized_slot := str(slot)
|
||||
var item_id := _equipment_item_id(equipment, normalized_slot)
|
||||
var item := state.get_item_def(item_id)
|
||||
var unequip_button := Button.new()
|
||||
unequip_button.text = _format_equipment_unequip_text(selected, normalized_slot, item_id, item)
|
||||
unequip_button.pressed.connect(_on_armory_unequip_pressed.bind(normalized_slot))
|
||||
armory_list.add_child(unequip_button)
|
||||
|
||||
var item_ids := state.get_equippable_item_ids(armory_unit_id)
|
||||
if item_ids.is_empty():
|
||||
var empty_equipment_label := Label.new()
|
||||
@@ -3197,19 +3212,38 @@ func _on_armory_equip_pressed(item_id: String) -> void:
|
||||
_rebuild_armory_menu()
|
||||
_update_hud()
|
||||
return
|
||||
if campaign_state.try_save_prebattle_loadout(state.get_player_roster_snapshot(), state.get_inventory_snapshot()):
|
||||
_on_log_added("Saved pre-battle equipment.")
|
||||
else:
|
||||
_play_ui_cancel()
|
||||
_on_log_added("Could not save pre-battle equipment.")
|
||||
_load_scenario(active_scenario_id)
|
||||
_show_briefing()
|
||||
if not _save_prebattle_equipment_change():
|
||||
return
|
||||
_rebuild_armory_menu()
|
||||
_update_hud()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _on_armory_unequip_pressed(slot: String) -> void:
|
||||
if battle_started or _is_prebattle_prep_locked() or armory_unit_id.is_empty():
|
||||
return
|
||||
if not state.try_unequip_item(armory_unit_id, slot):
|
||||
_rebuild_armory_menu()
|
||||
_update_hud()
|
||||
return
|
||||
if not _save_prebattle_equipment_change():
|
||||
return
|
||||
_rebuild_armory_menu()
|
||||
_update_hud()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _save_prebattle_equipment_change() -> bool:
|
||||
if campaign_state.try_save_prebattle_loadout(state.get_player_roster_snapshot(), state.get_inventory_snapshot()):
|
||||
_on_log_added("Saved pre-battle equipment.")
|
||||
return true
|
||||
_play_ui_cancel()
|
||||
_on_log_added("Could not save pre-battle equipment.")
|
||||
_load_scenario(active_scenario_id)
|
||||
_show_briefing()
|
||||
return false
|
||||
|
||||
|
||||
func _rebuild_shop_menu() -> void:
|
||||
if shop_list == null:
|
||||
return
|
||||
@@ -4153,6 +4187,22 @@ func _rebuild_equip_menu(selected: Dictionary) -> void:
|
||||
equipment_label.text = "Equipped: %s" % _format_unit_equipment(selected)
|
||||
equip_list.add_child(equipment_label)
|
||||
|
||||
var equipped_slots := state.get_equipped_equipment_slots(str(selected["id"]))
|
||||
if not equipped_slots.is_empty():
|
||||
var unequip_title := Label.new()
|
||||
unequip_title.text = "Unequip"
|
||||
equip_list.add_child(unequip_title)
|
||||
var equipment := state.get_equipment_snapshot(str(selected["id"]))
|
||||
for slot in equipped_slots:
|
||||
var normalized_slot := str(slot)
|
||||
var item_id := _equipment_item_id(equipment, normalized_slot)
|
||||
var item := state.get_item_def(item_id)
|
||||
var unequip_button := Button.new()
|
||||
unequip_button.text = _format_equipment_unequip_text(selected, normalized_slot, item_id, item)
|
||||
unequip_button.disabled = _is_input_locked()
|
||||
unequip_button.pressed.connect(_on_equip_unequip_pressed.bind(normalized_slot))
|
||||
equip_list.add_child(unequip_button)
|
||||
|
||||
var item_ids := state.get_equippable_item_ids(selected["id"])
|
||||
if item_ids.is_empty():
|
||||
var empty_label := Label.new()
|
||||
@@ -4229,6 +4279,39 @@ func _format_equipment_option_text(unit: Dictionary, item_id: String, item: Dict
|
||||
return _join_strings(parts, " ")
|
||||
|
||||
|
||||
func _format_equipment_unequip_text(unit: Dictionary, slot: String, item_id: String, item: Dictionary) -> String:
|
||||
var parts := [
|
||||
"Unequip %s:" % slot.capitalize(),
|
||||
_item_display_name(item_id)
|
||||
]
|
||||
var change_text := _format_equipment_unequip_change_text(unit, slot, item)
|
||||
if not change_text.is_empty():
|
||||
parts.append(change_text)
|
||||
return _join_strings(parts, " ")
|
||||
|
||||
|
||||
func _format_equipment_unequip_change_text(unit: Dictionary, slot: String, item: Dictionary) -> String:
|
||||
if unit.is_empty() or item.is_empty():
|
||||
return ""
|
||||
var parts := []
|
||||
var current_bonuses := _equipment_bonus_map(item)
|
||||
for stat in ["hp", "mp", "atk", "def", "int", "agi"]:
|
||||
var delta := -int(current_bonuses.get(stat, 0))
|
||||
if delta == 0:
|
||||
continue
|
||||
parts.append("%s %d" % [stat.to_upper(), delta])
|
||||
if slot == "weapon":
|
||||
var current_range := _format_equipment_item_range_text(item)
|
||||
if not current_range.is_empty():
|
||||
parts.append("RNG returns to class")
|
||||
var effectiveness_text := _format_equipment_effectiveness_text(item)
|
||||
if not effectiveness_text.is_empty():
|
||||
parts.append("Loses %s" % effectiveness_text)
|
||||
if parts.is_empty():
|
||||
return "Returns to inventory"
|
||||
return "Change %s" % _join_strings(parts, ", ")
|
||||
|
||||
|
||||
func _format_equipment_change_text(unit: Dictionary, item: Dictionary) -> String:
|
||||
if unit.is_empty() or item.is_empty():
|
||||
return ""
|
||||
@@ -4379,6 +4462,17 @@ func _on_equip_item_pressed(item_id: String) -> void:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _on_equip_unequip_pressed(slot: String) -> void:
|
||||
var selected := state.get_selected_unit()
|
||||
if selected.is_empty():
|
||||
return
|
||||
if state.try_unequip_item(selected["id"], slot):
|
||||
_rebuild_equip_menu(state.get_selected_unit())
|
||||
_refresh_ranges()
|
||||
_update_hud()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _on_equip_cancel_pressed() -> void:
|
||||
_play_ui_cancel()
|
||||
_hide_equip_menu()
|
||||
|
||||
212
tools/smoke_equipment_unequip.gd
Normal file
212
tools/smoke_equipment_unequip.gd
Normal file
@@ -0,0 +1,212 @@
|
||||
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
|
||||
Reference in New Issue
Block a user