An experimental, status effects-as-entities system for Bevy.
1use crate::bundle_inspector::BundleInspector;
2use crate::registry::EffectMergeRegistry;
3use crate::{EffectMode, EffectedBy, Effecting};
4use bevy_ecs::prelude::*;
5use bevy_log::{warn, warn_once};
6
7/// Applies an effect to a target entity.
8/// This *might* spawn a new entity, depending on what effects are already applied to the target.
9///
10/// This is normally used via [`with_effect`](EffectCommandsExt::with_effect)
11/// or related spawners ([`EffectedBy::spawn`](SpawnRelated::spawn)).
12pub struct AddEffectCommand<B: Bundle> {
13 /// The entity to apply the effect to.
14 pub target: Entity,
15 /// The effect to apply.
16 pub bundle: B,
17}
18
19impl<B: Bundle> AddEffectCommand<B> {
20 /// Returns the bundle with the relationship component.
21 fn bundle_full(self) -> (Effecting, B) {
22 (Effecting(self.target), self.bundle)
23 }
24
25 /// Merges the [stashed bundle](Self::stash_bundle) with an entity from the given world.
26 /// This is done by calling the [`EffectMergeFn`] for all components in the [registry](EffectMergeRegistry).
27 /// Components not in the registry will be copied to the target entity.
28 fn merge(self, world: &mut World, existing_entity: Entity) {
29 world
30 .try_resource_scope::<BundleInspector, ()>(|world, inspector| {
31 world.try_resource_scope::<EffectMergeRegistry, ()>(|world, registry| {
32 for incoming_component_id in inspector.get_ref().archetype().components() {
33 let type_id = inspector.get_type_id(*incoming_component_id).unwrap();
34
35 if let Some(merge) = registry.merges.get(&type_id) {
36 let entity_mut = world.entity_mut(existing_entity);
37
38 if entity_mut.contains_type_id(type_id) {
39 merge(entity_mut, inspector.get_ref());
40 continue;
41 }
42 }
43
44 unsafe {
45 // SAFETY: `incoming_component_id` `type_id` were extracted from the inspector.
46 _ = inspector.copy_to_world(world, existing_entity, type_id, *incoming_component_id)
47 .inspect_err(|e| {
48 warn!("{e}");
49 });
50 }
51 }
52 })
53 .or_else(|| {
54 warn_once!("No `EffectMergeRegistry` found. Did you forget to add the `AlchemyPlugin`?");
55 None
56 });
57 })
58 .or_else(|| {
59 warn_once!("No `BundleInspector` found. Did you forget to add the `AlchemyPlugin`?");
60 None
61 });
62 }
63}
64
65impl<B: Bundle + Clone> Command for AddEffectCommand<B> {
66 fn apply(self, world: &mut World) {
67 let mut inspector = world.get_resource_or_init::<BundleInspector>();
68 let (name, mode) = inspector
69 .stash_bundle(self.bundle.clone())
70 .get_effect_meta();
71
72 if mode == EffectMode::Stack {
73 world.spawn(self.bundle_full());
74 return;
75 }
76
77 let Some(effected_by) = world.get::<EffectedBy>(self.target).map(|e| e.collection()) else {
78 world.spawn(self.bundle_full());
79 return;
80 };
81
82 // Find previous entity that is:
83 // 1. effecting the same target,
84 // 2. and has the same name (ID).
85 let old_entity = effected_by.iter().find_map(|entity| {
86 let other_mode = world.get::<EffectMode>(*entity)?;
87
88 // Todo Think more about.
89 if mode != *other_mode {
90 return None;
91 }
92
93 let other_name = world.get::<Name>(*entity);
94
95 if name.as_ref() == other_name {
96 return Some(*entity);
97 }
98
99 None
100 });
101
102 let Some(old_entity) = old_entity else {
103 world.spawn(self.bundle_full());
104 return;
105 };
106
107 match mode {
108 EffectMode::Stack => unreachable!(),
109 EffectMode::Insert => {
110 world.entity_mut(old_entity).insert(self.bundle);
111 }
112 EffectMode::Merge => {
113 // Ensure that all components are registered in the main world for cloning into.
114 world.register_bundle::<B>();
115 self.merge(world, old_entity)
116 }
117 }
118 }
119}
120
121/// Uses commands to apply effects to a specific target entity.
122///
123/// This is normally used during [`with_effects`](EffectCommandsExt::with_effects).
124///
125/// # Example
126#[doc = include_str!("../docs/with_effects_example.md")]
127pub struct EffectSpawner<'a> {
128 target: Entity,
129 commands: &'a mut Commands<'a, 'a>,
130}
131
132impl<'a> EffectSpawner<'a> {
133 /// Applies an effect to the target entity.
134 /// This *might* spawn a new entity, depending on what effects are already applied to the target.
135 ///
136 /// This is normally used during [`with_effects`](EffectCommandsExt::with_effects).
137 ///
138 /// # Example
139 #[doc = include_str!("../docs/with_effects_example.md")]
140 pub fn spawn<B: Bundle + Clone>(&mut self, bundle: B) {
141 self.commands.queue(AddEffectCommand {
142 target: self.target,
143 bundle,
144 });
145 }
146}
147
148/// An extension trait for adding effect methods to [`EntityCommands`].
149pub trait EffectCommandsExt {
150 /// Applies an effect to this entity.
151 /// This *might* spawn a new entity, depending on what effects are already applied to it.
152 ///
153 /// For applying multiple effects, see [`with_effects`](Self::with_effects).
154 ///
155 /// # Example
156 #[doc = include_str!("../docs/with_effect_example.md")]
157 fn with_effect<B: Bundle + Clone>(&mut self, bundle: B) -> &mut Self;
158
159 /// Applies effects to this entity by taking a function that operates on an [`EffectSpawner`].
160 ///
161 /// For applying a single effect, see [`with_effect`](Self::with_effect).
162 ///
163 /// # Example
164 #[doc = include_str!("../docs/with_effects_example.md")]
165 fn with_effects(&mut self, f: impl FnOnce(&mut EffectSpawner)) -> &mut Self;
166}
167
168impl EffectCommandsExt for EntityCommands<'_> {
169 fn with_effect<B: Bundle + Clone>(&mut self, bundle: B) -> &mut Self {
170 let target = self.id();
171 self.commands().queue(AddEffectCommand { target, bundle });
172 self
173 }
174
175 fn with_effects(&mut self, f: impl FnOnce(&mut EffectSpawner)) -> &mut Self {
176 f(&mut EffectSpawner {
177 target: self.id(),
178 commands: &mut self.commands(),
179 });
180 self
181 }
182}