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> {
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)
}
fn parse_usize(input: &str) -> IResult<&str, usize> {
map_res(delimited(multispace0, digit1, multispace0), usize::from_str).parse(input)
}
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)
}
fn default_indices(input: &str) -> IResult<&str, Vec<usize>> {
delimited(
preceded(tag("default_indices:"), multispace0),
parse_usize_vec,
multispace0,
)
.parse(input)
}
fn width(input: &str) -> IResult<&str, usize> {
delimited(
preceded(tag("width:"), multispace0),
parse_usize,
multispace0,
)
.parse(input)
}
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)
}
fn parse_direction_parens(input: &str) -> IResult<&str, Direction> {
delimited(char('('), parse_direction, char(')')).parse(input)
}
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))
}
fn parse_state_line(input: &str) -> IResult<&str, Vec<SpriteState>> {
separated_list0(multispace1_nonewline, parse_state).parse(input)
}
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);
}
}
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]
);
}
}