logic_constructor 0.1.0

Move combat and ability logic out of code and into HOCON config — designers tweak damage, healing, and targeting in a text file; the engine parses it into typed actions and runs them against your entities.
Documentation
use hocon_rs::Value;

use crate::collision_kind::CollisionKind;
use crate::lc_action::LcAction;
use crate::lc_entity_type::LcEntityType;
use crate::lc_single_action_config::{LcSingleActionConfig, LcConfigRaw};
use crate::parser::collision_kind_parser::parse_collision_kind;

/// Parses a HOCON value into an `LcConfigRaw`, preserving the un-parsed effect value.
///
/// Supports two formats:
/// 1. Simple format: `{ DealDamage: 10 }` — the whole object is the effect value;
///    collision defaults to `OTHER`.
/// 2. Full format: `{ lca: { DealDamage: 10 }, collision: "Self" }` — `lca` is the
///    effect value, `collision` is parsed via [`parse_collision_kind`].
///
/// The library does not interpret the effect value; consumers do that with their own
/// effect parser when finalizing into [`LcConfig<T>`].
pub fn parse_lc_config_raw(value: &Value) -> Result<LcConfigRaw, String> {
    if let Value::Object(map) = value {
        if let (Some(lca_value), Some(collision_value)) = (map.get("lca"), map.get("collision")) {
            let collision = parse_collision_kind(collision_value)?;
            return Ok(LcConfigRaw {
                effect_value: lca_value.clone(),
                collision,
            });
        }
        if map.contains_key("lca") && !map.contains_key("collision") {
            return Err("LcaConfig requires 'collision' field".to_string());
        }
        if map.contains_key("collision") && !map.contains_key("lca") {
            return Err("LcaConfig requires 'lca' field".to_string());
        }
        return Ok(LcConfigRaw {
            effect_value: value.clone(),
            collision: CollisionKind::OTHER,
        });
    }

    Err(format!(
        "LcaConfig parser expects an object, got: {:?}",
        value
    ))
}

/// Finalizes a HOCON value into a typed [`LcConfig<T>`] by invoking `parse_effect`
/// on the effect value. The closure owns all effect-type knowledge — the library
/// only handles the collision-kind plumbing.
pub fn parse_lc_config<T, F>(
    value: &Value,
    parse_effect: &F,
) -> Result<LcSingleActionConfig<T>, String>
where
    T: LcEntityType,
    F: Fn(&Value) -> Result<Box<dyn LcAction<T>>, String>,
{
    let raw = parse_lc_config_raw(value)?;
    let effect = parse_effect(&raw.effect_value)?;
    Ok(LcSingleActionConfig {
        action: effect,
        collision: raw.collision,
    })
}

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

    #[test]
    fn test_parse_simple_format_raw() {
        let hocon_str = r#"{ DealDamage: 10 }"#;
        let value: Value = Config::parse_str(hocon_str, None).unwrap();

        let result = parse_lc_config_raw(&value).unwrap();
        assert_eq!(result.collision, CollisionKind::OTHER);
        // effect_value is the whole object
        assert!(
            result
                .effect_value
                .as_object()
                .unwrap()
                .contains_key("DealDamage")
        );
    }

    #[test]
    fn test_parse_full_format_self() {
        let hocon_str = r#"
        {
            lca: { DealDamage: 25 }
            collision: "Self"
        }
        "#;
        let value: Value = Config::parse_str(hocon_str, None).unwrap();

        let result = parse_lc_config_raw(&value).unwrap();
        assert_eq!(result.collision, CollisionKind::SELF);
    }

    #[test]
    fn test_parse_full_format_same_kind() {
        let hocon_str = r#"
        {
            lca: { DealDamage: 15 }
            collision: "SameKind"
        }
        "#;
        let value: Value = Config::parse_str(hocon_str, None).unwrap();

        let result = parse_lc_config_raw(&value).unwrap();
        assert_eq!(result.collision, CollisionKind::SAME_KIND);
    }

    #[test]
    fn test_parse_full_format_other() {
        let hocon_str = r#"
        {
            lca: { DealDamage: 5 }
            collision: "Other"
        }
        "#;
        let value: Value = Config::parse_str(hocon_str, None).unwrap();

        let result = parse_lc_config_raw(&value).unwrap();
        assert_eq!(result.collision, CollisionKind::OTHER);
    }

    #[test]
    fn test_parse_full_unquoted_collision() {
        let hocon_str = r#"
        {
            lca: { DealDamage: 5 }
            collision: Self
        }
        "#;
        let value: Value = Config::parse_str(hocon_str, None).unwrap();

        let result = parse_lc_config_raw(&value).unwrap();
        assert_eq!(result.collision, CollisionKind::SELF);
    }

    #[test]
    fn test_parse_missing_lca_field() {
        let hocon_str = r#"
        {
            collision: "Self"
        }
        "#;
        let value: Value = Config::parse_str(hocon_str, None).unwrap();

        let result = parse_lc_config_raw(&value);
        assert!(result.is_err());
        if let Err(err) = result {
            assert!(err.contains("requires 'lca' field"));
        }
    }

    #[test]
    fn test_parse_missing_collision_field() {
        let hocon_str = r#"
        {
            lca: { DealDamage: 10 }
        }
        "#;
        let value: Value = Config::parse_str(hocon_str, None).unwrap();

        let result = parse_lc_config_raw(&value);
        assert!(result.is_err());
        if let Err(err) = result {
            assert!(err.contains("requires 'collision' field"));
        }
    }

    #[test]
    fn test_parse_not_an_object() {
        let hocon_str = r#"value = 42"#;
        let parsed: Value = Config::parse_str(hocon_str, None).unwrap();
        let value = parsed.as_object().unwrap().get("value").unwrap();

        let result = parse_lc_config_raw(value);
        assert!(result.is_err());
        if let Err(err) = result {
            assert!(err.contains("expects an object"));
        }
    }
}