bevy-yoleck 0.4.0

Your Own Level Editor Creation Kit
Documentation
use std::any::TypeId;
use std::hash::Hash;
use std::marker::PhantomData;

use bevy::ecs::system::{EntityCommands, SystemParam};
use bevy::prelude::*;
use bevy::utils::HashMap;
use bevy_egui::egui;

use crate::knobs::{KnobFromCache, YoleckKnobsCache};
use crate::{BoxedArc, YoleckManaged};

/// Whether or not the Yoleck editor is active.
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum YoleckEditorState {
    /// Editor mode. The editor is active and can be used to edit entities.
    EditorActive,
    /// Game mode. Either the actual game or playtest from the editor mode.
    GameActive,
}

/// Sync the game's state back and forth when the level editor enters and exits playtest mode.
///
/// Add this as a plugin. When using it, there is no need to initialize the state with `add_state`
/// - `YoleckSyncWithEditorState` will initialize it to the value in `when_editor`.
///
/// ```no_run
/// # use bevy::prelude::*;
/// # use bevy_yoleck::{YoleckSyncWithEditorState, YoleckPluginForEditor, YoleckPluginForGame};
/// # use bevy_yoleck::bevy_egui::EguiPlugin;
/// #[derive(Debug, Clone, PartialEq, Eq, Hash)]
/// enum GameState {
///     Loading,
///     Game,
///     Editor,
/// }
///
/// # let mut app = App::new();
/// # let executable_started_in_editor_mode = true;
/// if executable_started_in_editor_mode {
///     // These two plugins are needed for editor mode:
///     app.add_plugin(EguiPlugin);
///     app.add_plugin(YoleckPluginForEditor);
///
///     app.add_plugin(YoleckSyncWithEditorState {
///         when_editor: GameState::Editor,
///         when_game: GameState::Game,
///     });
/// } else {
///     // This plugin is needed for game mode:
///     app.add_plugin(YoleckPluginForGame);
///
///     app.add_state(GameState::Loading);
/// }
pub struct YoleckSyncWithEditorState<T>
where
    T: 'static + Sync + Send + std::fmt::Debug + Clone + std::cmp::Eq + std::hash::Hash,
{
    pub when_editor: T,
    pub when_game: T,
}

impl<T> Plugin for YoleckSyncWithEditorState<T>
where
    T: 'static + Sync + Send + std::fmt::Debug + Clone + std::cmp::Eq + std::hash::Hash,
{
    fn build(&self, app: &mut App) {
        let when_editor = self.when_editor.clone();
        app.add_state(when_editor.clone());
        let when_game = self.when_game.clone();
        app.add_system(
            move |editor_state: Res<State<YoleckEditorState>>, mut game_state: ResMut<State<T>>| {
                let _ = game_state.set(match editor_state.current() {
                    YoleckEditorState::EditorActive => when_editor.clone(),
                    YoleckEditorState::GameActive => when_game.clone(),
                });
            },
        );
    }
}

#[derive(Clone, Copy)]
pub(crate) enum PopulateReason {
    EditorInit,
    EditorUpdate,
    RealGame,
}

/// A context for [`YoleckPopulate::populate`].
pub struct YoleckPopulateContext<'a> {
    pub(crate) reason: PopulateReason,
    // I may add stuff that need 'a later, and I don't want to change the signature
    pub(crate) _phantom_data: PhantomData<&'a ()>,
}

impl<'a> YoleckPopulateContext<'a> {
    /// `true` if the entity is created in editor mode, `false` if created in playtest or actual game.
    pub fn is_in_editor(&self) -> bool {
        match self.reason {
            PopulateReason::EditorInit => true,
            PopulateReason::EditorUpdate => true,
            PopulateReason::RealGame => false,
        }
    }

    /// `true` if this is this is the first time the entity is populated, `false` if the entity was
    /// popultated before.
    pub fn is_first_time(&self) -> bool {
        match self.reason {
            PopulateReason::EditorInit => true,
            PopulateReason::EditorUpdate => false,
            PopulateReason::RealGame => true,
        }
    }
}

/// A context for [`YoleckEdit::edit`].
pub struct YoleckEditContext<'a> {
    entity: Entity,
    pub(crate) passed: &'a mut HashMap<Entity, HashMap<TypeId, BoxedArc>>,
    knobs_cache: &'a mut YoleckKnobsCache,
}

impl<'a> YoleckEditContext<'a> {
    /// Get data sent to the entity from external systems (usually from (usually a [ViewPort Editing OverLay](crate::vpeol))
    ///
    /// The data is sent using [a directive event](crate::YoleckDirective::pass_to_entity).
    ///
    /// ```no_run
    /// # use bevy::prelude::*;
    /// # use bevy_yoleck::{YoleckEdit, egui};
    /// # struct Example {
    /// #     position: Vec2,
    /// # }
    /// fn edit_example(mut edit: YoleckEdit<Example>) {
    ///     edit.edit(|ctx, data, _ui| {
    ///         if let Some(pos) = ctx.get_passed_data::<Vec2>() {
    ///             data.position = *pos;
    ///         }
    ///     });
    /// }
    /// ```
    pub fn get_passed_data<T: 'static>(&self) -> Option<&T> {
        if let Some(dynamic) = self
            .passed
            .get(&self.entity)
            .and_then(|m| m.get(&TypeId::of::<T>()))
        {
            dynamic.downcast_ref()
        } else {
            None
        }
    }

    /// Create a knob - an helper entity the level editor can use to edit the entity.
    ///
    /// ```no_run
    /// # use bevy::prelude::*;
    /// # use bevy_yoleck::{YoleckEdit, egui};
    /// # struct KidWithBalloon {
    /// #     position: Vec2,
    /// #     baloon_offset: Vec2,
    /// # }
    /// # #[derive(Resource)]
    /// # struct MyAssets {
    /// #     baloon_sprite: Handle<Image>,
    /// # }
    /// fn edit_kid_with_balloon(mut edit: YoleckEdit<KidWithBalloon>, mut commands: Commands, assets: Res<MyAssets>) {
    ///     edit.edit(|ctx, data, _ui| {
    ///         let mut balloon_knob = ctx.knob(&mut commands, "balloon");
    ///         let knob_position = data.position + data.baloon_offset;
    ///         balloon_knob.cmd.insert(SpriteBundle {
    ///             transform: Transform::from_translation(knob_position.extend(1.0)),
    ///             texture: assets.baloon_sprite.clone(),
    ///             ..Default::default()
    ///         });
    ///         if let Some(new_baloon_pos) = balloon_knob.get_passed_data::<Vec2>() {
    ///             data.baloon_offset = *new_baloon_pos - data.position;
    ///         }
    ///     });
    /// }
    /// ```
    pub fn knob<'b, 'w, 's, K>(
        &mut self,
        commands: &'b mut Commands<'w, 's>,
        key: K,
    ) -> YoleckKnobHandle<'w, 's, 'b>
    where
        K: 'static + Send + Sync + Hash + Eq,
    {
        let KnobFromCache { cmd, is_new } = self.knobs_cache.access(key, commands);
        let passed = self.passed.remove(&cmd.id()).unwrap_or_default();
        YoleckKnobHandle {
            cmd,
            is_new,
            passed,
        }
    }
}

#[doc(hidden)]
#[derive(Resource)]
pub struct YoleckUiForEditSystem(pub egui::Ui);

/// Parameter for systems that edit entities. See [`YoleckEdit::edit`].
#[derive(SystemParam)]
pub struct YoleckEdit<'w, 's, T: 'static> {
    #[allow(dead_code)]
    query: Query<'w, 's, &'static mut YoleckManaged>,
    #[allow(dead_code)]
    context: ResMut<'w, YoleckUserSystemContext>,
    ui: ResMut<'w, YoleckUiForEditSystem>,
    knobs_cache: ResMut<'w, YoleckKnobsCache>,
    #[system_param(ignore)]
    _phantom_data: PhantomData<fn() -> T>,
}

