shk_parser 0.1.1

A parser for Stronghold Kingdoms attack formation files (.cas)
Documentation
use std::fmt;

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

#[cfg(feature = "tsify")]
use tsify::Tsify;

#[cfg(feature = "tsify")]
use wasm_bindgen::prelude::*;

/// Represents a coordinate position on the battlefield
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "tsify", derive(Tsify))]
#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))]
pub struct Position {
    pub x: u8,
    pub y: u8,
}

impl Position {
    #[must_use]
    pub const fn new(x: u8, y: u8) -> Self {
        Self { x, y }
    }
}

impl fmt::Display for Position {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

/// Captain abilities with their specific parameters
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "tsify", derive(Tsify))]
#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))]
#[repr(u8)]
pub enum CaptainAbility {
    /// Wait/delay for a specified time
    Delay = 100,
    /// Rally nearby troops
    RallyingCry = 101,
    /// Shoot arrows at target location
    ArrowVolley { target: Position } = 102,
    /// Battle cry to boost morale
    BattleCry = 103,
    /// Catapult volley at target location
    CatapultsVolley { target: Position } = 104,
    /// Unknown/unsupported ability
    Unknown(u8),
}

impl CaptainAbility {
    /// Create a `CaptainAbility` from raw bytes
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - Target coordinates are required but not provided
    /// - Extra data is insufficient for the ability type
    pub fn from_bytes(ability_id: u8, extra_data: &[u8]) -> Result<Self, String> {
        match ability_id {
            100 => Ok(Self::Delay),
            101 => Ok(Self::RallyingCry),
            102 => {
                if extra_data.len() < 2 {
                    return Err("Arrow Volley requires target coordinates".into());
                }
                Ok(Self::ArrowVolley {
                    target: Position::new(extra_data[0], extra_data[1]),
                })
            }
            103 => Ok(Self::BattleCry),
            104 => {
                if extra_data.len() < 2 {
                    return Err("Catapults Volley requires target coordinates".into());
                }
                Ok(Self::CatapultsVolley {
                    target: Position::new(extra_data[0], extra_data[1]),
                })
            }
            other => Ok(Self::Unknown(other)),
        }
    }

    /// Returns true if this captain ability requires target coordinates
    #[must_use]
    pub const fn has_target_for_id(ability_id: u8) -> bool {
        matches!(ability_id, 102 | 104)
    }

    /// Get the human-readable name of this ability
    #[must_use]
    pub const fn name(&self) -> &'static str {
        match self {
            Self::Delay => "Delay",
            Self::RallyingCry => "Rallying Cry",
            Self::ArrowVolley { .. } => "Arrow Volley",
            Self::BattleCry => "Battle Cry",
            Self::CatapultsVolley { .. } => "Catapults Volley",
            Self::Unknown(_) => "Unknown Ability",
        }
    }

    /// Get the target position if this ability has one
    #[must_use]
    pub const fn target(&self) -> Option<&Position> {
        match self {
            Self::ArrowVolley { target } | Self::CatapultsVolley { target } => Some(target),
            _ => None,
        }
    }
}

impl fmt::Display for CaptainAbility {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.target() {
            Some(target) => write!(f, "{} targeting {}", self.name(), target),
            None => write!(f, "{}", self.name()),
        }
    }
}

/// Different types of military units
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "tsify", derive(Tsify))]
#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))]
pub enum UnitType {
    /// Basic archer unit
    Archer,
    /// Basic pikeman unit
    Pikeman,
    /// Siege catapult with target
    Catapult { target: Position },
    /// Captain with special ability and timing
    Captain {
        ability: CaptainAbility,
        wait_time: u8,
    },
    /// Unknown/unsupported unit type
    Unknown(u8),
}

impl UnitType {
    /// Create a `UnitType` from raw bytes
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - Required target coordinates are missing for catapults
    /// - Required wait time data is missing for captains
    /// - Captain target coordinates are missing when required
    pub fn from_bytes(unit_type_raw: u8, extra_data: &[u8]) -> Result<Self, String> {
        match unit_type_raw {
            92 => Ok(Self::Archer),
            93 => Ok(Self::Pikeman),
            94 => {
                if extra_data.len() < 2 {
                    return Err("Catapult requires target coordinates".into());
                }
                Ok(Self::Catapult {
                    target: Position::new(extra_data[0], extra_data[1]),
                })
            }
            100..=105 => {
                // Captain abilities
                if extra_data.is_empty() {
                    return Err("Captain requires wait time".into());
                }

                let wait_time = extra_data[0];

                if CaptainAbility::has_target_for_id(unit_type_raw) {
                    // Captain with target: x, y, type, wait_time, target_x, target_y
                    if extra_data.len() < 3 {
                        return Err("Captain with target requires target coordinates".into());
                    }
                    let ability = CaptainAbility::from_bytes(unit_type_raw, &extra_data[1..])?;
                    Ok(Self::Captain { ability, wait_time })
                } else {
                    // Simple captain: x, y, type, wait_time
                    let ability = CaptainAbility::from_bytes(unit_type_raw, &[])?;
                    Ok(Self::Captain { ability, wait_time })
                }
            }
            other => Ok(Self::Unknown(other)),
        }
    }

    /// Returns the expected record size in bytes for this unit type
    #[must_use]
    pub const fn record_size_for_raw_type(unit_type_raw: u8) -> usize {
        match unit_type_raw {
            94 => 5, // Catapult: x, y, type, target_x, target_y
            100..=105 => {
                // Captain abilities: check if they need targets
                if CaptainAbility::has_target_for_id(unit_type_raw) {
                    6 // x, y, type, wait_time, target_x, target_y
                } else {
                    4 // x, y, type, wait_time
                }
            }
            _ => 3, // Simple units: x, y, type
        }
    }

    /// Get the human-readable name of this unit type
    #[must_use]
    pub const fn name(&self) -> &'static str {
        match self {
            Self::Archer => "Archer",
            Self::Pikeman => "Pikeman",
            Self::Catapult { .. } => "Catapult",
            Self::Captain { .. } => "Captain",
            Self::Unknown(_) => "Unknown Unit",
        }
    }
}

impl fmt::Display for UnitType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Archer => write!(f, "Archer"),
            Self::Pikeman => write!(f, "Pikeman"),
            Self::Catapult { target } => write!(f, "Catapult targeting {target}"),
            Self::Captain { ability, wait_time } => {
                write!(f, "Captain using {ability} for {wait_time} seconds")
            }
            Self::Unknown(id) => write!(f, "Unknown unit (type {id})"),
        }
    }
}

/// A unit record representing a single military unit
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "tsify", derive(Tsify))]
#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))]
pub struct UnitRecord {
    /// Position on the battlefield
    pub position: Position,
    /// Type and configuration of the unit
    pub unit_type: UnitType,
}

impl UnitRecord {
    /// Create a new unit record
    #[must_use]
    pub const fn new(x: u8, y: u8, unit_type: UnitType) -> Self {
        Self {
            position: Position::new(x, y),
            unit_type,
        }
    }

    /// Get the unit's position
    #[must_use]
    pub const fn position(&self) -> &Position {
        &self.position
    }

    /// Get the unit's type
    #[must_use]
    pub const fn unit_type(&self) -> &UnitType {
        &self.unit_type
    }
}

impl fmt::Display for UnitRecord {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} at {}", self.unit_type, self.position)
    }
}