bevy_alchemy 0.5.0

An experimental, status effects-as-entities system for Bevy.
Documentation
//! 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::{AlchemyPlugin, Delay, EffectCommandsExt, 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, Clone)]
struct Poison {
    damage: i32,
}

/// Spawn a target on startup.
fn init_scene(mut commands: Commands) {
    commands.spawn((Name::new("Target"), Health(100)));
    commands.spawn((
        Node {
            margin: UiRect::all(Val::Px(10.0)),
            ..default()
        },
        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((
        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.
    ));
}

/// Runs every frame and deals the poison damage.
fn deal_poison_damage(
    effects: Query<(&Effecting, &Delay, &Poison)>,
    mut targets: Query<&mut Health>,
) {
    for (target, 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.
        health.0 -= poison.damage;
    }
}

fn update_ui(
    mut ui: Single<&mut Text>,
    target: Single<&Health>,
    effects: Query<(Entity, &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, lifetime, delay) in &effects {
        ui.0 += &format!(
            "{} - {:.1}s (tick in {:.1}s)\n",
            entity,
            lifetime.timer.remaining_secs(),
            delay.timer.remaining_secs()
        );
    }
}