198 lines
4.5 KiB
Rust
198 lines
4.5 KiB
Rust
|
use anyhow::{bail, Context, Result};
|
||
|
use image::io::Reader;
|
||
|
use serde::Deserialize;
|
||
|
use std::{collections::HashMap, path::PathBuf};
|
||
|
|
||
|
use crate::{handle::SpriteHandle, Content};
|
||
|
|
||
|
pub(crate) mod syntax {
|
||
|
use serde::Deserialize;
|
||
|
use std::path::PathBuf;
|
||
|
|
||
|
use super::RepeatMode;
|
||
|
|
||
|
// Raw serde syntax structs.
|
||
|
// These are never seen by code outside this crate.
|
||
|
|
||
|
#[derive(Debug, Deserialize)]
|
||
|
#[serde(untagged)]
|
||
|
pub enum Sprite {
|
||
|
Static(StaticSprite),
|
||
|
Frames(FrameSprite),
|
||
|
}
|
||
|
|
||
|
#[derive(Debug, Deserialize)]
|
||
|
pub struct StaticSprite {
|
||
|
pub file: PathBuf,
|
||
|
}
|
||
|
|
||
|
#[derive(Debug, Deserialize)]
|
||
|
pub struct FrameSprite {
|
||
|
pub frames: Vec<PathBuf>,
|
||
|
pub duration: f32,
|
||
|
pub repeat: RepeatMode,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// How to replay a texture's animation
|
||
|
#[derive(Debug, Deserialize, Clone, Copy)]
|
||
|
pub enum RepeatMode {
|
||
|
/// Play this animation once, and stop at the last frame
|
||
|
#[serde(rename = "once")]
|
||
|
Once,
|
||
|
|
||
|
/// After the first frame, jump to the last frame
|
||
|
#[serde(rename = "repeat")]
|
||
|
Repeat,
|
||
|
}
|
||
|
|
||
|
impl RepeatMode {
|
||
|
/// Represent this repeatmode as an integer
|
||
|
/// Used to pass this enum into shaders
|
||
|
pub fn as_int(&self) -> u32 {
|
||
|
match self {
|
||
|
Self::Once => 0,
|
||
|
Self::Repeat => 1,
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Represents a sprite that may be used in the game.
|
||
|
#[derive(Debug, Clone)]
|
||
|
pub struct Sprite {
|
||
|
/// The name of this sprite
|
||
|
pub name: String,
|
||
|
|
||
|
/// This sprite's handle
|
||
|
pub handle: SpriteHandle,
|
||
|
|
||
|
/// The file names of frames of this sprite.
|
||
|
/// unanimated sprites have one frame.
|
||
|
pub frames: Vec<PathBuf>,
|
||
|
|
||
|
/// The speed of this sprite's animation.
|
||
|
/// unanimated sprites have zero fps.
|
||
|
pub fps: f32,
|
||
|
|
||
|
/// How to replay this sprite's animation
|
||
|
pub repeat: RepeatMode,
|
||
|
|
||
|
/// Aspect ratio of this sprite (width / height)
|
||
|
pub aspect: f32,
|
||
|
}
|
||
|
|
||
|
impl crate::Build for Sprite {
|
||
|
type InputSyntaxType = HashMap<String, syntax::Sprite>;
|
||
|
|
||
|
fn build(sprites: Self::InputSyntaxType, ct: &mut Content) -> Result<()> {
|
||
|
for (sprite_name, t) in sprites {
|
||
|
match t {
|
||
|
syntax::Sprite::Static(t) => {
|
||
|
let file = ct.image_root.join(&t.file);
|
||
|
let reader = Reader::open(&file).with_context(|| {
|
||
|
format!(
|
||
|
"Failed to read file `{}` in sprite `{}`",
|
||
|
file.display(),
|
||
|
sprite_name,
|
||
|
)
|
||
|
})?;
|
||
|
let dim = reader.into_dimensions().with_context(|| {
|
||
|
format!(
|
||
|
"Failed to get dimensions of file `{}` in sprite `{}`",
|
||
|
file.display(),
|
||
|
sprite_name,
|
||
|
)
|
||
|
})?;
|
||
|
|
||
|
let h = SpriteHandle {
|
||
|
index: ct.sprites.len(),
|
||
|
aspect: dim.0 as f32 / dim.1 as f32,
|
||
|
};
|
||
|
|
||
|
if sprite_name == ct.starfield_sprite_name {
|
||
|
if ct.starfield_handle.is_none() {
|
||
|
ct.starfield_handle = Some(h)
|
||
|
} else {
|
||
|
// This can't happen, since this is a hashmap.
|
||
|
unreachable!("Found two starfield sprites! Something is very wrong.")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
ct.sprite_index.insert(sprite_name.clone(), h);
|
||
|
|
||
|
ct.sprites.push(Self {
|
||
|
name: sprite_name,
|
||
|
frames: vec![t.file],
|
||
|
fps: 0.0,
|
||
|
handle: h,
|
||
|
repeat: RepeatMode::Once,
|
||
|
aspect: dim.0 as f32 / dim.1 as f32,
|
||
|
});
|
||
|
}
|
||
|
syntax::Sprite::Frames(t) => {
|
||
|
let mut dim = None;
|
||
|
for f in &t.frames {
|
||
|
let file = ct.image_root.join(f);
|
||
|
let reader = Reader::open(&file).with_context(|| {
|
||
|
format!(
|
||
|
"Failed to read file `{}` in sprite `{}`",
|
||
|
file.display(),
|
||
|
sprite_name,
|
||
|
)
|
||
|
})?;
|
||
|
let d = reader.into_dimensions().with_context(|| {
|
||
|
format!(
|
||
|
"Failed to get dimensions of file `{}` in sprite `{}`",
|
||
|
file.display(),
|
||
|
sprite_name,
|
||
|
)
|
||
|
})?;
|
||
|
match dim {
|
||
|
None => dim = Some(d),
|
||
|
Some(e) => {
|
||
|
if d != e {
|
||
|
bail!(
|
||
|
"Failed to load frames of sprite `{}` because frames have different sizes.",
|
||
|
sprite_name,
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
let dim = dim.unwrap();
|
||
|
|
||
|
let h = SpriteHandle {
|
||
|
index: ct.sprites.len(),
|
||
|
aspect: dim.0 as f32 / dim.1 as f32,
|
||
|
};
|
||
|
|
||
|
if sprite_name == ct.starfield_sprite_name {
|
||
|
unreachable!("Starfield texture may not be animated")
|
||
|
}
|
||
|
|
||
|
let fps = t.duration / t.frames.len() as f32;
|
||
|
|
||
|
ct.sprite_index.insert(sprite_name.clone(), h);
|
||
|
ct.sprites.push(Self {
|
||
|
name: sprite_name,
|
||
|
frames: t.frames,
|
||
|
fps,
|
||
|
handle: h,
|
||
|
repeat: t.repeat,
|
||
|
aspect: dim.0 as f32 / dim.1 as f32,
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ct.starfield_handle.is_none() {
|
||
|
bail!(
|
||
|
"Could not find a starfield texture (name: `{}`)",
|
||
|
ct.starfield_sprite_name
|
||
|
)
|
||
|
}
|
||
|
|
||
|
return Ok(());
|
||
|
}
|
||
|
}
|