impl<'w, 's, T: 'static> YoleckEdit<'w, 's, T> {
    /// Implement entity editing.
    ///
    /// A system that uses [`YoleckEdit`] needs to be added to an handler using
    /// [`edit_with`](crate::YoleckTypeHandler::edit_with). These systems usually only need to
    /// call this method with a closure that accepts three arguments:
    ///
    /// * A context
    /// * The data to be edited.
    /// * An egui UI handler.
    ///
    /// The closure is then responsible for allowing the user to edit the data using the UI and
    /// using [data passed from other systems](YoleckEditContext::get_passed_data).
    ///
    /// ```no_run
    /// # use bevy::prelude::*;
    /// # use bevy_yoleck::{YoleckEdit, egui, YoleckTypeHandler, YoleckExtForApp};
    /// # use serde::{Deserialize, Serialize};
    /// # #[derive(Clone, PartialEq, Serialize, Deserialize)]
    /// # struct Example {
    /// #     number: u32,
    /// # }
    /// # let mut app = App::new();
    /// app.add_yoleck_handler({
    ///     YoleckTypeHandler::<Example>::new("Example")
    ///         .edit_with(edit_example)
    /// });
    ///
    /// fn edit_example(mut edit: YoleckEdit<Example>) {
    ///     edit.edit(|_ctx, data, ui| {
    ///         ui.add(egui::Slider::new(&mut data.number, 0..=10));
    ///     });
    /// }
    /// ```
    pub fn edit(&mut self, mut dlg: impl FnMut(&mut YoleckEditContext, &mut T, &mut egui::Ui)) {
        match &mut *self.context {
            YoleckUserSystemContext::Nope
            | YoleckUserSystemContext::PopulateEdited(_)
            | YoleckUserSystemContext::PopulateInitiated { .. } => {
                panic!("Wrong state");
            }
            YoleckUserSystemContext::Edit { entity, passed } => {
                let mut edit_context = YoleckEditContext {
                    entity: *entity,
                    passed,
                    knobs_cache: &mut self.knobs_cache,
                };
                let mut yoleck_managed = self
                    .query
                    .get_mut(*entity)
                    .expect("Edited entity does not exist");
                let data = yoleck_managed
                    .data
                    .downcast_mut::<T>()
                    .expect("Edited data is of wrong type");
                dlg(&mut edit_context, data, &mut self.ui.0);
            }
        }
    }
}

/// An handle for intearcing with a knob from an [edit system](YoleckEdit::edit).
pub struct YoleckKnobHandle<'w, 's, 'a> {
    /// The command of the knob entity.
    pub cmd: EntityCommands<'w, 's, 'a>,
    /// `true` if the knob entity is just created this frame.
    pub is_new: bool,
    passed: HashMap<TypeId, BoxedArc>,
}

impl YoleckKnobHandle<'_, '_, '_> {
    /// Get data sent to the knob from external systems (usually interaciton from the level
    /// editor). See [`YoleckEditContext::get_passed_data`].
    pub fn get_passed_data<T: 'static>(&self) -> Option<&T> {
        if let Some(dynamic) = self.passed.get(&TypeId::of::<T>()) {
            dynamic.downcast_ref()
        } else {
            None
        }
    }
}

/// Parameter for systems that populate entities. See [`YoleckPopulate::populate`].
#[derive(SystemParam)]
pub struct YoleckPopulate<'w, 's, T: 'static> {
    query: Query<'w, 's, &'static mut YoleckManaged>,
    context: Res<'w, YoleckUserSystemContext>,
    commands: Commands<'w, 's>,
    #[system_param(ignore)]
    _phantom_data: PhantomData<fn() -> T>,
}

