bevy_spritesheet 0.6.0

bevy extensions with support for selecting a sprite index depending on an associated state.
Documentation
//! Helpers to build component containing sprite with multiple layers.

use std::{collections::HashMap, fs::File, io::Write, path::Path};

use bevy::prelude::*;
use bevy_assets_extensions::{assets::bundle::BundleEntry, static_bundle::StaticAssetsBundle};
use ccutils::containers::Apply;

use crate::error;

/// Configuration of a multi layer
#[derive(StaticAssetsBundle, Asset, TypePath)]
#[static_assets_bundle(extensions = ["configuration.ron"])]
pub struct Configuration {
    #[asset(key = "layers")]
    layers: Vec<String>,
}

impl Configuration {
    /// Create a new configuration from a vector
    pub fn new(layers: Vec<String>) -> Self {
        Self { layers }
    }
    /// Layers
    pub fn layers(&self) -> &Vec<String> {
        &self.layers
    }
    /// Set layer
    pub fn set_layer(&mut self, idx: usize, name: impl Into<String>) {
        self.layers.get_mut(idx).apply(|l| *l = name.into());
    }
    /// Save the configuration to the given path
    pub fn save_to_file(&self, path: impl AsRef<Path>) -> Result<()> {
        let mut s = HashMap::<String, BundleEntry>::new();
        s.insert(
            "layers".to_string(),
            BundleEntry::StringList(self.layers.clone()),
        );

        let bytes = ron::ser::to_string(&s)?;
        File::create(path)?.write_all(bytes.as_bytes())?;
        Ok(())
    }
    /// Load the configuration from the given path
    pub fn load_from_file(path: impl AsRef<Path>) -> Result<Configuration> {
        let mut bundle: HashMap<String, BundleEntry> =
            ron::from_str(&std::fs::read_to_string(path)?)?;
        if let Some(layers) = bundle.remove("layers") {
            match layers {
                BundleEntry::StringList(list) => Ok(Configuration { layers: list }),
                _ => Err(error::Error::InvalidConfigurationError(
                    "Extpected 'layers' to be a StringList".into(),
                ))?,
            }
        } else {
            Err(error::Error::InvalidConfigurationError(
                "Missing 'layers'".into(),
            ))?
        }
    }
}

/// Source of layers.
#[derive(Debug, StaticAssetsBundle, Asset, TypePath)]
pub struct LayersSource {
    #[asset(key = "layers")]
    layers: HashMap<String, Handle<Image>>,
    #[asset(key = "order")]
    order: Vec<String>,
    #[asset(key = "default_layers")]
    default_layers: Vec<String>,
    #[asset(key = "atlas_layout")]
    atlas_layout: Handle<TextureAtlasLayout>,
    #[asset(key = "sprite_sheet")]
    sprite_sheet: Handle<crate::Sheet>,
}

impl LayersSource {
    /// Return the order for layers
    pub fn order(&self) -> &Vec<String> {
        &self.order
    }
    /// Name of layers
    pub fn layers_names(&self) -> Vec<String> {
        self.layers.keys().map(|k| k.to_owned()).collect()
    }
    /// Name of layers for group
    pub fn layers_names_for(&self, name: impl AsRef<str>) -> Vec<String> {
        let name = name.as_ref();
        self.layers
            .keys()
            .filter(|n| n.starts_with(name))
            .map(|k| k.to_owned())
            .collect()
    }

    /// Return the sprite sheet
    pub fn sprite_sheet(&self) -> &Handle<crate::Sheet> {
        &self.sprite_sheet
    }
    /// Return the default layers
    pub fn default_layers(&self) -> &Vec<String> {
        &self.default_layers
    }
    /// Return the handle to the atlas layout
    pub fn atlas_layout(&self) -> &Handle<TextureAtlasLayout> {
        &self.atlas_layout
    }
}

/// Extension to EntityCommand
pub trait EntityCommandsExt {
    /// Spawn layers
    fn spawn_layers(
        &mut self,
        configuration: &Configuration,
        source: &LayersSource,
        offset: Option<&Vec3>,
    ) -> &mut Self;
}

impl<'a> EntityCommandsExt for EntityCommands<'a> {
    fn spawn_layers(
        &mut self,
        configuration: &Configuration,
        source: &LayersSource,
        offset: Option<&Vec3>,
    ) -> &mut Self {
        self.with_children(|c| {
            for layer in &configuration.layers {
                match source.layers.get(layer) {
                    Some(handle) => {
                        let mut se = c.spawn(Sprite {
                            texture_atlas: Some(TextureAtlas {
                                layout: source.atlas_layout.clone(),
                                index: 0,
                            }),
                            image: handle.to_owned(),
                            ..Default::default()
                        });
                        if let Some(offset) = offset {
                            se.insert(Transform::from_translation(offset.to_owned()));
                        }
                    }
                    None => bevy::log::error!("Cannot find layer '{}'", layer),
                }
            }
        })
    }
}

/// Apply the Configuration to the children of an entity
pub fn apply_configuration(
    entity: Entity,
    children_query: &Query<&Children>,
    sprite_query: &mut Query<&mut Sprite>,
    configuration: &Configuration,
    layers_source: &LayersSource,
) -> Result<()> {
    let children = children_query.get(entity)?;
    if children.len() != configuration.layers.len() {
        Err(format!(
            "Number of children ({}) must match number of layers ({}) in configuration",
            children.len(),
            configuration.layers.len()
        ))?;
    }
    for (child, layer) in children.into_iter().zip(configuration.layers()) {
        let mut sprite = sprite_query.get_mut(child.to_owned())?;
        match layers_source.layers.get(layer) {
            Some(handle) => sprite.image = handle.to_owned(),
            None => bevy::log::error!("Cannot find layer '{}'", layer),
        }
    }
    Ok(())
}