Cleaned up starfield and added sprite start edge

master
Mark 2024-01-20 10:47:18 -08:00
parent 7d5b244492
commit 56160a8abe
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
12 changed files with 159 additions and 98 deletions

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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<String, SpriteHandle>,
/// The texture to use for starfield stars
starfield_handle: Option<SpriteHandle>,
/// 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) {

View File

@ -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<super::Config> {
pub fn build(self, asset_root: &Path, atlas: &SpriteAtlas) -> Result<super::Config> {
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

View File

@ -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\"",)

View File

@ -51,7 +51,7 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)]
pub struct CompleteSprite {
pub section: HashMap<String, SpriteSection>,
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<String, AnimSectionHandle>,
) -> Result<super::SpriteStart> {
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<String, AnimSectionHandle>,
) -> Result<super::SectionEdge> {
@ -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<SpriteSection>,
@ -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<Item = &SpriteSection> {
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(&section_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(());
}
}

View File

@ -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 {

View File

@ -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,

View File

@ -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(),

View File

@ -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(),

View File

@ -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]),