Reworked sprite content
parent
7334ebd00e
commit
ad34dc4f70
|
@ -27,8 +27,8 @@ file = "ship/gypsum.png"
|
||||||
|
|
||||||
[sprite."ship::peregrine"]
|
[sprite."ship::peregrine"]
|
||||||
timing.duration = 2
|
timing.duration = 2
|
||||||
repeat = "reverse"
|
top = "reverse"
|
||||||
random_start_frame = true
|
bot = "reverse"
|
||||||
frames = [
|
frames = [
|
||||||
"ship/peregrine/01.png",
|
"ship/peregrine/01.png",
|
||||||
"ship/peregrine/02.png",
|
"ship/peregrine/02.png",
|
||||||
|
@ -73,7 +73,6 @@ file = "ui/landscape-mask.png"
|
||||||
|
|
||||||
[sprite."particle::blaster"]
|
[sprite."particle::blaster"]
|
||||||
timing.duration = 0.15
|
timing.duration = 0.15
|
||||||
repeat = "once"
|
|
||||||
frames = [
|
frames = [
|
||||||
"particle/blaster/01.png",
|
"particle/blaster/01.png",
|
||||||
"particle/blaster/02.png",
|
"particle/blaster/02.png",
|
||||||
|
@ -84,7 +83,6 @@ frames = [
|
||||||
|
|
||||||
[sprite."particle::explosion::tiny"]
|
[sprite."particle::explosion::tiny"]
|
||||||
timing.fps = 15
|
timing.fps = 15
|
||||||
repeat = "once"
|
|
||||||
frames = [
|
frames = [
|
||||||
"particle/explosion-tiny/01.png",
|
"particle/explosion-tiny/01.png",
|
||||||
"particle/explosion-tiny/02.png",
|
"particle/explosion-tiny/02.png",
|
||||||
|
@ -96,7 +94,6 @@ frames = [
|
||||||
|
|
||||||
[sprite."particle::explosion::small"]
|
[sprite."particle::explosion::small"]
|
||||||
timing.fps = 15
|
timing.fps = 15
|
||||||
repeat = "once"
|
|
||||||
frames = [
|
frames = [
|
||||||
"particle/explosion-small/01.png",
|
"particle/explosion-small/01.png",
|
||||||
"particle/explosion-small/02.png",
|
"particle/explosion-small/02.png",
|
||||||
|
@ -109,7 +106,6 @@ frames = [
|
||||||
|
|
||||||
[sprite."particle::explosion::medium"]
|
[sprite."particle::explosion::medium"]
|
||||||
timing.fps = 15
|
timing.fps = 15
|
||||||
repeat = "once"
|
|
||||||
frames = [
|
frames = [
|
||||||
"particle/explosion-medium/01.png",
|
"particle/explosion-medium/01.png",
|
||||||
"particle/explosion-medium/02.png",
|
"particle/explosion-medium/02.png",
|
||||||
|
@ -124,7 +120,6 @@ frames = [
|
||||||
|
|
||||||
[sprite."particle::explosion::large"]
|
[sprite."particle::explosion::large"]
|
||||||
timing.fps = 15
|
timing.fps = 15
|
||||||
repeat = "once"
|
|
||||||
frames = [
|
frames = [
|
||||||
"particle/explosion-large/01.png",
|
"particle/explosion-large/01.png",
|
||||||
"particle/explosion-large/02.png",
|
"particle/explosion-large/02.png",
|
||||||
|
@ -139,7 +134,6 @@ frames = [
|
||||||
|
|
||||||
[sprite."particle::explosion::huge"]
|
[sprite."particle::explosion::huge"]
|
||||||
timing.fps = 15
|
timing.fps = 15
|
||||||
repeat = "once"
|
|
||||||
frames = [
|
frames = [
|
||||||
"particle/explosion-huge/01.png",
|
"particle/explosion-huge/01.png",
|
||||||
"particle/explosion-huge/02.png",
|
"particle/explosion-huge/02.png",
|
||||||
|
@ -156,9 +150,8 @@ frames = [
|
||||||
|
|
||||||
[sprite."particle::spark::blue"]
|
[sprite."particle::spark::blue"]
|
||||||
timing.duration = 0.3
|
timing.duration = 0.3
|
||||||
#timing.rng = 0.2 # each frame will be independently sped up/slowed by this factor
|
top = "reverse"
|
||||||
#timing.uniform_rng = 0.2 # one factor for all frames
|
bot = "reverse"
|
||||||
repeat = "reverse"
|
|
||||||
frames = [
|
frames = [
|
||||||
"particle/spark-blue/01.png",
|
"particle/spark-blue/01.png",
|
||||||
"particle/spark-blue/02.png",
|
"particle/spark-blue/02.png",
|
||||||
|
@ -170,7 +163,6 @@ frames = [
|
||||||
[sprite."particle::spark::yellow"]
|
[sprite."particle::spark::yellow"]
|
||||||
timing.duration = 0.3
|
timing.duration = 0.3
|
||||||
timing.rng = 0.2
|
timing.rng = 0.2
|
||||||
repeat = "once"
|
|
||||||
frames = [
|
frames = [
|
||||||
"particle/spark-yellow/01.png",
|
"particle/spark-yellow/01.png",
|
||||||
"particle/spark-yellow/02.png",
|
"particle/spark-yellow/02.png",
|
||||||
|
@ -182,7 +174,6 @@ frames = [
|
||||||
[sprite."particle::spark::red"]
|
[sprite."particle::spark::red"]
|
||||||
timing.duration = 0.3
|
timing.duration = 0.3
|
||||||
timing.rng = 0.2
|
timing.rng = 0.2
|
||||||
repeat = "once"
|
|
||||||
frames = [
|
frames = [
|
||||||
"particle/spark-red/01.png",
|
"particle/spark-red/01.png",
|
||||||
"particle/spark-red/02.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,
|
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 {
|
impl Hash for SpriteHandle {
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
self.index.hash(state)
|
self.index.hash(state)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
//! This subcrate is responsible for loading, parsing, validating game content,
|
//! This subcrate is responsible for loading, parsing, validating game content,
|
||||||
//! which is usually stored in `./content`.
|
//! which is usually stored in `./content`.
|
||||||
|
|
||||||
|
mod animautomaton;
|
||||||
mod handle;
|
mod handle;
|
||||||
mod part;
|
mod part;
|
||||||
mod util;
|
mod util;
|
||||||
|
@ -18,6 +19,7 @@ use std::{
|
||||||
use toml;
|
use toml;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
pub use animautomaton::*;
|
||||||
pub use handle::*;
|
pub use handle::*;
|
||||||
pub use part::*;
|
pub use part::*;
|
||||||
|
|
||||||
|
@ -279,11 +281,13 @@ impl Content {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the handle for the starfield sprite
|
/// Get the handle for the starfield sprite
|
||||||
pub fn get_starfield_handle(&self) -> SpriteHandle {
|
pub fn get_starfield_texture(&self) -> u32 {
|
||||||
match self.starfield_handle {
|
let h = match self.starfield_handle {
|
||||||
Some(h) => h,
|
Some(h) => h,
|
||||||
None => unreachable!("Starfield sprite hasn't been loaded yet!"),
|
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
|
/// Get a handle from a sprite name
|
||||||
|
@ -306,9 +310,9 @@ impl Content {
|
||||||
return &self.sprite_atlas.atlas_list;
|
return &self.sprite_atlas.atlas_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a sprite from a path
|
/// Get a texture by its index
|
||||||
pub fn get_image(&self, p: &Path) -> &SpriteAtlasImage {
|
pub fn get_image(&self, idx: u32) -> &SpriteAtlasImage {
|
||||||
self.sprite_atlas.index.get(p).unwrap()
|
&self.sprite_atlas.index[idx as usize]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get an outfit from a handle
|
/// Get an outfit from a handle
|
||||||
|
|
|
@ -57,7 +57,8 @@ pub(crate) mod syntax {
|
||||||
TextOrFloat::Text(s) => {
|
TextOrFloat::Text(s) => {
|
||||||
if s == "inherit" {
|
if s == "inherit" {
|
||||||
let sprite = content.get_sprite(sprite);
|
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 {
|
} else {
|
||||||
bail!("bad effect lifetime, must be float or \"inherit\"",)
|
bail!("bad effect lifetime, must be float or \"inherit\"",)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,5 +18,5 @@ pub use ship::{
|
||||||
CollapseEffectSpawner, CollapseEvent, EffectCollapseEvent, EnginePoint, GunPoint, Ship,
|
CollapseEffectSpawner, CollapseEvent, EffectCollapseEvent, EnginePoint, GunPoint, Ship,
|
||||||
ShipCollapse,
|
ShipCollapse,
|
||||||
};
|
};
|
||||||
pub use sprite::{RepeatMode, Sprite};
|
pub use sprite::*;
|
||||||
pub use system::{System, SystemObject};
|
pub use system::{System, SystemObject};
|
||||||
|
|
|
@ -1,44 +1,36 @@
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use image::io::Reader;
|
use std::collections::HashMap;
|
||||||
use serde::Deserialize;
|
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
|
||||||
|
|
||||||
use crate::{handle::SpriteHandle, Content, ContentBuildContext};
|
use crate::{handle::SpriteHandle, Content, ContentBuildContext};
|
||||||
|
|
||||||
pub(crate) mod syntax {
|
pub(crate) mod syntax {
|
||||||
|
use crate::{Content, ContentBuildContext};
|
||||||
|
use anyhow::{anyhow, bail, Context, Ok, Result};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::path::PathBuf;
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
use super::RepeatMode;
|
use super::AnimSectionHandle;
|
||||||
|
|
||||||
// Raw serde syntax structs.
|
// Raw serde syntax structs.
|
||||||
// These are never seen by code outside this crate.
|
// These are never seen by code outside this crate.
|
||||||
|
|
||||||
|
/// Convenience variants of sprite definitions
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum Sprite {
|
pub enum Sprite {
|
||||||
Static(StaticSprite),
|
Static(StaticSprite),
|
||||||
Frames(FrameSprite),
|
OneSection(SpriteSection),
|
||||||
}
|
Complete(CompleteSprite),
|
||||||
|
|
||||||
#[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>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Two ways to specify animation length
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub enum TimingVariant {
|
pub enum TimingVariant {
|
||||||
|
/// The duration of this whole section
|
||||||
#[serde(rename = "duration")]
|
#[serde(rename = "duration")]
|
||||||
Duration(f32),
|
Duration(f32),
|
||||||
|
|
||||||
|
/// The fps of this section
|
||||||
#[serde(rename = "fps")]
|
#[serde(rename = "fps")]
|
||||||
Fps(f32),
|
Fps(f32),
|
||||||
}
|
}
|
||||||
|
@ -47,36 +39,176 @@ pub(crate) mod syntax {
|
||||||
pub struct Timing {
|
pub struct Timing {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub variant: TimingVariant,
|
pub variant: TimingVariant,
|
||||||
//pub uniform_rng: Option<f32>,
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// How to replay a texture's animation
|
/// An unanimated sprite
|
||||||
#[derive(Debug, Deserialize, Clone, Copy)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub enum RepeatMode {
|
pub struct StaticSprite {
|
||||||
/// Play this animation once, and stop at the last frame
|
pub file: PathBuf,
|
||||||
#[serde(rename = "once")]
|
}
|
||||||
Once,
|
|
||||||
|
|
||||||
/// After the first frame, jump to the last frame
|
/// The proper, full sprite definition
|
||||||
#[serde(rename = "repeat")]
|
#[derive(Debug, Deserialize)]
|
||||||
Repeat,
|
pub struct CompleteSprite {
|
||||||
|
pub section: HashMap<String, SpriteSection>,
|
||||||
|
pub default_section: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Play this animation in reverse after the last frame
|
/// A single animation section
|
||||||
#[serde(rename = "reverse")]
|
#[derive(Debug, Deserialize)]
|
||||||
Reverse,
|
pub struct SpriteSection {
|
||||||
}
|
pub frames: Vec<PathBuf>,
|
||||||
|
pub timing: Timing,
|
||||||
|
pub top: Option<SectionEdge>,
|
||||||
|
pub bot: Option<SectionEdge>,
|
||||||
|
}
|
||||||
|
|
||||||
impl RepeatMode {
|
impl SpriteSection {
|
||||||
/// Represent this repeatmode as an integer
|
pub fn add_to(
|
||||||
/// Used to pass this enum into shaders
|
&self,
|
||||||
pub fn as_int(&self) -> u32 {
|
_build_context: &mut ContentBuildContext,
|
||||||
match self {
|
content: &mut Content,
|
||||||
Self::Repeat => 0,
|
|
||||||
Self::Once => 1,
|
// An index of all sections in this sprite, used to resolve
|
||||||
Self::Reverse => 2,
|
// 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: should be pub crate
|
||||||
|
/// A handle for an animation section inside a sprite
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct AnimSectionHandle(usize);
|
||||||
|
|
||||||
|
/// An edge between two animation sections
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum SectionEdge {
|
||||||
|
/// Stop at the last frame of this section
|
||||||
|
Stop,
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
|
||||||
|
/// Restart this section from the opposite end
|
||||||
|
Restart,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a sprite that may be used in the game.
|
/// Represents a sprite that may be used in the game.
|
||||||
|
@ -88,25 +220,44 @@ pub struct Sprite {
|
||||||
/// This sprite's handle
|
/// This sprite's handle
|
||||||
pub handle: SpriteHandle,
|
pub handle: SpriteHandle,
|
||||||
|
|
||||||
/// The file names of frames of this sprite.
|
/// This sprite's default section
|
||||||
/// unanimated sprites have one frame.
|
pub default_section: AnimSectionHandle,
|
||||||
pub frames: Vec<PathBuf>,
|
|
||||||
|
|
||||||
/// The speed of this sprite's animation.
|
/// This sprite's animation sections
|
||||||
/// This is zero for unanimate sprites.
|
sections: Vec<SpriteSection>,
|
||||||
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,
|
|
||||||
|
|
||||||
/// Aspect ratio of this sprite (width / height)
|
/// Aspect ratio of this sprite (width / height)
|
||||||
pub aspect: f32,
|
pub aspect: f32,
|
||||||
|
}
|
||||||
|
|
||||||
/// If true, start on a random frame of this sprite.
|
impl Sprite {
|
||||||
pub random_start_frame: bool,
|
/// 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 {
|
impl crate::Build for Sprite {
|
||||||
|
@ -114,31 +265,32 @@ impl crate::Build for Sprite {
|
||||||
|
|
||||||
fn build(
|
fn build(
|
||||||
sprites: Self::InputSyntaxType,
|
sprites: Self::InputSyntaxType,
|
||||||
_build_context: &mut ContentBuildContext,
|
build_context: &mut ContentBuildContext,
|
||||||
content: &mut Content,
|
content: &mut Content,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
for (sprite_name, t) in sprites {
|
for (sprite_name, t) in sprites {
|
||||||
match t {
|
match t {
|
||||||
syntax::Sprite::Static(t) => {
|
syntax::Sprite::Static(t) => {
|
||||||
let file = content.config.sprite_root.join(&t.file);
|
let idx = match content.sprite_atlas.path_map.get(&t.file) {
|
||||||
let reader = Reader::open(&file).with_context(|| {
|
Some(s) => *s,
|
||||||
format!(
|
None => {
|
||||||
"Failed to read file `{}` in sprite `{}`",
|
return Err(
|
||||||
file.display(),
|
anyhow!("error while processing sprite `{}`", sprite_name,),
|
||||||
sprite_name,
|
)
|
||||||
)
|
.with_context(|| {
|
||||||
})?;
|
format!(
|
||||||
let dim = reader.into_dimensions().with_context(|| {
|
"file `{}` isn't in the sprite atlas, cannot proceed",
|
||||||
format!(
|
t.file.display()
|
||||||
"Failed to get dimensions of file `{}` in sprite `{}`",
|
)
|
||||||
file.display(),
|
});
|
||||||
sprite_name,
|
}
|
||||||
)
|
};
|
||||||
})?;
|
let img = &content.sprite_atlas.index[idx as usize];
|
||||||
|
let aspect = img.w / img.h;
|
||||||
|
|
||||||
let h = SpriteHandle {
|
let h = SpriteHandle {
|
||||||
index: content.sprites.len(),
|
index: content.sprites.len(),
|
||||||
aspect: dim.0 as f32 / dim.1 as f32,
|
aspect,
|
||||||
};
|
};
|
||||||
|
|
||||||
if sprite_name == content.config.starfield_sprite {
|
if sprite_name == content.config.starfield_sprite {
|
||||||
|
@ -154,71 +306,108 @@ impl crate::Build for Sprite {
|
||||||
|
|
||||||
content.sprites.push(Self {
|
content.sprites.push(Self {
|
||||||
name: sprite_name,
|
name: sprite_name,
|
||||||
frames: vec![t.file],
|
default_section: AnimSectionHandle(0),
|
||||||
frame_duration: 0.0,
|
sections: vec![SpriteSection {
|
||||||
//frame_uniform_rng: 0.0,
|
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,
|
handle: h,
|
||||||
repeat: RepeatMode::Once,
|
aspect,
|
||||||
aspect: dim.0 as f32 / dim.1 as f32,
|
|
||||||
random_start_frame: false,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
syntax::Sprite::Frames(t) => {
|
syntax::Sprite::OneSection(s) => {
|
||||||
let mut dim = None;
|
let mut section_names: HashMap<String, _> = HashMap::new();
|
||||||
for f in &t.frames {
|
// Name the one section in this sprite "anim"
|
||||||
let file = content.config.sprite_root.join(f);
|
section_names.insert("anim".to_owned(), AnimSectionHandle(0));
|
||||||
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 (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 {
|
let h = SpriteHandle {
|
||||||
index: content.sprites.len(),
|
index: content.sprites.len(),
|
||||||
aspect: dim.0 as f32 / dim.1 as f32,
|
aspect,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: remove?
|
||||||
if sprite_name == content.config.starfield_sprite {
|
if sprite_name == content.config.starfield_sprite {
|
||||||
unreachable!("Starfield texture may not be animated")
|
unreachable!("Starfield texture may not be animated")
|
||||||
}
|
}
|
||||||
|
|
||||||
let frame_duration = match t.timing.variant {
|
let mut sections = Vec::new();
|
||||||
syntax::TimingVariant::Duration(d) => d / t.frames.len() as f32,
|
sections.push(section);
|
||||||
syntax::TimingVariant::Fps(f) => 1.0 / f,
|
|
||||||
|
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.sprite_index.insert(sprite_name.clone(), h);
|
||||||
content.sprites.push(Self {
|
content.sprites.push(Self {
|
||||||
name: sprite_name,
|
name: sprite_name,
|
||||||
frames: t.frames,
|
sections,
|
||||||
frame_duration,
|
default_section: *section_names.get(&s.default_section).unwrap(),
|
||||||
//frame_uniform_rng: t.timing.uniform_rng.unwrap_or(0.0),
|
|
||||||
handle: h,
|
handle: h,
|
||||||
repeat: t.repeat,
|
aspect,
|
||||||
aspect: dim.0 as f32 / dim.1 as f32,
|
|
||||||
random_start_frame: t.random_start_frame.unwrap_or(false),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue