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}