Reworked sprite content
parent
7334ebd00e
commit
ad34dc4f70
|
@ -27,8 +27,8 @@ file = "ship/gypsum.png"
|
|||
|
||||
[sprite."ship::peregrine"]
|
||||
timing.duration = 2
|
||||
repeat = "reverse"
|
||||
random_start_frame = true
|
||||
top = "reverse"
|
||||
bot = "reverse"
|
||||
frames = [
|
||||
"ship/peregrine/01.png",
|
||||
"ship/peregrine/02.png",
|
||||
|
@ -73,7 +73,6 @@ file = "ui/landscape-mask.png"
|
|||
|
||||
[sprite."particle::blaster"]
|
||||
timing.duration = 0.15
|
||||
repeat = "once"
|
||||
frames = [
|
||||
"particle/blaster/01.png",
|
||||
"particle/blaster/02.png",
|
||||
|
@ -84,7 +83,6 @@ frames = [
|
|||
|
||||
[sprite."particle::explosion::tiny"]
|
||||
timing.fps = 15
|
||||
repeat = "once"
|
||||
frames = [
|
||||
"particle/explosion-tiny/01.png",
|
||||
"particle/explosion-tiny/02.png",
|
||||
|
@ -96,7 +94,6 @@ frames = [
|
|||
|
||||
[sprite."particle::explosion::small"]
|
||||
timing.fps = 15
|
||||
repeat = "once"
|
||||
frames = [
|
||||
"particle/explosion-small/01.png",
|
||||
"particle/explosion-small/02.png",
|
||||
|
@ -109,7 +106,6 @@ frames = [
|
|||
|
||||
[sprite."particle::explosion::medium"]
|
||||
timing.fps = 15
|
||||
repeat = "once"
|
||||
frames = [
|
||||
"particle/explosion-medium/01.png",
|
||||
"particle/explosion-medium/02.png",
|
||||
|
@ -124,7 +120,6 @@ frames = [
|
|||
|
||||
[sprite."particle::explosion::large"]
|
||||
timing.fps = 15
|
||||
repeat = "once"
|
||||
frames = [
|
||||
"particle/explosion-large/01.png",
|
||||
"particle/explosion-large/02.png",
|
||||
|
@ -139,7 +134,6 @@ frames = [
|
|||
|
||||
[sprite."particle::explosion::huge"]
|
||||
timing.fps = 15
|
||||
repeat = "once"
|
||||
frames = [
|
||||
"particle/explosion-huge/01.png",
|
||||
"particle/explosion-huge/02.png",
|
||||
|
@ -156,9 +150,8 @@ frames = [
|
|||
|
||||
[sprite."particle::spark::blue"]
|
||||
timing.duration = 0.3
|
||||
#timing.rng = 0.2 # each frame will be independently sped up/slowed by this factor
|
||||
#timing.uniform_rng = 0.2 # one factor for all frames
|
||||
repeat = "reverse"
|
||||
top = "reverse"
|
||||
bot = "reverse"
|
||||
frames = [
|
||||
"particle/spark-blue/01.png",
|
||||
"particle/spark-blue/02.png",
|
||||
|
@ -170,7 +163,6 @@ frames = [
|
|||
[sprite."particle::spark::yellow"]
|
||||
timing.duration = 0.3
|
||||
timing.rng = 0.2
|
||||
repeat = "once"
|
||||
frames = [
|
||||
"particle/spark-yellow/01.png",
|
||||
"particle/spark-yellow/02.png",
|
||||
|
@ -182,7 +174,6 @@ frames = [
|
|||
[sprite."particle::spark::red"]
|
||||
timing.duration = 0.3
|
||||
timing.rng = 0.2
|
||||
repeat = "once"
|
||||
frames = [
|
||||
"particle/spark-red/01.png",
|
||||
"particle/spark-red/02.png",
|
||||
|
|
|
@ -0,0 +1,234 @@
|
|||
use crate::{AnimSectionHandle, Content, SectionEdge, SpriteHandle};
|
||||
|
||||
/// A single frame's state
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SpriteAnimationFrame {
|
||||
/// The index of the texture we're fading from
|
||||
pub texture_a: u32,
|
||||
|
||||
/// The index of the texture we're fading to
|
||||
pub texture_b: u32,
|
||||
|
||||
/// Between 0.0 and 1.0, denoting how far we are between
|
||||
/// texture_a and texture_b
|
||||
/// 0.0 means fully show texture_a;
|
||||
/// 1.0 means fully show texture_b.
|
||||
pub fade: f32,
|
||||
}
|
||||
|
||||
impl SpriteAnimationFrame {
|
||||
/// Convenience method.
|
||||
/// Get texture index as an array
|
||||
pub fn texture_index(&self) -> [u32; 2] {
|
||||
[self.texture_a, self.texture_b]
|
||||
}
|
||||
}
|
||||
|
||||
/// What direction are we playing our animation in?
|
||||
#[derive(Debug, Clone)]
|
||||
enum AnimDirection {
|
||||
/// Top to bottom, with increasing frame indices
|
||||
/// (normal)
|
||||
Up,
|
||||
|
||||
/// Bottom to top, with decreasing frame indices
|
||||
/// (reverse)
|
||||
Down,
|
||||
|
||||
/// Stopped, no animation
|
||||
Stop,
|
||||
}
|
||||
|
||||
/// Manages a single sprite's animation state.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AnimAutomaton {
|
||||
/// The sprite we're animating
|
||||
sprite: SpriteHandle,
|
||||
|
||||
/// Which animation section we're on
|
||||
/// This MUST be a section from this Automaton's sprite
|
||||
current_section: AnimSectionHandle,
|
||||
|
||||
/// Which frame we're on
|
||||
current_frame: usize,
|
||||
|
||||
/// Where we are between frames.
|
||||
/// Always between zero and one.
|
||||
current_fade: f32,
|
||||
|
||||
/// In what direction are we playing the current section?
|
||||
current_direction: AnimDirection,
|
||||
|
||||
/// The texture we're fading from
|
||||
/// (if we're moving downwards)
|
||||
last_texture: u32,
|
||||
|
||||
/// The texture we're fading to
|
||||
/// (if we're moving downwards)
|
||||
next_texture: u32,
|
||||
}
|
||||
|
||||
impl AnimAutomaton {
|
||||
/// Create a new AnimAutomaton
|
||||
pub fn new(ct: &Content, sprite_handle: SpriteHandle) -> Self {
|
||||
let sprite = ct.get_sprite(sprite_handle);
|
||||
Self {
|
||||
current_direction: AnimDirection::Down,
|
||||
sprite: sprite.handle,
|
||||
current_frame: 0,
|
||||
current_fade: 0.0,
|
||||
current_section: sprite.default_section,
|
||||
|
||||
last_texture: *sprite
|
||||
.get_section(sprite.default_section)
|
||||
.frames
|
||||
.first()
|
||||
.unwrap(),
|
||||
|
||||
next_texture: *sprite
|
||||
.get_section(sprite.default_section)
|
||||
.frames
|
||||
.first()
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset this animation
|
||||
pub fn reset(&mut self, ct: &Content) {
|
||||
let sprite = ct.get_sprite(self.sprite);
|
||||
self.current_fade = 0.0;
|
||||
self.current_frame = 0;
|
||||
self.current_section = sprite.default_section
|
||||
}
|
||||
|
||||
/// Reverse this animation's direction
|
||||
pub fn reverse(&mut self) {
|
||||
match self.current_direction {
|
||||
AnimDirection::Stop => {}
|
||||
AnimDirection::Up => {
|
||||
self.current_direction = AnimDirection::Down;
|
||||
}
|
||||
AnimDirection::Down => {
|
||||
self.current_direction = AnimDirection::Up;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Step this animation by `t` seconds
|
||||
pub fn step(&mut self, ct: &Content, t: f32) {
|
||||
let sprite = ct.get_sprite(self.sprite);
|
||||
let current_section = sprite.get_section(self.current_section);
|
||||
|
||||
// Current_fade and current_frame keep track of where we are in the current section.
|
||||
// current_frame indexes this section frames. When it exceeds the number of frames
|
||||
// or falls below zero (when moving in reverse), we switch to the next section.
|
||||
//
|
||||
// current_fade keeps track of our state between frames. It is zero once a frame starts,
|
||||
// and we switch to the next frame when it hits 1.0. If we are stepping foward, it increases,
|
||||
// and if we are stepping backwards, it decreases.
|
||||
|
||||
// If this is zero, this section isn't animated.
|
||||
if current_section.frame_duration == 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
match self.current_direction {
|
||||
AnimDirection::Down => self.current_fade += t / current_section.frame_duration,
|
||||
AnimDirection::Up => self.current_fade -= t / current_section.frame_duration,
|
||||
AnimDirection::Stop => {}
|
||||
}
|
||||
|
||||
// We're stepping foward and finished this frame
|
||||
// (implies we're travelling downwards)
|
||||
if self.current_fade > 1.0 {
|
||||
while self.current_fade > 1.0 {
|
||||
self.current_fade -= 1.0;
|
||||
}
|
||||
|
||||
if self.current_frame < current_section.frames.len() - 1 {
|
||||
self.current_frame += 1;
|
||||
} else {
|
||||
match current_section.edge_bot {
|
||||
SectionEdge::Stop => {
|
||||
self.current_fade = 0.0;
|
||||
self.current_frame = current_section.frames.len() - 1;
|
||||
self.current_direction = AnimDirection::Stop;
|
||||
}
|
||||
SectionEdge::Top { section } => {
|
||||
self.current_section = section;
|
||||
self.current_frame = 0;
|
||||
}
|
||||
SectionEdge::Bot { section } => {
|
||||
let s = sprite.get_section(section);
|
||||
self.current_section = section;
|
||||
self.current_frame = s.frames.len() - 1;
|
||||
self.reverse();
|
||||
}
|
||||
SectionEdge::Restart => {
|
||||
self.current_frame = 0;
|
||||
}
|
||||
SectionEdge::Reverse => {
|
||||
// Jump to SECOND frame, since we've already shown the
|
||||
// first during the fade transition
|
||||
self.current_frame = current_section.frames.len() - 1;
|
||||
self.reverse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let current_section = sprite.get_section(self.current_section);
|
||||
self.last_texture = self.next_texture;
|
||||
self.next_texture = current_section.frames[self.current_frame];
|
||||
}
|
||||
|
||||
// We're stepping backward and finished this frame
|
||||
// (implies we're travelling upwards)
|
||||
if self.current_fade < 0.0 {
|
||||
while self.current_fade < 0.0 {
|
||||
self.current_fade += 1.0;
|
||||
}
|
||||
|
||||
if self.current_frame > 0 {
|
||||
self.current_frame -= 1;
|
||||
} else {
|
||||
match current_section.edge_top {
|
||||
SectionEdge::Stop => {
|
||||
self.current_fade = 0.0;
|
||||
self.current_frame = 0;
|
||||
self.current_direction = AnimDirection::Stop;
|
||||
}
|
||||
SectionEdge::Top { section } => {
|
||||
self.current_section = section;
|
||||
self.current_frame = 0;
|
||||
self.reverse();
|
||||
}
|
||||
SectionEdge::Bot { section } => {
|
||||
let s = sprite.get_section(section);
|
||||
self.current_section = section;
|
||||
self.current_frame = s.frames.len() - 1;
|
||||
}
|
||||
SectionEdge::Reverse => {
|
||||
self.current_frame = 0;
|
||||
self.reverse();
|
||||
}
|
||||
SectionEdge::Restart => {
|
||||
self.current_frame = current_section.frames.len() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let current_section = sprite.get_section(self.current_section);
|
||||
self.next_texture = self.last_texture;
|
||||
self.last_texture = current_section.frames[self.current_frame];
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current frame of this animation
|
||||
pub fn get_texture_idx(&self) -> SpriteAnimationFrame {
|
||||
return SpriteAnimationFrame {
|
||||
texture_a: self.last_texture,
|
||||
texture_b: self.next_texture,
|
||||
fade: self.current_fade,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -16,14 +16,6 @@ pub struct SpriteHandle {
|
|||
pub aspect: f32,
|
||||
}
|
||||
|
||||
impl SpriteHandle {
|
||||
/// The index of this sprite in content's sprite array.
|
||||
/// Render uses this to build its buffers.
|
||||
pub fn get_index(&self) -> u32 {
|
||||
self.index as u32
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for SpriteHandle {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.index.hash(state)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
//! This subcrate is responsible for loading, parsing, validating game content,
|
||||
//! which is usually stored in `./content`.
|
||||
|
||||
mod animautomaton;
|
||||
mod handle;
|
||||
mod part;
|
||||
mod util;
|
||||
|
@ -18,6 +19,7 @@ use std::{
|
|||
use toml;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
pub use animautomaton::*;
|
||||
pub use handle::*;
|
||||
pub use part::*;
|
||||
|
||||
|
@ -279,11 +281,13 @@ impl Content {
|
|||
}
|
||||
|
||||
/// Get the handle for the starfield sprite
|
||||
pub fn get_starfield_handle(&self) -> SpriteHandle {
|
||||
match self.starfield_handle {
|
||||
pub fn get_starfield_texture(&self) -> u32 {
|
||||
let h = match self.starfield_handle {
|
||||
Some(h) => h,
|
||||
None => unreachable!("Starfield sprite hasn't been loaded yet!"),
|
||||
}
|
||||
};
|
||||
let sprite = self.get_sprite(h);
|
||||
sprite.get_section(sprite.default_section).frames[0]
|
||||
}
|
||||
|
||||
/// Get a handle from a sprite name
|
||||
|
@ -306,9 +310,9 @@ impl Content {
|
|||
return &self.sprite_atlas.atlas_list;
|
||||
}
|
||||
|
||||
/// Get a sprite from a path
|
||||
pub fn get_image(&self, p: &Path) -> &SpriteAtlasImage {
|
||||
self.sprite_atlas.index.get(p).unwrap()
|
||||
/// Get a texture by its index
|
||||
pub fn get_image(&self, idx: u32) -> &SpriteAtlasImage {
|
||||
&self.sprite_atlas.index[idx as usize]
|
||||
}
|
||||
|
||||
/// Get an outfit from a handle
|
||||
|
|
|
@ -57,7 +57,8 @@ pub(crate) mod syntax {
|
|||
TextOrFloat::Text(s) => {
|
||||
if s == "inherit" {
|
||||
let sprite = content.get_sprite(sprite);
|
||||
sprite.frame_duration * sprite.frames.len() as f32
|
||||
let sec = sprite.get_section(sprite.default_section);
|
||||
sec.frame_duration * sec.frames.len() as f32
|
||||
} else {
|
||||
bail!("bad effect lifetime, must be float or \"inherit\"",)
|
||||
}
|
||||
|
|
|
@ -18,5 +18,5 @@ pub use ship::{
|
|||
CollapseEffectSpawner, CollapseEvent, EffectCollapseEvent, EnginePoint, GunPoint, Ship,
|
||||
ShipCollapse,
|
||||
};
|
||||
pub use sprite::{RepeatMode, Sprite};
|
||||
pub use sprite::*;
|
||||
pub use system::{System, SystemObject};
|
||||
|
|
|
@ -1,44 +1,36 @@
|
|||
use anyhow::{bail, Context, Result};
|
||||
use image::io::Reader;
|
||||
use serde::Deserialize;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{handle::SpriteHandle, Content, ContentBuildContext};
|
||||
|
||||
pub(crate) mod syntax {
|
||||
use crate::{Content, ContentBuildContext};
|
||||
use anyhow::{anyhow, bail, Context, Ok, Result};
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use super::RepeatMode;
|
||||
use super::AnimSectionHandle;
|
||||
|
||||
// Raw serde syntax structs.
|
||||
// These are never seen by code outside this crate.
|
||||
|
||||
/// Convenience variants of sprite definitions
|
||||
#[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 timing: Timing,
|
||||
pub repeat: RepeatMode,
|
||||
pub random_start_frame: Option<bool>,
|
||||
OneSection(SpriteSection),
|
||||
Complete(CompleteSprite),
|
||||
}
|
||||
|
||||
/// Two ways to specify animation length
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub enum TimingVariant {
|
||||
/// The duration of this whole section
|
||||
#[serde(rename = "duration")]
|
||||
Duration(f32),
|
||||
|
||||
/// The fps of this section
|
||||
#[serde(rename = "fps")]
|
||||
Fps(f32),
|
||||
}
|
||||
|
@ -47,36 +39,176 @@ pub(crate) mod syntax {
|
|||
pub struct Timing {
|
||||
#[serde(flatten)]
|
||||
pub variant: TimingVariant,
|
||||
//pub uniform_rng: Option<f32>,
|
||||
}
|
||||
|
||||
/// An unanimated sprite
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct StaticSprite {
|
||||
pub file: PathBuf,
|
||||
}
|
||||
|
||||
/// The proper, full sprite definition
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CompleteSprite {
|
||||
pub section: HashMap<String, SpriteSection>,
|
||||
pub default_section: String,
|
||||
}
|
||||
|
||||
/// A single animation section
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SpriteSection {
|
||||
pub frames: Vec<PathBuf>,
|
||||
pub timing: Timing,
|
||||
pub top: Option<SectionEdge>,
|
||||
pub bot: Option<SectionEdge>,
|
||||
}
|
||||
|
||||
impl SpriteSection {
|
||||
pub fn add_to(
|
||||
&self,
|
||||
_build_context: &mut ContentBuildContext,
|
||||
content: &mut Content,
|
||||
|
||||
// An index of all sections in this sprite, used to resolve
|
||||
// top and bot edges.
|
||||
all_sections: &HashMap<String, AnimSectionHandle>,
|
||||
) -> Result<((u32, u32), super::SpriteSection)> {
|
||||
// Make sure all frames have the same size and add them
|
||||
// to the frame vector
|
||||
let mut dim = None;
|
||||
let mut frames = Vec::new();
|
||||
for f in &self.frames {
|
||||
let idx = match content.sprite_atlas.path_map.get(f) {
|
||||
Some(s) => *s,
|
||||
None => {
|
||||
bail!("error: file `{}` isn't in the sprite atlas", f.display());
|
||||
}
|
||||
};
|
||||
let img = &content.sprite_atlas.index[idx as usize];
|
||||
|
||||
match dim {
|
||||
None => dim = Some(img.true_size),
|
||||
Some(e) => {
|
||||
if img.true_size != e {
|
||||
bail!("failed to load section frames because frames have different sizes.",)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frames.push(img.idx);
|
||||
}
|
||||
let dim = dim.unwrap();
|
||||
|
||||
let frame_duration = match self.timing.variant {
|
||||
TimingVariant::Duration(d) => d / self.frames.len() as f32,
|
||||
TimingVariant::Fps(f) => 1.0 / f,
|
||||
};
|
||||
|
||||
if frame_duration <= 0.0 {
|
||||
bail!("frame duration must be positive (and therefore nonzero).")
|
||||
}
|
||||
|
||||
let edge_top = match &self.top {
|
||||
Some(x) => x.resolve(all_sections)?,
|
||||
None => super::SectionEdge::Stop,
|
||||
};
|
||||
|
||||
let edge_bot = match &self.bot {
|
||||
Some(x) => x.resolve(all_sections)?,
|
||||
None => super::SectionEdge::Stop,
|
||||
};
|
||||
|
||||
return Ok((
|
||||
dim,
|
||||
super::SpriteSection {
|
||||
frames,
|
||||
frame_duration,
|
||||
edge_top,
|
||||
edge_bot,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// A link between two animation sections
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct SectionEdge {
|
||||
pub val: String,
|
||||
}
|
||||
|
||||
impl SectionEdge {
|
||||
pub fn resolve(
|
||||
&self,
|
||||
all_sections: &HashMap<String, AnimSectionHandle>,
|
||||
) -> Result<super::SectionEdge> {
|
||||
if self.val == "stop" {
|
||||
return Ok(super::SectionEdge::Stop);
|
||||
}
|
||||
|
||||
if self.val == "reverse" {
|
||||
return Ok(super::SectionEdge::Reverse);
|
||||
}
|
||||
|
||||
if self.val == "restart" {
|
||||
return Ok(super::SectionEdge::Restart);
|
||||
}
|
||||
|
||||
let (s, p) = match self.val.split_once(":") {
|
||||
Some(x) => x,
|
||||
None => {
|
||||
bail!("bad section edge specification `{}`", self.val);
|
||||
}
|
||||
};
|
||||
|
||||
let section = match all_sections.get(s) {
|
||||
Some(s) => *s,
|
||||
None => {
|
||||
return Err(anyhow!("bad section edge specification `{}`", self.val))
|
||||
.with_context(|| format!("section `{}` doesn't exist", s));
|
||||
}
|
||||
};
|
||||
|
||||
match p {
|
||||
"top" => Ok(super::SectionEdge::Top { section }),
|
||||
"bot" => Ok(super::SectionEdge::Bot { section }),
|
||||
_ => {
|
||||
return Err(anyhow!("bad section edge specification `{}`", self.val))
|
||||
.with_context(|| format!("invalid target `{}`", p));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
// TODO: should be pub crate
|
||||
/// A handle for an animation section inside a sprite
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct AnimSectionHandle(usize);
|
||||
|
||||
/// After the first frame, jump to the last frame
|
||||
#[serde(rename = "repeat")]
|
||||
Repeat,
|
||||
/// An edge between two animation sections
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SectionEdge {
|
||||
/// Stop at the last frame of this section
|
||||
Stop,
|
||||
|
||||
/// Play this animation in reverse after the last frame
|
||||
#[serde(rename = "reverse")]
|
||||
/// Play the given section from the bottm
|
||||
Bot {
|
||||
/// The section to play
|
||||
section: AnimSectionHandle,
|
||||
},
|
||||
|
||||
/// Play the given section from the top
|
||||
Top {
|
||||
/// The section to play
|
||||
section: AnimSectionHandle,
|
||||
},
|
||||
|
||||
/// Replay this section in the opposite direction
|
||||
Reverse,
|
||||
}
|
||||
|
||||
impl RepeatMode {
|
||||
/// Represent this repeatmode as an integer
|
||||
/// Used to pass this enum into shaders
|
||||
pub fn as_int(&self) -> u32 {
|
||||
match self {
|
||||
Self::Repeat => 0,
|
||||
Self::Once => 1,
|
||||
Self::Reverse => 2,
|
||||
}
|
||||
}
|
||||
/// Restart this section from the opposite end
|
||||
Restart,
|
||||
}
|
||||
|
||||
/// Represents a sprite that may be used in the game.
|
||||
|
@ -88,25 +220,44 @@ pub struct Sprite {
|
|||
/// This sprite's handle
|
||||
pub handle: SpriteHandle,
|
||||
|
||||
/// The file names of frames of this sprite.
|
||||
/// unanimated sprites have one frame.
|
||||
pub frames: Vec<PathBuf>,
|
||||
/// This sprite's default section
|
||||
pub default_section: AnimSectionHandle,
|
||||
|
||||
/// The speed of this sprite's animation.
|
||||
/// This is zero for unanimate sprites.
|
||||
pub frame_duration: f32,
|
||||
|
||||
/// All frames will be sped up/slowed by this factor.
|
||||
//pub frame_uniform_rng: f32,
|
||||
|
||||
/// How to replay this sprite's animation
|
||||
pub repeat: RepeatMode,
|
||||
/// This sprite's animation sections
|
||||
sections: Vec<SpriteSection>,
|
||||
|
||||
/// Aspect ratio of this sprite (width / height)
|
||||
pub aspect: f32,
|
||||
}
|
||||
|
||||
/// If true, start on a random frame of this sprite.
|
||||
pub random_start_frame: bool,
|
||||
impl Sprite {
|
||||
/// Get an animation section from a handle
|
||||
pub fn get_section(&self, section: AnimSectionHandle) -> &SpriteSection {
|
||||
&self.sections[section.0]
|
||||
}
|
||||
|
||||
/// Iterate this sprite's sections
|
||||
pub fn iter_sections(&self) -> impl Iterator<Item = &SpriteSection> {
|
||||
self.sections.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// A part of a sprite's animation
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SpriteSection {
|
||||
/// The texture index of each frame in this animation section.
|
||||
/// unanimated sections have one frame.
|
||||
pub frames: Vec<u32>,
|
||||
|
||||
/// The speed of this sprite's animation.
|
||||
/// This must always be positive (and therefore, nonzero)
|
||||
pub frame_duration: f32,
|
||||
|
||||
/// What to do when we reach the top of this section
|
||||
pub edge_top: SectionEdge,
|
||||
|
||||
/// What to do when we reach the bottom of this section
|
||||
pub edge_bot: SectionEdge,
|
||||
}
|
||||
|
||||
impl crate::Build for Sprite {
|
||||
|
@ -114,31 +265,32 @@ impl crate::Build for Sprite {
|
|||
|
||||
fn build(
|
||||
sprites: Self::InputSyntaxType,
|
||||
_build_context: &mut ContentBuildContext,
|
||||
build_context: &mut ContentBuildContext,
|
||||
content: &mut Content,
|
||||
) -> Result<()> {
|
||||
for (sprite_name, t) in sprites {
|
||||
match t {
|
||||
syntax::Sprite::Static(t) => {
|
||||
let file = content.config.sprite_root.join(&t.file);
|
||||
let reader = Reader::open(&file).with_context(|| {
|
||||
format!(
|
||||
"Failed to read file `{}` in sprite `{}`",
|
||||
file.display(),
|
||||
sprite_name,
|
||||
let idx = match content.sprite_atlas.path_map.get(&t.file) {
|
||||
Some(s) => *s,
|
||||
None => {
|
||||
return Err(
|
||||
anyhow!("error while processing sprite `{}`", sprite_name,),
|
||||
)
|
||||
})?;
|
||||
let dim = reader.into_dimensions().with_context(|| {
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to get dimensions of file `{}` in sprite `{}`",
|
||||
file.display(),
|
||||
sprite_name,
|
||||
"file `{}` isn't in the sprite atlas, cannot proceed",
|
||||
t.file.display()
|
||||
)
|
||||
})?;
|
||||
});
|
||||
}
|
||||
};
|
||||
let img = &content.sprite_atlas.index[idx as usize];
|
||||
let aspect = img.w / img.h;
|
||||
|
||||
let h = SpriteHandle {
|
||||
index: content.sprites.len(),
|
||||
aspect: dim.0 as f32 / dim.1 as f32,
|
||||
aspect,
|
||||
};
|
||||
|
||||
if sprite_name == content.config.starfield_sprite {
|
||||
|
@ -154,71 +306,108 @@ impl crate::Build for Sprite {
|
|||
|
||||
content.sprites.push(Self {
|
||||
name: sprite_name,
|
||||
frames: vec![t.file],
|
||||
frame_duration: 0.0,
|
||||
//frame_uniform_rng: 0.0,
|
||||
default_section: AnimSectionHandle(0),
|
||||
sections: vec![SpriteSection {
|
||||
frames: vec![img.idx],
|
||||
// We implement unanimated sprites with a very fast framerate
|
||||
// and STOP endpoints.
|
||||
frame_duration: 0.01,
|
||||
edge_top: SectionEdge::Stop,
|
||||
edge_bot: SectionEdge::Stop,
|
||||
}],
|
||||
handle: h,
|
||||
repeat: RepeatMode::Once,
|
||||
aspect: dim.0 as f32 / dim.1 as f32,
|
||||
random_start_frame: false,
|
||||
aspect,
|
||||
});
|
||||
}
|
||||
syntax::Sprite::Frames(t) => {
|
||||
let mut dim = None;
|
||||
for f in &t.frames {
|
||||
let file = content.config.sprite_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();
|
||||
syntax::Sprite::OneSection(s) => {
|
||||
let mut section_names: HashMap<String, _> = HashMap::new();
|
||||
// Name the one section in this sprite "anim"
|
||||
section_names.insert("anim".to_owned(), AnimSectionHandle(0));
|
||||
|
||||
let (dim, section) = s
|
||||
.add_to(build_context, content, §ion_names)
|
||||
.with_context(|| format!("while parsing sprite `{}`", sprite_name))?;
|
||||
let aspect = dim.0 as f32 / dim.1 as f32;
|
||||
let h = SpriteHandle {
|
||||
index: content.sprites.len(),
|
||||
aspect: dim.0 as f32 / dim.1 as f32,
|
||||
aspect,
|
||||
};
|
||||
|
||||
// TODO: remove?
|
||||
if sprite_name == content.config.starfield_sprite {
|
||||
unreachable!("Starfield texture may not be animated")
|
||||
}
|
||||
|
||||
let frame_duration = match t.timing.variant {
|
||||
syntax::TimingVariant::Duration(d) => d / t.frames.len() as f32,
|
||||
syntax::TimingVariant::Fps(f) => 1.0 / f,
|
||||
let mut sections = Vec::new();
|
||||
sections.push(section);
|
||||
|
||||
content.sprite_index.insert(sprite_name.clone(), h);
|
||||
content.sprites.push(Self {
|
||||
name: sprite_name,
|
||||
sections,
|
||||
default_section: AnimSectionHandle(0),
|
||||
handle: h,
|
||||
aspect,
|
||||
});
|
||||
}
|
||||
syntax::Sprite::Complete(s) => {
|
||||
let mut idx = 0;
|
||||
let mut section_names = HashMap::new();
|
||||
for (name, _) in &s.section {
|
||||
section_names.insert(name.to_owned(), AnimSectionHandle(idx));
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
if !section_names.contains_key(&s.default_section) {
|
||||
bail!(
|
||||
"could not load sprite `{}`, default section `{}` doesn't exist",
|
||||
sprite_name,
|
||||
s.default_section
|
||||
);
|
||||
}
|
||||
|
||||
let mut sections = Vec::with_capacity(idx);
|
||||
let mut dim = None;
|
||||
|
||||
// Make sure we add sections in order
|
||||
let mut names = section_names.iter().collect::<Vec<_>>();
|
||||
names.sort_by(|a, b| (a.1).0.cmp(&(b.1).0));
|
||||
|
||||
for (k, _) in names {
|
||||
let v = s.section.get(k).unwrap();
|
||||
let (d, s) = v
|
||||
.add_to(build_context, content, §ion_names)
|
||||
.with_context(|| format!("while parsing sprite `{}`", sprite_name))
|
||||
.with_context(|| format!("while parsing section `{}`", k))?;
|
||||
|
||||
// Make sure all dimensions are the same
|
||||
if dim.is_none() {
|
||||
dim = Some(d);
|
||||
} else if dim.unwrap() != d {
|
||||
bail!(
|
||||
"could not load sprite `{}`, image sizes in section `{}` are different",
|
||||
sprite_name,
|
||||
k
|
||||
);
|
||||
}
|
||||
|
||||
sections.push(s);
|
||||
}
|
||||
let dim = dim.unwrap();
|
||||
let aspect = dim.0 as f32 / dim.1 as f32;
|
||||
|
||||
let h = SpriteHandle {
|
||||
index: content.sprites.len(),
|
||||
aspect,
|
||||
};
|
||||
|
||||
content.sprite_index.insert(sprite_name.clone(), h);
|
||||
content.sprites.push(Self {
|
||||
name: sprite_name,
|
||||
frames: t.frames,
|
||||
frame_duration,
|
||||
//frame_uniform_rng: t.timing.uniform_rng.unwrap_or(0.0),
|
||||
sections,
|
||||
default_section: *section_names.get(&s.default_section).unwrap(),
|
||||
handle: h,
|
||||
repeat: t.repeat,
|
||||
aspect: dim.0 as f32 / dim.1 as f32,
|
||||
random_start_frame: t.random_start_frame.unwrap_or(false),
|
||||
aspect,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue