···11[package]
22name = "bevy_alchemy"
33-version = "0.3.0"
33+version = "0.4.0"
44edition = "2024"
55description = "An experimental, status effects-as-entities system for Bevy."
66categories = ["game-development"]
···27272828[dev-dependencies]
2929bevy = "0.18"
3030-bevy_auto_plugin = { version = "0.9" }
3030+bevy_auto_plugin = { version = "0.10.0" }
3131criterion = { version = "0.8" }
3232-immediate_stats = { version = "0.4", features = ["bevy_auto_plugin"] }
3232+immediate_stats = { version = "0.5.0", features = ["bevy_auto_plugin"] }
33333434[lints.rust]
3535missing_docs = "warn"
+7-11
README.md
···99### Applying Effects
1010Effects can be applied using `with_effect` or `with_effects` (similar to `with_child` and `with_children` respectively).
1111```rust ignore
1212-commands.entity(target).with_effect(EffectBundle {
1313- name: Name::new("Effect"),
1414- bundle: MyEffect,
1515- ..default()
1616-});
1212+commands.entity(target).with_effect((Name::new("Effect"), MyEffect));
1713```
1814They can also be added using spawn-style syntax.
1915```rust ignore
2016commands.spawn((
2117 Name::new("Target"),
2222- EffectedBy::spawn(EffectBundle {
2323- name: Name::new("Effect"),
2424- bundle: MyEffect,
2525- ..default()
2626- }),
1818+ EffectedBy::spawn(
1919+ Effect((Name::new("Effect"), MyEffect))
2020+ ),
2721));
2822```
2323+2424+Note that these methods *might* spawn a new entity, depending on what effects are already applied to the target.
29253026### Effect Modes
3127For some effects it makes sense to allow stacking, so a single entity could be effected by an effect multiple times.
···78747975| Bevy | Bevy Alchemy |
8076|--------|---------------|
8181-| `0.18` | `0.2` - `0.3` |
7777+| `0.18` | `0.2` - `0.4` |
8278| `0.17` | `0.1` |
···8899use bevy::prelude::*;
1010use bevy_alchemy::*;
1111-use bevy_auto_plugin::prelude::{AutoPlugin, auto_component, auto_system};
1111+use bevy_auto_plugin::prelude::{AutoPlugin, auto_plugin_build_hook, auto_system};
1212use immediate_stats::*;
13131414fn main() {
···24242525/// Tracks an entities current movement speed.
2626#[derive(Component, StatContainer)]
2727-#[auto_component(plugin = DecayingSpeedPlugin)]
2727+#[auto_plugin_build_hook(plugin = DecayingSpeedPlugin, hook = ResetComponentHook)]
2828struct MovementSpeed(Stat);
29293030/// Applies a speed boost, which decreases throughout its duration.
3131-#[derive(Component, Default)]
3131+#[derive(Component, Default, Clone)]
3232struct DecayingSpeed {
3333 start_speed_boost: Modifier,
3434}
···3737#[auto_system(plugin = DecayingSpeedPlugin, schedule = Startup)]
3838fn init_scene(mut commands: Commands) {
3939 commands.spawn((Name::new("Target"), MovementSpeed(Stat::new(100))));
4040- commands.spawn(Text::default());
4040+ commands.spawn((
4141+ Node {
4242+ margin: UiRect::all(Val::Px(10.0)),
4343+ ..default()
4444+ },
4545+ Text::default(),
4646+ ));
4147 commands.spawn(Camera2d);
4248}
4349···5258 return;
5359 }
54605555- commands.entity(*target).with_effect(EffectBundle {
5656- mode: EffectMode::Insert, // Block having multiple of effect stacked on a single target.
5757- bundle: (
5858- Lifetime::from_seconds(2.0), // The duration of the effect.
5959- DecayingSpeed {
6060- start_speed_boost: Modifier {
6161- bonus: 10,
6262- multiplier: 2.0,
6363- },
6161+ commands.entity(*target).with_effect((
6262+ EffectMode::Insert, // Block having multiple of effect stacked on a single target.
6363+ Lifetime::from_seconds(2.0), // The duration of the effect.
6464+ DecayingSpeed {
6565+ start_speed_boost: Modifier {
6666+ bonus: 10,
6767+ multiplier: 2.0,
6468 },
6565- ),
6666- ..default()
6767- });
6969+ },
7070+ ));
6871}
69727073/// Applies the effect to the target. Because of how Immediate Stats works, this needs to run every frame.
+15-14
examples/poison.rs
···55//! The `poison_falloff` example shows a different way to handle effect stacking.
6677use bevy::prelude::*;
88-use bevy_alchemy::{
99- AlchemyPlugin, Delay, EffectBundle, EffectCommandsExt, EffectTimer, Effecting, Lifetime,
1010-};
88+use bevy_alchemy::{AlchemyPlugin, Delay, EffectCommandsExt, EffectTimer, Effecting, Lifetime};
1191210fn main() {
1311 App::new()
···2220struct Health(i32);
23212422/// Deals damage over time to the target entity.
2525-#[derive(Component, Default)]
2323+#[derive(Component, Default, Clone)]
2624struct Poison {
2725 damage: i32,
2826}
···3028/// Spawn a target on startup.
3129fn init_scene(mut commands: Commands) {
3230 commands.spawn((Name::new("Target"), Health(100)));
3333- commands.spawn(Text::default());
3131+ commands.spawn((
3232+ Node {
3333+ margin: UiRect::all(Val::Px(10.0)),
3434+ ..default()
3535+ },
3636+ Text::default(),
3737+ ));
3438 commands.spawn(Camera2d);
3539}
3640···4448 return;
4549 }
46504747- commands.entity(*target).with_effect(EffectBundle {
4848- bundle: (
4949- Lifetime::from_seconds(3.0), // The duration of the effect.
5050- Delay::from_seconds(1.0) // The time between damage ticks.
5151- .trigger_immediately(), // Make damage tick immediately when the effect is applied.
5252- Poison { damage: 1 }, // The amount of damage to apply per tick.
5353- ),
5454- ..default()
5555- });
5151+ commands.entity(*target).with_effect((
5252+ Lifetime::from_seconds(3.0), // The duration of the effect.
5353+ Delay::from_seconds(1.0) // The time between damage ticks.
5454+ .trigger_immediately(), // Make damage tick immediately when the effect is applied.
5555+ Poison { damage: 1 }, // The amount of damage to apply per tick.
5656+ ));
5657}
57585859/// Runs every frame and deals the poison damage.
+18-15
examples/poison_falloff.rs
···991010use bevy::prelude::*;
1111use bevy_alchemy::{
1212- AlchemyPlugin, Delay, EffectBundle, EffectCommandsExt, EffectMode, EffectStacks, EffectTimer,
1313- Effecting, Lifetime,
1212+ AlchemyPlugin, Delay, EffectCommandsExt, EffectMode, EffectStacks, EffectTimer, Effecting,
1313+ Lifetime,
1414};
15151616fn main() {
···2626struct Health(i32);
27272828/// Deals damage over time to the target entity.
2929-#[derive(Component, Default)]
2929+#[derive(Component, Default, Clone)]
3030struct Poison {
3131 damage: i32,
3232}
···3434/// Spawn a target on startup.
3535fn init_scene(mut commands: Commands) {
3636 commands.spawn((Name::new("Target"), Health(500)));
3737- commands.spawn(Text::default());
3737+ commands.spawn((
3838+ Node {
3939+ margin: UiRect::all(Val::Px(10.0)),
4040+ ..default()
4141+ },
4242+ Text::default(),
4343+ ));
3844 commands.spawn(Camera2d);
3945}
4046···4854 return;
4955 }
50565151- commands.entity(*target).with_effect(EffectBundle {
5252- mode: EffectMode::Merge, // Stack tracking requires effect merging.
5353- bundle: (
5454- EffectStacks::default(), // Enable stack tracking.
5555- Lifetime::from_seconds(3.0), // The duration of the effect.
5656- Delay::from_seconds(1.0) // The time between damage ticks.
5757- .trigger_immediately(), // Make damage tick immediately when the effect is applied.
5858- Poison { damage: 5 }, // The amount of damage to apply per tick.
5959- ),
6060- ..default()
6161- });
5757+ commands.entity(*target).with_effect((
5858+ EffectMode::Merge, // Stack tracking requires effect merging.
5959+ EffectStacks::default(), // Enable stack tracking.
6060+ Lifetime::from_seconds(3.0), // The duration of the effect.
6161+ Delay::from_seconds(1.0) // The time between damage ticks.
6262+ .trigger_immediately(), // Make damage tick immediately when the effect is applied.
6363+ Poison { damage: 5 }, // The amount of damage to apply per tick.
6464+ ));
6265}
63666467/// Runs every frame and deals the poison damage.
-24
src/bundle.rs
···11-use crate::EffectMode;
22-use bevy_ecs::prelude::*;
33-44-/// A "bundle" of components/settings used when applying an effect.
55-/// Due to technical limitations, this doesn't actually implement [`Bundle`].
66-/// Instead, purpose build commands ([`with_effect`](crate::command::EffectCommandsExt::with_effect))
77-/// or related spawners ([`EffectedBy::spawn`](SpawnRelated::spawn)) should be used.
88-///
99-/// # Examples
1010-/// ### [`with_effect`](crate::command::EffectCommandsExt::with_effect)
1111-#[doc = include_str!("../docs/with_effect_example.md")]
1212-/// ### [`with_effects`](crate::command::EffectCommandsExt::with_effects) + [`EffectSpawner`](crate::command::EffectSpawner)
1313-#[doc = include_str!("../docs/with_effects_example.md")]
1414-/// ### [`EffectedBy::spawn`](SpawnRelated::spawn)
1515-#[doc = include_str!("../docs/effected_by_spawn_example.md")]
1616-#[derive(Default)]
1717-pub struct EffectBundle<B: Bundle> {
1818- /// The name/ID of the effect. Effects with different IDs have no effect on one another.
1919- pub name: Name,
2020- /// Describes the logic used when new effect collides with an existing one.
2121- pub mode: EffectMode,
2222- /// Components that will be added to the effect. This is where the actual effect components get added.
2323- pub bundle: B,
2424-}
···11-use crate::bundle::EffectBundle;
11+use crate::bundle_inspector::BundleInspector;
22use crate::registry::{EffectMergeFn, EffectMergeRegistry};
33use crate::{EffectMode, EffectedBy, Effecting};
44use bevy_ecs::entity_disabling::Disabled;
55use bevy_ecs::prelude::*;
66-use bevy_ecs::ptr::MovingPtr;
77-use bevy_ecs::spawn::SpawnableList;
86use bevy_log::warn_once;
97use std::any::TypeId;
108···1715 /// The entity to apply the effect to.
1816 pub target: Entity,
1917 /// The effect to apply.
2020- pub bundle: EffectBundle<B>,
1818+ pub bundle: B,
2119}
22202321impl<B: Bundle> AddEffectCommand<B> {
2424- fn spawn(self, world: &mut World) -> Entity {
2525- let entity = world.spawn_empty();
2626- let id = entity.id();
2727- self.insert(entity);
2828- id
2929- }
3030-3131- fn insert(self, mut entity: EntityWorldMut) {
3232- entity.insert((
3333- Effecting(self.target),
3434- self.bundle.name,
3535- self.bundle.mode,
3636- self.bundle.bundle,
3737- ));
2222+ fn bundle_full(self) -> (Effecting, B) {
2323+ (Effecting(self.target), self.bundle)
3824 }
39254026 /// Inserts into the existing entity, and then merges the old effect into it using [`EffectMergeRegistry`].
···7056 temp
7157 };
72587373- self.insert(world.entity_mut(new_effect));
5959+ world.entity_mut(new_effect).insert(self.bundle);
74607561 // Call merge function on those copied components.
7662 {
···10086 }
10187}
10288103103-impl<B: Bundle> Command for AddEffectCommand<B> {
8989+impl<B: Bundle + Clone> Command for AddEffectCommand<B> {
10490 fn apply(self, world: &mut World) {
105105- if self.bundle.mode == EffectMode::Stack {
106106- self.spawn(world);
9191+ let mut inspector = world.get_resource_or_init::<BundleInspector>();
9292+ let (name, mode) = inspector.get_effect_meta(self.bundle.clone());
9393+9494+ if mode == EffectMode::Stack {
9595+ world.spawn(self.bundle_full());
10796 return;
10897 }
10998110110- let Some(effected_by) = world
111111- .get::<EffectedBy>(self.target)
112112- .map(|e| e.collection().clone())
113113- else {
114114- self.spawn(world);
9999+ let Some(effected_by) = world.get::<EffectedBy>(self.target).map(|e| e.collection()) else {
100100+ world.spawn(self.bundle_full());
115101 return;
116102 };
117103···122108 let other_mode = world.get::<EffectMode>(*entity)?;
123109124110 // Todo Think more about.
125125- if self.bundle.mode != *other_mode {
111111+ if mode != *other_mode {
126112 return None;
127113 }
128114129129- let name = world.get::<Name>(*entity)?;
115115+ let other_name = world.get::<Name>(*entity);
130116131131- if name == &self.bundle.name {
117117+ if name.as_ref() == other_name {
132118 return Some(*entity);
133119 }
134120···136122 });
137123138124 let Some(old_entity) = old_entity else {
139139- self.spawn(world);
125125+ world.spawn(self.bundle_full());
140126 return;
141127 };
142128143143- match self.bundle.mode {
129129+ match mode {
144130 EffectMode::Stack => unreachable!(),
145145- EffectMode::Insert => self.insert(world.entity_mut(old_entity)),
131131+ EffectMode::Insert => {
132132+ world.entity_mut(old_entity).insert(self.bundle);
133133+ }
146134 EffectMode::Merge => self.merge(world, old_entity),
147135 }
148136 }
149137}
150138151151-// Todo This is probably bad practice/has larger performance cost.
152152-impl<B: Bundle> SpawnableList<Effecting> for EffectBundle<B> {
153153- fn spawn(this: MovingPtr<'_, Self>, world: &mut World, target: Entity) {
154154- let bundle = this.read();
155155- world.commands().queue(AddEffectCommand { target, bundle });
156156- }
157157-158158- fn size_hint(&self) -> usize {
159159- 0
160160- }
161161-}
162162-163139/// Uses commands to apply effects to a specific target entity.
164140///
165141/// This is normally used during [`with_effects`](EffectCommandsExt::with_effects).
···179155 ///
180156 /// # Example
181157 #[doc = include_str!("../docs/with_effects_example.md")]
182182- pub fn spawn<B: Bundle>(&mut self, bundle: EffectBundle<B>) {
158158+ pub fn spawn<B: Bundle + Clone>(&mut self, bundle: B) {
183159 self.commands.queue(AddEffectCommand {
184160 target: self.target,
185161 bundle,
···196172 ///
197173 /// # Example
198174 #[doc = include_str!("../docs/with_effect_example.md")]
199199- fn with_effect<B: Bundle>(&mut self, bundle: EffectBundle<B>) -> &mut Self;
175175+ fn with_effect<B: Bundle + Clone>(&mut self, bundle: B) -> &mut Self;
200176201201- /// Applies effects to this entity by taking a function that operates on a [`EffectSpawner`].
177177+ /// Applies effects to this entity by taking a function that operates on an [`EffectSpawner`].
202178 ///
203179 /// For applying a single effect, see [`with_effect`](Self::with_effect).
204180 ///
···208184}
209185210186impl EffectCommandsExt for EntityCommands<'_> {
211211- fn with_effect<B: Bundle>(&mut self, bundle: EffectBundle<B>) -> &mut Self {
187187+ fn with_effect<B: Bundle + Clone>(&mut self, bundle: B) -> &mut Self {
212188 let target = self.id();
213189 self.commands().queue(AddEffectCommand { target, bundle });
214190 self
+5-2
src/lib.rs
···11#![doc = include_str!("../README.md")]
2233-mod bundle;
33+mod bundle_inspector;
44mod command;
55mod component;
66mod registry;
77mod relation;
88+mod spawnable_list;
89910use bevy_app::{App, Plugin};
1011use bevy_ecs::prelude::*;
1112use bevy_reflect::Reflect;
1213use bevy_reflect::prelude::ReflectDefault;
13141414-pub use bundle::*;
1515+use crate::bundle_inspector::BundleInspector;
1516pub use command::*;
1617pub use component::*;
1718pub use registry::*;
1819pub use relation::*;
2020+pub use spawnable_list::*;
19212022/// Setup required types and systems for `bevy_alchemy`.
2123pub struct AlchemyPlugin;
···2830 .register_type::<Lifetime>()
2931 .register_type::<Delay>()
3032 .register_type::<TimerMergeMode>()
3333+ .init_resource::<BundleInspector>()
3134 .init_resource::<EffectMergeRegistry>()
3235 .add_plugins(TimerPlugin)
3336 .add_plugins(StackPlugin);
+28
src/spawnable_list.rs
···11+use crate::{AddEffectCommand, Effecting};
22+use bevy_ecs::prelude::*;
33+use bevy_ecs::ptr::MovingPtr;
44+use bevy_ecs::spawn::SpawnableList;
55+66+/// A wrapper over a [`Bundle`] indicating that an effect should be applied with that [`Bundle`].
77+/// This is intended to be used in [`EffectedBy::spawn`](SpawnRelated::spawn).
88+///
99+/// This *might* spawn a new entity, depending on what effects are already applied to the target.
1010+/// # Example
1111+#[doc = include_str!("../docs/effected_by_spawn_example.md")]
1212+#[derive(Default)]
1313+pub struct Effect<B: Bundle>(pub B);
1414+1515+// Todo This is probably bad practice/has larger performance cost.
1616+impl<B: Bundle + Clone> SpawnableList<Effecting> for Effect<B> {
1717+ fn spawn(this: MovingPtr<'_, Self>, world: &mut World, target: Entity) {
1818+ let bundle = this.read();
1919+ world.commands().queue(AddEffectCommand {
2020+ target,
2121+ bundle: bundle.0,
2222+ });
2323+ }
2424+2525+ fn size_hint(&self) -> usize {
2626+ 0
2727+ }
2828+}