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
  • Coverage
  • 52.11%
    37 out of 71 items documented0 out of 44 items with examples
  • Size
  • Source code size: 71.38 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 1.19 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 16s Average build duration of successful builds.
  • all releases: 16s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Homepage
  • optical002/rust-logic-constructor
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • optical002

Logic Constructor

Logic Constructor lets you 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 — no recompile, no glue code per ability.

Example

This is a simplified example which uses data from tests/fixture.rs.

fn main() {
    // 1. Configuration and parsing
    let hocon = r#"
    light-attack = [
        { DealDamage: 15 }
        { lca: { Heal: 4 }, collision: "Self" }
    ]
    "#;

    let parsed: Value = Config::parse_str(hocon, None).unwrap();
    let light_attack_config_value = parsed.as_object().unwrap().get("light-attack").unwrap();

    let light_attack: LcActionConfig<LcGameEntity> =
        parse_lc_action_config(light_attack_config_value, &parse_game_effect).unwrap();

    // 2. Running
    let player = Player { hp: Rc::new(RefCell::new(100.0)) };
    let enemy  = Enemy  { hp: Rc::new(RefCell::new(100.0)) };

    run_lca(&light_attack, &player.clone().into(), &enemy.clone().into());

    assert_eq!(*enemy.hp.borrow(), 85.0);    // OTHER hit enemy: -15
    assert_eq!(*player.hp.borrow(), 104.0);  // SELF healed source: +4
}

Configuration & Boilerplate

  • Define entities which would qualify for running logic constructor actions on.
  • Define logic constructor actions.

Defining entities

Configuration

First we are going to define our entities structures.

pub struct Player {
    hp: Rc<RefCell<f32>>,
}

pub struct Enemy {
    hp: Rc<RefCell<f32>>,
}

// ... and more

Second we should define an enum which acts as a "registry" for all possible entities we will be using for logic constructor (it's best practice to call it LcGameEntity).

pub enum LcGameEntity {
    Player(Player),
    Enemy(Enemy),
    // ... and more
}

Boilerplate

Now we have to tie it down to the logic constructor engine.

We have to implement LcEntityType for our game entities enum (also make sure that LcEntityTypeId would be a unique value per entity type).

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

Define helper From implementations for every type, so it would be easier to pass in entities into run_lca function.

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

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

// ... and more

Additionally for ease of use we can define helper methods on our LcGameEntity, so it would be easier to implement logic constructor actions, like so.

impl LcGameEntity {
    pub fn health(&self) ->Rc<RefCell<f32>> {
        match self {
            LcGameEntity::Player(e) => e.hp.clone(),
            LcGameEntity::Enemy(e) => e.hp.clone(),
            // ... and more
        }
    }
}

Defining actions

Boilerplate

Before configuring our actions, first we will define some boilerplate so it would be easier later on to define our actions.

First we will define our Action trait which combines with our LcGameEntity.

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

Then since we will always need to implement LcAction<LcGameEntity> per action, we can write simple macro to help use out reduce boilerplate per action definition.

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())
            }
        }
    }
}

Configuration

Now when we have our boilerplate in place, we can define our actions, we will define a struct which will hold information for a specific action and implement a trait, which we will tell how to apply an action from source to target.

// Define action and it's data.
pub struct DealDamage {
    pub amount: f32,
}

// Define how it applies from source onto target.
impl_lc_game_action!(DealDamage);
impl LcGameAction for DealDamage {
    fn apply(&self, _source: LcGameEntity, target: LcGameEntity) {
        *target.health.borrow_mut() -= self.amount;
    }
}

pub struct Heal {
    pub amount: f32,
}

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

// ... and more

Parsing

We of course also need to parse the data from hocon configuration, since we are defining which actions are available inside the config.

The parsers can be defined in many ways, but it's goal is to take hoccon Value type and parse it into our LcGameAction. Later on when parsing from hocon config we will be injecting this parser function into run_lca.

pub fn parse_game_action(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 })),
        // ... and more
        other => Err(format!("unknown action: {}", other)),
    }
}

Roadmap

This is only scrathing the surface of what is the real potential of logic constructor in time will be adding way more features as I'll be using this library to develop my own games.

Features

  • Buffs - have actions performed when applied or removed. Another action would be to apply a buff.
  • Stats & Math - inside configs instead of typing out raw number for damage you will be able to write 10 * STR or something similar.
  • Periodic Actions - an action which is applied every x times, or per custom rules. For example while player has slow buff it ticks damage every 2 seconds.
  • Effects - They will be able to last over time like a moment speed reduction while buff is applied.