# Combat Tactics System Design ## Overview Units gain a list of selectable combat tactics that modify how they participate in combat. Tactics encapsulate their own stat logic — each tactic knows how to produce offensive stats and determine the relevant defensive stat, rather than exposing configuration for the combat system to interpret. Both attacker and defender select a tactic during the combat proposal phase, with AI auto-selecting for non-player units. ## Data Model ### CombatTactic Base Class A new base `CombatTactic` resource (`resources/resource_definitions/combat_tactic.gd`): **Properties:** - `name: String` — display name (e.g., "Attack", "Heavy Strike", "Fireball", "Defend") - `range: CombatTacticRange` — exported resource determining range validity (see below) **Methods (virtual, overridden by subclasses):** - `func get_offensive_stats(unit: Unit) -> Dictionary` — returns `{"atk": int, "hit": int}` with the tactic's effective offensive values pulled from the unit's stats and modified as needed. Returns `null` if the tactic does not attack (e.g., Defend). - `func get_relevant_defense(unit: Unit) -> int` — returns the defense value from the given unit that applies against this tactic's attack. E.g., a physical tactic returns `unit.current_stats.phys_def`, a magical one returns `unit.current_stats.magic_def`. - `func deals_damage() -> bool` — returns whether this tactic produces an attack. Base implementation returns `get_offensive_stats() != null`. Subclasses like Defend override to return `false`. The combat system never interprets damage types or applies modifiers — it just calls these methods. ### CombatTactic Subclasses **`AttackCombatTactic`** extends `CombatTactic`: - `get_offensive_stats(unit)` returns `{"atk": unit.current_stats.phys_atk, "hit": unit.current_stats.hit}` - `get_relevant_defense(unit)` returns `unit.current_stats.phys_def` **`DefendCombatTactic`** extends `CombatTactic`: - `get_offensive_stats(unit)` returns `null` - `deals_damage()` returns `false` - `get_relevant_defense(unit)` returns `unit.current_stats.phys_def` (default for display; unused in resolution) Future subclasses (not built now, examples for illustration): - `MagicAttackCombatTactic` — pulls `magic_atk`/`magic_def`, could apply modifiers - `HeavyAttackCombatTactic` — pulls `phys_atk + 5` and `hit - 20` ### CombatTacticRange Hierarchy Range checking is polymorphic via a resource subclass hierarchy: **`CombatTacticRange`** (base Resource, `resources/resource_definitions/combat_tactic_range.gd`): - `func is_valid_range(distance: int, unit: Unit) -> bool` — abstract, returns false by default **`FixedCombatTacticRange`** extends `CombatTacticRange`: - `tactic_range: int` — the fixed max range in tiles - Returns `distance <= tactic_range` **`AnyCombatTacticRange`** extends `CombatTacticRange`: - Always returns `true` **`UnitMatchingCombatTacticRange`** extends `CombatTacticRange`: - Returns `distance <= unit.current_stats.atk_range` ### Built-in Tactics Two built-in tactic instances, always available to all units: - **Attack** — `AttackCombatTactic` with `range: UnitMatchingCombatTacticRange` - **Defend** — `DefendCombatTactic` with `range: AnyCombatTacticRange` These are instantiated in code (not `.tres` files) since they use specific subclasses. Future tactics with more complex behavior would also be subclasses; a generic parameterized subclass can be added later when patterns emerge across many similar tactics. ## Unit Changes ### Tactic Assignment `Unit` gains: - `@export var tactics: Array[CombatTactic]` — additional tactics beyond the built-ins (e.g., Fireball, Heavy Strike) - At `_ready()`, Attack and Defend are appended to the unit's tactic list automatically. Custom tactics from the export list are kept as-is. This guarantees every unit always has Attack and Defend. ## CombatProposal Changes ### CombatantStats Additions `CombatantStats` gains: - `available_tactics: Array[CombatTactic]` — tactics filtered to those valid for the combat distance - `selected_tactic: CombatTactic` — the chosen tactic (defaults to Attack) The stat snapshot (`atk`, `def`, `hit`, etc.) is produced by calling the selected tactic's methods: - `atk` and `hit` come from `selected_tactic.get_offensive_stats(unit)` (null if the tactic doesn't attack) - `def` comes from the **opponent's** `selected_tactic.get_relevant_defense(this_unit)` — the defense stat used is determined by what attack is incoming, not what this unit is doing - When the opponent's tactic doesn't deal damage, `def` defaults to `phys_def` for display purposes ### Stat Recalculation When a tactic is changed on either side: 1. The combatant's offensive stats recalculate via `new_tactic.get_offensive_stats(unit)` 2. The opponent's defensive stat recalculates via `new_tactic.get_relevant_defense(opponent)` 3. Both sides' snapshots update accordingly ## CombatSystem Changes ### Proposal Creation `create_proposal(attacker, defender)` expands to: 1. Calculate tile distance between attacker and defender 2. For each unit, filter their tactics list by `tactic.range.is_valid_range(distance, unit)` 3. Default-select Attack for both sides 4. Snapshot stats with Attack's modifiers applied (none for the base case) 5. For AI combatants, auto-select optimal tactic (see AI Selection below) ### New Method: update_tactic `update_tactic(proposal, is_attacker: bool, tactic: CombatTactic)`: - Sets the combatant's `selected_tactic` - Recalculates that combatant's offensive stats via `tactic.get_offensive_stats(unit)` - Recalculates the opponent's defensive stat via `tactic.get_relevant_defense(opponent)` - Called by the UI when the player picks a different tactic ### Combat Resolution `apply_proposal()` changes: - If a combatant's `selected_tactic.deals_damage()` returns `false`, skip their attack entirely (no hit roll, no damage) - Otherwise, resolution works as before: roll against `hit`, apply `atk - def` damage - The stats in the snapshot are already produced by the tactic's methods, so resolution logic stays simple ## AI Tactic Selection For non-player combatants, auto-select the tactic that maximizes predicted damage: 1. For each available tactic where `tactic.deals_damage()` is true: - Get offensive stats via `tactic.get_offensive_stats(unit)` - Get opponent's relevant defense via `tactic.get_relevant_defense(opponent)` - Calculate effective damage: `offensive.atk - defense` 2. Pick the tactic with the highest damage 3. If all tactics result in 0 or negative damage, select Defend 4. This logic should be isolated in its own method for easy future modification (e.g., factoring in hit chance later) ## Combat UI Changes ### Tactic Selector Each combatant's side in the proposal panel gains a tactic dropdown/list: - Shows `available_tactics` by name - Defaults to Attack - On selection change, calls `CombatSystem.update_tactic()` and refreshes the displayed stats - For AI combatants, the selector is shown as read-only so the player can see what the AI chose ### Existing UI - Stat labels (ATK, DEF, HIT, SPD) continue working as-is — they read from the snapshot which now includes tactic modifiers - Fight/cancel buttons and overall flow unchanged - The selected tactic name should be visible for each combatant ## File Changes Summary ### New Files - `resources/resource_definitions/combat_tactic.gd` — CombatTactic base class - `resources/resource_definitions/attack_combat_tactic.gd` — AttackCombatTactic subclass - `resources/resource_definitions/defend_combat_tactic.gd` — DefendCombatTactic subclass - `resources/resource_definitions/combat_tactic_range.gd` — base range class - `resources/resource_definitions/fixed_combat_tactic_range.gd` - `resources/resource_definitions/any_combat_tactic_range.gd` - `resources/resource_definitions/unit_matching_combat_tactic_range.gd` ### Modified Files - `nodes/unit.gd` — add tactics export, append built-in Attack/Defend at ready - `resources/resource_definitions/combat_proposal.gd` — add available_tactics, selected_tactic to CombatantStats - `nodes/combat_system.gd` — distance calculation, tactic filtering, update_tactic method, AI selection, tactic-driven stat resolution - `scripts/combat_ui.gd` — tactic selector UI elements, refresh on tactic change - `prefabs/combat_ui.tscn` — add tactic selector nodes to proposal panel ## Out of Scope - SP/resource costs for tactics - Healing tactics (targeting allies) — future work, would be a new CombatTactic subclass - Generic parameterized tactic subclass (for when many tactics share a pattern) - Complex AI (hit chance weighting, situational awareness)