Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bevy_alchemy"
version = "0.2.0"
version = "0.2.1"
edition = "2024"
description = "An experimental, status effects-as-entities system for Bevy."
categories = ["game-development"]
Expand Down Expand Up @@ -41,6 +41,10 @@ unused_qualifications = "warn"
name = "poison"
path = "examples/poison.rs"

[[example]]
name = "poison_falloff"
path = "examples/poison_falloff.rs"

[[example]]
name = "decaying_speed"
path = "examples/immediate_stats/decaying_speed.rs"
Expand Down
9 changes: 5 additions & 4 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# Examples

| Example | Description |
|-----------------------|-----------------------------------|
| [`poison`](poison.rs) | A simple damage-over-time effect. |
| Example | Description |
|---------------------------------------|--------------------------------------------------------------------------------|
| [`poison`](poison.rs) | A simple damage-over-time effect. |
| [`poison_falloff`](poison_falloff.rs) | A damage-over-time effect where the damage falls off as more stacks are added. |

## Immediate Stats
Examples in the `immediate_stats` subdirectory utilize the [`immediate_stats`](https://github.com/AlephCubed/immediate_stats) crate, which I also created.
Some of these examples include a copy that utilizes [`bevy_auto_plugin`](https://github.com/StrikeForceZero/bevy_auto_plugin), which should behave exactly the same.
Some of these examples include a version that utilizes [`bevy_auto_plugin`](https://github.com/StrikeForceZero/bevy_auto_plugin), which should behave exactly the same.

| Example | Description |
|--------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
Expand Down
3 changes: 2 additions & 1 deletion examples/immediate_stats/decaying_speed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
//! to add a decaying movement speed buff.
//! This means that the strength of the buff decreases throughout its duration.
//!
//! This uses [`EffectMode::Merge`], which prevents having multiple of the effect applied at the same time (no 10x speed multiplier for you).
//! This uses [`EffectMode::Merge`], which prevents having multiple of the effect applied at the
//! same time (no 10x speed multiplier for you).
//!
//! There is a second version of this example, which uses Bevy Auto Plugin.

Expand Down
6 changes: 4 additions & 2 deletions examples/immediate_stats/decaying_speed_auto_plugin.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
//! This example shows using [Immediate Stats](https://github.com/AlephCubed/immediate_stats)
//! to add a decaying movement speed buff, using Bevy Auto Plugin (there is a second version of this example which just uses normal Bevy).
//! to add a decaying movement speed buff, using Bevy Auto Plugin
//! (there is a second version of this example which just uses normal Bevy).
//! This means that the strength of the buff decreases throughout its duration.
//!
//! This uses [`EffectMode::Merge`], which prevents having multiple of the effect applied at the same time (no 10x speed multiplier for you).
//! This uses [`EffectMode::Merge`], which prevents having multiple of the effect applied at the
//! same time (no 10x speed multiplier for you).

use bevy::prelude::*;
use bevy_alchemy::*;
Expand Down
4 changes: 4 additions & 0 deletions examples/poison.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
//! A simple damage-over-time effect.
//!
//! Each application of the effect is its own entity, meaning an entity can be poisoned multiple times.
//! This can be changed by using a different [`EffectMode`](bevy_alchemy::EffectMode).
//! The `poison_falloff` example shows a different way to handle effect stacking.

use bevy::prelude::*;
use bevy_alchemy::{
Expand Down
109 changes: 109 additions & 0 deletions examples/poison_falloff.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//! A damage-over-time effect where the damage falls off as more stacks are added.
//!
//! When an entity is already poisoned, subsequent applications deal less damage.
//! In this case the first stack deals 5 damage, the next 4, then 3, and so on.
//!
//! This works by [merging](EffectMode::Merge) the effects into a single entity and using the
//! [number of stacks](EffectStacks) in damage calculations.
//! A slightly simpler version is available in the `poison` example.

use bevy::prelude::*;
use bevy_alchemy::{
AlchemyPlugin, Delay, EffectBundle, EffectCommandsExt, EffectMode, EffectStacks, EffectTimer,
Effecting, Lifetime,
};

fn main() {
App::new()
.add_plugins((DefaultPlugins, AlchemyPlugin))
.add_systems(Startup, init_scene)
.add_systems(Update, (on_space_pressed, deal_poison_damage))
.add_systems(PostUpdate, update_ui)
.run();
}

#[derive(Component)]
struct Health(i32);

/// Deals damage over time to the target entity.
#[derive(Component, Default)]
struct Poison {
damage: i32,
}

/// Spawn a target on startup.
fn init_scene(mut commands: Commands) {
commands.spawn((Name::new("Target"), Health(500)));
commands.spawn(Text::default());
commands.spawn(Camera2d);
}

/// When space is pressed, apply poison to the target.
fn on_space_pressed(
mut commands: Commands,
keyboard_input: Res<ButtonInput<KeyCode>>,
target: Single<Entity, With<Health>>,
) {
if !keyboard_input.just_pressed(KeyCode::Space) {
return;
}

commands.entity(*target).with_effect(EffectBundle {
mode: EffectMode::Merge, // Stack tracking requires effect merging.
bundle: (
EffectStacks::default(), // Enable stack tracking.
Lifetime::from_seconds(4.0), // The duration of the effect.
Delay::from_seconds(1.0), // The time between damage ticks.
Poison { damage: 5 }, // The amount of damage to apply per tick.
),
..default()
});
}

/// Runs every frame and deals the poison damage.
fn deal_poison_damage(
effects: Query<(&Effecting, &EffectStacks, &Delay, &Poison)>,
mut targets: Query<&mut Health>,
) {
for (target, stacks, delay, poison) in effects {
// We wait until the delay finishes to apply the damage.
if !delay.timer.is_finished() {
continue;
}

// Skip if the target doesn't have health.
let Ok(mut health) = targets.get_mut(target.0) else {
continue;
};

// Otherwise, deal the damage scaled with the number of stacks.
// Each subsequent stack has a decreasing effect, the first deals 5 damage, the next 4, then 3, and so on.
let stacks = poison.damage.min(stacks.0 as i32); // Clamp stacks to prevent negative damage.
let sub = (stacks * (stacks - 1)) / 2;
let damage = poison.damage * stacks - sub;

info!("Dealt {damage} damage!");

health.0 -= damage;
}
}

fn update_ui(
mut ui: Single<&mut Text>,
target: Single<&Health>,
effects: Query<(Entity, &EffectStacks, &Lifetime, &Delay), With<Poison>>,
) {
ui.0 = "Press Space to apply poison\n\n".to_string();

ui.0 += &format!("Health: {}\n\n", target.0);

for (entity, stacks, lifetime, delay) in &effects {
ui.0 += &format!(
"{}, {} stacks - {:.1}s (tick in {:.1}s)\n",
entity,
stacks.0,
lifetime.timer.remaining_secs(),
delay.timer.remaining_secs()
);
}
}
5 changes: 5 additions & 0 deletions src/component.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod stack;
mod timer;

pub use stack::*;
pub use timer::*;
62 changes: 62 additions & 0 deletions src/component/stack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use crate::EffectMergeRegistry;
use bevy_app::{App, Plugin};
use bevy_ecs::prelude::ReflectComponent;
use bevy_ecs::prelude::{Component, Entity, EntityWorldMut};
use bevy_reflect::Reflect;
use bevy_reflect::prelude::ReflectDefault;
use std::ops::{Add, AddAssign, Deref, DerefMut};

pub(crate) struct StackPlugin;

impl Plugin for StackPlugin {
fn build(&self, app: &mut App) {
app.world_mut()
.resource_mut::<EffectMergeRegistry>()
.register::<EffectStacks>(merge_effect_stacks);
}
}

/// Tracks the number stacks of a [merge effect](crate::EffectMode::Merge) that have been applied to an entity.
#[derive(Component, Reflect, Eq, PartialEq, Ord, PartialOrd, Debug, Copy, Clone)]
#[reflect(Component, Default, PartialEq, Debug, Clone)]
pub struct EffectStacks(pub u8);

impl Default for EffectStacks {
fn default() -> Self {
Self(1)
}
}

impl Deref for EffectStacks {
type Target = u8;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl DerefMut for EffectStacks {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

impl Add<u8> for EffectStacks {
type Output = Self;

fn add(self, rhs: u8) -> Self::Output {
Self(self.0 + rhs)
}
}

impl AddAssign<u8> for EffectStacks {
fn add_assign(&mut self, rhs: u8) {
self.0 += rhs
}
}

/// Merge logic for [`EffectStacks`].
fn merge_effect_stacks(mut new: EntityWorldMut, outgoing: Entity) {
let outgoing = *new.world().get::<EffectStacks>(outgoing).unwrap();
*new.get_mut::<EffectStacks>().unwrap() += outgoing.0;
}
2 changes: 1 addition & 1 deletion src/timer.rs → src/component/timer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use bevy_reflect::Reflect;
use bevy_time::{Time, Timer, TimerMode};
use std::time::Duration;

pub(super) struct TimerPlugin;
pub(crate) struct TimerPlugin;

impl Plugin for TimerPlugin {
fn build(&self, app: &mut App) {
Expand Down
7 changes: 4 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

mod bundle;
mod command;
mod component;
mod registry;
mod relation;
mod timer;

use bevy_app::{App, Plugin};
use bevy_ecs::prelude::*;
Expand All @@ -13,9 +13,9 @@ use bevy_reflect::prelude::ReflectDefault;

pub use bundle::*;
pub use command::*;
pub use component::*;
pub use registry::*;
pub use relation::*;
pub use timer::*;

/// Setup required types and systems for `bevy_alchemy`.
pub struct AlchemyPlugin;
Expand All @@ -29,7 +29,8 @@ impl Plugin for AlchemyPlugin {
.register_type::<Delay>()
.register_type::<TimerMergeMode>()
.init_resource::<EffectMergeRegistry>()
.add_plugins(TimerPlugin);
.add_plugins(TimerPlugin)
.add_plugins(StackPlugin);
}
}

Expand Down
6 changes: 3 additions & 3 deletions tests/spawn_syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ fn spawnable_list_stack() {

let effects: Vec<u8> = world
.query::<&MyEffect>()
.iter(&mut world)
.iter(&world)
.map(|c| c.0)
.collect();

Expand Down Expand Up @@ -60,7 +60,7 @@ fn spawnable_list_insert() {

let effects: Vec<u8> = world
.query::<&MyEffect>()
.iter(&mut world)
.iter(&world)
.map(|c| c.0)
.collect();

Expand Down Expand Up @@ -100,7 +100,7 @@ fn spawnable_list_mixed() {

let effects: Vec<u8> = world
.query::<&MyEffect>()
.iter(&mut world)
.iter(&world)
.map(|c| c.0)
.collect();

Expand Down
Loading