impl<'w, 's, T: 'static> YoleckPopulate<'w, 's, T> {
    /// Implement entity populating.
    ///
    /// A system that uses [`YoleckPopulate`] needs to be added to an handler using
    /// [`populate_with`](crate::YoleckTypeHandler::populate_with). These systems usually only
    /// need to call this method with a closure that accepts three arguments:
    ///
    /// * A context
    /// * The data to be used for populating.
    /// * A Bevy command.
    ///
    /// The closure is then responsible for adding components to the command based on data from the
    /// entity. The closure may also add children - but since this method may be called to
    /// re-populate an already populated entity that already has children, if it does so it should
    /// use `despawn_descendants` to remove existing children.
    ///
    /// ```no_run
    /// # use bevy::prelude::*;
    /// # use bevy_yoleck::{YoleckPopulate, YoleckTypeHandler, YoleckExtForApp};
    /// # use serde::{Deserialize, Serialize};
    /// # #[derive(Clone, PartialEq, Serialize, Deserialize)]
    /// # struct Example {
    /// #     position: Vec2,
    /// # }
    /// # #[derive(Resource)]
    /// # struct GameAssets {
    /// #     example_sprite: Handle<Image>,
    /// # }
    /// # let mut app = App::new();
    /// app.add_yoleck_handler({
    ///     YoleckTypeHandler::<Example>::new("Example")
    ///         .populate_with(populate_example)
    /// });
    ///
    /// fn populate_example(mut populate: YoleckPopulate<Example>, assets: Res<GameAssets>) {
    ///     populate.populate(|_ctx, data, mut cmd| {
    ///         cmd.insert(SpriteBundle {
    ///             sprite: Sprite {
    ///                 custom_size: Some(Vec2::new(100.0, 100.0)),
    ///                 ..Default::default()
    ///             },
    ///             transform: Transform::from_translation(data.position.extend(0.0)),
    ///             texture: assets.example_sprite.clone(),
    ///             ..Default::default()
    ///         });
    ///     });
    /// }
    /// ```
    pub fn populate(
        &mut self,
        mut dlg: impl FnMut(&YoleckPopulateContext, &mut T, EntityCommands),
    ) {
        match &*self.context {
            YoleckUserSystemContext::Nope | YoleckUserSystemContext::Edit { .. } => {
                panic!("Wrong state");
            }
            YoleckUserSystemContext::PopulateEdited(entity) => {
                let populate_context = YoleckPopulateContext {
                    reason: PopulateReason::EditorUpdate,
                    _phantom_data: Default::default(),
                };
                let mut yoleck_managed = self
                    .query
                    .get_mut(*entity)
                    .expect("Edited entity does not exist");
                let data = yoleck_managed
                    .data
                    .downcast_mut::<T>()
                    .expect("Edited data is of wrong type");
                dlg(&populate_context, data, self.commands.entity(*entity));
            }
            YoleckUserSystemContext::PopulateInitiated {
                is_in_editor,
                entities,
            } => {
                let populate_context = YoleckPopulateContext {
                    reason: if *is_in_editor {
                        PopulateReason::EditorInit
                    } else {
                        PopulateReason::RealGame
                    },
                    _phantom_data: Default::default(),
                };
                for entity in entities {
                    let mut yoleck_managed = self
                        .query
                        .get_mut(*entity)
                        .expect("Edited entity does not exist");
                    let data = yoleck_managed
                        .data
                        .downcast_mut::<T>()
                        .expect("Edited data is of wrong type");
                    dlg(&populate_context, data, self.commands.entity(*entity));
                }
            }
        }
    }
}

#[derive(Resource)]
pub enum YoleckUserSystemContext {
    Nope,
    Edit {
        entity: Entity,
        passed: HashMap<Entity, HashMap<TypeId, BoxedArc>>,
    },
    PopulateEdited(Entity),
    PopulateInitiated {
        is_in_editor: bool,
        entities: Vec<Entity>,
    },
}

impl YoleckUserSystemContext {
    pub(crate) fn get_edit_entity(&self) -> Entity {
        if let Self::Edit { entity, .. } = self {
            *entity
        } else {
            panic!("Wrong state");
        }
    }
}

/// Events emitted by the Yoleck editor.
///
/// Modules that provide editing overlays over the viewport (like [vpeol](crate::vpeol)) can
/// use these events to update their status to match with the editor.
#[derive(Debug)]
pub enum YoleckEditorEvent {
    EntitySelected(Entity),
    EntityDeselected(Entity),
    EditedEntityPopulated(Entity),
}