| ActionKind variants: ShieldWall, UnshieldWall, Brace, Unbrace, Shove, Rage, Cleave, WarCry in mc-core/src/action.rs |
| DisabledReason variants: NotInfantryLine, NotInfantryShock, AlreadyShieldWall, NotShieldWall, AlreadyBraced, NotBraced, AlreadyRaging, WarCryUsed, NoAdjacentTarget, ShoveBlocked in mc-core/src/action.rs |
| UnitCapability state fields: is_shield_wall, is_braced, rage_turns_remaining, war_cry_used_this_battle in mc-core/src/action.rs |
| MapUnit state fields: is_shield_wall, is_braced, rage_turns_remaining, war_cry_used_this_battle in mc-turn/src/game_state.rs |
| legal_actions gates: infantry_line + infantry_shock keyword checks with toggle logic; Rage gates Fortify and Sentry via is_raging check in mc-core/src/action.rs |
| Cross-turn phase: Rage countdown (rage_turns_remaining -= 1) in processor.rs prologue |
| Handlers: handle_shield_wall, handle_unshield_wall, handle_brace, handle_unbrace, handle_rage (clears fortify/sentry on entry), handle_war_cry in mc-turn/src/action_handlers.rs |
| Combat hooks: shield_wall_ranged_defence_bonus (+50%), rage_attack_bonus (+40%), war_cry_attack_debuff (-10%), brace_cancels_charge, CombatParams fields in mc-combat/src/keywords.rs and resolver.rs |
| JSON keyword wiring: infantry_line and infantry_shock keys in unit_actions.json |
| Vocabulary keys: all action_/tooltip_action_/disabled_reason_ keys for all 6 actions in vocabulary.json |
| Tests (legal_actions): 8 infantry tests in mc-core/src/action.rs; rage_gates_fortify, rage_gates_sentry |
| Tests (combat hooks): rage_bonus_applies_when_raging, rage_bonus_absent_when_not_raging, war_cry_debuff_reduces_attack, shield_wall_defence_bonus_vs_ranged, shield_wall_no_bonus_vs_melee in mc-combat/src/keywords.rs |
| AI policy: documented no-hook decision in mc-ai/src/tactical/movement.rs |
| Shove displacement: handle_shove in mc-turn/src/action_handlers/infantry.rs iterates ODD_Q_NEIGHBORS, finds first adjacent enemy, computes push destination (one more step in same direction), checks passability + occupancy, moves enemy and clears formation_id |
| Tests (Shove): shove_pushes_enemy_to_empty_hex, shove_breaks_formation, shove_no_adjacent_enemy_returns_no_adjacent_target, shove_blocked_when_destination_occupied in action_handlers/infantry.rs |
| Cleave AoE: resolver emits cleave_secondary_damage = 50% of primary in CombatResult when attacker_has_cleave=true; bridge selects adjacent target. CombatParams.attacker_has_cleave + CombatResult.cleave_secondary_damage in mc-combat/src/resolver.rs |
| Tests (Cleave): cleave_hits_one_adjacent_enemy_at_50_pct, cleave_secondary_zero_without_cleave in mc-combat/src/resolver.rs |
| WarCry adjacency-scan: handle_war_cry in action_handlers/infantry.rs sets war_cry_debuff_turns_remaining=1 on all adjacent enemy units; MapUnit.war_cry_debuff_turns_remaining ticked in processor.rs; bridge sets CombatParams.attacker_war_cry_debuff = unit.war_cry_debuff_turns_remaining > 0 |
| Tests (WarCry): war_cry_debuff_applied_to_adjacent_enemies, war_cry_does_not_debuff_non_adjacent_enemies, war_cry_twice_in_battle_errors in action_handlers/infantry.rs |
| Brace first-strike: in mc-combat/src/resolver.rs resolve(), brace_first_strike_active triggers when defender_is_braced && !is_ranged && !has_first_strike; if attacker killed by retaliation, final_damage_to_defender=0 |
| Tests (Brace first-strike): brace_first_strikes_attacker, brace_first_strike_no_effect_if_attacker_survives, brace_first_strike_does_not_apply_to_ranged in mc-combat/src/resolver.rs |
| action_handlers.rs factored into action_handlers/ submodule (mod.rs + infantry.rs + ranged.rs + cavalry.rs) per 500-LOC rule |