Skip to main content

bevy_magic/enchanting/
mod.rs

1//! Enchanting system — attaches persistent magical effects to [`Enchantable`] entities.
2//!
3//! An [`Enchantment`] is a named, persistent effect whose magic comes from either
4//! inline [`Rune`]s or a loaded [`Spell`] asset.  Any entity that bears the
5//! [`Enchantable`] marker component can receive enchantments via
6//! [`ApplyEnchantmentMessage`] and have them removed via [`RemoveEnchantmentMessage`].
7//!
8//! # Items
9//!
10//! Items (weapons, armour, consumables, etc.) are enchantable by simply adding
11//! [`Enchantable`] to the item entity.
12//! The enchantment's [`CastContext`] will name the item as the target; rune
13//! systems are free to walk equipped-item relationships with normal queries.
14//!
15//! # Triggers
16//!
17//! By default enchantments are **timed** — each rune fires according to its own
18//! `delay()` / `interval()` schedule.  Set [`EnchantmentTrigger::OnDemand`] on
19//! an [`Enchantment`] to suppress timer-driven firing; instead the runes run
20//! only when a [`TriggerEnchantmentMessage`] naming that enchantment is sent,
21//! which is useful for "on-hit", "on-equip", or other event-driven effects.
22//!
23//! # Quick start
24//!
25//! ```rust,ignore
26//! use std::sync::Arc;
27//! use bevy::prelude::*;
28//! use bevy_magic::{
29//!     enchanting::prelude::*,
30//!     prelude::*,
31//! };
32//!
33//! fn setup(mut commands: Commands) {
34//!     // Spawn an enchantable sword entity.
35//!     commands.spawn((Name::new("Sword"), Enchantable));
36//! }
37//!
38//! fn enchant_sword(mut commands: Commands, sword: Query<Entity, With<Enchantable>>) {
39//!     let applier = Entity::PLACEHOLDER;
40//!     for entity in &sword {
41//!         let enchantment = Enchantment::from_runes(
42//!             "Flame Edge",
43//!             "Deals periodic fire damage.",
44//!             applier,
45//!             vec![Box::new(my_fire_rune)],
46//!         );
47//!         commands.apply_enchantment(entity, enchantment);
48//!     }
49//! }
50//! ```
51
52use std::sync::Arc;
53
54use bevy::{
55    ecs::{
56        message::MessageCursor,
57        system::{BoxedSystem, SystemId},
58    },
59    prelude::*,
60};
61
62use crate::{
63    runes::{BoxedRune, CastContext}, spell::Spell
64};
65
66pub mod prelude {
67    pub use super::{
68        ActiveEnchantments, Enchantable, Enchantment, EnchantmentSource, EnchantmentTrigger,
69        ApplyEnchantmentMessage, RemoveEnchantmentMessage, TriggerEnchantmentMessage,
70    };
71}
72
73// ---------------------------------------------------------------------------
74// EnchantmentTrigger
75// ---------------------------------------------------------------------------
76
77/// Determines when an enchantment's runes fire.
78///
79/// The default is [`EnchantmentTrigger::Timed`], which preserves the existing
80/// timer-driven behaviour where each rune fires according to its `delay()` and
81/// `interval()`.
82///
83/// Use [`EnchantmentTrigger::OnDemand`] for event-driven effects (on-hit,
84/// on-equip, etc.) — runes only run when a [`TriggerEnchantmentMessage`] is
85/// sent that names this enchantment.
86#[derive(Clone, Debug, Default)]
87pub enum EnchantmentTrigger {
88    /// Runes fire according to each rune's own `delay()` / `interval()` timers
89    /// (default behaviour).
90    #[default]
91    Timed,
92    /// Runes fire only when a [`TriggerEnchantmentMessage`] names this enchantment.
93    /// The enchantment persists until explicitly removed.
94    OnDemand,    /// Runes fire whenever `source` casts a spell.
95    ///
96    /// `source` is the entity holding the enchantment. Targets are the spell targets.
97    OnCast,
98    /// Runes fire when the source entity dies/despawns.
99    ///
100    /// This can be used for death throes and explosive weapons.
101    OnDespawn,
102    /// Runes fire when the source enters/triggers an area event.
103    ///
104    /// Use `commands.trigger_enchantment(source, name, Some(area_targets))`.
105    OnTriggerArea,}
106
107// ---------------------------------------------------------------------------
108// EnchantmentSource
109// ---------------------------------------------------------------------------
110
111/// Defines where an enchantment's effects come from.
112pub enum EnchantmentSource {
113    /// Inline [`Rune`]s executed when the enchantment ticks.
114    Runes(Vec<BoxedRune>),
115    /// A loaded [`Spell`] asset whose runes drive the enchantment effect.
116    Spell(Handle<Spell>),
117}
118
119// ---------------------------------------------------------------------------
120// Enchantment
121// ---------------------------------------------------------------------------
122
123/// A persistent magical effect to be applied to an [`Enchantable`] entity.
124///
125/// Construct with [`Enchantment::from_runes`] or [`Enchantment::from_spell`], then
126/// send an [`ApplyEnchantmentMessage`] (or use [`crate::CommandsExt::apply_enchantment`])
127/// to attach it to an entity.
128pub struct Enchantment {
129    /// Human-readable name — also used as the key for removal.
130    pub name: String,
131    /// Flavour / tooltip text.
132    pub description: String,
133    /// Entity that applied this enchantment; passed as `caster` in [`CastContext`].
134    pub applier: Entity,
135    /// Source of this enchantment's effects.
136    pub source: EnchantmentSource,
137    /// When runes should fire (default: [`EnchantmentTrigger::Timed`]).
138    pub trigger: EnchantmentTrigger,
139}
140
141impl Enchantment {
142    /// Creates an enchantment driven by inline runes.
143    pub fn from_runes(
144        name: impl Into<String>,
145        description: impl Into<String>,
146        applier: Entity,
147        runes: Vec<BoxedRune>,
148    ) -> Self {
149        Self {
150            name: name.into(),
151            description: description.into(),
152            applier,
153            source: EnchantmentSource::Runes(runes),
154            trigger: EnchantmentTrigger::default(),
155        }
156    }
157
158    /// Creates an enchantment driven by a loaded [`Spell`] asset.
159    pub fn from_spell(
160        name: impl Into<String>,
161        description: impl Into<String>,
162        applier: Entity,
163        spell: Handle<Spell>,
164    ) -> Self {
165        Self {
166            name: name.into(),
167            description: description.into(),
168            applier,
169            source: EnchantmentSource::Spell(spell),
170            trigger: EnchantmentTrigger::default(),
171        }
172    }
173
174    /// Override the trigger mode for this enchantment.
175    ///
176    /// # Example
177    /// ```rust,ignore
178    /// Enchantment::from_runes("On-Hit Burn", "...", applier, runes)
179    ///     .with_trigger(EnchantmentTrigger::OnDemand)
180    /// ```
181    pub fn with_trigger(mut self, trigger: EnchantmentTrigger) -> Self {
182        self.trigger = trigger;
183        self
184    }
185}
186
187// ---------------------------------------------------------------------------
188// Enchantable component
189// ---------------------------------------------------------------------------
190
191/// Marker component. Only entities with this component accept [`ApplyEnchantmentMessage`].
192///
193/// Add it to any entity you want to be enchantable — weapons, armour, characters, etc.
194#[derive(Component, Default, Debug)]
195pub struct Enchantable;
196
197// ---------------------------------------------------------------------------
198// Messages
199// ---------------------------------------------------------------------------
200
201/// Send this message to apply an [`Enchantment`] to an [`Enchantable`] entity.
202///
203/// The enchantment is wrapped in [`Arc`] so the message is cheaply `Clone`.
204///
205/// # Example
206///
207/// ```rust,ignore
208/// commands.apply_enchantment(entity, my_enchantment);
209/// ```
210#[derive(Message, Clone)]
211pub struct ApplyEnchantmentMessage {
212    /// Target entity — must have [`Enchantable`].
213    pub target: Entity,
214    /// The enchantment to apply.
215    pub enchantment: Arc<Enchantment>,
216}
217
218/// Send this message to remove a named enchantment from an entity.
219#[derive(Message, Clone, Debug)]
220pub struct RemoveEnchantmentMessage {
221    /// The entity to un-enchant.
222    pub target: Entity,
223    /// Name of the enchantment to remove (case-sensitive).
224    pub name: String,
225}
226
227/// Send this message to fire the runes of an [`EnchantmentTrigger::OnDemand`]
228/// enchantment.
229///
230/// `source` is the entity that hosts the enchantment. `targets` are the entity
231/// or entities affected by the triggered effect.
232///
233/// # Example
234/// ```rust,ignore
235/// // Inside an "on-hit" observer or system:
236/// commands.trigger_enchantment(sword_entity, "Flame Edge", Some(vec![goblin]))
237/// ```
238#[derive(Message, Clone, Debug)]
239pub struct TriggerEnchantmentMessage {
240    /// Entity that carries the enchantment.
241    pub source: Entity,
242    /// Name of the enchantment to fire (case-sensitive).
243    pub name: String,
244    /// Targets to receive the triggered effect.
245    pub targets: Vec<Entity>,
246}
247
248// ---------------------------------------------------------------------------
249// Internal tracking
250// ---------------------------------------------------------------------------
251
252/// One active enchantment slot on an entity — carries public metadata.
253pub struct ActiveEnchantmentEntry {
254    /// The name of this enchantment (matches what was applied).
255    pub name: String,
256    /// Description of this enchantment.
257    pub description: String,
258    pub(crate) applier: Entity,
259    pub(crate) trigger: EnchantmentTrigger,
260    pub(crate) rune_executions: Vec<ActiveEnchantRune>,
261}
262
263impl ActiveEnchantmentEntry {
264    pub fn trigger(&self) -> EnchantmentTrigger {
265        self.trigger.clone()
266    }
267}
268
269pub(crate) struct ActiveEnchantRune {
270    pub system_id: SystemId<In<CastContext>>,
271    pub timer: Timer,
272    pub repeating: bool,
273}
274
275/// Tracks all active enchantments on an entity.
276///
277/// Added automatically when a first enchantment is applied; removed when the
278/// last one completes or is explicitly removed.
279#[derive(Component, Default)]
280pub struct ActiveEnchantments {
281    /// All currently active enchantment slots.
282    pub enchantments: Vec<ActiveEnchantmentEntry>,
283}
284
285impl ActiveEnchantments {
286    /// Returns `true` if the entity bears an enchantment with the given name.
287    pub fn has_enchantment(&self, name: &str) -> bool {
288        self.enchantments.iter().any(|e| e.name == name)
289    }
290
291    /// Number of currently active enchantments.
292    pub fn count(&self) -> usize {
293        self.enchantments.len()
294    }
295
296    /// Iterates over the names of all active enchantments.
297    pub fn names(&self) -> impl Iterator<Item = &str> {
298        self.enchantments.iter().map(|e| e.name.as_str())
299    }
300}
301
302// ---------------------------------------------------------------------------
303// Message cursors (internal)
304// ---------------------------------------------------------------------------
305
306#[derive(Resource, Default)]
307pub(crate) struct ApplyEnchantmentCursor(pub MessageCursor<ApplyEnchantmentMessage>);
308
309#[derive(Resource, Default)]
310pub(crate) struct RemoveEnchantmentCursor(pub MessageCursor<RemoveEnchantmentMessage>);
311
312#[derive(Resource, Default)]
313pub(crate) struct TriggerEnchantmentCursor(pub MessageCursor<TriggerEnchantmentMessage>);
314
315// ---------------------------------------------------------------------------
316// Systems
317// ---------------------------------------------------------------------------
318
319/// Processes pending [`ApplyEnchantmentMessage`]s, building rune systems and
320/// inserting [`ActiveEnchantments`] onto target entities.
321///
322/// Silently ignores messages whose target lacks the [`Enchantable`] component or
323/// whose [`Spell`] asset is not yet loaded.
324pub(crate) fn apply_enchantments(world: &mut World) {
325    let messages: Vec<ApplyEnchantmentMessage> = world.resource_scope(|world, mut cursor: Mut<ApplyEnchantmentCursor>| {
326        let messages_res = world.resource::<Messages<ApplyEnchantmentMessage>>();
327        cursor.0.read(messages_res).cloned().collect()
328    });
329
330    for msg in messages {
331        let target = msg.target;
332        let enchantment = Arc::clone(&msg.enchantment);
333
334        // Only process entities that opted in.
335        let target_entity = match world.get_entity(target) {
336            Ok(ent) => ent,
337            Err(_) => continue,
338        };
339
340        if target_entity.get::<Enchantable>().is_none() {
341            warn_once!(
342                "ApplyEnchantmentMessage: entity {:?} does not have the Enchantable component — ignoring.",
343                target
344            );
345            continue;
346        }
347
348        // Build boxed systems from the enchantment source.  The result is a Vec
349        // of (system, delay_secs, repeating) tuples, all with 'static lifetimes so
350        // no borrow from Assets<Spell> escapes this block.
351        let opt_systems: Option<Vec<(BoxedSystem<In<CastContext>, ()>, f32, bool)>> =
352            match &enchantment.source {
353                EnchantmentSource::Runes(runes) => Some(
354                    runes
355                        .iter()
356                        .map(|r| (r.build(), r.delay().as_secs_f32(), !r.interval().is_zero()))
357                        .collect(),
358                ),
359                EnchantmentSource::Spell(spell_handle) => {
360                    let assets = world.resource::<Assets<Spell>>();
361                    match assets.get(spell_handle) {
362                        None => {
363                            warn_once!(
364                                "ApplyEnchantmentMessage: spell asset {:?} is not loaded — ignoring enchantment '{}'.",
365                                spell_handle, enchantment.name
366                            );
367                            None
368                        }
369                        Some(spell) => Some(
370                            spell
371                                .runes
372                                .iter()
373                                .map(|r| {
374                                    (r.build(), r.delay().as_secs_f32(), !r.interval().is_zero())
375                                })
376                                .collect(),
377                        ),
378                    }
379                    // `assets` borrow released here.
380                }
381            };
382
383        let boxed_systems = match opt_systems {
384            None => continue,
385            Some(v) => v,
386        };
387
388        // Register each system and build execution entries.
389        let mut rune_executions = Vec::with_capacity(boxed_systems.len());
390        for (system, delay, repeating) in boxed_systems {
391            let system_id: SystemId<In<CastContext>> = world.register_boxed_system(system);
392            let timer = Timer::from_seconds(
393                delay,
394                if repeating {
395                    TimerMode::Repeating
396                } else {
397                    TimerMode::Once
398                },
399            );
400            rune_executions.push(ActiveEnchantRune {
401                system_id,
402                timer,
403                repeating,
404            });
405        }
406
407        let entry = ActiveEnchantmentEntry {
408            name: enchantment.name.clone(),
409            description: enchantment.description.clone(),
410            applier: enchantment.applier,
411            trigger: enchantment.trigger.clone(),
412            rune_executions,
413        };
414
415        // Insert or update the component on the target.
416        if let Ok(mut entity_mut) = world.get_entity_mut(target) {
417            if let Some(mut active) = entity_mut.get_mut::<ActiveEnchantments>() {
418                active.enchantments.push(entry);
419            } else {
420                entity_mut.insert(ActiveEnchantments {
421                    enchantments: vec![entry],
422                });
423            }
424        }
425    }
426}
427
428/// Processes pending [`RemoveEnchantmentMessage`]s, dropping matching enchantment
429/// slots from the target entity's [`ActiveEnchantments`].
430pub(crate) fn remove_enchantments(world: &mut World) {
431    let messages: Vec<RemoveEnchantmentMessage> = world.resource_scope(|world, mut cursor: Mut<RemoveEnchantmentCursor>| {
432        let messages_res = world.resource::<Messages<RemoveEnchantmentMessage>>();
433        cursor.0.read(messages_res).cloned().collect()
434    });
435
436    for msg in messages {
437        if let Ok(mut entity_mut) = world.get_entity_mut(msg.target) {
438            if let Some(mut active) = entity_mut.get_mut::<ActiveEnchantments>() {
439                active.enchantments.retain(|e| e.name != msg.name);
440            }
441        }
442    }
443}
444
445/// Ticks all **timed** enchantment rune timers and runs rune systems when they fire.
446///
447/// [`EnchantmentTrigger::OnDemand`] enchantments are skipped here — their runes
448/// fire only via [`trigger_enchantments`].  Non-repeating runes are dropped after
449/// firing.  Enchantment slots with no remaining rune executions are pruned, and
450/// the [`ActiveEnchantments`] component is removed when it becomes empty.
451pub(crate) fn tick_enchantments(world: &mut World) {
452    let delta = world.resource::<Time>().delta();
453
454    let mut systems_to_run: Vec<(SystemId<In<CastContext>>, CastContext)> = Vec::new();
455    let mut entities_to_cleanup: Vec<Entity> = Vec::new();
456    // (entity, spawn_origin, [(applier, system_id)]) — snapshotted before rune systems run
457    let mut despawn_watchlist: Vec<(Entity, Vec3, Vec<(Entity, SystemId<In<CastContext>>)>)> =
458        Vec::new();
459
460    for (entity, mut active, maybe_tf) in world
461        .query::<(Entity, &mut ActiveEnchantments, Option<&Transform>)>()
462        .iter_mut(world)
463    {
464        // Snapshot OnDespawn rune info while the entity + transform are still live.
465        let ondespawn_runes: Vec<(Entity, SystemId<In<CastContext>>)> = active
466            .enchantments
467            .iter()
468            .filter(|e| matches!(e.trigger, EnchantmentTrigger::OnDespawn))
469            .flat_map(|e| {
470                let applier = e.applier;
471                e.rune_executions.iter().map(move |r| (applier, r.system_id))
472            })
473            .collect();
474        if !ondespawn_runes.is_empty() {
475            let origin = maybe_tf.map(|tf| tf.translation).unwrap_or(Vec3::ZERO);
476            despawn_watchlist.push((entity, origin, ondespawn_runes));
477        }
478
479        active.enchantments.retain_mut(|enchantment| {
480            // Only Timed enchantments are driven by the timer path.
481            // OnDemand, OnDespawn, OnCast, and OnTriggerArea fire through
482            // their own dedicated paths and must never be auto-ticked here.
483            if !matches!(enchantment.trigger, EnchantmentTrigger::Timed) {
484                return true;
485            }
486
487            let applier = enchantment.applier;
488            enchantment.rune_executions.retain_mut(|rune| {
489                rune.timer.tick(delta);
490                if rune.timer.just_finished() {
491                    systems_to_run.push((
492                        rune.system_id,
493                        CastContext {
494                            caster: applier,
495                            targets: vec![entity],
496                            origin: None,
497                        },
498                    ));
499                    if rune.repeating {
500                        rune.timer.reset();
501                        true
502                    } else {
503                        false
504                    }
505                } else {
506                    true
507                }
508            });
509            !enchantment.rune_executions.is_empty()
510        });
511
512        if active.enchantments.is_empty() {
513            entities_to_cleanup.push(entity);
514        }
515    }
516
517    for entity in entities_to_cleanup {
518        if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
519            entity_mut.remove::<ActiveEnchantments>();
520        }
521    }
522
523    for (system_id, mut context) in systems_to_run {
524        context.targets.retain(|&e| world.get_entity(e).is_ok());
525        let _ = world.run_system_with(system_id, context);
526    }
527
528    // Fire OnDespawn runes for any entity that was just killed by the rune systems above.
529    // The entity is gone from the world — use the pre-death origin snapshot in CastContext.
530    for (entity, origin, rune_infos) in despawn_watchlist {
531        if world.get_entity(entity).is_err() {
532            for (caster, system_id) in rune_infos {
533                let ctx = CastContext {
534                    caster,
535                    targets: vec![],
536                    origin: Some(origin),
537                };
538                let _ = world.run_system_with(system_id, ctx);
539            }
540        }
541    }
542}
543
544/// Processes pending [`TriggerEnchantmentMessage`]s, firing the runes of each
545/// named [`EnchantmentTrigger::OnDemand`] enchantment once.
546///
547/// The enchantment persists after firing; send a [`RemoveEnchantmentMessage`]
548/// to tear it down explicitly.
549pub(crate) fn trigger_enchantments(world: &mut World) {
550    let messages: Vec<TriggerEnchantmentMessage> = world.resource_scope(|world, mut cursor: Mut<TriggerEnchantmentCursor>| {
551        let messages_res = world.resource::<Messages<TriggerEnchantmentMessage>>();
552        cursor.0.read(messages_res).cloned().collect()
553    });
554
555    let mut systems_to_run: Vec<(SystemId<In<CastContext>>, CastContext)> = Vec::new();
556
557    for msg in messages {
558        let source = msg.source;
559        let Ok(mut entity_mut) = world.get_entity_mut(source) else {
560            continue;
561        };
562        let Some(mut active) = entity_mut.get_mut::<ActiveEnchantments>() else {
563            continue;
564        };
565
566        for enchantment in active.enchantments.iter_mut() {
567            if enchantment.name != msg.name {
568                continue;
569            }
570            if matches!(enchantment.trigger, EnchantmentTrigger::Timed) {
571                warn_once!(
572                    "TriggerEnchantmentMessage: enchantment '{}' on {:?} is Timed — use direct timed flow.",
573                    msg.name, source
574                );
575                continue;
576            }
577            let applier = enchantment.applier;
578            for rune in &enchantment.rune_executions {
579                systems_to_run.push((
580                    rune.system_id,
581                    CastContext {
582                        caster: applier,
583                        targets: msg.targets.clone(),
584                        origin: None,
585                    },
586                ));
587            }
588        }
589    }
590
591    for (system_id, mut context) in systems_to_run {
592        context.targets.retain(|&e| world.get_entity(e).is_ok());
593        let _ = world.run_system_with(system_id, context);
594    }
595}
596
597/// Snapshot of one `OnDespawn` rune to be fired after the entity is gone.
598pub(crate) struct PendingDespawnRune {
599    pub system_id: SystemId<In<CastContext>>,
600    pub caster: Entity,
601    pub origin: Vec3,
602}
603
604/// Buffer populated by [`ondespawn_trigger_enchantments`] and drained by
605/// [`flush_despawn_triggers`] later in the same frame.
606#[derive(Resource, Default)]
607pub(crate) struct PendingDespawnTriggers(pub Vec<PendingDespawnRune>);
608
609/// Observer that fires when an entity with `OnDespawn` enchantments is
610/// despawned.  Snapshots the rune system IDs and the entity's world position
611/// into [`PendingDespawnTriggers`] so they can be executed after the entity
612/// is fully removed.
613pub(crate) fn ondespawn_trigger_enchantments(
614    event: On<Despawn>,
615    mut pending: ResMut<PendingDespawnTriggers>,
616    query: Query<(Entity, &ActiveEnchantments, Option<&Transform>)>,
617) {
618    let Ok((_entity, active, maybe_tf)) = query.get(event.entity) else {
619        return;
620    };
621    let origin = maybe_tf.map(|tf| tf.translation).unwrap_or(Vec3::ZERO);
622
623    for enchantment in &active.enchantments {
624        if !matches!(enchantment.trigger, EnchantmentTrigger::OnDespawn) {
625            continue;
626        }
627        let caster = enchantment.applier;
628        for rune in &enchantment.rune_executions {
629            pending.0.push(PendingDespawnRune {
630                system_id: rune.system_id,
631                caster,
632                origin,
633            });
634        }
635    }
636}
637
638/// Drains [`PendingDespawnTriggers`] and runs each queued rune system.
639///
640/// Runs at the end of the `Update` chain — after the tick and trigger systems
641/// — so it executes in the same frame as the despawn.
642pub(crate) fn flush_despawn_triggers(world: &mut World) {
643    let runes = world.resource_scope(|_world, mut pending: Mut<PendingDespawnTriggers>| {
644        std::mem::take(&mut pending.0)
645    });
646
647    for rune in runes {
648        let ctx = CastContext {
649            caster: rune.caster,
650            targets: vec![],
651            origin: Some(rune.origin),
652        };
653        let _ = world.run_system_with(rune.system_id, ctx);
654    }
655}