bevy_enoki 0.6.0

2D Particle system plugin, that works well on wasm and mobile
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![allow(rustdoc::redundant_explicit_links)]
#![doc = include_str!("../README.md")]

use self::prelude::{
    Particle2dMaterial, ParticleEffectInstance, ParticleSpawnerState, ParticleStore,
};
use crate::sprite::SpriteParticle2dMaterial;
use bevy_app::{App, First, Plugin, PostUpdate, Update};
use bevy_asset::{load_internal_asset, uuid_handle, Asset, AssetApp, AssetEvent, Assets, Handle};
use bevy_camera::{
    primitives::Aabb,
    visibility::{add_visibility_class, Visibility, VisibilityClass, VisibilitySystems},
};
use bevy_color::LinearRgba;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
    component::Component,
    schedule::{common_conditions::on_message, IntoScheduleConfigs},
};
use bevy_math::Vec2;
use bevy_reflect::{Reflect, TypePath};
use bevy_render::sync_world::SyncToRenderWorld;
use bevy_shader::Shader;
use bevy_transform::components::Transform;
use color::ColorParticle2dMaterial;
use serde::{Deserialize, Serialize};
use values::Rval;

mod color;
mod curve;
mod loader;
mod material;
mod sprite;
mod update;
mod values;

#[allow(unused)]
pub mod prelude {
    pub use super::color::ColorParticle2dMaterial;
    pub use super::curve::{LerpThat, MultiCurve /* , ParticleEaseFunction */};
    pub use super::loader::ParticleEffectLoader;
    pub use super::material::{Particle2dMaterial, Particle2dMaterialPlugin};
    pub use super::sprite::SpriteParticle2dMaterial;
    pub use super::update::{OneShot, ParticleEffectInstance, ParticleSpawnerState, ParticleStore};
    pub use super::values::{Random, Rval};
    pub use super::{
        Attractor, EmissionShape, EnokiPlugin, NoAutoAabb, Particle2dEffect, ParticleEffectHandle,
        ParticleSpawner,
    };
}

pub(crate) const PARTICLE_VERTEX_OUT: Handle<Shader> =
    uuid_handle!("8d6bc2d4-7577-4890-a3a4-0faea3a27448");
pub(crate) const PARTICLE_VERTEX: Handle<Shader> =
    uuid_handle!("57c98346-305c-461a-8cdc-7b3fac8be0ca");
pub(crate) const PARTICLE_COLOR_FRAG: Handle<Shader> =
    uuid_handle!("f60a0cf3-19d3-4425-b6f8-b06bf7ba2f34");
pub(crate) const PARTICLE_SPRITE_FRAG: Handle<Shader> =
    uuid_handle!("9b13ccf9-eea1-4515-bdd1-1b4131368f71");

pub struct EnokiPlugin;
impl Plugin for EnokiPlugin {
    fn build(&self, app: &mut App) {
        load_internal_asset!(
            app,
            PARTICLE_VERTEX_OUT,
            "shaders/particle_vertex_out.wgsl",
            Shader::from_wgsl
        );

        load_internal_asset!(
            app,
            PARTICLE_VERTEX,
            "shaders/particle_vertex.wgsl",
            Shader::from_wgsl
        );

        load_internal_asset!(
            app,
            PARTICLE_COLOR_FRAG,
            "shaders/particle_color_frag.wgsl",
            Shader::from_wgsl
        );

        load_internal_asset!(
            app,
            PARTICLE_SPRITE_FRAG,
            "shaders/particle_sprite_frag.wgsl",
            Shader::from_wgsl
        );

        app.add_plugins(material::Particle2dMaterialPlugin::<SpriteParticle2dMaterial>::default());
        app.add_plugins(material::Particle2dMaterialPlugin::<ColorParticle2dMaterial>::default());

        app.register_type::<update::ParticleStore>();
        app.register_type::<update::ParticleSpawnerState>();
        app.register_type::<update::ParticleSpawnerState>();
        app.register_type::<update::Particle>();
        app.register_type::<ParticleEffectHandle>();
        app.init_asset::<Particle2dEffect>();
        app.init_asset_loader::<loader::ParticleEffectLoader>();

        let _ = app
            .world_mut()
            .resource_mut::<Assets<ColorParticle2dMaterial>>()
            .insert(
                &Handle::<ColorParticle2dMaterial>::default(),
                ColorParticle2dMaterial::default(),
            );

        let _ = app
            .world_mut()
            .resource_mut::<Assets<Particle2dEffect>>()
            .insert(
                &Handle::<Particle2dEffect>::default(),
                Particle2dEffect::default(),
            );

        app.add_systems(
            First,
            loader::on_asset_loaded.run_if(on_message::<AssetEvent<Particle2dEffect>>),
        );

        app.add_systems(
            Update,
            (
                loader::reload_effect,
                update::clone_effect,
                update::remove_finished_spawner,
                update::update_spawner,
            ),
        );

        app.add_systems(
            PostUpdate,
            (
                update::calculcate_particle_bounds.in_set(VisibilitySystems::CalculateBounds),
                // check_visibility.in_set(VisibilitySystems::CheckVisibility),
            ),
        );
    }
}

