zzstat 0.5.0

A deterministic, hardcode-free stat calculation engine designed for MMORPGs
Documentation
//! Context information for stat resolution.
//!
//! The `StatContext` provides a way to pass game state information
//! (combat state, zone type, difficulty, etc.) to sources and transforms
//! for conditional calculations. The core does not interpret this data;
//! it's simply passed through.

use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};

/// Context information for stat resolution.
///
/// Contains combat state, target info, zone, difficulty, etc.
/// The core does not interpret this data - it's passed through to
/// sources and transforms for conditional calculations.
///
/// # Examples
///
/// ```rust
/// use zzstat::StatContext;
///
/// let mut context = StatContext::new();
/// context.set("in_combat", true);
/// context.set("zone_type", "pvp");
/// context.set("difficulty", 5);
///
/// let in_combat: Option<bool> = context.get("in_combat");
/// assert_eq!(in_combat, Some(true));
/// ```
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StatContext {
    /// Generic key-value pairs for context data.
    data: FxHashMap<String, serde_json::Value>,
}

impl StatContext {
    /// Create a new empty context.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use zzstat::StatContext;
    ///
    /// let context = StatContext::new();
    /// ```
    pub fn new() -> Self {
        Self::default()
    }

    /// Set a context value.
    ///
    /// The value must be serializable. If serialization fails, the value
    /// is silently not added.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use zzstat::StatContext;
    ///
    /// let mut context = StatContext::new();
    /// context.set("in_combat", true);
    /// context.set("player_level", 50);
    /// context.set("zone_name", "Dungeon");
    /// ```
    pub fn set(&mut self, key: impl Into<String>, value: impl Serialize) {
        if let Ok(json_value) = serde_json::to_value(value) {
            self.data.insert(key.into(), json_value);
        }
    }

    /// Get a context value.
    ///
    /// Returns `None` if the key doesn't exist or if the value
    /// cannot be deserialized to the requested type.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use zzstat::StatContext;
    ///
    /// let mut context = StatContext::new();
    /// context.set("difficulty", 5);
    ///
    /// let difficulty: Option<i32> = context.get("difficulty");
    /// assert_eq!(difficulty, Some(5));
    ///
    /// let missing: Option<i32> = context.get("missing");
    /// assert_eq!(missing, None);
    /// ```
    pub fn get<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Option<T> {
        self.data
            .get(key)
            .and_then(|v| serde_json::from_value(v.clone()).ok())
    }

    /// Check if a key exists in the context.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use zzstat::StatContext;
    ///
    /// let mut context = StatContext::new();
    /// context.set("key", "value");
    ///
    /// assert!(context.contains_key("key"));
    /// assert!(!context.contains_key("missing"));
    /// ```
    pub fn contains_key(&self, key: &str) -> bool {
        self.data.contains_key(key)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_context_set_get() {
        let mut ctx = StatContext::new();
        ctx.set("difficulty", 5);

        let difficulty: Option<i32> = ctx.get("difficulty");
        assert_eq!(difficulty, Some(5));
    }

    #[test]
    fn test_context_missing_key() {
        let ctx = StatContext::new();
        let value: Option<i32> = ctx.get("missing");
        assert_eq!(value, None);
    }

    #[test]
    fn test_context_different_types() {
        let mut ctx = StatContext::new();
        ctx.set("int", 42);
        ctx.set("float", 3.5);
        ctx.set("bool", true);
        ctx.set("string", "hello");
        ctx.set("vec", vec![1, 2, 3]);

        assert_eq!(ctx.get::<i32>("int"), Some(42));
        assert_eq!(ctx.get::<f64>("float"), Some(3.5));
        assert_eq!(ctx.get::<bool>("bool"), Some(true));
        assert_eq!(ctx.get::<String>("string"), Some("hello".to_string()));
        assert_eq!(ctx.get::<Vec<i32>>("vec"), Some(vec![1, 2, 3]));
    }

    #[test]
    fn test_context_type_mismatch() {
        let mut ctx = StatContext::new();
        ctx.set("value", 42);

        // Try to get as wrong type
        let result: Option<String> = ctx.get("value");
        assert_eq!(result, None);
    }

    #[test]
    fn test_context_contains_key() {
        let mut ctx = StatContext::new();
        ctx.set("key1", "value1");

        assert!(ctx.contains_key("key1"));
        assert!(!ctx.contains_key("key2"));
    }

    #[test]
    fn test_context_overwrite() {
        let mut ctx = StatContext::new();
        ctx.set("value", 10);
        assert_eq!(ctx.get::<i32>("value"), Some(10));

        ctx.set("value", 20);
        assert_eq!(ctx.get::<i32>("value"), Some(20));
    }

    #[test]
    fn test_context_serialization() {
        use serde_json;

        let mut ctx = StatContext::new();
        ctx.set("int", 42);
        ctx.set("string", "hello");

        // Serialize
        let json = serde_json::to_string(&ctx).unwrap();
        assert!(json.contains("int"));
        assert!(json.contains("hello"));

        // Deserialize
        let deserialized: StatContext = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.get::<i32>("int"), Some(42));
        assert_eq!(
            deserialized.get::<String>("string"),
            Some("hello".to_string())
        );
    }

    #[test]
    fn test_context_clone() {
        let mut ctx1 = StatContext::new();
        ctx1.set("value", 42);

        let ctx2 = ctx1.clone();
        assert_eq!(ctx2.get::<i32>("value"), Some(42));

        // Modify original
        ctx1.set("value", 100);
        assert_eq!(ctx1.get::<i32>("value"), Some(100));
        // Clone should be unchanged
        assert_eq!(ctx2.get::<i32>("value"), Some(42));
    }
}