bevy_states_utils 0.3.0

Small utils for bevy states, such as nested states and Gc
Documentation
//! Various utilty plugins for dealing with bevy states.

#![warn(
    clippy::pedantic,
    clippy::clone_on_ref_ptr,
    clippy::create_dir,
    clippy::filetype_is_file,
    clippy::fn_to_numeric_cast_any,
    clippy::if_then_some_else_none,
    missing_docs,
    clippy::missing_docs_in_private_items,
    missing_copy_implementations,
    missing_debug_implementations,
    clippy::missing_const_for_fn,
    clippy::mixed_read_write_in_expression,
    clippy::panic,
    clippy::partial_pub_fields,
    clippy::same_name_method,
    clippy::str_to_string,
    clippy::suspicious_xor_used_as_pow,
    clippy::try_err,
    clippy::unneeded_field_pattern,
    clippy::use_debug,
    clippy::verbose_file_reads,
    clippy::expect_used
)]
#![deny(
    clippy::unwrap_used,
    clippy::unreachable,
    clippy::unimplemented,
    clippy::todo,
    clippy::dbg_macro,
    clippy::error_impl_error,
    clippy::exit,
    clippy::panic_in_result_fn,
    clippy::tests_outside_test_module
)]

use bevy::prelude::*;

/// Extension trait for `bevy::prelude::App`
pub trait AppExtension {
    /// This adds registers the required systems for `Gc<S>` to work
    #[cfg(feature = "gc")]
    fn add_gc_state<S: States>(&mut self);
    /// Mark a resource to be removed when exsiting a specific stage
    #[cfg(feature = "gc")]
    fn add_gc_resource<S: States, R: Resource>(&mut self, state: S);
    /// Setup nested states, when the parent state enters the specified state the child will be
    /// switched into the given state, if the parent leavs the state the child will be switched to
    /// its default state
    #[cfg(feature = "nested")]
    fn add_substate<P: States, S: States + Default>(&mut self, parent: P, sub: S);
}

/// Despawn this entity once the app leave the specified state
#[derive(Component, Debug)]
pub struct Gc<S: States>(pub S);

impl AppExtension for App {
    fn add_gc_state<S: States>(&mut self) {
        self.add_systems(
            PostUpdate,
            |mut commands: Commands, state: Res<State<S>>, query: Query<(Entity, &Gc<S>)>| {
                if state.is_changed() {
                    for (entity, expected_state) in query.iter() {
                        if state.get() != &expected_state.0 {
                            commands.entity(entity).despawn_recursive();
                        }
                    }
                }
            },
        );
    }

    fn add_gc_resource<S: States, R: Resource>(&mut self, expected_state: S) {
        self.add_systems(
            PostUpdate,
            move |mut commands: Commands, state: Res<State<S>>| {
                if state.is_changed() {
                    if state.get() != &expected_state {
                        commands.remove_resource::<R>();
                    }
                }
            },
        );
    }

    // TODO: make this update the state faster!
    #[cfg(feature = "nested")]
    fn add_substate<P: States, S: States + Default>(&mut self, parent: P, sub: S) {
        let parent_clone = parent.clone();
        self.add_systems(
            Update,
            (
                move |mut commands: Commands, current_parent: Res<State<P>>| {
                    if current_parent.is_changed() && current_parent.get() == &parent {
                        commands.insert_resource(NextState(Some(sub.clone())));
                    }
                },
                move |mut commands: Commands, current_parent: Res<State<P>>| {
                    if current_parent.is_changed() && current_parent.get() != &parent_clone {
                        commands.insert_resource(NextState(Some(S::default())));
                    }
                },
            ),
        );
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[derive(States, Default, Clone, Hash, Eq, PartialEq, Debug)]
    enum StatesA {
        #[default]
        A,
        B,
    }
    #[derive(States, Default, Clone, Hash, Eq, PartialEq, Debug)]
    enum StatesB {
        #[default]
        A,
        B,
    }

    #[test]
    fn gc_resource() {
        let mut app = App::new();

        app.init_state::<StatesA>();
        app.add_gc_state::<StatesA>();

        let id = app.world.spawn(Gc(StatesA::A)).id();
        app.update();
        assert!(app.world.get::<Gc<StatesA>>(id).is_some());

        app.insert_resource(NextState(Some(StatesA::B)));
        app.update();
        assert!(app.world.get::<Gc<StatesA>>(id).is_none());
    }

    #[derive(Resource)]
    struct Res;

    #[test]
    fn gc() {
        let mut app = App::new();

        app.init_state::<StatesA>();
        app.add_gc_resource::<_, Res>(StatesA::A);

        app.insert_resource(Res);
        app.update();
        assert!(app.world.get_resource::<Res>().is_some());

        app.insert_resource(NextState(Some(StatesA::B)));
        app.update();
        assert!(app.world.get_resource::<Res>().is_none());
    }

    #[test]
    fn nested() {
        let mut app = App::new();

        app.init_state::<StatesA>();
        app.init_state::<StatesB>();
        app.add_substate(StatesA::B, StatesB::B);

        app.update();
        assert_eq!(
            app.world.get_resource::<State<StatesB>>().unwrap().get(),
            &StatesB::A
        );

        app.insert_resource(NextState(Some(StatesA::B)));
        app.update();
        app.update();
        assert_eq!(
            app.world.get_resource::<State<StatesB>>().unwrap().get(),
            &StatesB::B
        );

        app.insert_resource(NextState(Some(StatesA::A)));
        app.update();
        app.update();
        assert_eq!(
            app.world.get_resource::<State<StatesB>>().unwrap().get(),
            &StatesB::A
        );
    }
}