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.

2 changes: 1 addition & 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.1"
version = "0.3.0"
edition = "2024"
description = "An experimental, status effects-as-entities system for Bevy."
categories = ["game-development"]
Expand Down
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,18 @@ fn deal_poison_damage(
}
```

### Timers
Two timers are added by the crate:
1. `Lifetime` - Despawns the effect when the timer ends.
2. `Delay` - A repeating timer used for the delay between effect applications.
### Utility Components
A handful of components are included that are intended to make it easier to create common effects.

| Component | Description |
|----------------|-------------------------------------------------------------------------------|
| `Lifetime` | A timer that despawns the effect when the timer finishes. |
| `Delay` | A repeating timer used for the delay between effect applications. |
| `EffectStacks` | Tracks the number of times a merge-mode effect has been applied to an entity. |

### Bevy Version Compatibility

| Bevy | Bevy Alchemy |
|--------|--------------|
| `0.18` | `0.2` |
| `0.17` | `0.1` |
| Bevy | Bevy Alchemy |
|--------|---------------|
| `0.18` | `0.2` - `0.3` |
| `0.17` | `0.1` |
5 changes: 3 additions & 2 deletions examples/poison.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ fn on_space_pressed(

commands.entity(*target).with_effect(EffectBundle {
bundle: (
Lifetime::from_seconds(4.0), // The duration of the effect.
Delay::from_seconds(1.0), // The time between damage ticks.
Lifetime::from_seconds(3.0), // The duration of the effect.
Delay::from_seconds(1.0) // The time between damage ticks.
.trigger_immediately(), // Make damage tick immediately when the effect is applied.
Poison { damage: 1 }, // The amount of damage to apply per tick.
),
..default()
Expand Down
5 changes: 3 additions & 2 deletions examples/poison_falloff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ fn on_space_pressed(
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.
Lifetime::from_seconds(3.0), // The duration of the effect.
Delay::from_seconds(1.0) // The time between damage ticks.
.trigger_immediately(), // Make damage tick immediately when the effect is applied.
Poison { damage: 5 }, // The amount of damage to apply per tick.
),
..default()
Expand Down
2 changes: 1 addition & 1 deletion src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ impl<B: Bundle> AddEffectCommand<B> {
fn merge(self, world: &mut World, existing_entity: Entity) {
if !world.contains_resource::<EffectMergeRegistry>() {
warn_once!(
"No `EffectComponentMergeRegistry` found. Did you forget to add the `StatusEffectPlugin`?"
"No `EffectComponentMergeRegistry` found. Did you forget to add the `AlchemyPlugin`?"
);
return;
}
Expand Down
34 changes: 30 additions & 4 deletions src/component/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ pub(crate) struct StackPlugin;
impl Plugin for StackPlugin {
fn build(&self, app: &mut App) {
app.world_mut()
.resource_mut::<EffectMergeRegistry>()
.get_resource_or_init::<EffectMergeRegistry>()
.register::<EffectStacks>(merge_effect_stacks);
}
}

/// Tracks the number stacks of a [merge effect](crate::EffectMode::Merge) that have been applied to an entity.
/// Tracks the number of times a [merge-mode](crate::EffectMode::Merge) effect has 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);
Expand All @@ -41,6 +41,20 @@ impl DerefMut for EffectStacks {
}
}

impl Add for EffectStacks {
type Output = Self;

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

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

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

Expand All @@ -55,8 +69,20 @@ impl AddAssign<u8> for EffectStacks {
}
}

/// Merge logic for [`EffectStacks`].
fn merge_effect_stacks(mut new: EntityWorldMut, outgoing: Entity) {
impl From<u8> for EffectStacks {
fn from(value: u8) -> Self {
EffectStacks(value)
}
}

impl From<EffectStacks> for u8 {
fn from(value: EffectStacks) -> Self {
value.0
}
}

/// A [merge function](crate::EffectMergeFn) for the [`EffectStacks`] component.
pub fn merge_effect_stacks(mut new: EntityWorldMut, outgoing: Entity) {
let outgoing = *new.world().get::<EffectStacks>(outgoing).unwrap();
*new.get_mut::<EffectStacks>().unwrap() += outgoing.0;
}
109 changes: 70 additions & 39 deletions src/component/timer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,23 @@ pub(crate) struct TimerPlugin;
impl Plugin for TimerPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PreUpdate, (despawn_finished_lifetimes, tick_delay).chain());
register_timer_merge_functions(&mut app.world_mut().resource_mut::<EffectMergeRegistry>());
app.world_mut()
.get_resource_or_init::<EffectMergeRegistry>()
.register::<Lifetime>(merge_effect_timer::<Lifetime>)
.register::<Delay>(merge_effect_timer::<Delay>);
}
}

/// Registers the default merge logic for [`Lifetime`] and [`Delay`].
pub fn register_timer_merge_functions(registry: &mut EffectMergeRegistry) {
registry
.register::<Lifetime>(merge_timer::<Lifetime>)
.register::<Delay>(merge_timer::<Delay>);
}

/// Merge logic for [`Lifetime`] and [`Delay`].
fn merge_timer<T: EffectTimer + Component<Mutability = Mutable> + Clone>(
/// A [merge function](crate::EffectMergeFn) for [`EffectTimer`] components ([`Lifetime`] and [`Delay`]).
pub fn merge_effect_timer<T: EffectTimer + Component<Mutability = Mutable> + Clone>(
mut new: EntityWorldMut,
outgoing: Entity,
) {
let outgoing = new.world().get::<T>(outgoing).unwrap().clone();
new.get_mut::<T>().unwrap().merge(&outgoing);
}

// Todo With more getters/settings, `merge` could have a default implementation.
/// A timer which is used for status effects and includes a [`TimerMergeMode`].
/// A [timer](Timer) which is used for status effects and includes a [`TimerMergeMode`].
pub trait EffectTimer: Sized {
/// Creates a new timer from a duration.
fn new(duration: Duration) -> Self;
Expand All @@ -48,9 +43,44 @@ pub trait EffectTimer: Sized {
/// A builder that overwrites the current merge mode with a new value.
fn with_mode(self, mode: TimerMergeMode) -> Self;

/// Merges a new timer (self) with the old one (other).
/// Returns reference to the internal timer.
fn get_timer(&self) -> &Timer;

/// Returns mutable reference to the internal timer.
fn get_timer_mut(&mut self) -> &mut Timer;

/// Returns reference to the timer's merge mode.
fn get_mode(&self) -> &TimerMergeMode;

/// Returns mutable reference to the timer's merge mode.
fn get_mode_mut(&mut self) -> &mut TimerMergeMode;

/// Merges an old timer (self) with the new one (incoming).
/// Behaviour depends on the current [`TimerMergeMode`].
fn merge(&mut self, incoming: &Self);
fn merge(&mut self, incoming: &Self) {
match self.get_mode() {
TimerMergeMode::Replace => {}
TimerMergeMode::Keep => *self.get_timer_mut() = incoming.get_timer().clone(),
TimerMergeMode::Fraction => {
let fraction = incoming.get_timer().fraction();
let duration = self.get_timer().duration().as_secs_f32();
self.get_timer_mut()
.set_elapsed(Duration::from_secs_f32(fraction * duration));
}
TimerMergeMode::Max => {
let old = incoming.get_timer().remaining_secs();
let new = self.get_timer().remaining_secs();

if old > new {
*self.get_timer_mut() = incoming.get_timer().clone();
}
}
TimerMergeMode::Sum => {
let duration = incoming.get_timer().duration() + self.get_timer().duration();
self.get_timer_mut().set_duration(duration);
}
}
}
}

macro_rules! impl_effect_timer {
Expand All @@ -68,35 +98,26 @@ macro_rules! impl_effect_timer {
self
}

fn merge(&mut self, other: &Self) {
match self.mode {
TimerMergeMode::Replace => {}
TimerMergeMode::Keep => self.timer = other.timer.clone(),
TimerMergeMode::Fraction => {
let fraction = other.timer.fraction();
let duration = self.timer.duration().as_secs_f32();
self.timer
.set_elapsed(Duration::from_secs_f32(fraction * duration));
}
TimerMergeMode::Max => {
let old = other.timer.remaining_secs();
let new = self.timer.remaining_secs();

if old > new {
self.timer = other.timer.clone();
}
}
TimerMergeMode::Sum => {
self.timer
.set_duration(other.timer.duration() + self.timer.duration());
}
}
fn get_timer(&self) -> &Timer {
&self.timer
}

fn get_timer_mut(&mut self) -> &mut Timer {
&mut self.timer
}

fn get_mode(&self) -> &TimerMergeMode {
&self.mode
}

fn get_mode_mut(&mut self) -> &mut TimerMergeMode {
&mut self.mode
}
}
};
}

/// Despawns the entity when the timer finishes.
/// A timer that despawns the effect when the timer finishes.
#[doc(alias = "Duration")]
#[derive(Component, Reflect, Eq, PartialEq, Debug, Clone)]
#[reflect(Component, PartialEq, Debug, Clone)]
Expand All @@ -118,7 +139,7 @@ impl Default for Lifetime {
}
}

/// A repeating timer used for the delay between effect applications.
/// A repeating timer used for the delay between effect applications.
#[derive(Component, Reflect, Eq, PartialEq, Debug, Clone)]
#[reflect(Component, PartialEq, Debug, Clone)]
pub struct Delay {
Expand All @@ -130,6 +151,16 @@ pub struct Delay {

impl_effect_timer!(Delay, TimerMode::Repeating);

impl Delay {
/// Makes the timer [almost finished](Timer::almost_finish), leaving 1ns of remaining time.
/// This allows effects to trigger immediately when applied.
#[doc(alias = "trigger_on_start", alias = "almost_finish")]
pub fn trigger_immediately(mut self) -> Self {
self.timer.almost_finish();
self
}
}

impl Default for Delay {
fn default() -> Self {
Self {
Expand Down
3 changes: 1 addition & 2 deletions src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@ pub type EffectMergeFn = fn(new: EntityWorldMut, outgoing: Entity);
///
/// fn main() {
/// let mut world = World::new();
/// world.init_resource::<EffectMergeRegistry>();
///
/// world.resource_mut::<EffectMergeRegistry>()
/// world.get_resource_or_init::<EffectMergeRegistry>()
/// .register::<MyEffect>(merge_my_effect);
/// }
///
Expand Down
10 changes: 6 additions & 4 deletions tests/merge_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ fn init_world() -> World {
let mut world = World::new();

let mut registry = EffectMergeRegistry::default();
register_timer_merge_functions(&mut registry);
registry
.register::<Lifetime>(merge_effect_timer::<Lifetime>)
.register::<Delay>(merge_effect_timer::<Delay>);

world.insert_resource(registry);

Expand Down Expand Up @@ -41,7 +43,7 @@ fn stack() {

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

Expand Down Expand Up @@ -70,7 +72,7 @@ fn insert() {

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

Expand Down Expand Up @@ -108,7 +110,7 @@ fn mixed() {

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

Expand Down
Loading