feat(@projects/@magic-civilization): restructure action handlers into modular sub-systems

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-02 19:38:09 -04:00
parent 8e98b51bfc
commit 2d66a3aadf

View file

@ -4,6 +4,15 @@
//! `processor.rs` routes external action invocations here. Move/Attack/Combat
//! are NOT routed here — they require target coordinates and use the existing
//! pathfinding and combat subsystems; the bridge calls those paths directly.
//!
//! Archetype-specific handlers live in sub-modules:
//! - `infantry` — ShieldWall, Brace, Shove, Rage, Cleave, WarCry (p2-53f)
//! - `ranged` — AimedShot, FireArrows, StopFireArrows (p2-53g)
//! - `cavalry` — Pursue (p2-53h)
mod infantry;
mod ranged;
mod cavalry;
use crate::action::{ActionKind, DisabledReason};
use crate::game_state::GameState;
@ -81,25 +90,32 @@ pub fn invoke(
ActionKind::Unsentry => handle_unsentry(state, player_idx, unit_idx),
// p2-53g ranged actions
// Volley requires a target hex — routed through the bridge's target-pick path.
// Queue-drain (pending_volley_requests + process_volley_requests phase) is
// not yet implemented; deferred pending bridge plumbing.
ActionKind::Volley => Err(ActionError { kind, reason: DisabledReason::WrongTerrain }),
ActionKind::AimedShot => handle_aimed_shot(state, player_idx, unit_idx),
ActionKind::FireArrows => handle_fire_arrows(state, player_idx, unit_idx),
ActionKind::StopFireArrows => handle_stop_fire_arrows(state, player_idx, unit_idx),
ActionKind::AimedShot => ranged::handle_aimed_shot(state, player_idx, unit_idx),
ActionKind::FireArrows => ranged::handle_fire_arrows(state, player_idx, unit_idx),
ActionKind::StopFireArrows => ranged::handle_stop_fire_arrows(state, player_idx, unit_idx),
// p2-53h cavalry actions
// Charge and Wheel require target coords — routed through bridge.
// Charge requires a target hex — routed through bridge's target-pick path.
// Queue-drain (pending_charge_requests) not yet implemented; deferred pending
// bridge plumbing.
// Wheel requires facing/edge state not yet present on MapUnit; deferred.
ActionKind::Charge | ActionKind::Wheel => Err(ActionError { kind, reason: DisabledReason::WrongTerrain }),
ActionKind::Pursue => handle_pursue(state, player_idx, unit_idx),
ActionKind::Pursue => cavalry::handle_pursue(state, player_idx, unit_idx),
// p2-53f infantry line actions
ActionKind::ShieldWall => handle_shield_wall(state, player_idx, unit_idx),
ActionKind::UnshieldWall => handle_unshield_wall(state, player_idx, unit_idx),
ActionKind::Brace => handle_brace(state, player_idx, unit_idx),
ActionKind::Unbrace => handle_unbrace(state, player_idx, unit_idx),
ActionKind::Shove | ActionKind::Cleave => Err(ActionError {
kind,
reason: DisabledReason::WrongTerrain,
}),
ActionKind::Rage => handle_rage(state, player_idx, unit_idx),
ActionKind::WarCry => handle_war_cry(state, player_idx, unit_idx),
ActionKind::ShieldWall => infantry::handle_shield_wall(state, player_idx, unit_idx),
ActionKind::UnshieldWall => infantry::handle_unshield_wall(state, player_idx, unit_idx),
ActionKind::Brace => infantry::handle_brace(state, player_idx, unit_idx),
ActionKind::Unbrace => infantry::handle_unbrace(state, player_idx, unit_idx),
ActionKind::Shove => infantry::handle_shove(state, player_idx, unit_idx),
// p2-53f infantry shock actions
ActionKind::Rage => infantry::handle_rage(state, player_idx, unit_idx),
// Cleave: the combat resolver emits `cleave_secondary_damage` (50% of primary);
// the bridge selects the adjacent target and applies it. No pre-action handler
// needed — Cleave fires as part of the attack action, not as a standalone toggle.
ActionKind::Cleave => Err(ActionError { kind, reason: DisabledReason::WrongTerrain }),
ActionKind::WarCry => infantry::handle_war_cry(state, player_idx, unit_idx),
// p2-53i engineer actions — multi-turn; require hex targets from bridge.
ActionKind::BuildBridge
| ActionKind::SapWall
@ -132,7 +148,7 @@ pub fn invoke(
}
}
fn get_unit_mut<'a>(
pub(crate) fn get_unit_mut<'a>(
state: &'a mut GameState,
player_idx: usize,
unit_idx: usize,
@ -404,177 +420,6 @@ fn handle_disembark(
Ok(())
}
// ── p2-53g ranged action handlers ────────────────────────────────────────────
fn handle_aimed_shot(
state: &mut GameState,
player_idx: usize,
unit_idx: usize,
) -> Result<(), ActionError> {
let unit = get_unit_mut(state, player_idx, unit_idx, ActionKind::AimedShot)?;
if unit.aimed_shot_pending {
return Err(ActionError {
kind: ActionKind::AimedShot,
reason: DisabledReason::AlreadyAiming,
});
}
unit.aimed_shot_pending = true;
Ok(())
}
fn handle_fire_arrows(
state: &mut GameState,
player_idx: usize,
unit_idx: usize,
) -> Result<(), ActionError> {
let unit = get_unit_mut(state, player_idx, unit_idx, ActionKind::FireArrows)?;
if unit.is_fire_arrows {
return Err(ActionError {
kind: ActionKind::FireArrows,
reason: DisabledReason::AlreadyFireArrows,
});
}
unit.is_fire_arrows = true;
Ok(())
}
fn handle_stop_fire_arrows(
state: &mut GameState,
player_idx: usize,
unit_idx: usize,
) -> Result<(), ActionError> {
let unit = get_unit_mut(state, player_idx, unit_idx, ActionKind::StopFireArrows)?;
if !unit.is_fire_arrows {
return Err(ActionError {
kind: ActionKind::StopFireArrows,
reason: DisabledReason::NotFireArrows,
});
}
unit.is_fire_arrows = false;
Ok(())
}
// ── p2-53h cavalry action handlers ────────────────────────────────────────────
fn handle_pursue(
state: &mut GameState,
player_idx: usize,
unit_idx: usize,
) -> Result<(), ActionError> {
let unit = get_unit_mut(state, player_idx, unit_idx, ActionKind::Pursue)?;
if unit.is_pursuing {
return Err(ActionError {
kind: ActionKind::Pursue,
reason: DisabledReason::AlreadyPursuing,
});
}
unit.is_pursuing = true;
Ok(())
}
// ── p2-53f infantry line action handlers ─────────────────────────────────────
fn handle_shield_wall(
state: &mut GameState,
player_idx: usize,
unit_idx: usize,
) -> Result<(), ActionError> {
let unit = get_unit_mut(state, player_idx, unit_idx, ActionKind::ShieldWall)?;
if unit.is_shield_wall {
return Err(ActionError {
kind: ActionKind::ShieldWall,
reason: DisabledReason::AlreadyShieldWall,
});
}
unit.is_shield_wall = true;
Ok(())
}
fn handle_unshield_wall(
state: &mut GameState,
player_idx: usize,
unit_idx: usize,
) -> Result<(), ActionError> {
let unit = get_unit_mut(state, player_idx, unit_idx, ActionKind::UnshieldWall)?;
if !unit.is_shield_wall {
return Err(ActionError {
kind: ActionKind::UnshieldWall,
reason: DisabledReason::NotShieldWall,
});
}
unit.is_shield_wall = false;
Ok(())
}
fn handle_brace(
state: &mut GameState,
player_idx: usize,
unit_idx: usize,
) -> Result<(), ActionError> {
let unit = get_unit_mut(state, player_idx, unit_idx, ActionKind::Brace)?;
if unit.is_braced {
return Err(ActionError {
kind: ActionKind::Brace,
reason: DisabledReason::AlreadyBraced,
});
}
unit.is_braced = true;
Ok(())
}
fn handle_unbrace(
state: &mut GameState,
player_idx: usize,
unit_idx: usize,
) -> Result<(), ActionError> {
let unit = get_unit_mut(state, player_idx, unit_idx, ActionKind::Unbrace)?;
if !unit.is_braced {
return Err(ActionError {
kind: ActionKind::Unbrace,
reason: DisabledReason::NotBraced,
});
}
unit.is_braced = false;
Ok(())
}
// ── p2-53f infantry shock action handlers ────────────────────────────────────
fn handle_rage(
state: &mut GameState,
player_idx: usize,
unit_idx: usize,
) -> Result<(), ActionError> {
let unit = get_unit_mut(state, player_idx, unit_idx, ActionKind::Rage)?;
if unit.rage_turns_remaining > 0 {
return Err(ActionError {
kind: ActionKind::Rage,
reason: DisabledReason::AlreadyRaging,
});
}
// +40% attack for 2 turns; Fortify and Sentry are incompatible (cleared here).
unit.rage_turns_remaining = 2;
unit.is_fortified = false;
unit.is_sentrying = false;
Ok(())
}
fn handle_war_cry(
state: &mut GameState,
player_idx: usize,
unit_idx: usize,
) -> Result<(), ActionError> {
let unit = get_unit_mut(state, player_idx, unit_idx, ActionKind::WarCry)?;
if unit.war_cry_used_this_battle {
return Err(ActionError {
kind: ActionKind::WarCry,
reason: DisabledReason::WarCryUsed,
});
}
unit.war_cry_used_this_battle = true;
Ok(())
}
// ── p2-53i: Scout action handlers ───────────────────────────────────────────
fn handle_stealth(
@ -887,7 +732,6 @@ mod tests {
strategic_axes: Default::default(),
scoring_weights: Default::default(),
expansion_points: 0,
city_buildings: vec![],
city_improvements: Default::default(),
city_ecology: vec![],
tech_state: None,