diff --git a/content/config.toml b/content/config.toml index 07c8988..f0ac483 100644 --- a/content/config.toml +++ b/content/config.toml @@ -28,8 +28,8 @@ starfield.max_size = 1.8 # Z-axis (parallax) range for starfield stars starfield.min_dist = 75.0 starfield.max_dist = 200.0 -# Name of starfield sprite -starfield.sprite = "starfield" +# Path to starfield sprite texture +starfield.texture = "starfield.png" # Zoom level bounds. diff --git a/content/sprite.toml b/content/sprite.toml index 47ef0c3..b3481c9 100644 --- a/content/sprite.toml +++ b/content/sprite.toml @@ -1,17 +1,41 @@ # TODO: # random start frame -# repeat once: stay on last frame # blending mode: alpha / half-alpha / additive - -[sprite."starfield"] -file = "starfield.png" - [sprite."star::star"] file = "star/B-09.png" [sprite."flare::ion"] -file = "flare/1.png" +start_at = "rise:top" +random_start_frame = false + +section.idle.timing.duration = 5 +#section.idle.repeat = "reverse" +section.idle.frames = ["flare/1.png", "flare/4.png", "flare/5.png"] +section.idle.top = "reverse" +section.idle.bot = "reverse" +# stop: stop on last frame (special) +# restart: go to opposite end (same as self:tail) +# repeat: reverse and play again +# TODO: implement random +# spec: "idle:bot", "idle:top", or "idle:random" + +section.rise.timing.duration = 0.15 +section.rise.top = "stop" +section.rise.bot = "run:top" +section.rise.frames = [ + "flare/6.png", + "flare/5.png", + "flare/4.png", + "flare/3.png", + "flare/2.png", +] + +section.run.timing.duration = 0.01 +section.run.top = "stop" +section.run.bot = "stop" +section.run.frames = ["flare/1.png"] + [sprite."planet::earth"] file = "planet/earth.png" @@ -70,6 +94,19 @@ file = "ui/landscape/test.png" [sprite."ui::landscapemask"] file = "ui/landscape-mask.png" +[sprite."ui::planet::button"] +start_at = "off:top" +random_start_frame = false + +section.off.top = "stop" +section.off.bot = "stop" +section.off.timing.fps = 60 +section.off.frames = ["ui/planet-button-off.png"] + +section.on.top = "stop" +section.on.bot = "stop" +section.on.timing.fps = 60 +section.on.frames = ["ui/planet-button-on.png"] [sprite."particle::blaster"] timing.duration = 0.15 diff --git a/crates/content/src/animautomaton.rs b/crates/content/src/animautomaton.rs index ed57167..44b1252 100644 --- a/crates/content/src/animautomaton.rs +++ b/crates/content/src/animautomaton.rs @@ -1,4 +1,4 @@ -use crate::{AnimSectionHandle, Content, SectionEdge, SpriteHandle}; +use crate::{AnimSectionHandle, Content, SectionEdge, SpriteHandle, SpriteStart}; /// A single frame's state #[derive(Debug, Clone)] @@ -72,33 +72,35 @@ impl AnimAutomaton { /// Create a new AnimAutomaton pub fn new(ct: &Content, sprite_handle: SpriteHandle) -> Self { let sprite = ct.get_sprite(sprite_handle); + + let (current_section, texture, current_direction) = match sprite.start_at { + SpriteStart::Top { section } => ( + section, + *sprite.get_section(section).frames.first().unwrap(), + AnimDirection::Down, + ), + SpriteStart::Bot { section } => ( + section, + *sprite.get_section(section).frames.last().unwrap(), + AnimDirection::Up, + ), + }; + 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(), + current_direction, + current_section, + last_texture: texture, + next_texture: texture, } } /// 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 + *self = Self::new(ct, self.sprite); } /// Reverse this animation's direction diff --git a/crates/content/src/lib.rs b/crates/content/src/lib.rs index c3881ae..7319fd1 100644 --- a/crates/content/src/lib.rs +++ b/crates/content/src/lib.rs @@ -141,8 +141,6 @@ pub struct Content { /// Map strings to texture names. /// This is only necessary because we need to hard-code a few texture names for UI elements. sprite_index: HashMap, - /// The texture to use for starfield stars - starfield_handle: Option, /// Keeps track of which images are in which texture sprite_atlas: SpriteAtlas, @@ -206,7 +204,7 @@ impl Content { let mut content = Self { config: { if let Some(c) = root.config { - c.build(&asset_root) + c.build(&asset_root, &atlas) .with_context(|| "while parsing config table")? } else { bail!("failed loading content: no config table specified") @@ -222,7 +220,6 @@ impl Content { factions: Vec::new(), effects: Vec::new(), sprite_index: HashMap::new(), - starfield_handle: None, }; // TODO: enforce sprite and image limits @@ -280,16 +277,6 @@ impl Content { (0..self.systems.len()).map(|x| SystemHandle { index: x }) } - /// Get the handle for the starfield sprite - pub fn get_starfield_texture(&self) -> u32 { - let h = match self.starfield_handle { - Some(h) => h, - None => unreachable!("Starfield sprite hasn't been loaded yet!"), - }; - let sprite = self.get_sprite(h); - sprite.get_section(sprite.default_section).frames[0] - } - /// Get a handle from a sprite name pub fn get_sprite_handle(&self, name: &str) -> SpriteHandle { return match self.sprite_index.get(name) { diff --git a/crates/content/src/part/config.rs b/crates/content/src/part/config.rs index 06b4b9d..b707dbe 100644 --- a/crates/content/src/part/config.rs +++ b/crates/content/src/part/config.rs @@ -2,6 +2,7 @@ use std::path::PathBuf; pub(crate) mod syntax { use anyhow::{bail, Result}; + use galactica_packer::SpriteAtlas; use serde::Deserialize; use std::path::{Path, PathBuf}; @@ -19,7 +20,7 @@ pub(crate) mod syntax { impl Config { // TODO: clean up build trait - pub fn build(self, asset_root: &Path) -> Result { + pub fn build(self, asset_root: &Path, atlas: &SpriteAtlas) -> Result { for i in &self.fonts.files { if !asset_root.join(i).exists() { bail!("font file `{}` doesn't exist", i.display()); @@ -35,6 +36,16 @@ pub(crate) mod syntax { // An insufficient limit will result in some tiles not being drawn let starfield_instance_limit = 12 * starfield_count as u64; + let starfield_texture = match atlas.path_map.get(&self.starfield.texture) { + Some(s) => *s, + None => { + bail!( + "starfield texture `{}` doesn't exist", + self.starfield.texture.display() + ) + } + }; + return Ok(super::Config { sprite_root: asset_root.join(self.sprite_root), font_files: self @@ -53,7 +64,7 @@ pub(crate) mod syntax { starfield_min_dist: self.starfield.min_dist, starfield_max_size: self.starfield.max_size, starfield_min_size: self.starfield.min_size, - starfield_sprite: self.starfield.sprite, + starfield_texture, starfield_count, starfield_density, starfield_size, @@ -80,7 +91,7 @@ pub(crate) mod syntax { pub max_size: f32, pub min_dist: f32, pub max_dist: f32, - pub sprite: String, + pub texture: PathBuf, } } @@ -117,8 +128,8 @@ pub struct Config { /// Maximum z-distance of starfield star, in game units pub starfield_max_dist: f32, - /// Name of starfield sprite - pub starfield_sprite: String, + /// Index of starfield texture + pub starfield_texture: u32, /// Size of a square starfield tile, in game units. /// A tile of size STARFIELD_Z_MAX * screen-size-in-game-units diff --git a/crates/content/src/part/effect.rs b/crates/content/src/part/effect.rs index 6195bd8..57c5dfe 100644 --- a/crates/content/src/part/effect.rs +++ b/crates/content/src/part/effect.rs @@ -8,7 +8,7 @@ pub(crate) mod syntax { use galactica_util::to_radians; use serde::Deserialize; - use crate::{Content, ContentBuildContext, EffectHandle}; + use crate::{Content, ContentBuildContext, EffectHandle, SpriteStart}; // Raw serde syntax structs. // These are never seen by code outside this crate. @@ -56,8 +56,12 @@ pub(crate) mod syntax { TextOrFloat::Float(f) => f, TextOrFloat::Text(s) => { if s == "inherit" { + // Match lifetime of first section of sprite let sprite = content.get_sprite(sprite); - let sec = sprite.get_section(sprite.default_section); + let sec = match sprite.start_at { + SpriteStart::Top { section } => sprite.get_section(section), + SpriteStart::Bot { section } => sprite.get_section(section), + }; sec.frame_duration * sec.frames.len() as f32 } else { bail!("bad effect lifetime, must be float or \"inherit\"",) diff --git a/crates/content/src/part/sprite.rs b/crates/content/src/part/sprite.rs index de61224..a23a71f 100644 --- a/crates/content/src/part/sprite.rs +++ b/crates/content/src/part/sprite.rs @@ -51,7 +51,7 @@ pub(crate) mod syntax { #[derive(Debug, Deserialize)] pub struct CompleteSprite { pub section: HashMap, - pub default_section: String, + pub start_at: SectionEdge, } /// A single animation section @@ -109,12 +109,12 @@ pub(crate) mod syntax { } let edge_top = match &self.top { - Some(x) => x.resolve(all_sections)?, + Some(x) => x.resolve_as_edge(all_sections)?, None => super::SectionEdge::Stop, }; let edge_bot = match &self.bot { - Some(x) => x.resolve(all_sections)?, + Some(x) => x.resolve_as_edge(all_sections)?, None => super::SectionEdge::Stop, }; @@ -138,7 +138,23 @@ pub(crate) mod syntax { } impl SectionEdge { - pub fn resolve( + pub fn resolve_as_start( + &self, + all_sections: &HashMap, + ) -> Result { + let e = self + .resolve_as_edge(all_sections) + .with_context(|| format!("while resolving start edge"))?; + match e { + super::SectionEdge::Bot { section } => Ok(super::SpriteStart::Bot { section }), + super::SectionEdge::Top { section } => Ok(super::SpriteStart::Top { section }), + _ => { + bail!("bad section start specification `{}`", self.val); + } + } + } + + pub fn resolve_as_edge( &self, all_sections: &HashMap, ) -> Result { @@ -184,7 +200,7 @@ pub(crate) mod syntax { // TODO: should be pub crate /// A handle for an animation section inside a sprite #[derive(Debug, Copy, Clone)] -pub struct AnimSectionHandle(usize); +pub struct AnimSectionHandle(pub(crate) usize); /// An edge between two animation sections #[derive(Debug, Clone)] @@ -211,6 +227,22 @@ pub enum SectionEdge { Restart, } +/// Where to start an animation +#[derive(Debug, Clone)] +pub enum SpriteStart { + /// 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, + }, +} + /// Represents a sprite that may be used in the game. #[derive(Debug, Clone)] pub struct Sprite { @@ -220,8 +252,8 @@ pub struct Sprite { /// This sprite's handle pub handle: SpriteHandle, - /// This sprite's default section - pub default_section: AnimSectionHandle, + /// Where this sprite starts playing + pub start_at: SpriteStart, /// This sprite's animation sections sections: Vec, @@ -236,6 +268,14 @@ impl Sprite { &self.sections[section.0] } + /// Get this sprite's first frame + pub fn get_first_frame(&self) -> u32 { + match self.start_at { + SpriteStart::Bot { section } => *self.get_section(section).frames.last().unwrap(), + SpriteStart::Top { section } => *self.get_section(section).frames.first().unwrap(), + } + } + /// Iterate this sprite's sections pub fn iter_sections(&self) -> impl Iterator { self.sections.iter() @@ -293,20 +333,13 @@ impl crate::Build for Sprite { aspect, }; - if sprite_name == content.config.starfield_sprite { - if content.starfield_handle.is_none() { - content.starfield_handle = Some(h) - } else { - // This can't happen, since this is a hashmap. - unreachable!("Found two starfield sprites! Something is very wrong.") - } - } - content.sprite_index.insert(sprite_name.clone(), h); content.sprites.push(Self { name: sprite_name, - default_section: AnimSectionHandle(0), + start_at: SpriteStart::Top { + section: AnimSectionHandle(0), + }, sections: vec![SpriteSection { frames: vec![img.idx], // We implement unanimated sprites with a very fast framerate @@ -333,11 +366,6 @@ impl crate::Build for Sprite { aspect, }; - // TODO: remove? - if sprite_name == content.config.starfield_sprite { - unreachable!("Starfield texture may not be animated") - } - let mut sections = Vec::new(); sections.push(section); @@ -345,7 +373,9 @@ impl crate::Build for Sprite { content.sprites.push(Self { name: sprite_name, sections, - default_section: AnimSectionHandle(0), + start_at: SpriteStart::Bot { + section: AnimSectionHandle(0), + }, handle: h, aspect, }); @@ -358,13 +388,10 @@ impl crate::Build for Sprite { 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 start_at = s + .start_at + .resolve_as_start(§ion_names) + .with_context(|| format!("while loading sprite `{}`", sprite_name))?; let mut sections = Vec::with_capacity(idx); let mut dim = None; @@ -405,7 +432,7 @@ impl crate::Build for Sprite { content.sprites.push(Self { name: sprite_name, sections, - default_section: *section_names.get(&s.default_section).unwrap(), + start_at, handle: h, aspect, }); @@ -413,13 +440,6 @@ impl crate::Build for Sprite { } } - if content.starfield_handle.is_none() { - bail!( - "Could not find a starfield texture (name: `{}`)", - content.config.starfield_sprite - ) - } - return Ok(()); } } diff --git a/crates/render/src/gpustate/phys.rs b/crates/render/src/gpustate/phys.rs index ccb3844..7d5479e 100644 --- a/crates/render/src/gpustate/phys.rs +++ b/crates/render/src/gpustate/phys.rs @@ -240,7 +240,7 @@ impl GPUState { ); let sprite = state.ct.get_sprite(o.sprite); - let texture_a = sprite.get_section(sprite.default_section).frames[0]; // ANIMATE + let texture_a = sprite.get_first_frame(); // ANIMATE // Push this object's instance self.state.push_object_buffer(ObjectInstance { diff --git a/crates/render/src/gpustate/render.rs b/crates/render/src/gpustate/render.rs index 40f7edc..ebb65f3 100644 --- a/crates/render/src/gpustate/render.rs +++ b/crates/render/src/gpustate/render.rs @@ -49,7 +49,7 @@ impl<'a> super::GPUState { // Write all new particles to GPU buffer for i in input.particles.iter() { let sprite = input.ct.get_sprite(i.sprite); - let texture_a = sprite.get_section(sprite.default_section).frames[0]; // ANIMATE + let texture_a = sprite.get_first_frame(); // ANIMATE self.state.push_particle_buffer(ParticleInstance { position: [i.pos.x, i.pos.y], @@ -306,7 +306,7 @@ impl<'a> super::GPUState { ], window_scale: [self.state.window.scale_factor() as f32, 0.0], window_aspect: [self.state.window_aspect, 0.0], - starfield_sprite: [input.ct.get_starfield_texture(), 0], + starfield_sprite: [input.ct.get_config().starfield_texture, 0], starfield_tile_size: [input.ct.get_config().starfield_size, 0.0], starfield_size_limits: [ input.ct.get_config().starfield_min_size, diff --git a/crates/render/src/ui/radar.rs b/crates/render/src/ui/radar.rs index 3c0b77a..6b398c1 100644 --- a/crates/render/src/ui/radar.rs +++ b/crates/render/src/ui/radar.rs @@ -61,7 +61,7 @@ impl Radar { let arrow_sprite = input.ct.get_sprite_handle("ui::centerarrow"); let sprite = input.ct.get_sprite(input.ct.get_sprite_handle("ui::radar")); - let texture_a = sprite.get_section(sprite.default_section).frames[0]; // ANIMATE + let texture_a = sprite.get_first_frame(); // ANIMATE // Push this object's instance state.push_ui_buffer(UiInstance { @@ -92,7 +92,7 @@ impl Radar { } let sprite = input.ct.get_sprite(planet_sprite); - let texture_a = sprite.get_section(sprite.default_section).frames[0]; // ANIMATE + let texture_a = sprite.get_first_frame(); // ANIMATE // Push this object's instance state.push_ui_buffer(UiInstance { @@ -153,7 +153,7 @@ impl Radar { + (d * (radar_size / 2.0)); let sprite = input.ct.get_sprite(ship_sprite); - let texture_a = sprite.get_section(sprite.default_section).frames[0]; // ANIMATE + let texture_a = sprite.get_first_frame(); // ANIMATE state.push_ui_buffer(UiInstance { anchor: PositionAnchor::NwC.to_int(), @@ -181,7 +181,7 @@ impl Radar { let size = 7.0f32.min((0.8 - m) * 70.0); let sprite = input.ct.get_sprite(sprite); - let texture_a = sprite.get_section(sprite.default_section).frames[0]; // ANIMATE + let texture_a = sprite.get_first_frame(); // ANIMATE state.push_ui_buffer(UiInstance { anchor: PositionAnchor::NwNw.to_int(), @@ -253,7 +253,7 @@ impl Radar { + Rotation2::new(angle) * Vector2::new(0.915 * (radar_size / 2.0), 0.0); let sprite = input.ct.get_sprite(arrow_sprite); - let texture_a = sprite.get_section(sprite.default_section).frames[0]; // ANIMATE + let texture_a = sprite.get_first_frame(); // ANIMATE state.push_ui_buffer(UiInstance { anchor: PositionAnchor::NwC.to_int(), diff --git a/crates/render/src/ui/status.rs b/crates/render/src/ui/status.rs index e197692..163b2f3 100644 --- a/crates/render/src/ui/status.rs +++ b/crates/render/src/ui/status.rs @@ -48,7 +48,7 @@ impl Status { let sprite = input .ct .get_sprite(input.ct.get_sprite_handle("ui::status")); - let texture_a = sprite.get_section(sprite.default_section).frames[0]; // ANIMATE + let texture_a = sprite.get_first_frame(); // ANIMATE state.push_ui_buffer(UiInstance { anchor: PositionAnchor::NeNe.to_int(), diff --git a/crates/render/src/ui/util/sprite.rs b/crates/render/src/ui/util/sprite.rs index 03e880d..88fdfc1 100644 --- a/crates/render/src/ui/util/sprite.rs +++ b/crates/render/src/ui/util/sprite.rs @@ -45,7 +45,7 @@ impl UiSprite { } let sprite = input.ct.get_sprite(self.sprite); - let texture_a = sprite.get_section(sprite.default_section).frames[0]; // ANIMATE + let texture_a = sprite.get_first_frame(); // ANIMATE state.push_ui_buffer(UiInstance { anchor: PositionAnchor::CC.to_int(), @@ -59,7 +59,7 @@ impl UiSprite { .mask .map(|x| { let sprite = input.ct.get_sprite(x); - let texture_b = sprite.get_section(sprite.default_section).frames[0]; // ANIMATE + let texture_b = sprite.get_first_frame(); // ANIMATE [1, texture_b] }) .unwrap_or([0, 0]), @@ -101,7 +101,7 @@ impl UiElement for UiSprite { } let sprite = input.ct.get_sprite(self.sprite); - let texture_a = sprite.get_section(sprite.default_section).frames[0]; // ANIMATE + let texture_a = sprite.get_first_frame(); // ANIMATE state.push_ui_buffer(UiInstance { anchor: PositionAnchor::CNw.to_int(), @@ -115,7 +115,7 @@ impl UiElement for UiSprite { .mask .map(|x| { let sprite = input.ct.get_sprite(x); - let texture_b = sprite.get_section(sprite.default_section).frames[0]; // ANIMATE + let texture_b = sprite.get_first_frame(); // ANIMATE [1, texture_b] }) .unwrap_or([0, 0]),