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}