/// adding this component will disabled auto
/// aabb caluclation. Aabb resolves to it's default size.
#[derive(Component)]
pub struct NoAutoAabb;

/// tag component for visibilty check
#[derive(Clone, Component, Default)]
#[require(VisibilityClass)]
#[component(on_add = add_visibility_class::<RenderParticleTag>)]
pub struct RenderParticleTag;

/// The main particle spawner components
/// has required components
#[derive(Component, DerefMut, Deref, Clone)]
#[require(
    ParticleSpawnerState,
    ParticleEffectInstance,
    ParticleEffectHandle,
    ParticleStore,
    Transform,
    Visibility,
    Aabb,
    SyncToRenderWorld,
    RenderParticleTag
)]
pub struct ParticleSpawner<T: Particle2dMaterial>(pub Handle<T>);

impl<T: Particle2dMaterial> From<Handle<T>> for ParticleSpawner<T> {
    fn from(value: Handle<T>) -> Self {
        Self(value)
    }
}

impl Default for ParticleSpawner<ColorParticle2dMaterial> {
    fn default() -> Self {
        ParticleSpawner(Handle::default())
    }
}

#[derive(Deserialize, Reflect, Default, Clone, Debug, Serialize, PartialEq)]
#[reflect]
pub enum EmissionShape {
    #[default]
    Point,
    Circle(f32),
}

#[derive(Deserialize, Serialize, Clone, Debug, Reflect)]
pub struct Attractor {
    pub position: Vec2,
    pub strength: f32,
    pub min_distance: f32,
}

/// holds the effect asset. Changing the Asset, will
/// effect all spanwers using it. Instead use `ParticleEffectInstance`,
/// which is a unique copy for each spawner,
#[derive(Component, Reflect, Deref, DerefMut, Default)]
#[reflect]
pub struct ParticleEffectHandle(pub Handle<Particle2dEffect>);

impl From<Handle<Particle2dEffect>> for ParticleEffectHandle {
    fn from(value: Handle<Particle2dEffect>) -> Self {
        Self(value)
    }
}

/// The particle effect asset.
#[derive(Asset, TypePath, Deserialize, Serialize, Clone, Debug)]
pub struct Particle2dEffect {
    pub spawn_rate: f32,
    pub spawn_amount: u32,
    pub emission_shape: EmissionShape,
    pub lifetime: Rval<f32>,
    pub linear_speed: Option<Rval<f32>>,
    pub linear_acceleration: Option<Rval<f32>>,
    pub direction: Option<Rval<Vec2>>,
    pub angular_speed: Option<Rval<f32>>,
    pub angular_acceleration: Option<Rval<f32>>,
    pub scale: Option<Rval<f32>>,
    pub color: Option<LinearRgba>,
    pub gravity_direction: Option<Rval<Vec2>>,
    pub gravity_speed: Option<Rval<f32>>,
    pub linear_damp: Option<Rval<f32>>,
    pub angular_damp: Option<Rval<f32>>,
    pub scale_curve: Option<curve::MultiCurve<f32>>,
    pub color_curve: Option<curve::MultiCurve<LinearRgba>>,
    pub attractors: Option<Vec<Attractor>>,
    pub relative_positioning: Option<bool>,
}

impl Default for Particle2dEffect {
    fn default() -> Self {
        Self {
            spawn_rate: 0.1,
            spawn_amount: 1,
            emission_shape: EmissionShape::Point,
            lifetime: Rval::new(1., 0.0),
            linear_speed: Some(Rval(100., 0.1)),
            linear_acceleration: None,
            direction: Some(Rval(Vec2::Y, 0.1)),
            angular_speed: None,
            angular_acceleration: None,
            scale: Some(Rval(5., 1.)),
            color: None,
            gravity_direction: None,
            gravity_speed: None,
            linear_damp: None,
            angular_damp: None,
            scale_curve: None,
            color_curve: None,
            attractors: None,
            relative_positioning: None,
        }
    }
}