Skip to main content

bevy_magic/runes/
mod.rs

1use std::collections::HashMap;
2use std::sync::{Arc, RwLock};
3
4use bevy::ecs::system::{BoxedSystem, SystemId};
5use bevy::prelude::*;
6use serde::de::Error;
7
8/// Context passed to each [`Rune`] when a spell is executed.
9///
10/// Contains the caster and an arbitrary number of targets, supporting
11/// single-target, multi-target, and self-cast spells uniformly.
12///
13/// `origin` carries a pre-computed world position.  It is primarily used by
14/// post-death (`OnDespawn`) runes that fire after the caster entity is gone,
15/// so rune logic can fall back to this position instead of querying the
16/// (now-despawned) entity's [`Transform`].
17#[derive(Clone, Debug)]
18pub struct CastContext {
19    /// The entity that is casting the spell.
20    pub caster: Entity,
21    /// Every entity targeted by this cast.  May be empty for self-cast spells.
22    pub targets: Vec<Entity>,
23    /// Optional world position override.  Populated for `OnDespawn` enchantments
24    /// so runes can locate the origin without a live entity.
25    pub origin: Option<Vec3>,
26}
27
28impl CastContext {
29    /// Creates a self-cast context with no targets.
30    pub fn new(caster: Entity) -> Self {
31        Self {
32            caster,
33            targets: Vec::new(),
34            origin: None,
35        }
36    }
37
38    /// Builder-style: attach targets to the context.
39    pub fn with_targets(mut self, targets: impl IntoIterator<Item = Entity>) -> Self {
40        self.targets = targets.into_iter().collect();
41        self
42    }
43
44    pub fn with_target(mut self, target: Entity) -> Self {
45        self.targets.push(target);
46        self
47    }
48
49    /// Set the `origin` position override (used by `OnDespawn` runes).
50    pub fn with_origin(mut self, origin: Vec3) -> Self {
51        self.origin = Some(origin);
52        self
53    }
54}
55
56// ---------------------------------------------------------------------------
57// RuneRegistry
58// ---------------------------------------------------------------------------
59
60
61type RuneDeserializationFn = fn(ron::value::Value) -> Result<Box<dyn Rune>, ron::Error>;
62
63#[derive(Default)]
64struct RuneRegistryInner {
65    deserializers: HashMap<String, RuneDeserializationFn>,
66}
67
68impl RuneRegistryInner {
69    fn register<R>(&mut self, name: &str)
70    where
71        R: Rune + for<'de> serde::Deserialize<'de>,
72    {
73        fn deser<R: Rune + for<'de> serde::Deserialize<'de>>(
74            v: ron::value::Value,
75        ) -> Result<Box<dyn Rune>, ron::Error> {
76            // `ron::value::Value` implements `Deserializer`, so we can hand it directly
77            // to the type we want to build.  the error type is already `ron::Error`.
78            let r: R = serde::Deserialize::deserialize(v)?;
79            Ok(Box::new(r) as Box<dyn Rune>)
80        }
81        self.deserializers.insert(name.to_string(), deser::<R>);
82    }
83
84    fn deserialize_rune(&self, mut value: ron::value::Value) -> Result<BoxedRune, RuneDeserializeError> {
85        // extract and consume the "type" field from the map
86        let type_name = if let ron::value::Value::Map(ref mut map) = value {
87            let key = ron::value::Value::String("type".to_string());
88            match map.remove(&key) {
89                Some(ron::value::Value::String(s)) => s,
90                _ => return Err(RuneDeserializeError::MissingType(format!("{:?}", value))),
91            }
92        } else {
93            return Err(RuneDeserializeError::MissingType(format!("{:?}", value)));
94        };
95
96        let deser_fn = self
97            .deserializers
98            .get(&type_name)
99            .ok_or_else(|| RuneDeserializeError::UnknownType(type_name.clone()))?;
100
101        deser_fn(value).map_err(RuneDeserializeError::Ron)
102    }
103}
104
105
106#[derive(Resource, Clone, Default)]
107pub(crate) struct RuneRegistry(Arc<RwLock<RuneRegistryInner>>);
108
109impl RuneRegistry {
110    /// Register a concrete rune type so it can be deserialized from RON.
111    ///
112    /// `name` must match the string returned by [`Rune::name`] for that type.
113    ///
114    /// The underlying representation is RON rather than JSON, but the procedure is
115    /// otherwise the same.
116    pub fn register<R: TypePath>(&self)
117    where
118        R: Rune + for<'de> serde::Deserialize<'de>,
119    {
120        let mut name = R::short_type_path().to_string();
121        if name.ends_with("Rune") {
122            name.truncate(name.len() - 4);
123        }
124        self.0.write().unwrap().register::<R>(&name.to_lowercase());
125    }
126
127
128    /// Deserialize a single rune from a RON value that must include a `"type"` field.
129    ///
130    /// The `"type"` field is consumed and used to look up the registered deserializer.
131    pub fn deserialize_rune(&self, value: ron::value::Value) -> Result<BoxedRune, RuneDeserializeError> {
132        match self.0.read() {
133            Ok(registry) => registry.deserialize_rune(value),
134            Err(_) => Err(RuneDeserializeError::Ron(ron::Error::custom(
135                "rune registry lock poisoned",
136            ))),
137        }
138    }
139}
140
141/// Errors produced while deserializing a [`Rune`] from RON.
142#[derive(Debug, thiserror::Error)]
143pub enum RuneDeserializeError {
144    #[error("rune RON object is missing the required \"type\" field: {0}")]
145    MissingType(String),
146    #[error("unknown rune type \"{0}\" — was it registered with RuneRegistry?")]
147    UnknownType(String),
148    #[error("RON error deserializing rune: {0}")]
149    Ron(ron::Error),
150}
151
152// ---------------------------------------------------------------------------
153// Rune trait
154// ---------------------------------------------------------------------------
155
156/// The atomic unit of a magic spell.
157///
158/// A [`crate::spell::Spell`] is composed of an ordered sequence of [`Rune`]s.
159/// Each rune's effect is a standard Bevy one-shot system that receives the
160/// [`CastContext`] via [`In<CastContext>`] and may freely declare [`Query`],
161/// [`Res`], [`Commands`], and any other system params.
162///
163/// # Implementing a custom rune
164///
165/// 1. Derive `serde::Deserialize` on your struct so it can be loaded from RON.
166/// 2. Implement the two required trait methods.
167/// 3. Register the type before spell assets are loaded:
168///    `registry.register::<MyRune>()`.
169///
170/// ```rust,ignore
171/// use serde::Deserialize;
172/// use bevy::prelude::*;
173/// use bevy::ecs::system::BoxedSystem;
174/// use bevy_magic::runes::{CastContext, Rune};
175///
176/// #[derive(Clone, Deserialize)]
177/// pub struct KnockbackRune { pub force: f32 }
178///
179/// impl Rune for KnockbackRune {
180///     fn name() -> &'static str { "knockback" }
181///
182///     fn build(&self) -> BoxedSystem<In<CastContext>, ()> {
183///         let data = self.clone();
184///         Box::new(IntoSystem::into_system(
185///             move |In(ctx): In<CastContext>| {
186///                 for &target in &ctx.targets {
187///                     info!("knockback {} → {:?}", data.force, target);
188///                 }
189///             },
190///         ))
191///     }
192/// }
193/// ```
194///
195/// # Serialization format
196///
197/// Runes are deserialized from a RON map with a `"type"` discriminant key.
198///
199/// ```ron
200/// (type: "damage", amount: 50.0, damage_type: fire)
201/// ```
202pub trait Rune:  Send + Sync + 'static {
203    /// Build a one-shot Bevy system that applies this rune's effect.
204    ///
205    /// Called at most once per (spell, rune-index) pair after load.  The
206    /// returned system is registered and its [`SystemId`] is cached in
207    /// [`crate::plugin::RuneSystemCache`].
208    fn build(&self) -> BoxedSystem<In<CastContext>, ()>;
209
210    /// How long to wait before the first invocation (default = 0).
211    fn delay(&self) -> std::time::Duration {
212        std::time::Duration::ZERO
213    }
214
215    /// If non-zero, the rune will repeat at this interval (default = 0).
216    fn interval(&self) -> std::time::Duration {
217        std::time::Duration::ZERO
218    }
219}
220
221pub type BoxedRune = Box<dyn Rune>;
222
223// ---------------------------------------------------------------------------
224// Active spell execution tracking
225// ---------------------------------------------------------------------------
226
227/// Tracks in-flight spell/rune executions on a caster entity.
228#[derive(Component)]
229pub struct ActiveSpells {
230    pub(crate) spells: Vec<SpellExecution>,
231}
232
233pub(crate) struct SpellExecution {
234    pub ctx: CastContext,
235    pub runes: Vec<PendingRune>,
236}
237
238pub(crate) struct PendingRune {
239    pub system: SystemId<In<CastContext>>,
240    pub timer: Timer,
241    pub repeating: bool,
242}
243
244impl ActiveSpells {
245    pub fn new() -> Self {
246        Self {
247            spells: Vec::new(),
248        }
249    }
250
251    /// Returns the number of active spell executions.
252    pub fn spell_count(&self) -> usize {
253        self.spells.len()
254    }
255
256    pub(crate) fn add_spell(
257        &mut self,
258        ctx: CastContext,
259        rune_systems: Vec<(SystemId<In<CastContext>>, Timer, bool)>,
260    ) {
261        self.spells.push(SpellExecution {
262            ctx,
263            runes: rune_systems
264                .into_iter()
265                .map(|(sys, timer, repeating)| PendingRune {
266                    system: sys,
267                    timer,
268                    repeating,
269                })
270                .collect(),
271        });
272    }
273}
274
275impl Default for ActiveSpells {
276    fn default() -> Self {
277        Self::new()
278    }
279}