Skip to main content

bevy_magic/
plugin.rs

1//! [`MagicPlugin`], [`CastSpellMessage`], and the exclusive cast-dispatch system.
2
3use std::collections::HashMap;
4
5use bevy::{
6    ecs::{
7        message::MessageCursor,
8        system::{BoxedSystem, SystemId},
9    },
10    prelude::*,
11};
12
13use crate::{
14    enchanting::{
15        ActiveEnchantments, ApplyEnchantmentCursor, ApplyEnchantmentMessage,
16        EnchantmentTrigger, PendingDespawnTriggers, RemoveEnchantmentCursor,
17        RemoveEnchantmentMessage, TriggerEnchantmentCursor, TriggerEnchantmentMessage,
18        apply_enchantments, flush_despawn_triggers, ondespawn_trigger_enchantments,
19        remove_enchantments, tick_enchantments, trigger_enchantments,
20    },
21    runes::{ActiveSpells, CastContext, Rune, RuneRegistry},
22    spell::{Spell, SpellAssetLoader},
23};
24
25// ---------------------------------------------------------------------------
26// Public message
27// ---------------------------------------------------------------------------
28
29/// Send this message to cast a spell.
30///
31/// # Multi-target support
32///
33/// `targets` is a `Vec<Entity>`, so a single cast can hit any number of
34/// entities.  Pass an empty vec for self-cast / area spells that determine
35/// targets inside their own rune logic.
36#[derive(Message, Clone, Debug)]
37pub struct CastSpellMessage {
38    /// The entity casting the spell.
39    pub caster: Entity,
40    /// All targeted entities (empty = self-cast or rune determines targets).
41    pub targets: Vec<Entity>,
42    /// Handle to the spell asset to cast.
43    pub spell: Handle<Spell>,
44}
45
46// ---------------------------------------------------------------------------
47// Internal resources
48// ---------------------------------------------------------------------------
49
50/// Stores this system's position in the [`Messages<CastSpellMessage>`] ring
51/// buffer so messages are never processed twice across frames.
52#[derive(Resource, Default)]
53struct SpellCastCursor(MessageCursor<CastSpellMessage>);
54
55/// Caches the registered [`SystemId`] for each rune in each loaded spell.
56///
57/// Keys are `(AssetId<Spell>, rune_index)`.  An entry is created the first
58/// time a spell is cast after load, and removed when the asset is modified or
59/// unloaded.  Keeping one-shot system entities alive between casts means every
60/// rune is initialized exactly once.
61#[derive(Resource, Default)]
62struct RuneSystemCache {
63    systems: HashMap<(AssetId<Spell>, usize), SystemId<In<CastContext>>>,
64}
65
66impl RuneSystemCache {
67    /// Returns the cached `SystemId` for the given spell and rune index, if it
68    /// exists.
69    pub fn get(&self, spell_id: AssetId<Spell>, rune_index: usize) -> Option<SystemId<In<CastContext>>> {
70        self.systems.get(&(spell_id, rune_index)).cloned()
71    }
72
73    /// Inserts a `SystemId` into the cache for the given spell and rune index.
74    pub fn insert(&mut self, spell_id: AssetId<Spell>, rune_index: usize, system_id: SystemId<In<CastContext>>) {
75        self.systems.insert((spell_id, rune_index), system_id);
76    }
77
78    #[allow(unused)]
79    /// Removes the cache entry for the given spell and rune index.
80    pub fn remove(&mut self, spell_id: AssetId<Spell>, rune_index: usize) {
81        self.systems.remove(&(spell_id, rune_index));
82    }
83
84    /// Clears all cache entries for the given spell.
85    pub fn clear_spell(&mut self, spell_id: AssetId<Spell>) {
86        self.systems.retain(|(id, _), _| *id != spell_id);
87    }
88}
89
90/// [`SystemSet`] label for all magic processing systems.
91///
92/// Use `.after(MagicSystems)` on your own `Update` systems to guarantee they
93/// run after spell casting, enchantment ticking, and despawn triggers have all
94/// been resolved in the current frame.
95#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
96pub struct MagicSystems;
97
98// ---------------------------------------------------------------------------
99// Plugin
100// ---------------------------------------------------------------------------
101
102/// Bevy plugin that wires up the entire magic system.
103///
104/// # Prerequisites
105/// [`bevy::asset::AssetPlugin`] (included in [`bevy::DefaultPlugins`]) **must**
106/// be present before `MagicPlugin`.
107pub struct MagicPlugin {
108    rune_registrations: Vec<Box<dyn Fn(&RuneRegistry) + Send + Sync>>,
109}
110
111impl Default for MagicPlugin {
112    fn default() -> Self {
113        MagicPlugin {
114            rune_registrations: Vec::new(),
115        }
116    }
117}
118
119impl MagicPlugin {
120    /// Add a custom rune type to be registered when the plugin initializes.
121    pub fn rune<R>(mut self) -> Self
122    where
123        R: Rune + TypePath + for<'de> serde::Deserialize<'de> + 'static,
124    {
125        self.rune_registrations
126            .push(Box::new(move |registry: &RuneRegistry| {
127                registry.register::<R>();
128            }));
129        self
130    }
131}
132
133impl Plugin for MagicPlugin {
134    fn build(&self, app: &mut App) {
135        let registry = RuneRegistry::default();
136        // register any user-added runes
137        for f in &self.rune_registrations {
138            f(&registry);
139        }
140
141        app.init_asset::<Spell>()
142            .insert_resource(registry.clone())
143            .register_asset_loader(SpellAssetLoader { registry: registry.clone() })
144            .add_message::<CastSpellMessage>()
145            .add_message::<ApplyEnchantmentMessage>()
146            .add_message::<RemoveEnchantmentMessage>()
147            .add_message::<TriggerEnchantmentMessage>()
148            .init_resource::<SpellCastCursor>()
149            .init_resource::<RuneSystemCache>()
150            .init_resource::<ApplyEnchantmentCursor>()
151            .init_resource::<RemoveEnchantmentCursor>()
152            .init_resource::<TriggerEnchantmentCursor>()
153            .init_resource::<PendingDespawnTriggers>()
154            .add_observer(ondespawn_trigger_enchantments)
155            .add_systems(
156                Update,
157                (
158                    invalidate_spell_cache,
159                    execute_cast_spell_events,
160                    tick_spell_executions,
161                    apply_enchantments,
162                    remove_enchantments,
163                    tick_enchantments,
164                    trigger_enchantments,
165                    flush_despawn_triggers,
166                )
167                    .chain()
168                    .in_set(MagicSystems),
169            );
170    }
171}
172
173// ---------------------------------------------------------------------------
174// Cache-invalidation system
175// ---------------------------------------------------------------------------
176
177/// Removes cache entries when a [`Spell`] asset is modified or unloaded.
178///
179/// Runs in `Update`, always **before** [`execute_cast_spell_events`].  When a
180/// spell is hot-reloaded the stale [`SystemId`]s are evicted; the next cast
181/// rebuilds them via `Rune::build`.
182///
183/// The orphaned one-shot system entities are left in the world intentionally —
184/// unregistering requires exclusive world access; the footprint of a few extra
185/// ECS entities is negligible and they are never run again.
186fn invalidate_spell_cache(
187    mut events: MessageReader<AssetEvent<Spell>>,
188    mut cache: ResMut<RuneSystemCache>,
189) {
190    for event in events.read() {
191        match event {
192            AssetEvent::Modified { id } | AssetEvent::Removed { id } => {
193                cache.clear_spell(*id);
194            }
195            _ => {}
196        }
197    }
198}
199
200// ---------------------------------------------------------------------------
201// Rune execution ticking system
202// ---------------------------------------------------------------------------
203
204/// Ticks all active spell executions and runs rune systems when their timers finish.
205fn tick_spell_executions(world: &mut World) {
206    let time_delta = world.resource::<Time>().delta();
207    
208    // Collect systems to run separately to avoid borrow conflicts
209    let mut systems_to_run: Vec<(SystemId<In<CastContext>>, CastContext)> = Vec::new();
210    
211    // Collect entities with ActiveSpells to avoid borrow conflicts
212    let casters: Vec<Entity> = world
213        .query_filtered::<Entity, With<ActiveSpells>>()
214        .iter(world)
215        .collect();
216
217    // Tick timers and collect systems to run
218    for caster in casters.iter() {
219        if let Some(mut active) = world.entity_mut(*caster).get_mut::<ActiveSpells>() {
220            active.spells.retain_mut(|spell| {
221                spell.runes.retain_mut(|rune| {
222                    rune.timer.tick(time_delta);
223                    if rune.timer.just_finished() {
224                        systems_to_run.push((rune.system, spell.ctx.clone()));
225                        if rune.repeating {
226                            rune.timer.reset();
227                            true  // keep the rune
228                        } else {
229                            false  // remove after one shot
230                        }
231                    } else {
232                        true  // keep, still waiting
233                    }
234                });
235                !spell.runes.is_empty()  // remove spell when all runes are done
236            });
237        }
238    }
239
240    // Remove ActiveSpells component from entities that completed all spells
241    for caster in casters {
242        if let Some(active) = world.entity(caster).get::<ActiveSpells>() {
243            if active.spells.is_empty() {
244                world.entity_mut(caster).remove::<ActiveSpells>();
245            }
246        }
247    }
248
249    // Run collected systems after releasing borrows
250    for (system_id, mut context) in systems_to_run {
251        context.targets.retain(|&e| world.get_entity(e).is_ok());
252        let _ = world.run_system_with(system_id, context);
253    }
254}
255
256// ---------------------------------------------------------------------------
257// Dispatch system
258// ---------------------------------------------------------------------------
259
260/// Exclusive-world system that processes every pending [`CastSpellMessage`].
261pub fn execute_cast_spell_events(world: &mut World) {
262    let messages: Vec<CastSpellMessage> = world.resource_scope(|world, mut cursor: Mut<SpellCastCursor>| {
263        let messages_res = world.resource::<Messages<CastSpellMessage>>();
264        cursor.0.read(messages_res).cloned().collect()
265    });
266
267    // --- Process each cast message -------------------------------------------
268    for message in messages {
269        let spell_id = message.spell.id();
270        let context = CastContext {
271            caster: message.caster,
272            targets: message.targets.clone(),
273            origin: None,
274        };
275
276        // 1. Build boxed systems for cache misses while holding borrows.
277        let missing: Vec<(usize, BoxedSystem<In<CastContext>, ()>)> = {
278            let cache = world.resource::<RuneSystemCache>();
279            match world.resource::<Assets<Spell>>().get(&message.spell) {
280                None => continue,
281                Some(spell) => (0..spell.runes.len())
282                    .filter(|&i| !cache.systems.contains_key(&(spell_id, i)))
283                    .map(|i| (i, spell.runes[i].build()))
284                    .collect(),
285            }
286        }; // borrows on Assets<Spell> and RuneSystemCache dropped here
287
288        // 2. Register new systems and cache their SystemIds.
289        for (i, boxed) in missing {
290            let id = world.register_boxed_system(boxed);
291            world
292                .resource_mut::<RuneSystemCache>()
293                .insert(spell_id, i, id);
294        }
295
296        // 3. Gather cached SystemIds with timing metadata.
297        let spell_opt = world.resource::<Assets<Spell>>().get(&message.spell);
298        if spell_opt.is_none() {
299            continue;
300        }
301        let spell = spell_opt.unwrap();
302        
303        let cache = world.resource::<RuneSystemCache>();
304        let mut rune_systems: Vec<(SystemId<In<CastContext>>, Timer, bool)> = Vec::new();
305        
306        for i in 0..spell.runes.len() {
307            if let Some(sys_id) = cache.get(spell_id, i) {
308                let rune = &spell.runes[i];
309                let delay = rune.delay();
310                let interval = rune.interval();
311                
312                // Create a timer initialized to the delay duration
313                let timer = Timer::from_seconds(
314                    delay.as_secs_f32(),
315                    if interval.is_zero() {
316                        TimerMode::Once
317                    } else {
318                        TimerMode::Repeating
319                    },
320                );
321                
322                let repeating = !interval.is_zero();
323                rune_systems.push((sys_id, timer, repeating));
324            }
325        }
326
327        // 4. Add spell execution to caster's ActiveSpells, or create if missing.
328        if let Ok(mut entity) = world.get_entity_mut(message.caster) {
329            if let Some(mut active) = entity.get_mut::<ActiveSpells>() {
330                active.add_spell(context.clone(), rune_systems);
331            } else {
332                let mut active = ActiveSpells::new();
333                active.add_spell(context.clone(), rune_systems);
334                entity.insert(active);
335            }
336        }
337
338        // 5. Fire any OnCast enchantments on the caster immediately.
339        let mut oncast_systems: Vec<(SystemId<In<CastContext>>, CastContext)> = Vec::new();
340        if let Some(active_enchantments) = world.entity(message.caster).get::<ActiveEnchantments>() {
341            for enchantment in active_enchantments.enchantments.iter() {
342                if matches!(enchantment.trigger, EnchantmentTrigger::OnCast) {
343                    for rune in &enchantment.rune_executions {
344                        oncast_systems.push((
345                            rune.system_id,
346                            CastContext {
347                                caster: message.caster,
348                                targets: message.targets.clone(),
349                                origin: None,
350                            },
351                        ));
352                    }
353                }
354            }
355        }
356
357        for (system_id, context) in oncast_systems {
358            let _ = world.run_system_with(system_id, context);
359        }
360    }
361}