diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 958a289..a1ea4e3 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -2074,7 +2074,7 @@ func _update_hud() -> void: selected_label.text = "No unit selected." else: selected_label.text = _format_unit_focus_text(hovered_unit, "Hover") - wait_button.disabled = true + _update_wait_button({}) _update_tactic_button({}) _hide_tactic_menu() _update_item_button({}) @@ -2084,7 +2084,7 @@ func _update_hud() -> void: else: _update_hud_unit_portrait(selected) selected_label.text = _format_unit_focus_text(selected, "Selected") - wait_button.disabled = selected.get("acted", false) or not state.can_player_act() or _is_input_locked() + _update_wait_button(selected) _update_tactic_button(selected) _update_item_button(selected) _update_equip_button(selected) @@ -2095,11 +2095,9 @@ func _update_hud() -> void: if equip_menu != null and equip_menu.visible: _rebuild_equip_menu(selected) - end_turn_button.disabled = not state.can_player_act() or _is_input_locked() + _update_end_turn_button() restart_button.disabled = campaign_complete_screen - if threat_button != null: - threat_button.button_pressed = show_threat_overlay - threat_button.disabled = not battle_started or state.battle_status != BattleState.STATUS_ACTIVE or campaign_complete_screen + _update_threat_button() func _format_objective_hud_text() -> String: @@ -4887,32 +4885,118 @@ func _on_new_campaign_pressed() -> void: _update_hud() +func _set_action_button_state(button: Button, label: String, disabled: bool, tooltip := "") -> void: + if button == null: + return + button.text = label + button.disabled = disabled + button.tooltip_text = tooltip + + +func _set_action_button_blocked(button: Button, base_label: String, reason_label: String, tooltip: String) -> void: + _set_action_button_state(button, "%s - %s" % [base_label, reason_label], true, tooltip) + + +func _action_phase_block_reason() -> Dictionary: + if campaign_complete_screen: + return { + "label": "Campaign Done", + "tooltip": "The campaign is complete." + } + if not battle_started: + return { + "label": "Start Battle", + "tooltip": "Begin the battle before using combat actions." + } + if state.battle_status != BattleState.STATUS_ACTIVE: + return { + "label": "Battle Over", + "tooltip": "Combat actions are unavailable after the battle ends." + } + if state.current_team != BattleState.TEAM_PLAYER: + return { + "label": "Enemy Turn", + "tooltip": "Wait for the player phase." + } + if _is_input_locked(): + return { + "label": "Busy", + "tooltip": "Finish the current dialogue, result, or animation first." + } + return {} + + +func _update_wait_button(selected: Dictionary) -> void: + if wait_button == null: + return + if selected.is_empty(): + _set_action_button_blocked(wait_button, "Wait", "Select Unit", "Select a controllable player unit before waiting.") + return + if bool(selected.get("acted", false)): + _set_action_button_blocked(wait_button, "Wait", "Done", "This unit has already acted.") + return + var phase_block := _action_phase_block_reason() + if not phase_block.is_empty(): + _set_action_button_blocked(wait_button, "Wait", str(phase_block.get("label", "Busy")), str(phase_block.get("tooltip", ""))) + return + _set_action_button_state(wait_button, "Wait", false, "End this unit's action for the current turn.") + + +func _update_end_turn_button() -> void: + if end_turn_button == null: + return + var phase_block := _action_phase_block_reason() + if not phase_block.is_empty(): + _set_action_button_blocked(end_turn_button, "End Turn", str(phase_block.get("label", "Busy")), str(phase_block.get("tooltip", ""))) + return + _set_action_button_state(end_turn_button, "End Turn", false, "End the player phase and let enemies act.") + + +func _update_threat_button() -> void: + if threat_button == null: + return + threat_button.button_pressed = show_threat_overlay + if campaign_complete_screen: + _set_action_button_blocked(threat_button, "Threat", "Campaign Done", "The campaign is complete.") + return + if not battle_started: + _set_action_button_blocked(threat_button, "Threat", "Start Battle", "Begin the battle before showing enemy threat range.") + return + if state.battle_status != BattleState.STATUS_ACTIVE: + _set_action_button_blocked(threat_button, "Threat", "Battle Over", "Threat range is only available during active battles.") + return + _set_action_button_state(threat_button, "Threat", false, "Toggle enemy attack and hostile tactic reach.") + + func _update_tactic_button(selected: Dictionary) -> void: if tactic_button == null: return if selected.is_empty(): - tactic_button.text = "Tactic" - tactic_button.disabled = true + _set_action_button_blocked(tactic_button, "Tactic", "Select Unit", "Select a controllable player unit before using tactics.") return var skill_ids := state.get_skill_ids(selected["id"]) if skill_ids.is_empty(): - tactic_button.text = "Tactic" - tactic_button.disabled = true + _set_action_button_blocked(tactic_button, "Tactic", "None", "This unit has no tactics.") return if state.is_unit_skill_sealed(selected["id"]): - tactic_button.text = "Tactic - Sealed" - tactic_button.disabled = true + _set_action_button_blocked(tactic_button, "Tactic", "Sealed", "This unit is sealed and cannot use tactics.") + return + if bool(selected.get("acted", false)): + _set_action_button_blocked(tactic_button, "Tactic", "Done", "This unit has already acted.") + return + var phase_block := _action_phase_block_reason() + if not phase_block.is_empty(): + _set_action_button_blocked(tactic_button, "Tactic", str(phase_block.get("label", "Busy")), str(phase_block.get("tooltip", ""))) return if not selected_skill_id.is_empty(): var skill := state.get_skill_def(selected_skill_id) - tactic_button.text = "Tactic: %s" % str(skill.get("name", selected_skill_id)) + _set_action_button_state(tactic_button, "Tactic: %s" % str(skill.get("name", selected_skill_id)), false, "Target this tactic on the map.") elif tactic_menu != null and tactic_menu.visible: - tactic_button.text = "Close Tactic" + _set_action_button_state(tactic_button, "Close Tactic", false, "Close the tactics menu.") else: - tactic_button.text = "Tactic" - tactic_button.disabled = selected.get("acted", false) or not state.can_player_act() or _is_input_locked() + _set_action_button_state(tactic_button, "Tactic", false, "Open tactics. %d known." % skill_ids.size()) func _show_tactic_menu(selected: Dictionary) -> void: @@ -5066,24 +5150,28 @@ func _update_item_button(selected: Dictionary) -> void: if item_button == null: return if selected.is_empty(): - item_button.text = "Item" - item_button.disabled = true + _set_action_button_blocked(item_button, "Item", "Select Unit", "Select a controllable player unit before using items.") return + if bool(selected.get("acted", false)): + _set_action_button_blocked(item_button, "Item", "Done", "This unit has already acted.") + return + var phase_block := _action_phase_block_reason() + if not phase_block.is_empty(): + _set_action_button_blocked(item_button, "Item", str(phase_block.get("label", "Busy")), str(phase_block.get("tooltip", ""))) + return var item_ids := state.get_usable_item_ids() if item_ids.is_empty(): - item_button.text = "Item" - item_button.disabled = true + _set_action_button_blocked(item_button, "Item", "None", "No consumable items are available.") return if not selected_item_id.is_empty(): var item := state.get_item_def(selected_item_id) - item_button.text = "Item: %s" % str(item.get("name", selected_item_id)) + _set_action_button_state(item_button, "Item: %s" % str(item.get("name", selected_item_id)), false, "Target this item on the map.") elif item_menu != null and item_menu.visible: - item_button.text = "Close Item" + _set_action_button_state(item_button, "Close Item", false, "Close the item menu.") else: - item_button.text = "Item" - item_button.disabled = selected.get("acted", false) or not state.can_player_act() or _is_input_locked() + _set_action_button_state(item_button, "Item", false, "Open items. %d consumable types available." % item_ids.size()) func _show_item_menu(selected: Dictionary) -> void: @@ -5185,15 +5273,23 @@ func _update_equip_button(selected: Dictionary) -> void: if equip_button == null: return if selected.is_empty(): - equip_button.text = "Equip" - equip_button.disabled = true + _set_action_button_blocked(equip_button, "Equip", "Select Unit", "Select a controllable player unit before changing equipment.") return + if bool(selected.get("acted", false)): + _set_action_button_blocked(equip_button, "Equip", "Done", "This unit has already acted.") + return + if bool(selected.get("moved", false)): + _set_action_button_blocked(equip_button, "Equip", "Moved", "Equipment can only be changed before this unit moves.") + return + var phase_block := _action_phase_block_reason() + if not phase_block.is_empty(): + _set_action_button_blocked(equip_button, "Equip", str(phase_block.get("label", "Busy")), str(phase_block.get("tooltip", ""))) + return if equip_menu != null and equip_menu.visible: - equip_button.text = "Close Equip" + _set_action_button_state(equip_button, "Close Equip", false, "Close the equipment menu.") else: - equip_button.text = "Equip" - equip_button.disabled = selected.get("moved", false) or selected.get("acted", false) or not state.can_player_act() or _is_input_locked() + _set_action_button_state(equip_button, "Equip", false, "Change weapons, armor, or accessories before moving.") func _show_equip_menu(selected: Dictionary) -> void: diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index bcd97b4..f7022d3 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -44,6 +44,7 @@ func _init() -> void: _check_scene_texture_loading(failures) _check_hud_focus_text(failures) _check_hover_intent_badges(failures) + _check_action_button_disabled_reasons(failures) _check_terrain_and_unit_presentation(failures) _check_attack_motion_profiles(failures) _check_attack_motion_signal(failures) @@ -237,6 +238,85 @@ func _check_hud_focus_text(failures: Array[String]) -> void: scene.free() +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() + 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 != "Tactic - Sealed" or not scene.tactic_button.tooltip_text.to_lower().contains("sealed"): + failures.append("sealed unit should explain disabled tactic button: %s | %s" % [scene.tactic_button.text, scene.tactic_button.tooltip_text]) + 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 not scene.wait_button.text.contains("Done"): + failures.append("acted unit wait button should show Done: %s" % scene.wait_button.text) + if not scene.tactic_button.text.contains("Done"): + failures.append("acted unit tactic button should show Done: %s" % scene.tactic_button.text) + if not scene.item_button.text.contains("Done"): + failures.append("acted unit item button should show Done: %s" % scene.item_button.text) + if not scene.equip_button.text.contains("Done"): + failures.append("acted unit equip button should show Done: %s" % scene.equip_button.text) + + cao_cao["acted"] = false + cao_cao["moved"] = true + scene._update_equip_button(cao_cao) + if scene.equip_button.text != "Equip - Moved" or not scene.equip_button.tooltip_text.to_lower().contains("before"): + failures.append("moved unit should explain disabled equip button: %s | %s" % [scene.equip_button.text, scene.equip_button.tooltip_text]) + + cao_cao["moved"] = false + scene.state.set_inventory_snapshot({}) + scene._update_item_button(cao_cao) + if scene.item_button.text != "Item - None" or not scene.item_button.tooltip_text.to_lower().contains("no consumable"): + failures.append("empty inventory should explain disabled item button: %s | %s" % [scene.item_button.text, scene.item_button.tooltip_text]) + + scene.state.current_team = BattleState.TEAM_ENEMY + scene._update_end_turn_button() + if scene.end_turn_button.text != "End Turn - Enemy Turn" or not scene.end_turn_button.tooltip_text.to_lower().contains("player phase"): + failures.append("enemy phase should explain disabled end turn button: %s | %s" % [scene.end_turn_button.text, scene.end_turn_button.tooltip_text]) + + scene.state.current_team = BattleState.TEAM_PLAYER + scene.battle_started = false + scene._update_threat_button() + if scene.threat_button.text != "Threat - Start Battle" or not scene.threat_button.tooltip_text.to_lower().contains("begin"): + failures.append("inactive battle should explain disabled threat button: %s | %s" % [scene.threat_button.text, scene.threat_button.tooltip_text]) + _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"):