Compare commits
6 Commits
57f2b97f40
...
d09158a324
Author | SHA1 | Date |
---|---|---|
Mark | d09158a324 | |
Mark | 64f930b391 | |
Mark | 7820458404 | |
Mark | e61c580247 | |
Mark | 267ec5a40a | |
Mark | fa719d46b9 |
|
@ -642,6 +642,7 @@ dependencies = [
|
||||||
"galactica-packer",
|
"galactica-packer",
|
||||||
"galactica-util",
|
"galactica-util",
|
||||||
"image",
|
"image",
|
||||||
|
"lazy_static",
|
||||||
"nalgebra",
|
"nalgebra",
|
||||||
"rapier2d",
|
"rapier2d",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
@ -64,3 +64,4 @@ rand = "0.8.5"
|
||||||
walkdir = "2.4.0"
|
walkdir = "2.4.0"
|
||||||
toml = "0.8.8"
|
toml = "0.8.8"
|
||||||
glyphon = "0.4.1"
|
glyphon = "0.4.1"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
|
2
TODO.md
2
TODO.md
|
@ -1,7 +1,6 @@
|
||||||
# Specific projects
|
# Specific projects
|
||||||
|
|
||||||
## Currently working on:
|
## Currently working on:
|
||||||
- first: fix animation transition timings
|
|
||||||
- first: fix particles & physics
|
- first: fix particles & physics
|
||||||
- clickable buttons
|
- clickable buttons
|
||||||
- planet outfitter
|
- planet outfitter
|
||||||
|
@ -218,6 +217,7 @@
|
||||||
- Naming: atlas, sprite, image, frame, texture
|
- Naming: atlas, sprite, image, frame, texture
|
||||||
- Outfits may not change unless you've landed. They might not change ever for CC ships!
|
- Outfits may not change unless you've landed. They might not change ever for CC ships!
|
||||||
- All angle adjustments happen in content & shaders
|
- All angle adjustments happen in content & shaders
|
||||||
|
- Reserved texture: index zero
|
||||||
|
|
||||||
|
|
||||||
## Ideas
|
## Ideas
|
||||||
|
|
|
@ -27,3 +27,4 @@ walkdir = { workspace = true }
|
||||||
nalgebra = { workspace = true }
|
nalgebra = { workspace = true }
|
||||||
image = { workspace = true }
|
image = { workspace = true }
|
||||||
rapier2d = { workspace = true }
|
rapier2d = { workspace = true }
|
||||||
|
lazy_static = { workspace = true }
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
//! 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 spriteautomaton;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
|
@ -13,16 +13,16 @@ use galactica_packer::{SpriteAtlas, SpriteAtlasImage};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fs::File,
|
fs::File,
|
||||||
hash::Hash,
|
|
||||||
io::Read,
|
io::Read,
|
||||||
|
num::NonZeroU32,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
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::*;
|
||||||
|
pub use spriteautomaton::*;
|
||||||
|
|
||||||
mod syntax {
|
mod syntax {
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
|
@ -304,8 +304,8 @@ impl Content {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a texture by its index
|
/// Get a texture by its index
|
||||||
pub fn get_image(&self, idx: u32) -> &SpriteAtlasImage {
|
pub fn get_image(&self, idx: NonZeroU32) -> &SpriteAtlasImage {
|
||||||
&self.sprite_atlas.index[idx as usize]
|
&self.sprite_atlas.get_by_idx(idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get an outfit from a handle
|
/// Get an outfit from a handle
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::path::PathBuf;
|
use std::{num::NonZeroU32, path::PathBuf};
|
||||||
|
|
||||||
pub(crate) mod syntax {
|
pub(crate) mod syntax {
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
|
@ -36,8 +36,8 @@ pub(crate) mod syntax {
|
||||||
// An insufficient limit will result in some tiles not being drawn
|
// An insufficient limit will result in some tiles not being drawn
|
||||||
let starfield_instance_limit = 12 * starfield_count as u64;
|
let starfield_instance_limit = 12 * starfield_count as u64;
|
||||||
|
|
||||||
let starfield_texture = match atlas.path_map.get(&self.starfield.texture) {
|
let starfield_texture = match atlas.get_idx_by_path(&self.starfield.texture) {
|
||||||
Some(s) => *s,
|
Some(s) => s,
|
||||||
None => {
|
None => {
|
||||||
bail!(
|
bail!(
|
||||||
"starfield texture `{}` doesn't exist",
|
"starfield texture `{}` doesn't exist",
|
||||||
|
@ -129,7 +129,7 @@ pub struct Config {
|
||||||
pub starfield_max_dist: f32,
|
pub starfield_max_dist: f32,
|
||||||
|
|
||||||
/// Index of starfield texture
|
/// Index of starfield texture
|
||||||
pub starfield_texture: u32,
|
pub starfield_texture: NonZeroU32,
|
||||||
|
|
||||||
/// Size of a square starfield tile, in game units.
|
/// Size of a square starfield tile, in game units.
|
||||||
/// A tile of size STARFIELD_Z_MAX * screen-size-in-game-units
|
/// A tile of size STARFIELD_Z_MAX * screen-size-in-game-units
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
handle::SpriteHandle, Content, ContentBuildContext, EffectHandle, OutfitHandle, OutfitSpace,
|
handle::SpriteHandle, Content, ContentBuildContext, EffectHandle, OutfitHandle, OutfitSpace,
|
||||||
StartEdge,
|
SectionEdge,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) mod syntax {
|
pub(crate) mod syntax {
|
||||||
|
@ -148,10 +148,10 @@ pub struct Outfit {
|
||||||
pub engine_flare_sprite: Option<SpriteHandle>,
|
pub engine_flare_sprite: Option<SpriteHandle>,
|
||||||
|
|
||||||
/// Jump to this edge when engines turn on
|
/// Jump to this edge when engines turn on
|
||||||
pub engine_flare_on_start: Option<StartEdge>,
|
pub engine_flare_on_start: Option<SectionEdge>,
|
||||||
|
|
||||||
/// Jump to this edge when engines turn off
|
/// Jump to this edge when engines turn off
|
||||||
pub engine_flare_on_stop: Option<StartEdge>,
|
pub engine_flare_on_stop: Option<SectionEdge>,
|
||||||
|
|
||||||
/// Shield hit points
|
/// Shield hit points
|
||||||
pub shield_strength: f32,
|
pub shield_strength: f32,
|
||||||
|
@ -289,30 +289,75 @@ impl crate::Build for Outfit {
|
||||||
};
|
};
|
||||||
o.engine_thrust = engine.thrust;
|
o.engine_thrust = engine.thrust;
|
||||||
o.engine_flare_sprite = Some(sprite_handle);
|
o.engine_flare_sprite = Some(sprite_handle);
|
||||||
|
let sprite = content.get_sprite(sprite_handle);
|
||||||
|
|
||||||
|
// Flare animation will traverse this edge when the player presses the thrust key
|
||||||
|
// This leads from the idle animation to the transition animation
|
||||||
o.engine_flare_on_start = {
|
o.engine_flare_on_start = {
|
||||||
let x = engine.flare.on_start;
|
let x = engine.flare.on_start;
|
||||||
if x.is_none() {
|
if x.is_none() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let x = x.unwrap();
|
let x = x.unwrap();
|
||||||
Some(
|
let mut e = x
|
||||||
x.resolve_as_start(sprite_handle, build_context)
|
.resolve_as_edge(sprite_handle, build_context, 0.0)
|
||||||
.with_context(|| format!("in outfit `{}`", outfit_name))?,
|
.with_context(|| format!("in outfit `{}`", outfit_name))?;
|
||||||
)
|
match e {
|
||||||
|
// Inherit duration from transition sequence
|
||||||
|
SectionEdge::Top {
|
||||||
|
section,
|
||||||
|
ref mut duration,
|
||||||
|
}
|
||||||
|
| SectionEdge::Bot {
|
||||||
|
section,
|
||||||
|
ref mut duration,
|
||||||
|
} => {
|
||||||
|
*duration = sprite.get_section(section).frame_duration;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"bad edge `{}`: must be `top` or `bot`",
|
||||||
|
x.val
|
||||||
|
))
|
||||||
|
.with_context(|| format!("in outfit `{}`", outfit_name));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some(e)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Flare animation will traverse this edge when the player releases the thrust key
|
||||||
|
// This leads from the idle animation to the transition animation
|
||||||
o.engine_flare_on_stop = {
|
o.engine_flare_on_stop = {
|
||||||
let x = engine.flare.on_stop;
|
let x = engine.flare.on_stop;
|
||||||
if x.is_none() {
|
if x.is_none() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let x = x.unwrap();
|
let x = x.unwrap();
|
||||||
Some(
|
let mut e = x
|
||||||
x.resolve_as_start(sprite_handle, build_context)
|
.resolve_as_edge(sprite_handle, build_context, 0.0)
|
||||||
.with_context(|| format!("in outfit `{}`", outfit_name))?,
|
.with_context(|| format!("in outfit `{}`", outfit_name))?;
|
||||||
)
|
match e {
|
||||||
|
// Inherit duration from transition sequence
|
||||||
|
SectionEdge::Top {
|
||||||
|
section,
|
||||||
|
ref mut duration,
|
||||||
|
}
|
||||||
|
| SectionEdge::Bot {
|
||||||
|
section,
|
||||||
|
ref mut duration,
|
||||||
|
} => {
|
||||||
|
*duration = sprite.get_section(section).frame_duration;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"bad edge `{}`: must be `top` or `bot`",
|
||||||
|
x.val
|
||||||
|
))
|
||||||
|
.with_context(|| format!("in outfit `{}`", outfit_name));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some(e)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{handle::SpriteHandle, Content, ContentBuildContext};
|
use crate::{handle::SpriteHandle, Content, ContentBuildContext};
|
||||||
|
@ -71,15 +72,15 @@ pub(crate) mod syntax {
|
||||||
// Make sure all frames have the same size and add them
|
// Make sure all frames have the same size and add them
|
||||||
// to the frame vector
|
// to the frame vector
|
||||||
let mut dim = None;
|
let mut dim = None;
|
||||||
let mut frames = Vec::new();
|
let mut frames: Vec<u32> = Vec::new();
|
||||||
for f in &self.frames {
|
for f in &self.frames {
|
||||||
let idx = match content.sprite_atlas.path_map.get(f) {
|
let idx = match content.sprite_atlas.get_idx_by_path(f) {
|
||||||
Some(s) => *s,
|
Some(s) => s,
|
||||||
None => {
|
None => {
|
||||||
bail!("error: file `{}` isn't in the sprite atlas", f.display());
|
bail!("error: file `{}` isn't in the sprite atlas", f.display());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let img = &content.sprite_atlas.index[idx as usize];
|
let img = &content.sprite_atlas.get_by_idx(idx);
|
||||||
|
|
||||||
match dim {
|
match dim {
|
||||||
None => dim = Some(img.true_size),
|
None => dim = Some(img.true_size),
|
||||||
|
@ -90,8 +91,13 @@ pub(crate) mod syntax {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
frames.push(img.idx);
|
frames.push(img.idx.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if frames.len() == 0 {
|
||||||
|
bail!("sprite sections must not be empty",)
|
||||||
|
}
|
||||||
|
|
||||||
let dim = dim.unwrap();
|
let dim = dim.unwrap();
|
||||||
|
|
||||||
let frame_duration = match self.timing.variant {
|
let frame_duration = match self.timing.variant {
|
||||||
|
@ -104,12 +110,12 @@ pub(crate) mod syntax {
|
||||||
}
|
}
|
||||||
|
|
||||||
let edge_top = match &self.top {
|
let edge_top = match &self.top {
|
||||||
Some(x) => x.resolve_as_edge(this_sprite, build_context)?,
|
Some(x) => x.resolve_as_edge(this_sprite, build_context, frame_duration)?,
|
||||||
None => super::SectionEdge::Stop,
|
None => super::SectionEdge::Stop,
|
||||||
};
|
};
|
||||||
|
|
||||||
let edge_bot = match &self.bot {
|
let edge_bot = match &self.bot {
|
||||||
Some(x) => x.resolve_as_edge(this_sprite, build_context)?,
|
Some(x) => x.resolve_as_edge(this_sprite, build_context, frame_duration)?,
|
||||||
None => super::SectionEdge::Stop,
|
None => super::SectionEdge::Stop,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -139,11 +145,11 @@ pub(crate) mod syntax {
|
||||||
build_context: &ContentBuildContext,
|
build_context: &ContentBuildContext,
|
||||||
) -> Result<super::StartEdge> {
|
) -> Result<super::StartEdge> {
|
||||||
let e = self
|
let e = self
|
||||||
.resolve_as_edge(sprite, build_context)
|
.resolve_as_edge(sprite, build_context, 0.0)
|
||||||
.with_context(|| format!("while resolving start edge"))?;
|
.with_context(|| format!("while resolving start edge"))?;
|
||||||
match e {
|
match e {
|
||||||
super::SectionEdge::Bot { section } => Ok(super::StartEdge::Bot { section }),
|
super::SectionEdge::Bot { section, .. } => Ok(super::StartEdge::Bot { section }),
|
||||||
super::SectionEdge::Top { section } => Ok(super::StartEdge::Top { section }),
|
super::SectionEdge::Top { section, .. } => Ok(super::StartEdge::Top { section }),
|
||||||
_ => {
|
_ => {
|
||||||
bail!("bad section start specification `{}`", self.val);
|
bail!("bad section start specification `{}`", self.val);
|
||||||
}
|
}
|
||||||
|
@ -154,19 +160,27 @@ pub(crate) mod syntax {
|
||||||
&self,
|
&self,
|
||||||
sprite: SpriteHandle,
|
sprite: SpriteHandle,
|
||||||
build_context: &ContentBuildContext,
|
build_context: &ContentBuildContext,
|
||||||
|
duration: f32,
|
||||||
) -> Result<super::SectionEdge> {
|
) -> Result<super::SectionEdge> {
|
||||||
let all_sections = build_context.sprite_section_index.get(&sprite).unwrap();
|
let all_sections = build_context.sprite_section_index.get(&sprite).unwrap();
|
||||||
|
|
||||||
|
if self.val == "hidden" {
|
||||||
|
return Ok(super::SectionEdge::Top {
|
||||||
|
section: crate::AnimSectionHandle::Hidden,
|
||||||
|
duration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if self.val == "stop" {
|
if self.val == "stop" {
|
||||||
return Ok(super::SectionEdge::Stop);
|
return Ok(super::SectionEdge::Stop);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.val == "reverse" {
|
if self.val == "reverse" {
|
||||||
return Ok(super::SectionEdge::Reverse);
|
return Ok(super::SectionEdge::Reverse { duration });
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.val == "restart" {
|
if self.val == "repeat" {
|
||||||
return Ok(super::SectionEdge::Restart);
|
return Ok(super::SectionEdge::Repeat { duration });
|
||||||
}
|
}
|
||||||
|
|
||||||
let (s, p) = match self.val.split_once(":") {
|
let (s, p) = match self.val.split_once(":") {
|
||||||
|
@ -185,8 +199,8 @@ pub(crate) mod syntax {
|
||||||
};
|
};
|
||||||
|
|
||||||
match p {
|
match p {
|
||||||
"top" => Ok(super::SectionEdge::Top { section }),
|
"top" => Ok(super::SectionEdge::Top { section, duration }),
|
||||||
"bot" => Ok(super::SectionEdge::Bot { section }),
|
"bot" => Ok(super::SectionEdge::Bot { section, duration }),
|
||||||
_ => {
|
_ => {
|
||||||
return Err(anyhow!("bad section edge specification `{}`", self.val))
|
return Err(anyhow!("bad section edge specification `{}`", self.val))
|
||||||
.with_context(|| format!("invalid target `{}`", p));
|
.with_context(|| format!("invalid target `{}`", p));
|
||||||
|
@ -196,10 +210,24 @@ pub(crate) mod syntax {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: should be pub crate
|
|
||||||
/// A handle for an animation section inside a sprite
|
/// A handle for an animation section inside a sprite
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct AnimSectionHandle(pub(crate) usize);
|
pub enum AnimSectionHandle {
|
||||||
|
/// The hidden section
|
||||||
|
Hidden,
|
||||||
|
|
||||||
|
/// An index into this sprite's section array
|
||||||
|
Idx(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnimSectionHandle {
|
||||||
|
fn get_idx(&self) -> Option<usize> {
|
||||||
|
match self {
|
||||||
|
Self::Hidden => None,
|
||||||
|
Self::Idx(idx) => Some(*idx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An edge between two animation sections
|
/// An edge between two animation sections
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
@ -211,19 +239,31 @@ pub enum SectionEdge {
|
||||||
Bot {
|
Bot {
|
||||||
/// The section to play
|
/// The section to play
|
||||||
section: AnimSectionHandle,
|
section: AnimSectionHandle,
|
||||||
|
|
||||||
|
/// The length of this edge, in seconds
|
||||||
|
duration: f32,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Play the given section from the top
|
/// Play the given section from the top
|
||||||
Top {
|
Top {
|
||||||
/// The section to play
|
/// The section to play
|
||||||
section: AnimSectionHandle,
|
section: AnimSectionHandle,
|
||||||
|
|
||||||
|
/// The length of this edge, in seconds
|
||||||
|
duration: f32,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Replay this section in the opposite direction
|
/// Replay this section in the opposite direction
|
||||||
Reverse,
|
Reverse {
|
||||||
|
/// The length of this edge, in seconds
|
||||||
|
duration: f32,
|
||||||
|
},
|
||||||
|
|
||||||
/// Restart this section from the opposite end
|
/// Restart this section from the opposite end
|
||||||
Restart,
|
Repeat {
|
||||||
|
/// The length of this edge, in seconds
|
||||||
|
duration: f32,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where to start an animation
|
/// Where to start an animation
|
||||||
|
@ -245,8 +285,14 @@ pub enum StartEdge {
|
||||||
impl Into<SectionEdge> for StartEdge {
|
impl Into<SectionEdge> for StartEdge {
|
||||||
fn into(self) -> SectionEdge {
|
fn into(self) -> SectionEdge {
|
||||||
match self {
|
match self {
|
||||||
Self::Bot { section } => SectionEdge::Bot { section },
|
Self::Bot { section } => SectionEdge::Bot {
|
||||||
Self::Top { section } => SectionEdge::Top { section },
|
section,
|
||||||
|
duration: 0.0,
|
||||||
|
},
|
||||||
|
Self::Top { section } => SectionEdge::Top {
|
||||||
|
section,
|
||||||
|
duration: 0.0,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -270,13 +316,25 @@ pub struct Sprite {
|
||||||
pub aspect: f32,
|
pub aspect: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref HIDDEN_SECTION: SpriteSection = SpriteSection {
|
||||||
|
frames: vec![0],
|
||||||
|
frame_duration: 0.0,
|
||||||
|
edge_bot: SectionEdge::Stop,
|
||||||
|
edge_top: SectionEdge::Stop,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
impl Sprite {
|
impl Sprite {
|
||||||
/// Get an animation section from a handle
|
/// Get an animation section from a handle
|
||||||
pub fn get_section(&self, section: AnimSectionHandle) -> &SpriteSection {
|
pub fn get_section(&self, section: AnimSectionHandle) -> &SpriteSection {
|
||||||
&self.sections[section.0]
|
match section {
|
||||||
|
AnimSectionHandle::Hidden => &HIDDEN_SECTION,
|
||||||
|
AnimSectionHandle::Idx(idx) => &self.sections[idx],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get this sprite's first frame
|
/// Get the index of the texture of this sprite's first frame
|
||||||
pub fn get_first_frame(&self) -> u32 {
|
pub fn get_first_frame(&self) -> u32 {
|
||||||
match self.start_at {
|
match self.start_at {
|
||||||
StartEdge::Bot { section } => *self.get_section(section).frames.last().unwrap(),
|
StartEdge::Bot { section } => *self.get_section(section).frames.last().unwrap(),
|
||||||
|
@ -327,8 +385,8 @@ impl crate::Build for Sprite {
|
||||||
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 idx = match content.sprite_atlas.path_map.get(&t.file) {
|
let idx = match content.sprite_atlas.get_idx_by_path(&t.file) {
|
||||||
Some(s) => *s,
|
Some(s) => s,
|
||||||
None => {
|
None => {
|
||||||
return Err(
|
return Err(
|
||||||
anyhow!("error while processing sprite `{}`", sprite_name,),
|
anyhow!("error while processing sprite `{}`", sprite_name,),
|
||||||
|
@ -341,7 +399,7 @@ impl crate::Build for Sprite {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let img = &content.sprite_atlas.index[idx as usize];
|
let img = &content.sprite_atlas.get_by_idx(idx);
|
||||||
let aspect = img.w / img.h;
|
let aspect = img.w / img.h;
|
||||||
|
|
||||||
let h = SpriteHandle {
|
let h = SpriteHandle {
|
||||||
|
@ -350,16 +408,16 @@ impl crate::Build for Sprite {
|
||||||
|
|
||||||
content.sprite_index.insert(sprite_name.clone(), h);
|
content.sprite_index.insert(sprite_name.clone(), h);
|
||||||
let mut smap = HashMap::new();
|
let mut smap = HashMap::new();
|
||||||
smap.insert("anim".to_string(), AnimSectionHandle(0));
|
smap.insert("anim".to_string(), AnimSectionHandle::Idx(0));
|
||||||
build_context.sprite_section_index.insert(h, smap);
|
build_context.sprite_section_index.insert(h, smap);
|
||||||
|
|
||||||
content.sprites.push(Self {
|
content.sprites.push(Self {
|
||||||
name: sprite_name,
|
name: sprite_name,
|
||||||
start_at: StartEdge::Top {
|
start_at: StartEdge::Top {
|
||||||
section: AnimSectionHandle(0),
|
section: AnimSectionHandle::Idx(0),
|
||||||
},
|
},
|
||||||
sections: vec![SpriteSection {
|
sections: vec![SpriteSection {
|
||||||
frames: vec![img.idx],
|
frames: vec![img.idx.into()],
|
||||||
// We implement unanimated sprites with a very fast framerate
|
// We implement unanimated sprites with a very fast framerate
|
||||||
// and STOP endpoints.
|
// and STOP endpoints.
|
||||||
frame_duration: 0.01,
|
frame_duration: 0.01,
|
||||||
|
@ -373,7 +431,7 @@ impl crate::Build for Sprite {
|
||||||
syntax::Sprite::OneSection(s) => {
|
syntax::Sprite::OneSection(s) => {
|
||||||
let mut section_names: HashMap<String, _> = HashMap::new();
|
let mut section_names: HashMap<String, _> = HashMap::new();
|
||||||
// Name the one section in this sprite "anim"
|
// Name the one section in this sprite "anim"
|
||||||
section_names.insert("anim".to_owned(), AnimSectionHandle(0));
|
section_names.insert("anim".to_owned(), AnimSectionHandle::Idx(0));
|
||||||
|
|
||||||
let sprite_handle = SpriteHandle {
|
let sprite_handle = SpriteHandle {
|
||||||
index: content.sprites.len(),
|
index: content.sprites.len(),
|
||||||
|
@ -382,7 +440,7 @@ impl crate::Build for Sprite {
|
||||||
.sprite_index
|
.sprite_index
|
||||||
.insert(sprite_name.clone(), sprite_handle);
|
.insert(sprite_name.clone(), sprite_handle);
|
||||||
let mut smap = HashMap::new();
|
let mut smap = HashMap::new();
|
||||||
smap.insert("anim".to_string(), AnimSectionHandle(0));
|
smap.insert("anim".to_string(), AnimSectionHandle::Idx(0));
|
||||||
build_context
|
build_context
|
||||||
.sprite_section_index
|
.sprite_section_index
|
||||||
.insert(sprite_handle, smap);
|
.insert(sprite_handle, smap);
|
||||||
|
@ -398,19 +456,18 @@ impl crate::Build for Sprite {
|
||||||
content.sprites.push(Self {
|
content.sprites.push(Self {
|
||||||
name: sprite_name,
|
name: sprite_name,
|
||||||
sections,
|
sections,
|
||||||
start_at: StartEdge::Bot {
|
start_at: StartEdge::Top {
|
||||||
section: AnimSectionHandle(0),
|
section: AnimSectionHandle::Idx(0),
|
||||||
},
|
},
|
||||||
handle: sprite_handle,
|
handle: sprite_handle,
|
||||||
aspect,
|
aspect,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
syntax::Sprite::Complete(s) => {
|
syntax::Sprite::Complete(s) => {
|
||||||
let mut idx = 0;
|
|
||||||
let mut section_names = HashMap::new();
|
let mut section_names = HashMap::new();
|
||||||
for (name, _) in &s.section {
|
for (name, _) in &s.section {
|
||||||
section_names.insert(name.to_owned(), AnimSectionHandle(idx));
|
section_names
|
||||||
idx += 1;
|
.insert(name.to_owned(), AnimSectionHandle::Idx(section_names.len()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let sprite_handle = SpriteHandle {
|
let sprite_handle = SpriteHandle {
|
||||||
|
@ -428,19 +485,19 @@ impl crate::Build for Sprite {
|
||||||
.resolve_as_start(sprite_handle, build_context)
|
.resolve_as_start(sprite_handle, build_context)
|
||||||
.with_context(|| format!("while loading sprite `{}`", sprite_name))?;
|
.with_context(|| format!("while loading sprite `{}`", sprite_name))?;
|
||||||
|
|
||||||
let mut sections = Vec::with_capacity(idx);
|
let mut sections = Vec::with_capacity(section_names.len());
|
||||||
let mut dim = None;
|
let mut dim = None;
|
||||||
|
|
||||||
// Make sure we add sections in order
|
// Make sure we add sections in order
|
||||||
let mut names = section_names.iter().collect::<Vec<_>>();
|
let mut names = section_names.iter().collect::<Vec<_>>();
|
||||||
names.sort_by(|a, b| (a.1).0.cmp(&(b.1).0));
|
names.sort_by(|a, b| (a.1).get_idx().unwrap().cmp(&(b.1).get_idx().unwrap()));
|
||||||
|
|
||||||
for (k, _) in names {
|
for (k, _) in names {
|
||||||
let v = s.section.get(k).unwrap();
|
let v = s.section.get(k).unwrap();
|
||||||
let (d, s) = v
|
let (d, s) = v
|
||||||
.add_to(sprite_handle, build_context, content)
|
.add_to(sprite_handle, build_context, content)
|
||||||
.with_context(|| format!("while parsing sprite `{}`", sprite_name))
|
.with_context(|| format!("while parsing section `{}`", k))
|
||||||
.with_context(|| format!("while parsing section `{}`", k))?;
|
.with_context(|| format!("while parsing sprite `{}`", sprite_name))?;
|
||||||
|
|
||||||
// Make sure all dimensions are the same
|
// Make sure all dimensions are the same
|
||||||
if dim.is_none() {
|
if dim.is_none() {
|
||||||
|
|
|
@ -25,7 +25,7 @@ impl AnimationState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What direction are we playing our animation in?
|
/// What direction are we playing our animation in?
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum AnimDirection {
|
enum AnimDirection {
|
||||||
/// Top to bottom, with increasing frame indices
|
/// Top to bottom, with increasing frame indices
|
||||||
/// (normal)
|
/// (normal)
|
||||||
|
@ -35,13 +35,13 @@ enum AnimDirection {
|
||||||
/// (reverse)
|
/// (reverse)
|
||||||
Down,
|
Down,
|
||||||
|
|
||||||
/// Stopped, no animation
|
/// Stopped on one frame
|
||||||
Stop,
|
Stop,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manages a single sprite's animation state.
|
/// Manages a single sprite's animation state.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AnimAutomaton {
|
pub struct SpriteAutomaton {
|
||||||
/// The sprite we're animating
|
/// The sprite we're animating
|
||||||
sprite: SpriteHandle,
|
sprite: SpriteHandle,
|
||||||
|
|
||||||
|
@ -53,8 +53,10 @@ pub struct AnimAutomaton {
|
||||||
current_frame: usize,
|
current_frame: usize,
|
||||||
|
|
||||||
/// Where we are between frames.
|
/// Where we are between frames.
|
||||||
/// Always between zero and one.
|
current_edge_progress: f32,
|
||||||
current_fade: f32,
|
|
||||||
|
/// The length of the current edge we're on, in seconds
|
||||||
|
current_edge_duration: f32,
|
||||||
|
|
||||||
/// In what direction are we playing the current section?
|
/// In what direction are we playing the current section?
|
||||||
current_direction: AnimDirection,
|
current_direction: AnimDirection,
|
||||||
|
@ -71,7 +73,7 @@ pub struct AnimAutomaton {
|
||||||
next_edge_override: Option<SectionEdge>,
|
next_edge_override: Option<SectionEdge>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnimAutomaton {
|
impl SpriteAutomaton {
|
||||||
/// Create a new AnimAutomaton
|
/// Create a new AnimAutomaton
|
||||||
pub fn new(ct: &Content, sprite_handle: SpriteHandle) -> Self {
|
pub fn new(ct: &Content, sprite_handle: SpriteHandle) -> Self {
|
||||||
let sprite = ct.get_sprite(sprite_handle);
|
let sprite = ct.get_sprite(sprite_handle);
|
||||||
|
@ -89,10 +91,19 @@ impl AnimAutomaton {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let sec = sprite.get_section(current_section);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
sprite: sprite.handle,
|
sprite: sprite.handle,
|
||||||
current_frame: 0,
|
current_frame: 0,
|
||||||
current_fade: 0.0,
|
|
||||||
|
current_edge_progress: match current_direction {
|
||||||
|
AnimDirection::Down => 0.0,
|
||||||
|
AnimDirection::Up => sec.frame_duration,
|
||||||
|
AnimDirection::Stop => unreachable!("how'd you get here?"),
|
||||||
|
},
|
||||||
|
|
||||||
|
current_edge_duration: sec.frame_duration,
|
||||||
next_edge_override: None,
|
next_edge_override: None,
|
||||||
|
|
||||||
current_direction,
|
current_direction,
|
||||||
|
@ -108,49 +119,108 @@ impl AnimAutomaton {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Force a transition to the given section right now
|
/// Force a transition to the given section right now
|
||||||
pub fn jump_to(&mut self, ct: &Content, start: StartEdge) {
|
pub fn jump_to(&mut self, ct: &Content, start: SectionEdge) {
|
||||||
self.take_edge(ct, start.into());
|
self.take_edge(ct, start);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn take_edge(&mut self, ct: &Content, e: SectionEdge) {
|
fn take_edge(&mut self, ct: &Content, e: SectionEdge) {
|
||||||
let sprite = ct.get_sprite(self.sprite);
|
let sprite = ct.get_sprite(self.sprite);
|
||||||
let current_section = sprite.get_section(self.current_section);
|
let current_section = sprite.get_section(self.current_section);
|
||||||
|
let last_direction = self.current_direction;
|
||||||
|
|
||||||
match e {
|
match e {
|
||||||
SectionEdge::Stop => {
|
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;
|
|
||||||
self.current_direction = AnimDirection::Down;
|
|
||||||
}
|
|
||||||
SectionEdge::Bot { section } => {
|
|
||||||
let s = sprite.get_section(section);
|
|
||||||
self.current_section = section;
|
|
||||||
self.current_frame = s.frames.len() - 1;
|
|
||||||
self.current_direction = AnimDirection::Up;
|
|
||||||
}
|
|
||||||
SectionEdge::Restart => {
|
|
||||||
self.current_frame = 0;
|
|
||||||
}
|
|
||||||
SectionEdge::Reverse => {
|
|
||||||
// Jump to SECOND frame, since we've already shown the
|
|
||||||
// first during the fade transition
|
|
||||||
|
|
||||||
match self.current_direction {
|
match self.current_direction {
|
||||||
AnimDirection::Stop => {}
|
AnimDirection::Stop => {}
|
||||||
AnimDirection::Up => {
|
AnimDirection::Up => {
|
||||||
self.current_frame = 0;
|
self.current_frame = 0;
|
||||||
self.current_direction = AnimDirection::Down;
|
|
||||||
}
|
}
|
||||||
AnimDirection::Down => {
|
AnimDirection::Down => {
|
||||||
self.current_frame = current_section.frames.len() - 1;
|
self.current_frame = current_section.frames.len() - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.current_edge_duration = 1.0;
|
||||||
|
self.current_direction = AnimDirection::Stop;
|
||||||
|
}
|
||||||
|
SectionEdge::Top { section, duration } => {
|
||||||
|
self.current_section = section;
|
||||||
|
self.current_edge_duration = duration;
|
||||||
|
self.current_frame = 0;
|
||||||
|
self.current_direction = AnimDirection::Down;
|
||||||
|
}
|
||||||
|
SectionEdge::Bot { section, duration } => {
|
||||||
|
let s = sprite.get_section(section);
|
||||||
|
self.current_section = section;
|
||||||
|
self.current_frame = s.frames.len() - 1;
|
||||||
|
self.current_edge_duration = duration;
|
||||||
|
self.current_direction = AnimDirection::Up;
|
||||||
|
}
|
||||||
|
SectionEdge::Repeat { duration } => {
|
||||||
|
match self.current_direction {
|
||||||
|
AnimDirection::Stop => {}
|
||||||
|
AnimDirection::Up => {
|
||||||
|
self.current_frame = current_section.frames.len() - 1;
|
||||||
|
}
|
||||||
|
AnimDirection::Down => {
|
||||||
|
self.current_frame = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.current_edge_duration = duration;
|
||||||
|
}
|
||||||
|
SectionEdge::Reverse { duration } => {
|
||||||
|
match self.current_direction {
|
||||||
|
AnimDirection::Stop => {}
|
||||||
|
AnimDirection::Up => {
|
||||||
|
// Jump to SECOND frame, since we've already shown the
|
||||||
|
// first during the fade transition
|
||||||
|
self.current_frame = {
|
||||||
|
if current_section.frames.len() == 1 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.current_direction = AnimDirection::Down;
|
||||||
|
}
|
||||||
|
AnimDirection::Down => {
|
||||||
|
self.current_frame = {
|
||||||
|
if current_section.frames.len() == 1 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
current_section.frames.len() - 2
|
||||||
|
}
|
||||||
|
};
|
||||||
self.current_direction = AnimDirection::Up;
|
self.current_direction = AnimDirection::Up;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.current_edge_duration = duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let last = match last_direction {
|
||||||
|
AnimDirection::Stop => self.last_texture,
|
||||||
|
AnimDirection::Down => self.next_texture,
|
||||||
|
AnimDirection::Up => self.last_texture,
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.current_direction {
|
||||||
|
AnimDirection::Stop => {
|
||||||
|
let current_section = sprite.get_section(self.current_section);
|
||||||
|
self.next_texture = current_section.frames[self.current_frame];
|
||||||
|
self.last_texture = current_section.frames[self.current_frame];
|
||||||
|
}
|
||||||
|
AnimDirection::Down => {
|
||||||
|
let current_section = sprite.get_section(self.current_section);
|
||||||
|
self.last_texture = last;
|
||||||
|
self.next_texture = current_section.frames[self.current_frame];
|
||||||
|
self.current_edge_progress = 0.0;
|
||||||
|
}
|
||||||
|
AnimDirection::Up => {
|
||||||
|
let current_section = sprite.get_section(self.current_section);
|
||||||
|
self.next_texture = last;
|
||||||
|
self.last_texture = current_section.frames[self.current_frame];
|
||||||
|
self.current_edge_progress = self.current_edge_duration;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -168,13 +238,12 @@ impl AnimAutomaton {
|
||||||
// and we switch to the next frame when it hits 1.0. If we are stepping foward, it increases,
|
// 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.
|
// and if we are stepping backwards, it decreases.
|
||||||
|
|
||||||
// This should always be positive, and this fact is enforced by the content loader.
|
// Note that frame_duration may be zero!
|
||||||
// if we get here, something is very wrong.
|
// This is only possible in the hidden texture, since
|
||||||
assert!(current_section.frame_duration > 0.0);
|
// user-provided sections are always checked to be positive.
|
||||||
|
assert!(current_section.frame_duration >= 0.0);
|
||||||
|
|
||||||
match self.current_direction {
|
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 => {
|
AnimDirection::Stop => {
|
||||||
// Edge case: we're stopped and got a request to transition.
|
// Edge case: we're stopped and got a request to transition.
|
||||||
// we should transition right away.
|
// we should transition right away.
|
||||||
|
@ -182,57 +251,57 @@ impl AnimAutomaton {
|
||||||
if let Some(e) = self.next_edge_override {
|
if let Some(e) = self.next_edge_override {
|
||||||
self.take_edge(ct, e);
|
self.take_edge(ct, e);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're stepping foward and finished this frame
|
return;
|
||||||
// (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 {
|
AnimDirection::Down => {
|
||||||
self.current_frame += 1;
|
self.current_edge_progress += t;
|
||||||
} else {
|
|
||||||
let e = {
|
// We're stepping foward and finished this frame
|
||||||
if self.next_edge_override.is_some() {
|
if self.current_edge_progress > self.current_edge_duration {
|
||||||
self.next_edge_override.take().unwrap()
|
if self.current_frame < current_section.frames.len() - 1 {
|
||||||
|
self.current_frame += 1;
|
||||||
|
self.last_texture = self.next_texture;
|
||||||
|
self.next_texture = current_section.frames[self.current_frame];
|
||||||
|
self.current_edge_progress = 0.0;
|
||||||
|
self.current_edge_duration = current_section.frame_duration;
|
||||||
} else {
|
} else {
|
||||||
current_section.edge_bot.clone()
|
let e = {
|
||||||
|
if self.next_edge_override.is_some() {
|
||||||
|
self.next_edge_override.take().unwrap()
|
||||||
|
} else {
|
||||||
|
current_section.edge_bot.clone()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.take_edge(ct, e);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
self.take_edge(ct, e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_section = sprite.get_section(self.current_section);
|
AnimDirection::Up => {
|
||||||
self.last_texture = self.next_texture;
|
self.current_edge_progress -= t;
|
||||||
self.next_texture = current_section.frames[self.current_frame];
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're stepping backward and finished this frame
|
// We're stepping backward and finished this frame
|
||||||
// (implies we're travelling upwards)
|
if self.current_edge_progress < 0.0 {
|
||||||
if self.current_fade < 0.0 {
|
if self.current_frame > 0 {
|
||||||
while self.current_fade < 0.0 {
|
self.current_frame -= 1;
|
||||||
self.current_fade += 1.0;
|
self.next_texture = self.last_texture;
|
||||||
}
|
self.last_texture = current_section.frames[self.current_frame];
|
||||||
|
self.current_edge_progress = current_section.frame_duration;
|
||||||
if self.current_frame > 0 {
|
self.current_edge_duration = current_section.frame_duration;
|
||||||
self.current_frame -= 1;
|
|
||||||
} else {
|
|
||||||
let e = {
|
|
||||||
if self.next_edge_override.is_some() {
|
|
||||||
self.next_edge_override.take().unwrap()
|
|
||||||
} else {
|
} else {
|
||||||
current_section.edge_top.clone()
|
let e = {
|
||||||
|
if self.next_edge_override.is_some() {
|
||||||
|
self.next_edge_override.take().unwrap()
|
||||||
|
} else {
|
||||||
|
current_section.edge_top.clone()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.take_edge(ct, e);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
self.take_edge(ct, e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_section = sprite.get_section(self.current_section);
|
|
||||||
self.next_texture = self.last_texture;
|
|
||||||
self.last_texture = current_section.frames[self.current_frame];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,7 +310,7 @@ impl AnimAutomaton {
|
||||||
return AnimationState {
|
return AnimationState {
|
||||||
texture_a: self.last_texture,
|
texture_a: self.last_texture,
|
||||||
texture_b: self.next_texture,
|
texture_b: self.next_texture,
|
||||||
fade: self.current_fade,
|
fade: self.current_edge_progress / self.current_edge_duration,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -17,7 +17,20 @@ use winit::{
|
||||||
window::WindowBuilder,
|
window::WindowBuilder,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() {
|
||||||
|
if let Err(err) = try_main() {
|
||||||
|
eprintln!("Galactica failed with an error");
|
||||||
|
|
||||||
|
let mut i = 0;
|
||||||
|
for e in err.chain().rev() {
|
||||||
|
eprintln!("{i:02}: {}", e);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_main() -> Result<()> {
|
||||||
let cache_dir = Path::new(ASSET_CACHE);
|
let cache_dir = Path::new(ASSET_CACHE);
|
||||||
fs::create_dir_all(cache_dir)?;
|
fs::create_dir_all(cache_dir)?;
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ use image::{imageops, GenericImageView, ImageBuffer, Rgba, RgbaImage};
|
||||||
use std::{
|
use std::{
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{Read, Write},
|
io::{Read, Write},
|
||||||
|
num::NonZeroU32,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -205,19 +206,19 @@ impl AtlasSet {
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
self.index
|
self.index.push(
|
||||||
.path_map
|
p,
|
||||||
.insert(p.to_path_buf(), self.index.index.len() as u32);
|
SpriteAtlasImage {
|
||||||
|
true_size: image_dim,
|
||||||
self.index.index.push(SpriteAtlasImage {
|
// Add one to account for hidden texture
|
||||||
true_size: image_dim,
|
idx: NonZeroU32::new(self.index.len() as u32 + 1).unwrap(),
|
||||||
idx: self.index.index.len() as u32,
|
atlas: atlas_idx as u32,
|
||||||
atlas: atlas_idx as u32,
|
x: (x + self.image_margin) as f32 / self.texture_width as f32,
|
||||||
x: (x + self.image_margin) as f32 / self.texture_width as f32,
|
y: (y + self.image_margin) as f32 / self.texture_height as f32,
|
||||||
y: (y + self.image_margin) as f32 / self.texture_height as f32,
|
w: image_dim.0 as f32 / self.texture_width as f32,
|
||||||
w: image_dim.0 as f32 / self.texture_width as f32,
|
h: image_dim.1 as f32 / self.texture_height as f32,
|
||||||
h: image_dim.1 as f32 / self.texture_height as f32,
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
return Ok(atlas_idx);
|
return Ok(atlas_idx);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,11 @@
|
||||||
|
|
||||||
//! This crate creates texture atlases from an asset tree.
|
//! This crate creates texture atlases from an asset tree.
|
||||||
|
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
num::NonZeroU32,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
@ -13,8 +17,9 @@ pub struct SpriteAtlasImage {
|
||||||
/// This is an index in SpriteAtlas.atlas_list
|
/// This is an index in SpriteAtlas.atlas_list
|
||||||
pub atlas: u32,
|
pub atlas: u32,
|
||||||
|
|
||||||
/// A globally unique, consecutively numbered index for this sprite
|
/// A globally unique, consecutively numbered index for this sprite.
|
||||||
pub idx: u32,
|
/// This is nonzero because index zero is reserved for the "hidden" texture.
|
||||||
|
pub idx: NonZeroU32,
|
||||||
|
|
||||||
/// The size of this image, in pixels
|
/// The size of this image, in pixels
|
||||||
pub true_size: (u32, u32),
|
pub true_size: (u32, u32),
|
||||||
|
@ -41,10 +46,10 @@ pub struct SpriteAtlasImage {
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct SpriteAtlas {
|
pub struct SpriteAtlas {
|
||||||
/// The images in this atlas
|
/// The images in this atlas
|
||||||
pub index: Vec<SpriteAtlasImage>,
|
pub(crate) index: Vec<SpriteAtlasImage>,
|
||||||
|
|
||||||
/// Map paths to image indices
|
/// Map paths to image indices
|
||||||
pub path_map: HashMap<PathBuf, u32>,
|
path_map: HashMap<PathBuf, NonZeroU32>,
|
||||||
|
|
||||||
/// The file names of the atlas textures we've generated
|
/// The file names of the atlas textures we've generated
|
||||||
pub atlas_list: Vec<String>,
|
pub atlas_list: Vec<String>,
|
||||||
|
@ -59,4 +64,30 @@ impl SpriteAtlas {
|
||||||
atlas_list: Vec::new(),
|
atlas_list: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a SpriteAtlasImage by index
|
||||||
|
pub fn get_by_idx(&self, idx: NonZeroU32) -> &SpriteAtlasImage {
|
||||||
|
&self.index[idx.get() as usize - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an image index from its path
|
||||||
|
/// returns None if this path isn't in this index
|
||||||
|
pub fn get_idx_by_path(&self, path: &Path) -> Option<NonZeroU32> {
|
||||||
|
self.path_map.get(path).map(|x| *x)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the number of images in this atlas
|
||||||
|
pub fn len(&self) -> u32 {
|
||||||
|
self.index.len() as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an image with the given path to this index
|
||||||
|
pub fn push(&mut self, p: &Path, i: SpriteAtlasImage) {
|
||||||
|
self.path_map.insert(
|
||||||
|
p.to_path_buf(),
|
||||||
|
NonZeroU32::new(self.index.len() as u32 + 1).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
self.index.push(i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,7 +103,7 @@ fn transform_vertex(obj: ObjectData, vertex_position: vec2<f32>, texture_index:
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return vec4<f32>(pos, 0.0, 1.0);;
|
return vec4<f32>(pos, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@vertex
|
@vertex
|
||||||
|
@ -113,35 +113,63 @@ fn vertex_main(
|
||||||
) -> VertexOutput {
|
) -> VertexOutput {
|
||||||
var out: VertexOutput;
|
var out: VertexOutput;
|
||||||
|
|
||||||
out.position = transform_vertex(
|
// Pick texture size by the size of the visible texture
|
||||||
objects[instance.object_index],
|
// (texture index 0 is special, it's the "hidden" texture)
|
||||||
vertex.position.xy,
|
if instance.texture_index.x == 0u && instance.texture_index.y == 0u {
|
||||||
instance.texture_index.x
|
out.position = vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
||||||
);
|
} else if instance.texture_index.x == 0u {
|
||||||
|
out.position = transform_vertex(
|
||||||
|
objects[instance.object_index],
|
||||||
|
vertex.position.xy,
|
||||||
|
instance.texture_index.y
|
||||||
|
);
|
||||||
|
} else if instance.texture_index.y == 0u {
|
||||||
|
out.position = transform_vertex(
|
||||||
|
objects[instance.object_index],
|
||||||
|
vertex.position.xy,
|
||||||
|
instance.texture_index.x
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
out.position = transform_vertex(
|
||||||
|
objects[instance.object_index],
|
||||||
|
vertex.position.xy,
|
||||||
|
instance.texture_index.x
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Compute texture coordinates
|
|
||||||
let age = global_data.current_time.x;
|
|
||||||
out.tween = instance.texture_fade;
|
out.tween = instance.texture_fade;
|
||||||
|
|
||||||
let t = global_atlas[instance.texture_index.x];
|
// Texture 0 is special, it's the empty texture
|
||||||
out.texture_index_a = u32(t.atlas_texture);
|
if instance.texture_index.x == 0u {
|
||||||
out.texture_coords_a = vec2(t.xpos, t.ypos);
|
out.texture_index_a = 0u;
|
||||||
if vertex.texture_coords.x == 1.0 {
|
out.texture_coords_a = vec2(0.0, 0.0);
|
||||||
out.texture_coords_a = out.texture_coords_a + vec2(t.width, 0.0);
|
} else {
|
||||||
}
|
let t = global_atlas[instance.texture_index.x];
|
||||||
if vertex.texture_coords.y == 1.0 {
|
out.texture_index_a = t.atlas_texture;
|
||||||
out.texture_coords_a = out.texture_coords_a + vec2(0.0, t.height);
|
out.texture_coords_a = vec2(t.xpos, t.ypos);
|
||||||
|
if vertex.texture_coords.x == 1.0 {
|
||||||
|
out.texture_coords_a = out.texture_coords_a + vec2(t.width, 0.0);
|
||||||
|
}
|
||||||
|
if vertex.texture_coords.y == 1.0 {
|
||||||
|
out.texture_coords_a = out.texture_coords_a + vec2(0.0, t.height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let b = global_atlas[instance.texture_index.y];
|
if instance.texture_index.y == 0u {
|
||||||
out.texture_index_b = u32(b.atlas_texture);
|
out.texture_index_b = u32(0u);
|
||||||
out.texture_coords_b = vec2(b.xpos, b.ypos);
|
out.texture_coords_b = vec2(0.0, 0.0);
|
||||||
if vertex.texture_coords.x == 1.0 {
|
} else {
|
||||||
out.texture_coords_b = out.texture_coords_b + vec2(b.width, 0.0);
|
let b = global_atlas[instance.texture_index.y];
|
||||||
}
|
out.texture_index_b = u32(b.atlas_texture);
|
||||||
if vertex.texture_coords.y == 1.0 {
|
out.texture_coords_b = vec2(b.xpos, b.ypos);
|
||||||
out.texture_coords_b = out.texture_coords_b + vec2(0.0, b.height);
|
if vertex.texture_coords.x == 1.0 {
|
||||||
|
out.texture_coords_b = out.texture_coords_b + vec2(b.width, 0.0);
|
||||||
|
}
|
||||||
|
if vertex.texture_coords.y == 1.0 {
|
||||||
|
out.texture_coords_b = out.texture_coords_b + vec2(0.0, b.height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
|
@ -150,19 +178,42 @@ fn vertex_main(
|
||||||
|
|
||||||
@fragment
|
@fragment
|
||||||
fn fragment_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
fn fragment_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
return mix(
|
|
||||||
textureSampleLevel(
|
var texture_a: vec4<f32> = vec4(0.0, 0.0, 0.0, 0.0);
|
||||||
|
if !(
|
||||||
|
(in.texture_index_a == 0u) &&
|
||||||
|
(in.texture_coords_a.x == 0.0) &&
|
||||||
|
(in.texture_coords_a.y == 0.0)
|
||||||
|
) {
|
||||||
|
texture_a = textureSampleLevel(
|
||||||
texture_array[in.texture_index_a],
|
texture_array[in.texture_index_a],
|
||||||
sampler_array[0],
|
sampler_array[0],
|
||||||
in.texture_coords_a,
|
in.texture_coords_a,
|
||||||
0.0
|
0.0
|
||||||
).rgba,
|
).rgba;
|
||||||
textureSampleLevel(
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var texture_b: vec4<f32> = vec4(0.0, 0.0, 0.0, 0.0);
|
||||||
|
if !(
|
||||||
|
(in.texture_index_b == 0u) &&
|
||||||
|
(in.texture_coords_b.x == 0.0) &&
|
||||||
|
(in.texture_coords_b.y == 0.0)
|
||||||
|
) {
|
||||||
|
texture_b = textureSampleLevel(
|
||||||
texture_array[in.texture_index_b],
|
texture_array[in.texture_index_b],
|
||||||
sampler_array[0],
|
sampler_array[0],
|
||||||
in.texture_coords_b,
|
in.texture_coords_b,
|
||||||
0.0
|
0.0
|
||||||
).rgba,
|
).rgba;
|
||||||
|
}
|
||||||
|
|
||||||
|
let color = mix(
|
||||||
|
texture_a,
|
||||||
|
texture_b,
|
||||||
in.tween
|
in.tween
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return color;
|
||||||
}
|
}
|
|
@ -33,12 +33,12 @@ var sampler_array: binding_array<sampler>;
|
||||||
|
|
||||||
// INCLUDE: anchor.wgsl
|
// INCLUDE: anchor.wgsl
|
||||||
|
|
||||||
@vertex
|
|
||||||
fn vertex_main(
|
|
||||||
vertex: VertexInput,
|
|
||||||
instance: InstanceInput,
|
|
||||||
) -> VertexOutput {
|
|
||||||
|
|
||||||
|
fn transform_vertex(
|
||||||
|
instance: InstanceInput,
|
||||||
|
vertex_position: vec3<f32>,
|
||||||
|
texture_index: u32,
|
||||||
|
) -> vec4<f32> {
|
||||||
|
|
||||||
let window_dim = global_data.window_size / global_data.window_scale.x;
|
let window_dim = global_data.window_size / global_data.window_scale.x;
|
||||||
let scale = instance.size / window_dim.y;
|
let scale = instance.size / window_dim.y;
|
||||||
|
@ -50,8 +50,8 @@ fn vertex_main(
|
||||||
// Apply scale and sprite aspect
|
// Apply scale and sprite aspect
|
||||||
// Note that our mesh starts centered at (0, 0). This is important!
|
// Note that our mesh starts centered at (0, 0). This is important!
|
||||||
var pos: vec2<f32> = vec2(
|
var pos: vec2<f32> = vec2(
|
||||||
vertex.position.x * scale * aspect,
|
vertex_position.x * scale * aspect,
|
||||||
vertex.position.y * scale
|
vertex_position.y * scale
|
||||||
);
|
);
|
||||||
|
|
||||||
// Apply rotation (and adjust sprite angle, since sprites point north)
|
// Apply rotation (and adjust sprite angle, since sprites point north)
|
||||||
|
@ -72,10 +72,27 @@ fn vertex_main(
|
||||||
vec2(instance.size * aspect, instance.size)
|
vec2(instance.size * aspect, instance.size)
|
||||||
);
|
);
|
||||||
|
|
||||||
var out: VertexOutput;
|
return vec4<f32>(pos, 0.0, 1.0);
|
||||||
out.position = vec4<f32>(pos, 1.0, 1.0);
|
}
|
||||||
out.color_transform = instance.color_transform;
|
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vertex_main(
|
||||||
|
vertex: VertexInput,
|
||||||
|
instance: InstanceInput,
|
||||||
|
) -> VertexOutput {
|
||||||
|
|
||||||
|
// TODO: this will break if we try to use texture 0.
|
||||||
|
// implement animations for ui sprites & fix that here.
|
||||||
|
|
||||||
|
let pos = transform_vertex(
|
||||||
|
instance,
|
||||||
|
vertex.position,
|
||||||
|
instance.texture_index.x,
|
||||||
|
);
|
||||||
|
|
||||||
|
var out: VertexOutput;
|
||||||
|
out.position = pos;
|
||||||
|
out.color_transform = instance.color_transform;
|
||||||
|
|
||||||
|
|
||||||
// TODO: function to get texture from sprite
|
// TODO: function to get texture from sprite
|
||||||
|
@ -108,7 +125,6 @@ fn vertex_main(
|
||||||
out.mask_index = vec2(0u, 0u);
|
out.mask_index = vec2(0u, 0u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -306,7 +306,7 @@ impl<'a> super::GPUState {
|
||||||
],
|
],
|
||||||
window_scale: [self.state.window.scale_factor() as f32, 0.0],
|
window_scale: [self.state.window.scale_factor() as f32, 0.0],
|
||||||
window_aspect: [self.state.window_aspect, 0.0],
|
window_aspect: [self.state.window_aspect, 0.0],
|
||||||
starfield_sprite: [input.ct.get_config().starfield_texture, 0],
|
starfield_sprite: [input.ct.get_config().starfield_texture.into(), 0],
|
||||||
starfield_tile_size: [input.ct.get_config().starfield_size, 0.0],
|
starfield_tile_size: [input.ct.get_config().starfield_size, 0.0],
|
||||||
starfield_size_limits: [
|
starfield_size_limits: [
|
||||||
input.ct.get_config().starfield_min_size,
|
input.ct.get_config().starfield_min_size,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::globaluniform::GlobalUniform;
|
use crate::globaluniform::GlobalUniform;
|
||||||
|
|
||||||
|
// TODO: build script?
|
||||||
/// Basic wgsl preprocesser
|
/// Basic wgsl preprocesser
|
||||||
pub(crate) fn preprocess_shader(
|
pub(crate) fn preprocess_shader(
|
||||||
shader: &str,
|
shader: &str,
|
||||||
|
|
|
@ -112,15 +112,23 @@ impl TextureArray {
|
||||||
for sprite in &ct.sprites {
|
for sprite in &ct.sprites {
|
||||||
for section in sprite.iter_sections() {
|
for section in sprite.iter_sections() {
|
||||||
for idx in §ion.frames {
|
for idx in §ion.frames {
|
||||||
let image = ct.get_image(*idx);
|
// Some atlas entries may be written twice here,
|
||||||
image_locations.data[*idx as usize] = AtlasImageLocation {
|
// but that's not really a problem. They're all the same!
|
||||||
xpos: image.x,
|
//
|
||||||
ypos: image.y,
|
// This happens rarely---only when two different sections
|
||||||
width: image.w,
|
// use the same frame.
|
||||||
height: image.h,
|
let idx = NonZeroU32::new(*idx);
|
||||||
atlas_texture: image.atlas,
|
if idx.is_some() {
|
||||||
_padding: Default::default(),
|
let image = ct.get_image(idx.unwrap());
|
||||||
};
|
image_locations.data[idx.unwrap().get() as usize] = AtlasImageLocation {
|
||||||
|
xpos: image.x,
|
||||||
|
ypos: image.y,
|
||||||
|
width: image.w,
|
||||||
|
height: image.h,
|
||||||
|
atlas_texture: image.atlas,
|
||||||
|
_padding: Default::default(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,6 +152,7 @@ pub struct UiInstance {
|
||||||
|
|
||||||
/// What texture to use for this instance
|
/// What texture to use for this instance
|
||||||
pub texture_index: [u32; 2],
|
pub texture_index: [u32; 2],
|
||||||
|
|
||||||
/// Fade parameter between textures
|
/// Fade parameter between textures
|
||||||
pub texture_fade: f32,
|
pub texture_fade: f32,
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use galactica_content::{AnimAutomaton, AnimationState, Content, FactionHandle, Projectile};
|
use galactica_content::{AnimationState, Content, FactionHandle, Projectile, SpriteAutomaton};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use rapier2d::{dynamics::RigidBodyHandle, geometry::ColliderHandle};
|
use rapier2d::{dynamics::RigidBodyHandle, geometry::ColliderHandle};
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ pub struct PhysProjectile {
|
||||||
pub content: Projectile,
|
pub content: Projectile,
|
||||||
|
|
||||||
/// This projectile's sprite animation state
|
/// This projectile's sprite animation state
|
||||||
anim: AnimAutomaton,
|
anim: SpriteAutomaton,
|
||||||
|
|
||||||
/// The remaining lifetime of this projectile, in seconds
|
/// The remaining lifetime of this projectile, in seconds
|
||||||
pub lifetime: f32,
|
pub lifetime: f32,
|
||||||
|
@ -40,7 +40,7 @@ impl PhysProjectile {
|
||||||
let size_rng = content.size_rng;
|
let size_rng = content.size_rng;
|
||||||
let lifetime = content.lifetime;
|
let lifetime = content.lifetime;
|
||||||
PhysProjectile {
|
PhysProjectile {
|
||||||
anim: AnimAutomaton::new(ct, content.sprite),
|
anim: SpriteAutomaton::new(ct, content.sprite),
|
||||||
rigid_body,
|
rigid_body,
|
||||||
collider,
|
collider,
|
||||||
content,
|
content,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use galactica_content::{
|
use galactica_content::{
|
||||||
AnimAutomaton, AnimationState, Content, EnginePoint, FactionHandle, OutfitHandle, ShipHandle,
|
AnimationState, Content, EnginePoint, FactionHandle, OutfitHandle, ShipHandle, SpriteAutomaton,
|
||||||
};
|
};
|
||||||
use nalgebra::{point, vector, Rotation2, Vector2};
|
use nalgebra::{point, vector, Rotation2, Vector2};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
@ -56,10 +56,10 @@ pub struct PhysSimShip {
|
||||||
pub(crate) data: ShipData,
|
pub(crate) data: ShipData,
|
||||||
|
|
||||||
/// This ship's sprite animation state
|
/// This ship's sprite animation state
|
||||||
anim: AnimAutomaton,
|
anim: SpriteAutomaton,
|
||||||
|
|
||||||
/// Animation state for each of this ship's engines
|
/// Animation state for each of this ship's engines
|
||||||
engine_anim: Vec<(EnginePoint, AnimAutomaton)>,
|
engine_anim: Vec<(EnginePoint, SpriteAutomaton)>,
|
||||||
|
|
||||||
/// This ship's controls
|
/// This ship's controls
|
||||||
pub(crate) controls: ShipControls,
|
pub(crate) controls: ShipControls,
|
||||||
|
@ -83,7 +83,7 @@ impl PhysSimShip {
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let ship_ct = ct.get_ship(handle);
|
let ship_ct = ct.get_ship(handle);
|
||||||
PhysSimShip {
|
PhysSimShip {
|
||||||
anim: AnimAutomaton::new(ct, ship_ct.sprite),
|
anim: SpriteAutomaton::new(ct, ship_ct.sprite),
|
||||||
rigid_body,
|
rigid_body,
|
||||||
collider,
|
collider,
|
||||||
data: ShipData::new(ct, handle, faction, personality),
|
data: ShipData::new(ct, handle, faction, personality),
|
||||||
|
@ -273,7 +273,7 @@ impl PhysSimShip {
|
||||||
.get_ship(self.data.get_content())
|
.get_ship(self.data.get_content())
|
||||||
.engines
|
.engines
|
||||||
.iter()
|
.iter()
|
||||||
.map(|e| (e.clone(), AnimAutomaton::new(ct, flare)))
|
.map(|e| (e.clone(), SpriteAutomaton::new(ct, flare)))
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,7 +300,7 @@ impl PhysSimShip {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get this ship's engine animations
|
/// Get this ship's engine animations
|
||||||
pub fn iter_engine_anim(&self) -> impl Iterator<Item = &(EnginePoint, AnimAutomaton)> {
|
pub fn iter_engine_anim(&self) -> impl Iterator<Item = &(EnginePoint, SpriteAutomaton)> {
|
||||||
self.engine_anim.iter()
|
self.engine_anim.iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue