From 96a1e8225e5d8234f5d65255c17585cc86138c5d Mon Sep 17 00:00:00 2001 From: Wickedness Date: Thu, 18 Jun 2026 14:54:11 +0900 Subject: [PATCH] Hide roster when no sortie choices --- docs/DATA_MODEL.md | 2 +- scripts/core/battle_state.gd | 15 ++++++++ scripts/scenes/battle_scene.gd | 4 +-- tools/smoke_roster_choices.gd | 62 ++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 tools/smoke_roster_choices.gd diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md index 5ea5be0..8ab9599 100644 --- a/docs/DATA_MODEL.md +++ b/docs/DATA_MODEL.md @@ -346,7 +346,7 @@ Scenario `shop.items` controls which positive-price items appear in the current 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. -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 `roster` is omitted, all player deployments sortie as before. +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. Scenario-only protected units can use `team: "player"`, `controllable: false`, `persist_progression: false`, and an optional `ai_target_priority`. They remain valid targets for enemies, healing, and defeat conditions, but the player cannot select, move, attack, change equipment, or place them in Formation, and they are omitted from campaign roster progression snapshots. `ai_target_priority` is a non-negative scenario hint that makes enemy movement and damage-skill AI treat that unit as a more attractive target. Events can later change the same value with `set_ai_target_priority`. diff --git a/scripts/core/battle_state.gd b/scripts/core/battle_state.gd index 87808f6..3ee24aa 100644 --- a/scripts/core/battle_state.gd +++ b/scripts/core/battle_state.gd @@ -1556,6 +1556,21 @@ func has_deployment_roster() -> bool: ) +func has_deployment_roster_choices() -> bool: + if not has_deployment_roster(): + return false + var deployed_count := get_deployed_player_count() + var max_units := get_deployment_max_units() + for unit in get_player_units(true): + if bool(unit.get("required_deployment", false)): + continue + if bool(unit.get("deployed", true)): + return true + if deployed_count < max_units: + return true + return false + + func get_deployed_player_count() -> int: return get_player_units(false).size() diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index ed3a82f..b4df172 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -2597,7 +2597,7 @@ func _show_briefing() -> void: if armory_button != null: armory_button.disabled = prep_locked or state.get_controllable_player_units().is_empty() if roster_button != null: - roster_button.disabled = prep_locked or not state.has_deployment_roster() or state.get_player_units(true).is_empty() + roster_button.disabled = prep_locked or not state.has_deployment_roster_choices() if formation_button != null: formation_button.disabled = prep_locked or state.get_formation_cells().is_empty() if save_button != null: @@ -2779,7 +2779,7 @@ func _hide_armory_menu() -> void: func _on_roster_pressed() -> void: - if battle_started or _is_prebattle_prep_locked() or not state.has_deployment_roster() or briefing_panel == null or not briefing_panel.visible: + if battle_started or _is_prebattle_prep_locked() or not state.has_deployment_roster_choices() or briefing_panel == null or not briefing_panel.visible: return _play_ui_click() if roster_menu != null and roster_menu.visible: diff --git a/tools/smoke_roster_choices.gd b/tools/smoke_roster_choices.gd new file mode 100644 index 0000000..73e631b --- /dev/null +++ b/tools/smoke_roster_choices.gd @@ -0,0 +1,62 @@ +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) + + 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 _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