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 std::cell::RefCell;
use std::rc::Rc;

use hocon_rs::Value;
use logic_constructor::prelude::*;

// --- LC Entities ---

#[derive(Clone)]
pub struct PlayerEntity {
    pub hp: Rc<RefCell<f32>>,
}

#[derive(Clone)]
pub struct EnemyEntity {
    pub hp: Rc<RefCell<f32>>,
}

#[derive(Clone)]
pub struct NpcEntity {
    pub age: i16,
}

// --- Entity Co-Product ---
#[derive(Clone)]
pub enum LcGameEntity {
    Player(PlayerEntity),
    Enemy(EnemyEntity),
    Npc(NpcEntity),
}

impl LcEntityType for LcGameEntity {
    fn type_id(&self) -> LcEntityTypeId {
        match self {
            LcGameEntity::Player(_) => 1,
            LcGameEntity::Enemy(_) => 2,
            LcGameEntity::Npc(_) => 3,
        }
    }
}

impl From<PlayerEntity> for LcEntity<LcGameEntity> {
    fn from(value: PlayerEntity) -> Self {
        LcEntity {
            game_entity: LcGameEntity::Player(value),
        }
    }
}

impl From<EnemyEntity> for LcEntity<LcGameEntity> {
    fn from(value: EnemyEntity) -> Self {
        LcEntity {
            game_entity: LcGameEntity::Enemy(value),
        }
    }
}

impl From<NpcEntity> for LcEntity<LcGameEntity> {
    fn from(value: NpcEntity) -> Self {
        LcEntity {
            game_entity: LcGameEntity::Npc(value),
        }
    }
}

impl LcGameEntity {
    pub fn maybe_health(&self) -> Option<Rc<RefCell<f32>>> {
        match self {
            LcGameEntity::Player(e) => Some(e.hp.clone()),
            LcGameEntity::Enemy(e) => Some(e.hp.clone()),
            LcGameEntity::Npc(_) => None,
        }
    }
}

macro_rules! impl_lc_game_action {
    ($t:ty) => {
        impl LcAction<LcGameEntity> for $t {
            fn apply(&self, source: &LcEntity<LcGameEntity>, target: &LcEntity<LcGameEntity>) {
                <Self as LcGameAction>::apply(
                    self,
                    source.game_entity.clone(),
                    target.game_entity.clone(),
                )
            }
            fn clone_box(&self) -> Box<dyn LcAction<LcGameEntity>> {
                Box::new(self.clone())
            }
        }
    };
}

pub trait LcGameAction: LcAction<LcGameEntity> {
    fn apply(&self, source: LcGameEntity, target: LcGameEntity);
}

// --- LC Actions ---

#[derive(Clone)]
pub struct DealDamage {
    pub amount: f32,
}

impl_lc_game_action!(DealDamage);
impl LcGameAction for DealDamage {
    fn apply(&self, _source: LcGameEntity, target: LcGameEntity) {
        if let Some(health) = target.maybe_health() {
            *health.borrow_mut() -= self.amount;
        }
    }
}

#[derive(Clone)]
pub struct Heal {
    pub amount: f32,
}

impl_lc_game_action!(Heal);
impl LcGameAction for Heal {
    fn apply(&self, _source: LcGameEntity, target: LcGameEntity) {
        if let Some(health) = target.maybe_health() {
            *health.borrow_mut() += self.amount;
        }
    }
}

pub fn parse_game_effect(value: &Value) -> Result<Box<dyn LcAction<LcGameEntity>>, String> {
    let obj = value
        .as_object()
        .ok_or_else(|| format!("expected object, got {:?}", value))?;
    if obj.len() != 1 {
        return Err(format!("expected single key, got {}", obj.len()));
    }
    let (name, inner) = obj.iter().next().unwrap();
    let amount = match inner {
        Value::Number(n) => n
            .as_f64()
            .ok_or_else(|| format!("{} expects a number", name))? as f32,
        _ => return Err(format!("{} expects a number", name)),
    };
    match name.as_str() {
        "DealDamage" => Ok(Box::new(DealDamage { amount })),
        "Heal" => Ok(Box::new(Heal { amount })),
        other => Err(format!("unknown effect: {}", other)),
    }
}