bevy-magic 0.18.0

A Bevy plugin for flexible, data-driven magic systems with enchanting and spellcasting.
Documentation
# bevy-magic


*Education is experience, and the essence of experience is self-reliance. - "The Once and Future King" by T.H. White*

A lightweight spellcasting system built on [Bevy](https://bevyengine.org/).
It provides abstractions for spells, runes, and a simple message-driven cast pipeline.

> [!NOTE]
> This is mainly for my personal use and future use, however, you may use it in your projects or contribute if you find it useful!

## Use


```toml
[dependencies]
bevy = "0.18"
bevy-magic = "0.18"
```

## Example


Below is a minimal example illustrating configuration of the plugin, definition of
custom runes, creation of a spell, and triggering a cast in a Bevy app.

```rust
use bevy::prelude::*;
use bevy_magic::{prelude::*, enchanting::prelude::*};

// --- custom rune types -----------------------------------------------------

#[derive(Debug, Clone, Reflect, Serialize, Deserialize)]

struct DamageRune { amount: f32 }

#[derive(Debug, Clone, Reflect, Serialize, Deserialize)]

struct BurningRune { damage_per_tick: f32 }

impl Rune for BurningRune {
    fn build(&self) -> BoxedSystem<In<CastContext>, ()> {
        let amount = self.damage_per_tick;
        Box::new(IntoSystem::into_system(move |In(ctx): In<CastContext>| {
            for &target in &ctx.targets {
                println!("burn: {:?} takes {} damage", target, amount);
            }
        }))
    }
}


impl Rune for DamageRune {
    fn build(&self) -> BoxedSystem<In<CastContext>, ()> {
        let data = self.clone();
        Box::new(IntoSystem::into_system(move |In(ctx): In<CastContext>| {
            for &target in &ctx.targets {
                println!("hit {:?} for {} damage", target, data.amount);
            }
        }))
    }
}

// --- app setup -------------------------------------------------------------

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(MagicPlugin::default()
            .rune::<DamageRune>()
            )
        .add_startup_system(setup)
        .add_system(on_cast)
        .run();
}

fn setup(mut commands: Commands, mut assets: ResMut<Assets<Spell>>) {
    // build a spell in code (could also be loaded from `assets/` folder as a `.spell` RON file)
    let fireball = Spell::new("Fireball", "Exploding orb")
        .with_rune(DamageRune { amount: 50.0 });

    let handle = assets.add(fireball);

    let caster = commands.spawn_empty().id();
    let target = commands.spawn_empty().id();

    // apply a timed enchantment on the target for periodic effect
    commands.apply_enchantment(
        target,
        Enchantment::from_runes(
            "Burning Aura",
            "Deals damage every second.",
            caster,
            vec![Box::new(BurningRune { damage_per_tick: 8.0 })],
        ),
    );

    // queue a cast message; will run on the next update tick
    commands.write_message(CastSpellMessage {
        caster,
        targets: vec![target],
        spell: handle,
    });
}

// For on-demand (event-driven) enchanting, use:
// commands.trigger_enchantment(source_entity, "Burning Aura", Some(vec![target_entity]));
// after applying the enchantment with `.with_trigger(EnchantmentTrigger::OnDemand)`.

// `source_entity` is the enchanted object (e.g. sword), and `targets` are the entities
// affected by the triggered effect (e.g. hit enemy).
fn on_cast(mut reader: MessageReader<CastSpellMessage>) {
    for msg in reader.read() {
        println!("spell cast by {:?} on {:?}", msg.caster, msg.targets);
    }
}
```

You can also drop spell files (RON) into `assets/spells/` and load them with `AssetServer`.

## Timing: Delays and Intervals


Runes support per-rune delays and intervals. When a spell is cast, the plugin automatically schedules each rune on the caster's `ActiveSpells` component and ticks them each frame.

```rust
#[derive(Debug, Clone, Reflect, Serialize, Deserialize)]

struct BurningRune {
    pub damage_per_tick: f32,
}

impl Rune for BurningRune {
    /// Half-second delay before damage starts ticking.
    fn delay(&self) -> std::time::Duration {
        std::time::Duration::from_secs_f32(0.5)
    }

    /// Tick every 1 second (repeating).
    fn interval(&self) -> std::time::Duration {
        std::time::Duration::from_secs_f32(1.0)
    }

    fn build(&self) -> BoxedSystem<In<CastContext>, ()> {
        let data = self.clone();
        Box::new(IntoSystem::into_system(move |In(ctx): In<CastContext>| {
            for &target in &ctx.targets {
                println!("burn: {} takes {} damage", target, data.damage_per_tick);
            }
        }))
    }
}
```

When this rune is part of a spell:
- The first execution is delayed by 0.5 seconds.
- Then it repeats every 1.0 second.

Runes without timing (both `delay()` and `interval()` return `Duration::ZERO`) execute
immediately and once, just like the `DamageRune` example above.

---

## Contributing

Contributions are welcome! Please open an issue or submit a pull request.

## License

MIT License. See the [LICENSE](LICENSE) file for details.