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. 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

[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.

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.

#[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 file for details.