An experimental, status effects-as-entities system for Bevy.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #8 from AlephCubed/dev

Replace `EffectBundle` with fully generic bundles.

authored by

Josiah Nelson and committed by
GitHub
33e2c806 465f6ed6

+372 -400
+15 -50
Cargo.lock
··· 359 359 360 360 [[package]] 361 361 name = "bevy_alchemy" 362 - version = "0.3.0" 362 + version = "0.4.0" 363 363 dependencies = [ 364 364 "bevy", 365 365 "bevy_app", ··· 544 544 545 545 [[package]] 546 546 name = "bevy_auto_plugin" 547 - version = "0.9.0" 547 + version = "0.10.0" 548 548 source = "registry+https://github.com/rust-lang/crates.io-index" 549 - checksum = "b00355e2dd268a7f9c7453773753edfedda475896c0b4e7b2d015aa04f4849fd" 549 + checksum = "f3c7761957a70588bf0b7c6120c97a47620dab684a622a0aa0a8c5c9161becd6" 550 550 dependencies = [ 551 551 "bevy_auto_plugin_proc_macros", 552 552 "bevy_auto_plugin_shared", ··· 556 556 557 557 [[package]] 558 558 name = "bevy_auto_plugin_proc_macros" 559 - version = "0.9.0" 559 + version = "0.10.0" 560 560 source = "registry+https://github.com/rust-lang/crates.io-index" 561 - checksum = "d85f2558b3a8f3d5e3e8df9debbff0665cad6c7c2bfde3537e7ff247fde3cd8f" 561 + checksum = "582998ec7135267343cdcfc441128b73bbc995be640e48e046e2a33f3ff5703f" 562 562 dependencies = [ 563 563 "bevy_auto_plugin_shared", 564 564 "proc-macro2", ··· 566 566 567 567 [[package]] 568 568 name = "bevy_auto_plugin_shared" 569 - version = "0.9.0" 569 + version = "0.10.0" 570 570 source = "registry+https://github.com/rust-lang/crates.io-index" 571 - checksum = "f6d825af42f08348a7c2307f50aabde362e774dd906c274556bd6b71794c8476" 571 + checksum = "999eb3756d075ac95e169f71333ac211e54e961ac55708a89731eb6134f38b7e" 572 572 dependencies = [ 573 573 "bevy_app", 574 - "darling 0.21.3", 574 + "darling", 575 575 "inventory", 576 576 "linkme", 577 577 "log", ··· 2264 2264 source = "registry+https://github.com/rust-lang/crates.io-index" 2265 2265 checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" 2266 2266 dependencies = [ 2267 - "darling_core 0.21.3", 2268 - "darling_macro 0.21.3", 2269 - ] 2270 - 2271 - [[package]] 2272 - name = "darling" 2273 - version = "0.23.0" 2274 - source = "registry+https://github.com/rust-lang/crates.io-index" 2275 - checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" 2276 - dependencies = [ 2277 - "darling_core 0.23.0", 2278 - "darling_macro 0.23.0", 2267 + "darling_core", 2268 + "darling_macro", 2279 2269 ] 2280 2270 2281 2271 [[package]] ··· 2293 2283 ] 2294 2284 2295 2285 [[package]] 2296 - name = "darling_core" 2297 - version = "0.23.0" 2298 - source = "registry+https://github.com/rust-lang/crates.io-index" 2299 - checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" 2300 - dependencies = [ 2301 - "ident_case", 2302 - "proc-macro2", 2303 - "quote", 2304 - "strsim", 2305 - "syn 2.0.111", 2306 - ] 2307 - 2308 - [[package]] 2309 2286 name = "darling_macro" 2310 2287 version = "0.21.3" 2311 2288 source = "registry+https://github.com/rust-lang/crates.io-index" 2312 2289 checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" 2313 2290 dependencies = [ 2314 - "darling_core 0.21.3", 2315 - "quote", 2316 - "syn 2.0.111", 2317 - ] 2318 - 2319 - [[package]] 2320 - name = "darling_macro" 2321 - version = "0.23.0" 2322 - source = "registry+https://github.com/rust-lang/crates.io-index" 2323 - checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" 2324 - dependencies = [ 2325 - "darling_core 0.23.0", 2291 + "darling_core", 2326 2292 "quote", 2327 2293 "syn 2.0.111", 2328 2294 ] ··· 3014 2980 3015 2981 [[package]] 3016 2982 name = "immediate_stats" 3017 - version = "0.4.0" 2983 + version = "0.5.0" 3018 2984 source = "registry+https://github.com/rust-lang/crates.io-index" 3019 - checksum = "d8619adda62694b9bf085e6134215a9439279a6cb4b305ea008f89cedf9828a6" 2985 + checksum = "d2e9bfbd15d2ae09094aa16193e6f603b5f721d9ebaa6918fa112389c44b59c4" 3020 2986 dependencies = [ 3021 2987 "bevy_app", 3022 2988 "bevy_auto_plugin", ··· 3027 2993 3028 2994 [[package]] 3029 2995 name = "immediate_stats_macros" 3030 - version = "0.4.0" 2996 + version = "0.5.0" 3031 2997 source = "registry+https://github.com/rust-lang/crates.io-index" 3032 - checksum = "cf0252f42204daee69054e2f4d8b102a99f9c1065cbc9e9826a0dfaef226aa22" 2998 + checksum = "94770b216cb1e5a378d3501e63314242e754c1b2c776fa1d9645eaec03fce0b0" 3033 2999 dependencies = [ 3034 - "darling 0.23.0", 3035 3000 "proc-macro-error", 3036 3001 "proc-macro2", 3037 3002 "quote",
+3 -3
Cargo.toml
··· 1 1 [package] 2 2 name = "bevy_alchemy" 3 - version = "0.3.0" 3 + version = "0.4.0" 4 4 edition = "2024" 5 5 description = "An experimental, status effects-as-entities system for Bevy." 6 6 categories = ["game-development"] ··· 27 27 28 28 [dev-dependencies] 29 29 bevy = "0.18" 30 - bevy_auto_plugin = { version = "0.9" } 30 + bevy_auto_plugin = { version = "0.10.0" } 31 31 criterion = { version = "0.8" } 32 - immediate_stats = { version = "0.4", features = ["bevy_auto_plugin"] } 32 + immediate_stats = { version = "0.5.0", features = ["bevy_auto_plugin"] } 33 33 34 34 [lints.rust] 35 35 missing_docs = "warn"
+7 -11
README.md
··· 9 9 ### Applying Effects 10 10 Effects can be applied using `with_effect` or `with_effects` (similar to `with_child` and `with_children` respectively). 11 11 ```rust ignore 12 - commands.entity(target).with_effect(EffectBundle { 13 - name: Name::new("Effect"), 14 - bundle: MyEffect, 15 - ..default() 16 - }); 12 + commands.entity(target).with_effect((Name::new("Effect"), MyEffect)); 17 13 ``` 18 14 They can also be added using spawn-style syntax. 19 15 ```rust ignore 20 16 commands.spawn(( 21 17 Name::new("Target"), 22 - EffectedBy::spawn(EffectBundle { 23 - name: Name::new("Effect"), 24 - bundle: MyEffect, 25 - ..default() 26 - }), 18 + EffectedBy::spawn( 19 + Effect((Name::new("Effect"), MyEffect)) 20 + ), 27 21 )); 28 22 ``` 23 + 24 + Note that these methods *might* spawn a new entity, depending on what effects are already applied to the target. 29 25 30 26 ### Effect Modes 31 27 For some effects it makes sense to allow stacking, so a single entity could be effected by an effect multiple times. ··· 78 74 79 75 | Bevy | Bevy Alchemy | 80 76 |--------|---------------| 81 - | `0.18` | `0.2` - `0.3` | 77 + | `0.18` | `0.2` - `0.4` | 82 78 | `0.17` | `0.1` |
+17 -20
benches/insert_mode.rs
··· 1 1 //! Benchmarks for applying insert-mode effects. 2 2 3 - use bevy_alchemy::{AlchemyPlugin, EffectBundle, EffectCommandsExt, EffectMode, EffectedBy}; 3 + use bevy_alchemy::{AlchemyPlugin, Effect, EffectCommandsExt, EffectMode, EffectedBy}; 4 4 use bevy_app::App; 5 5 use bevy_ecs::name::Name; 6 6 use bevy_ecs::prelude::{Component, Entity, SpawnRelated}; 7 7 use criterion::{Criterion, criterion_group, criterion_main}; 8 8 9 - #[derive(Component)] 9 + #[derive(Component, Clone)] 10 10 struct BenchEffect; 11 11 12 12 fn init_app() -> (App, Entity) { ··· 17 17 .world_mut() 18 18 .spawn(( 19 19 Name::new("Target"), 20 - EffectedBy::spawn(EffectBundle { 21 - name: Name::new("Effect"), 22 - mode: EffectMode::Insert, 23 - bundle: BenchEffect, 24 - }), 20 + EffectedBy::spawn(Effect(( 21 + Name::new("Effect"), 22 + EffectMode::Insert, 23 + BenchEffect, 24 + ))), 25 25 )) 26 26 .id(); 27 27 ··· 33 33 34 34 c.bench_function("Insert mode matched `with_effect`", |b| { 35 35 b.iter(|| { 36 - app.world_mut() 37 - .commands() 38 - .entity(entity) 39 - .with_effect(EffectBundle { 40 - name: Name::new("Effect"), 41 - mode: EffectMode::Insert, 42 - bundle: BenchEffect, 43 - }); 36 + app.world_mut().commands().entity(entity).with_effect(( 37 + Name::new("Effect"), 38 + EffectMode::Insert, 39 + BenchEffect, 40 + )); 44 41 app.world_mut().flush(); 45 42 }) 46 43 }); ··· 54 51 app.world_mut() 55 52 .commands() 56 53 .entity(entity) 57 - .insert(EffectedBy::spawn(EffectBundle { 58 - name: Name::new("Effect"), 59 - mode: EffectMode::Insert, 60 - bundle: BenchEffect, 61 - })); 54 + .insert(EffectedBy::spawn(Effect(( 55 + Name::new("Effect"), 56 + EffectMode::Insert, 57 + BenchEffect, 58 + )))); 62 59 app.world_mut().flush(); 63 60 }) 64 61 });
+12 -15
benches/stack_mode.rs
··· 1 1 //! Benchmarks for applying stack-mode effects. 2 2 3 - use bevy_alchemy::{AlchemyPlugin, EffectBundle, EffectCommandsExt, EffectMode, EffectedBy}; 3 + use bevy_alchemy::{AlchemyPlugin, Effect, EffectCommandsExt, EffectMode, EffectedBy}; 4 4 use bevy_app::App; 5 5 use bevy_ecs::name::Name; 6 6 use bevy_ecs::prelude::{Component, Entity, SpawnRelated}; 7 7 use criterion::{Criterion, criterion_group, criterion_main}; 8 8 9 - #[derive(Component)] 9 + #[derive(Component, Clone)] 10 10 struct BenchEffect; 11 11 12 12 fn init_app() -> (App, Entity) { ··· 23 23 24 24 c.bench_function("Stack mode `with_effect`", |b| { 25 25 b.iter(|| { 26 - app.world_mut() 27 - .commands() 28 - .entity(entity) 29 - .with_effect(EffectBundle { 30 - name: Name::new("Effect"), 31 - mode: EffectMode::Stack, 32 - bundle: BenchEffect, 33 - }); 26 + app.world_mut().commands().entity(entity).with_effect(( 27 + Name::new("Effect"), 28 + EffectMode::Stack, 29 + BenchEffect, 30 + )); 34 31 app.world_mut().flush(); 35 32 }) 36 33 }); ··· 44 41 app.world_mut() 45 42 .commands() 46 43 .entity(entity) 47 - .insert(EffectedBy::spawn(EffectBundle { 48 - name: Name::new("Effect"), 49 - mode: EffectMode::Stack, 50 - bundle: BenchEffect, 51 - })); 44 + .insert(EffectedBy::spawn(Effect(( 45 + Name::new("Effect"), 46 + EffectMode::Stack, 47 + BenchEffect, 48 + )))); 52 49 app.world_mut().flush(); 53 50 }) 54 51 });
+4 -6
docs/effected_by_spawn_example.md
··· 2 2 # use bevy::prelude::*; 3 3 # use bevy_alchemy::*; 4 4 # 5 - # #[derive(Component, Default)] 5 + # #[derive(Component, Default, Clone)] 6 6 # struct MyEffect; 7 7 # 8 8 # fn main() { ··· 11 11 # let mut commands = world.commands(); 12 12 commands.spawn(( 13 13 Name::new("Target"), 14 - EffectedBy::spawn(EffectBundle { 15 - name: Name::new("Effect"), 16 - bundle: MyEffect, 17 - ..default() 18 - }), 14 + EffectedBy::spawn( 15 + Effect((Name::new("Effect"), MyEffect)) 16 + ), 19 17 )); 20 18 # } 21 19 ```
+2 -6
docs/with_effect_example.md
··· 2 2 # use bevy::prelude::*; 3 3 # use bevy_alchemy::*; 4 4 # 5 - # #[derive(Component, Default)] 5 + # #[derive(Component, Default, Clone)] 6 6 # struct MyEffect; 7 7 # 8 8 # fn main() { 9 9 # let mut world = World::new(); 10 10 # let target = world.spawn_empty().id(); 11 11 # let mut commands = world.commands(); 12 - commands.entity(target).with_effect(EffectBundle { 13 - name: Name::new("Effect"), 14 - bundle: MyEffect, 15 - ..default() 16 - }); 12 + commands.entity(target).with_effect((Name::new("Effect"), MyEffect)); 17 13 # } 18 14 ```
+3 -12
docs/with_effects_example.md
··· 2 2 # use bevy::prelude::*; 3 3 # use bevy_alchemy::*; 4 4 # 5 - # #[derive(Component, Default)] 5 + # #[derive(Component, Default, Clone)] 6 6 # struct MyEffect; 7 7 # 8 8 # fn main() { ··· 10 10 # let target = world.spawn_empty().id(); 11 11 # let mut commands = world.commands(); 12 12 commands.entity(target).with_effects(|effects| { 13 - effects.spawn(EffectBundle { 14 - name: Name::new("EffectA"), 15 - bundle: MyEffect, 16 - ..default() 17 - }); 18 - 19 - effects.spawn(EffectBundle { 20 - name: Name::new("EffectB"), 21 - bundle: MyEffect, 22 - ..default() 23 - }); 13 + effects.spawn((Name::new("EffectA"), MyEffect)); 14 + effects.spawn((Name::new("EffectB"), MyEffect)); 24 15 }); 25 16 # } 26 17 ```
+17 -14
examples/immediate_stats/decaying_speed.rs
··· 26 26 struct MovementSpeed(Stat); 27 27 28 28 /// Applies a speed boost, which decreases throughout its duration. 29 - #[derive(Component, Default)] 29 + #[derive(Component, Default, Clone)] 30 30 struct DecayingSpeed { 31 31 start_speed_boost: Modifier, 32 32 } ··· 34 34 /// Spawn a target on startup. 35 35 fn init_scene(mut commands: Commands) { 36 36 commands.spawn((Name::new("Target"), MovementSpeed(Stat::new(100)))); 37 - commands.spawn(Text::default()); 37 + commands.spawn(( 38 + Node { 39 + margin: UiRect::all(Val::Px(10.0)), 40 + ..default() 41 + }, 42 + Text::default(), 43 + )); 38 44 commands.spawn(Camera2d); 39 45 } 40 46 ··· 48 54 return; 49 55 } 50 56 51 - commands.entity(*target).with_effect(EffectBundle { 52 - mode: EffectMode::Insert, // Block having multiple of effect stacked on a single target. 53 - bundle: ( 54 - Lifetime::from_seconds(2.0), // The duration of the effect. 55 - DecayingSpeed { 56 - start_speed_boost: Modifier { 57 - bonus: 10, 58 - multiplier: 2.0, 59 - }, 57 + commands.entity(*target).with_effect(( 58 + EffectMode::Insert, // Block having multiple of effect stacked on a single target. 59 + Lifetime::from_seconds(2.0), // The duration of the effect. 60 + DecayingSpeed { 61 + start_speed_boost: Modifier { 62 + bonus: 10, 63 + multiplier: 2.0, 60 64 }, 61 - ), 62 - ..default() 63 - }); 65 + }, 66 + )); 64 67 } 65 68 66 69 /// Applies the effect to the target. Because of how Immediate Stats works, this needs to run every frame.
+19 -16
examples/immediate_stats/decaying_speed_auto_plugin.rs
··· 8 8 9 9 use bevy::prelude::*; 10 10 use bevy_alchemy::*; 11 - use bevy_auto_plugin::prelude::{AutoPlugin, auto_component, auto_system}; 11 + use bevy_auto_plugin::prelude::{AutoPlugin, auto_plugin_build_hook, auto_system}; 12 12 use immediate_stats::*; 13 13 14 14 fn main() { ··· 24 24 25 25 /// Tracks an entities current movement speed. 26 26 #[derive(Component, StatContainer)] 27 - #[auto_component(plugin = DecayingSpeedPlugin)] 27 + #[auto_plugin_build_hook(plugin = DecayingSpeedPlugin, hook = ResetComponentHook)] 28 28 struct MovementSpeed(Stat); 29 29 30 30 /// Applies a speed boost, which decreases throughout its duration. 31 - #[derive(Component, Default)] 31 + #[derive(Component, Default, Clone)] 32 32 struct DecayingSpeed { 33 33 start_speed_boost: Modifier, 34 34 } ··· 37 37 #[auto_system(plugin = DecayingSpeedPlugin, schedule = Startup)] 38 38 fn init_scene(mut commands: Commands) { 39 39 commands.spawn((Name::new("Target"), MovementSpeed(Stat::new(100)))); 40 - commands.spawn(Text::default()); 40 + commands.spawn(( 41 + Node { 42 + margin: UiRect::all(Val::Px(10.0)), 43 + ..default() 44 + }, 45 + Text::default(), 46 + )); 41 47 commands.spawn(Camera2d); 42 48 } 43 49 ··· 52 58 return; 53 59 } 54 60 55 - commands.entity(*target).with_effect(EffectBundle { 56 - mode: EffectMode::Insert, // Block having multiple of effect stacked on a single target. 57 - bundle: ( 58 - Lifetime::from_seconds(2.0), // The duration of the effect. 59 - DecayingSpeed { 60 - start_speed_boost: Modifier { 61 - bonus: 10, 62 - multiplier: 2.0, 63 - }, 61 + commands.entity(*target).with_effect(( 62 + EffectMode::Insert, // Block having multiple of effect stacked on a single target. 63 + Lifetime::from_seconds(2.0), // The duration of the effect. 64 + DecayingSpeed { 65 + start_speed_boost: Modifier { 66 + bonus: 10, 67 + multiplier: 2.0, 64 68 }, 65 - ), 66 - ..default() 67 - }); 69 + }, 70 + )); 68 71 } 69 72 70 73 /// Applies the effect to the target. Because of how Immediate Stats works, this needs to run every frame.
+15 -14
examples/poison.rs
··· 5 5 //! The `poison_falloff` example shows a different way to handle effect stacking. 6 6 7 7 use bevy::prelude::*; 8 - use bevy_alchemy::{ 9 - AlchemyPlugin, Delay, EffectBundle, EffectCommandsExt, EffectTimer, Effecting, Lifetime, 10 - }; 8 + use bevy_alchemy::{AlchemyPlugin, Delay, EffectCommandsExt, EffectTimer, Effecting, Lifetime}; 11 9 12 10 fn main() { 13 11 App::new() ··· 22 20 struct Health(i32); 23 21 24 22 /// Deals damage over time to the target entity. 25 - #[derive(Component, Default)] 23 + #[derive(Component, Default, Clone)] 26 24 struct Poison { 27 25 damage: i32, 28 26 } ··· 30 28 /// Spawn a target on startup. 31 29 fn init_scene(mut commands: Commands) { 32 30 commands.spawn((Name::new("Target"), Health(100))); 33 - commands.spawn(Text::default()); 31 + commands.spawn(( 32 + Node { 33 + margin: UiRect::all(Val::Px(10.0)), 34 + ..default() 35 + }, 36 + Text::default(), 37 + )); 34 38 commands.spawn(Camera2d); 35 39 } 36 40 ··· 44 48 return; 45 49 } 46 50 47 - commands.entity(*target).with_effect(EffectBundle { 48 - bundle: ( 49 - Lifetime::from_seconds(3.0), // The duration of the effect. 50 - Delay::from_seconds(1.0) // The time between damage ticks. 51 - .trigger_immediately(), // Make damage tick immediately when the effect is applied. 52 - Poison { damage: 1 }, // The amount of damage to apply per tick. 53 - ), 54 - ..default() 55 - }); 51 + commands.entity(*target).with_effect(( 52 + Lifetime::from_seconds(3.0), // The duration of the effect. 53 + Delay::from_seconds(1.0) // The time between damage ticks. 54 + .trigger_immediately(), // Make damage tick immediately when the effect is applied. 55 + Poison { damage: 1 }, // The amount of damage to apply per tick. 56 + )); 56 57 } 57 58 58 59 /// Runs every frame and deals the poison damage.
+18 -15
examples/poison_falloff.rs
··· 9 9 10 10 use bevy::prelude::*; 11 11 use bevy_alchemy::{ 12 - AlchemyPlugin, Delay, EffectBundle, EffectCommandsExt, EffectMode, EffectStacks, EffectTimer, 13 - Effecting, Lifetime, 12 + AlchemyPlugin, Delay, EffectCommandsExt, EffectMode, EffectStacks, EffectTimer, Effecting, 13 + Lifetime, 14 14 }; 15 15 16 16 fn main() { ··· 26 26 struct Health(i32); 27 27 28 28 /// Deals damage over time to the target entity. 29 - #[derive(Component, Default)] 29 + #[derive(Component, Default, Clone)] 30 30 struct Poison { 31 31 damage: i32, 32 32 } ··· 34 34 /// Spawn a target on startup. 35 35 fn init_scene(mut commands: Commands) { 36 36 commands.spawn((Name::new("Target"), Health(500))); 37 - commands.spawn(Text::default()); 37 + commands.spawn(( 38 + Node { 39 + margin: UiRect::all(Val::Px(10.0)), 40 + ..default() 41 + }, 42 + Text::default(), 43 + )); 38 44 commands.spawn(Camera2d); 39 45 } 40 46 ··· 48 54 return; 49 55 } 50 56 51 - commands.entity(*target).with_effect(EffectBundle { 52 - mode: EffectMode::Merge, // Stack tracking requires effect merging. 53 - bundle: ( 54 - EffectStacks::default(), // Enable stack tracking. 55 - Lifetime::from_seconds(3.0), // The duration of the effect. 56 - Delay::from_seconds(1.0) // The time between damage ticks. 57 - .trigger_immediately(), // Make damage tick immediately when the effect is applied. 58 - Poison { damage: 5 }, // The amount of damage to apply per tick. 59 - ), 60 - ..default() 61 - }); 57 + commands.entity(*target).with_effect(( 58 + EffectMode::Merge, // Stack tracking requires effect merging. 59 + EffectStacks::default(), // Enable stack tracking. 60 + Lifetime::from_seconds(3.0), // The duration of the effect. 61 + Delay::from_seconds(1.0) // The time between damage ticks. 62 + .trigger_immediately(), // Make damage tick immediately when the effect is applied. 63 + Poison { damage: 5 }, // The amount of damage to apply per tick. 64 + )); 62 65 } 63 66 64 67 /// Runs every frame and deals the poison damage.
-24
src/bundle.rs
··· 1 - use crate::EffectMode; 2 - use bevy_ecs::prelude::*; 3 - 4 - /// A "bundle" of components/settings used when applying an effect. 5 - /// Due to technical limitations, this doesn't actually implement [`Bundle`]. 6 - /// Instead, purpose build commands ([`with_effect`](crate::command::EffectCommandsExt::with_effect)) 7 - /// or related spawners ([`EffectedBy::spawn`](SpawnRelated::spawn)) should be used. 8 - /// 9 - /// # Examples 10 - /// ### [`with_effect`](crate::command::EffectCommandsExt::with_effect) 11 - #[doc = include_str!("../docs/with_effect_example.md")] 12 - /// ### [`with_effects`](crate::command::EffectCommandsExt::with_effects) + [`EffectSpawner`](crate::command::EffectSpawner) 13 - #[doc = include_str!("../docs/with_effects_example.md")] 14 - /// ### [`EffectedBy::spawn`](SpawnRelated::spawn) 15 - #[doc = include_str!("../docs/effected_by_spawn_example.md")] 16 - #[derive(Default)] 17 - pub struct EffectBundle<B: Bundle> { 18 - /// The name/ID of the effect. Effects with different IDs have no effect on one another. 19 - pub name: Name, 20 - /// Describes the logic used when new effect collides with an existing one. 21 - pub mode: EffectMode, 22 - /// Components that will be added to the effect. This is where the actual effect components get added. 23 - pub bundle: B, 24 - }
+106
src/bundle_inspector.rs
··· 1 + use crate::EffectMode; 2 + use bevy_ecs::prelude::{Bundle, Entity, Name, Resource, World}; 3 + use bevy_ecs::relationship::RelationshipHookMode; 4 + 5 + #[derive(Resource)] 6 + pub(crate) struct BundleInspector { 7 + world: World, 8 + scratch_entity: Entity, 9 + } 10 + 11 + impl Default for BundleInspector { 12 + fn default() -> Self { 13 + let mut world = World::new(); 14 + let scratch_entity = world.spawn_empty().id(); 15 + Self { 16 + world, 17 + scratch_entity, 18 + } 19 + } 20 + } 21 + 22 + impl BundleInspector { 23 + pub fn get_effect_meta<B: Bundle>(&mut self, bundle: B) -> (Option<Name>, EffectMode) { 24 + let e = self.scratch_entity; 25 + self.world 26 + .entity_mut(e) 27 + .insert_with_relationship_hook_mode(bundle, RelationshipHookMode::Skip); 28 + 29 + let name = self.world.entity(e).get::<Name>().cloned(); 30 + 31 + let mode = self 32 + .world 33 + .entity_mut(e) 34 + .get::<EffectMode>() 35 + .copied() 36 + .unwrap_or_default(); 37 + 38 + self.world.entity_mut(e).clear(); 39 + 40 + (name, mode) 41 + } 42 + } 43 + 44 + #[cfg(test)] 45 + mod tests { 46 + use super::*; 47 + use crate::Effecting; 48 + 49 + #[test] 50 + fn get_effect_meta() { 51 + let mut inspector = BundleInspector::default(); 52 + 53 + let name = Name::new("Effect"); 54 + let mode = EffectMode::Insert; 55 + 56 + assert_eq!( 57 + inspector.get_effect_meta((name.clone(), mode)), 58 + (Some(name), mode) 59 + ); 60 + } 61 + 62 + #[test] 63 + fn get_effect_meta_no_name() { 64 + let mut inspector = BundleInspector::default(); 65 + 66 + let mode = EffectMode::Insert; 67 + 68 + assert_eq!(inspector.get_effect_meta(mode), (None, mode)); 69 + } 70 + 71 + #[test] 72 + fn get_effect_meta_no_mode() { 73 + let mut inspector = BundleInspector::default(); 74 + 75 + let name = Name::new("Effect"); 76 + 77 + assert_eq!( 78 + inspector.get_effect_meta(name.clone()), 79 + (Some(name), EffectMode::default()) 80 + ); 81 + } 82 + 83 + #[test] 84 + fn get_effect_meta_nothing() { 85 + let mut inspector = BundleInspector::default(); 86 + 87 + assert_eq!(inspector.get_effect_meta(()), (None, EffectMode::default())); 88 + } 89 + 90 + #[test] 91 + fn get_effect_mode_with_relation() { 92 + let mut inspector = BundleInspector::default(); 93 + 94 + let name = Name::new("Effect"); 95 + let mode = EffectMode::Insert; 96 + 97 + assert_eq!( 98 + inspector.get_effect_meta(( 99 + name.clone(), 100 + mode, 101 + Effecting(Entity::from_raw_u32(32).unwrap()) 102 + )), 103 + (Some(name), mode) 104 + ); 105 + } 106 + }
+25 -49
src/command.rs
··· 1 - use crate::bundle::EffectBundle; 1 + use crate::bundle_inspector::BundleInspector; 2 2 use crate::registry::{EffectMergeFn, EffectMergeRegistry}; 3 3 use crate::{EffectMode, EffectedBy, Effecting}; 4 4 use bevy_ecs::entity_disabling::Disabled; 5 5 use bevy_ecs::prelude::*; 6 - use bevy_ecs::ptr::MovingPtr; 7 - use bevy_ecs::spawn::SpawnableList; 8 6 use bevy_log::warn_once; 9 7 use std::any::TypeId; 10 8 ··· 17 15 /// The entity to apply the effect to. 18 16 pub target: Entity, 19 17 /// The effect to apply. 20 - pub bundle: EffectBundle<B>, 18 + pub bundle: B, 21 19 } 22 20 23 21 impl<B: Bundle> AddEffectCommand<B> { 24 - fn spawn(self, world: &mut World) -> Entity { 25 - let entity = world.spawn_empty(); 26 - let id = entity.id(); 27 - self.insert(entity); 28 - id 29 - } 30 - 31 - fn insert(self, mut entity: EntityWorldMut) { 32 - entity.insert(( 33 - Effecting(self.target), 34 - self.bundle.name, 35 - self.bundle.mode, 36 - self.bundle.bundle, 37 - )); 22 + fn bundle_full(self) -> (Effecting, B) { 23 + (Effecting(self.target), self.bundle) 38 24 } 39 25 40 26 /// Inserts into the existing entity, and then merges the old effect into it using [`EffectMergeRegistry`]. ··· 70 56 temp 71 57 }; 72 58 73 - self.insert(world.entity_mut(new_effect)); 59 + world.entity_mut(new_effect).insert(self.bundle); 74 60 75 61 // Call merge function on those copied components. 76 62 { ··· 100 86 } 101 87 } 102 88 103 - impl<B: Bundle> Command for AddEffectCommand<B> { 89 + impl<B: Bundle + Clone> Command for AddEffectCommand<B> { 104 90 fn apply(self, world: &mut World) { 105 - if self.bundle.mode == EffectMode::Stack { 106 - self.spawn(world); 91 + let mut inspector = world.get_resource_or_init::<BundleInspector>(); 92 + let (name, mode) = inspector.get_effect_meta(self.bundle.clone()); 93 + 94 + if mode == EffectMode::Stack { 95 + world.spawn(self.bundle_full()); 107 96 return; 108 97 } 109 98 110 - let Some(effected_by) = world 111 - .get::<EffectedBy>(self.target) 112 - .map(|e| e.collection().clone()) 113 - else { 114 - self.spawn(world); 99 + let Some(effected_by) = world.get::<EffectedBy>(self.target).map(|e| e.collection()) else { 100 + world.spawn(self.bundle_full()); 115 101 return; 116 102 }; 117 103 ··· 122 108 let other_mode = world.get::<EffectMode>(*entity)?; 123 109 124 110 // Todo Think more about. 125 - if self.bundle.mode != *other_mode { 111 + if mode != *other_mode { 126 112 return None; 127 113 } 128 114 129 - let name = world.get::<Name>(*entity)?; 115 + let other_name = world.get::<Name>(*entity); 130 116 131 - if name == &self.bundle.name { 117 + if name.as_ref() == other_name { 132 118 return Some(*entity); 133 119 } 134 120 ··· 136 122 }); 137 123 138 124 let Some(old_entity) = old_entity else { 139 - self.spawn(world); 125 + world.spawn(self.bundle_full()); 140 126 return; 141 127 }; 142 128 143 - match self.bundle.mode { 129 + match mode { 144 130 EffectMode::Stack => unreachable!(), 145 - EffectMode::Insert => self.insert(world.entity_mut(old_entity)), 131 + EffectMode::Insert => { 132 + world.entity_mut(old_entity).insert(self.bundle); 133 + } 146 134 EffectMode::Merge => self.merge(world, old_entity), 147 135 } 148 136 } 149 137 } 150 138 151 - // Todo This is probably bad practice/has larger performance cost. 152 - impl<B: Bundle> SpawnableList<Effecting> for EffectBundle<B> { 153 - fn spawn(this: MovingPtr<'_, Self>, world: &mut World, target: Entity) { 154 - let bundle = this.read(); 155 - world.commands().queue(AddEffectCommand { target, bundle }); 156 - } 157 - 158 - fn size_hint(&self) -> usize { 159 - 0 160 - } 161 - } 162 - 163 139 /// Uses commands to apply effects to a specific target entity. 164 140 /// 165 141 /// This is normally used during [`with_effects`](EffectCommandsExt::with_effects). ··· 179 155 /// 180 156 /// # Example 181 157 #[doc = include_str!("../docs/with_effects_example.md")] 182 - pub fn spawn<B: Bundle>(&mut self, bundle: EffectBundle<B>) { 158 + pub fn spawn<B: Bundle + Clone>(&mut self, bundle: B) { 183 159 self.commands.queue(AddEffectCommand { 184 160 target: self.target, 185 161 bundle, ··· 196 172 /// 197 173 /// # Example 198 174 #[doc = include_str!("../docs/with_effect_example.md")] 199 - fn with_effect<B: Bundle>(&mut self, bundle: EffectBundle<B>) -> &mut Self; 175 + fn with_effect<B: Bundle + Clone>(&mut self, bundle: B) -> &mut Self; 200 176 201 - /// Applies effects to this entity by taking a function that operates on a [`EffectSpawner`]. 177 + /// Applies effects to this entity by taking a function that operates on an [`EffectSpawner`]. 202 178 /// 203 179 /// For applying a single effect, see [`with_effect`](Self::with_effect). 204 180 /// ··· 208 184 } 209 185 210 186 impl EffectCommandsExt for EntityCommands<'_> { 211 - fn with_effect<B: Bundle>(&mut self, bundle: EffectBundle<B>) -> &mut Self { 187 + fn with_effect<B: Bundle + Clone>(&mut self, bundle: B) -> &mut Self { 212 188 let target = self.id(); 213 189 self.commands().queue(AddEffectCommand { target, bundle }); 214 190 self
+5 -2
src/lib.rs
··· 1 1 #![doc = include_str!("../README.md")] 2 2 3 - mod bundle; 3 + mod bundle_inspector; 4 4 mod command; 5 5 mod component; 6 6 mod registry; 7 7 mod relation; 8 + mod spawnable_list; 8 9 9 10 use bevy_app::{App, Plugin}; 10 11 use bevy_ecs::prelude::*; 11 12 use bevy_reflect::Reflect; 12 13 use bevy_reflect::prelude::ReflectDefault; 13 14 14 - pub use bundle::*; 15 + use crate::bundle_inspector::BundleInspector; 15 16 pub use command::*; 16 17 pub use component::*; 17 18 pub use registry::*; 18 19 pub use relation::*; 20 + pub use spawnable_list::*; 19 21 20 22 /// Setup required types and systems for `bevy_alchemy`. 21 23 pub struct AlchemyPlugin; ··· 28 30 .register_type::<Lifetime>() 29 31 .register_type::<Delay>() 30 32 .register_type::<TimerMergeMode>() 33 + .init_resource::<BundleInspector>() 31 34 .init_resource::<EffectMergeRegistry>() 32 35 .add_plugins(TimerPlugin) 33 36 .add_plugins(StackPlugin);
+28
src/spawnable_list.rs
··· 1 + use crate::{AddEffectCommand, Effecting}; 2 + use bevy_ecs::prelude::*; 3 + use bevy_ecs::ptr::MovingPtr; 4 + use bevy_ecs::spawn::SpawnableList; 5 + 6 + /// A wrapper over a [`Bundle`] indicating that an effect should be applied with that [`Bundle`]. 7 + /// This is intended to be used in [`EffectedBy::spawn`](SpawnRelated::spawn). 8 + /// 9 + /// This *might* spawn a new entity, depending on what effects are already applied to the target. 10 + /// # Example 11 + #[doc = include_str!("../docs/effected_by_spawn_example.md")] 12 + #[derive(Default)] 13 + pub struct Effect<B: Bundle>(pub B); 14 + 15 + // Todo This is probably bad practice/has larger performance cost. 16 + impl<B: Bundle + Clone> SpawnableList<Effecting> for Effect<B> { 17 + fn spawn(this: MovingPtr<'_, Self>, world: &mut World, target: Entity) { 18 + let bundle = this.read(); 19 + world.commands().queue(AddEffectCommand { 20 + target, 21 + bundle: bundle.0, 22 + }); 23 + } 24 + 25 + fn size_hint(&self) -> usize { 26 + 0 27 + } 28 + }
+68 -104
tests/merge_mode.rs
··· 5 5 use bevy_time::*; 6 6 use std::time::Duration; 7 7 8 - #[derive(Component, Debug, Eq, PartialEq, Default)] 8 + #[derive(Component, Debug, Eq, PartialEq, Default, Clone)] 9 9 struct MyEffect(u8); 10 10 11 11 fn init_world() -> World { ··· 28 28 let target = world.spawn_empty().id(); 29 29 30 30 world.commands().entity(target).with_effects(|effects| { 31 - effects.spawn(EffectBundle { 32 - bundle: MyEffect(0), 33 - ..Default::default() 34 - }); 35 - 36 - effects.spawn(EffectBundle { 37 - bundle: MyEffect(1), 38 - ..Default::default() 39 - }); 31 + effects.spawn(MyEffect(0)); 32 + effects.spawn(MyEffect(1)); 40 33 }); 41 34 42 35 world.flush(); ··· 57 50 58 51 let target = world.spawn_empty().id(); 59 52 60 - world.commands().entity(target).with_effect(EffectBundle { 61 - mode: EffectMode::Insert, 62 - bundle: MyEffect(0), 63 - ..Default::default() 64 - }); 65 - world.commands().entity(target).with_effect(EffectBundle { 66 - mode: EffectMode::Insert, 67 - bundle: MyEffect(1), 68 - ..Default::default() 69 - }); 53 + world 54 + .commands() 55 + .entity(target) 56 + .with_effect((EffectMode::Insert, MyEffect(0))); 57 + world 58 + .commands() 59 + .entity(target) 60 + .with_effect((EffectMode::Insert, MyEffect(1))); 70 61 71 62 world.flush(); 72 63 ··· 86 77 87 78 let target = world.spawn_empty().id(); 88 79 89 - world.commands().entity(target).with_effect(EffectBundle { 90 - bundle: MyEffect(0), 91 - ..Default::default() 92 - }); 93 - world.commands().entity(target).with_effect(EffectBundle { 94 - bundle: MyEffect(1), 95 - ..Default::default() 96 - }); 80 + world.commands().entity(target).with_effect(MyEffect(0)); 81 + world.commands().entity(target).with_effect(MyEffect(1)); 97 82 98 - world.commands().entity(target).with_effect(EffectBundle { 99 - mode: EffectMode::Insert, 100 - bundle: MyEffect(2), 101 - ..Default::default() 102 - }); 103 - world.commands().entity(target).with_effect(EffectBundle { 104 - mode: EffectMode::Insert, 105 - bundle: MyEffect(3), 106 - ..Default::default() 107 - }); 83 + world 84 + .commands() 85 + .entity(target) 86 + .with_effect((EffectMode::Insert, MyEffect(2))); 87 + world 88 + .commands() 89 + .entity(target) 90 + .with_effect((EffectMode::Insert, MyEffect(3))); 108 91 109 92 world.flush(); 110 93 ··· 126 109 127 110 let target = world.spawn_empty().id(); 128 111 let second_lifetime = Lifetime::from_seconds(2.0).with_mode(TimerMergeMode::Replace); 129 - world.commands().entity(target).with_effect(EffectBundle { 130 - mode: EffectMode::Merge, 131 - bundle: ( 132 - Lifetime::from_seconds(1.0).with_mode(TimerMergeMode::Replace), 133 - MyEffect(0), 134 - ), 135 - ..Default::default() 136 - }); 137 - world.commands().entity(target).with_effect(EffectBundle { 138 - mode: EffectMode::Merge, 139 - bundle: (second_lifetime.clone(), MyEffect(1)), 140 - ..Default::default() 141 - }); 112 + world.commands().entity(target).with_effect(( 113 + EffectMode::Merge, 114 + Lifetime::from_seconds(1.0).with_mode(TimerMergeMode::Replace), 115 + MyEffect(0), 116 + )); 117 + world.commands().entity(target).with_effect(( 118 + EffectMode::Merge, 119 + second_lifetime.clone(), 120 + MyEffect(1), 121 + )); 142 122 143 123 world.flush(); 144 124 ··· 158 138 159 139 let target = world.spawn_empty().id(); 160 140 let first_delay = Delay::from_seconds(1.0).with_mode(TimerMergeMode::Keep); 161 - world.commands().entity(target).with_effect(EffectBundle { 162 - mode: EffectMode::Merge, 163 - bundle: (first_delay.clone(), MyEffect(0)), 164 - ..Default::default() 165 - }); 166 - world.commands().entity(target).with_effect(EffectBundle { 167 - mode: EffectMode::Merge, 168 - bundle: ( 169 - Delay::from_seconds(2.0).with_mode(TimerMergeMode::Keep), 170 - MyEffect(1), 171 - ), 172 - ..Default::default() 173 - }); 141 + world.commands().entity(target).with_effect(( 142 + EffectMode::Merge, 143 + first_delay.clone(), 144 + MyEffect(0), 145 + )); 146 + world.commands().entity(target).with_effect(( 147 + EffectMode::Merge, 148 + Delay::from_seconds(2.0).with_mode(TimerMergeMode::Keep), 149 + MyEffect(1), 150 + )); 174 151 175 152 world.flush(); 176 153 ··· 192 169 let mut first_timer = Timer::from_seconds(2.0, TimerMode::Once); 193 170 first_timer.tick(Duration::from_secs_f32(1.0)); 194 171 195 - world.commands().entity(target).with_effect(EffectBundle { 196 - mode: EffectMode::Merge, 197 - bundle: ( 198 - Delay { 199 - timer: first_timer, 200 - mode: TimerMergeMode::Fraction, 201 - }, 202 - MyEffect(0), 203 - ), 204 - ..Default::default() 205 - }); 206 - world.commands().entity(target).with_effect(EffectBundle { 207 - mode: EffectMode::Merge, 208 - bundle: ( 209 - Delay::from_seconds(10.0).with_mode(TimerMergeMode::Fraction), 210 - MyEffect(1), 211 - ), 212 - ..Default::default() 213 - }); 172 + world.commands().entity(target).with_effect(( 173 + EffectMode::Merge, 174 + Delay { 175 + timer: first_timer, 176 + mode: TimerMergeMode::Fraction, 177 + }, 178 + MyEffect(0), 179 + )); 180 + world.commands().entity(target).with_effect(( 181 + EffectMode::Merge, 182 + Delay::from_seconds(10.0).with_mode(TimerMergeMode::Fraction), 183 + MyEffect(1), 184 + )); 214 185 215 186 world.flush(); 216 187 ··· 237 208 let target = world.spawn_empty().id(); 238 209 let max = Delay::from_seconds(3.0).with_mode(TimerMergeMode::Max); 239 210 240 - world.commands().entity(target).with_effect(EffectBundle { 241 - mode: EffectMode::Merge, 242 - bundle: ( 243 - Delay::from_seconds(1.0).with_mode(TimerMergeMode::Max), 244 - MyEffect(0), 245 - ), 246 - ..Default::default() 247 - }); 248 - world.commands().entity(target).with_effect(EffectBundle { 249 - mode: EffectMode::Merge, 250 - bundle: (max.clone(), MyEffect(1)), 251 - ..Default::default() 252 - }); 253 - world.commands().entity(target).with_effect(EffectBundle { 254 - mode: EffectMode::Merge, 255 - bundle: ( 256 - Delay::from_seconds(2.0).with_mode(TimerMergeMode::Max), 257 - MyEffect(2), 258 - ), 259 - ..Default::default() 260 - }); 211 + world.commands().entity(target).with_effect(( 212 + EffectMode::Merge, 213 + Delay::from_seconds(1.0).with_mode(TimerMergeMode::Max), 214 + MyEffect(0), 215 + )); 216 + world 217 + .commands() 218 + .entity(target) 219 + .with_effect((EffectMode::Merge, max.clone(), MyEffect(1))); 220 + world.commands().entity(target).with_effect(( 221 + EffectMode::Merge, 222 + Delay::from_seconds(2.0).with_mode(TimerMergeMode::Max), 223 + MyEffect(2), 224 + )); 261 225 262 226 world.flush(); 263 227
+8 -39
tests/spawn_syntax.rs
··· 3 3 use bevy_alchemy::*; 4 4 use bevy_ecs::prelude::*; 5 5 6 - #[derive(Component, Debug, Eq, PartialEq, Default)] 6 + #[derive(Component, Debug, Eq, PartialEq, Default, Clone)] 7 7 struct MyEffect(u8); 8 8 9 9 #[test] ··· 12 12 13 13 world.spawn(( 14 14 Name::new("Target"), 15 - EffectedBy::spawn(( 16 - EffectBundle { 17 - bundle: MyEffect(0), 18 - ..Default::default() 19 - }, 20 - EffectBundle { 21 - bundle: MyEffect(1), 22 - ..Default::default() 23 - }, 24 - )), 15 + EffectedBy::spawn((Effect(MyEffect(0)), Effect(MyEffect(1)))), 25 16 )); 26 17 27 18 world.flush(); ··· 43 34 world.spawn(( 44 35 Name::new("Target"), 45 36 EffectedBy::spawn(( 46 - EffectBundle { 47 - mode: EffectMode::Insert, 48 - bundle: MyEffect(0), 49 - ..Default::default() 50 - }, 51 - EffectBundle { 52 - mode: EffectMode::Insert, 53 - bundle: MyEffect(1), 54 - ..Default::default() 55 - }, 37 + Effect((EffectMode::Insert, MyEffect(0))), 38 + Effect((EffectMode::Insert, MyEffect(1))), 56 39 )), 57 40 )); 58 41 ··· 75 58 world.spawn(( 76 59 Name::new("Target"), 77 60 EffectedBy::spawn(( 78 - EffectBundle { 79 - bundle: MyEffect(0), 80 - ..Default::default() 81 - }, 82 - EffectBundle { 83 - bundle: MyEffect(1), 84 - ..Default::default() 85 - }, 86 - EffectBundle { 87 - mode: EffectMode::Insert, 88 - bundle: MyEffect(2), 89 - ..Default::default() 90 - }, 91 - EffectBundle { 92 - mode: EffectMode::Insert, 93 - bundle: MyEffect(3), 94 - ..Default::default() 95 - }, 61 + Effect(MyEffect(0)), 62 + Effect(MyEffect(1)), 63 + Effect((EffectMode::Insert, MyEffect(2))), 64 + Effect((EffectMode::Insert, MyEffect(3))), 96 65 )), 97 66 )); 98 67