bevy_spritesheet 0.6.0

bevy extensions with support for selecting a sprite index depending on an associated state.
Documentation
use bevy::platform::collections::HashMap;
use std::{str::FromStr, time::Duration};

use nom::{
    IResult, Parser,
    branch::alt,
    bytes::complete::tag,
    character::complete::{char, digit1, line_ending, multispace0},
    combinator::{map_res, value},
    multi::{separated_list0, separated_list1},
    sequence::{delimited, preceded, terminated},
};

use crate::prelude::*;

pub(crate) fn parse(input: &str) -> Result<Sheet> {
    let (input_leftover, sheet) = sheet(input)?;
    if !input_leftover.is_empty() {
        Err(Error::IncompleteParseError {
            input: input_leftover.to_owned(),
        })
    } else {
        Ok(sheet)
    }
}

fn sheet(input: &str) -> IResult<&str, Sheet> {
    let (input, animation) = animation(input)?;
    let (input, default_indices) = default_indices(input)?;
    let (input, width) = width(input)?;
    let (input, mappings) = mappings(input, width)?;
    Ok((input, Sheet::new(animation, default_indices, mappings)))
}

fn animation(input: &str) -> IResult<&str, Duration> {
    // Parse digits and then the 's' character
    delimited(
        preceded(tag("animation:"), multispace0),
        alt((
            map_res(
                terminated(digit1, tag("s")),
                |s: &str| -> bevy::prelude::Result<Duration> {
                    Ok(Duration::from_secs(s.parse()?))
                },
            ),
            map_res(
                terminated(digit1, tag("ms")),
                |s: &str| -> bevy::prelude::Result<Duration> {
                    Ok(Duration::from_millis(s.parse()?))
                },
            ),
        )),
        multispace0,
    )
    .parse(input)
}
/// Parses a single `usize`, trimming surrounding whitespace
fn parse_usize(input: &str) -> IResult<&str, usize> {
    map_res(delimited(multispace0, digit1, multispace0), usize::from_str).parse(input)
}

/// Parses a list like `[1, 2]` into a `Vec<usize>`
fn parse_usize_vec(input: &str) -> IResult<&str, Vec<usize>> {
    delimited(
        delimited(multispace0, tag("["), multispace0),
        separated_list0(delimited(multispace0, tag(","), multispace0), parse_usize),
        delimited(multispace0, tag("]"), multispace0),
    )
    .parse(input)
}

/// Parses a line like `default_indices: [1, 2]`
fn default_indices(input: &str) -> IResult<&str, Vec<usize>> {
    delimited(
        preceded(tag("default_indices:"), multispace0),
        parse_usize_vec,
        multispace0,
    )
    .parse(input)
}

/// Parses a line like `width: 10`
fn width(input: &str) -> IResult<&str, usize> {
    delimited(
        preceded(tag("width:"), multispace0),
        parse_usize,
        multispace0,
    )
    .parse(input)
}

// Parse "Left", "L", "Right", etc.
fn parse_direction(input: &str) -> IResult<&str, Direction> {
    alt((
        value(Direction::Left, alt((tag("Left"), tag("L")))),
        value(Direction::Right, alt((tag("Right"), tag("R")))),
        value(Direction::Top, alt((tag("Top"), tag("T")))),
        value(Direction::Bottom, alt((tag("Bottom"), tag("B")))),
    ))
    .parse(input)
}

// Parse (Direction)
fn parse_direction_parens(input: &str) -> IResult<&str, Direction> {
    delimited(char('('), parse_direction, char(')')).parse(input)
}

// Parse one state token like Idle(Left), I(R), R(Bottom)
fn parse_state(input: &str) -> IResult<&str, SpriteState> {
    let (input, (kind, direction)) = (
        alt((
            tag("Idle"),
            tag("I"),
            tag("Run"),
            tag("R"),
            tag("Walk"),
            tag("W"),
        )),
        parse_direction_parens,
    )
        .parse(input)?;

    let state = match kind {
        "Idle" | "I" => SpriteState::Idle { direction },
        "Walk" | "W" => SpriteState::Walk { direction },
        "Run" | "R" => SpriteState::Run { direction },
        _ => unreachable!(),
    };

    Ok((input, state))
}

// Parse one line into Vec<State>
fn parse_state_line(input: &str) -> IResult<&str, Vec<SpriteState>> {
    separated_list0(multispace1_nonewline, parse_state).parse(input)
}

// Parse all lines into Vec<Vec<State>>
fn parse_all_lines(input: &str) -> IResult<&str, Vec<Vec<SpriteState>>> {
    separated_list1(line_ending, parse_state_line).parse(input)
}

fn parse_mappings_body(
    width: usize,
) -> impl FnMut(&str) -> IResult<&str, HashMap<SpriteState, Vec<usize>>> {
    move |input| {
        let (input, states) = parse_all_lines.parse(input)?;

        let mut mappings = HashMap::<SpriteState, Vec<usize>>::new();

        for (line_num, line) in states.into_iter().enumerate() {
            for (column, state) in line.into_iter().enumerate() {
                let index = column + line_num * width;
                mappings.entry(state).or_default().push(index);
            }
        }

        // from Vec<Vec> -> hashmap
        Ok((input, mappings))
    }
}

pub fn multispace1_nonewline<T, E: nom::error::ParseError<T>>(input: T) -> IResult<T, T, E>
where
    T: nom::Input,
    <T as nom::Input>::Item: nom::AsChar,
{
    input.split_at_position1_complete(
        |item| {
            use nom::AsChar;
            let c = item.as_char();
            !(c == ' ' || c == '\t')
        },
        nom::error::ErrorKind::MultiSpace,
    )
}

pub fn multispace0_nonewline<T, E: nom::error::ParseError<T>>(input: T) -> IResult<T, T, E>
where
    T: nom::Input,
    <T as nom::Input>::Item: nom::AsChar,
{
    input.split_at_position_complete(|item| {
        use nom::AsChar;
        let c = item.as_char();
        !(c == ' ' || c == '\t')
    })
}

fn mappings(input: &str, width: usize) -> IResult<&str, HashMap<SpriteState, Vec<usize>>> {
    preceded(
        delimited(tag("mappings:"), multispace0_nonewline, line_ending),
        parse_mappings_body(width),
    )
    .parse(input)
}

#[cfg(test)]
mod tests {
    use crate::prelude::{Direction, SpriteState};
    #[test]
    fn test_parse_test_sheet() {
        let sheet = super::parse(include_str!("../../assets/test.sheet")).unwrap();
        assert_eq!(
            *sheet.get(&SpriteState::Walk {
                direction: Direction::Left
            }),
            vec![30, 31]
        );
        assert_eq!(
            *sheet.get(&SpriteState::Idle {
                direction: Direction::Left
            }),
            vec![0]
        );
        assert_eq!(
            *sheet.get(&SpriteState::Run {
                direction: Direction::Right
            }),
            vec![12, 13]
        );
    }
}