diff --git a/content/effects.toml b/content/effects.toml new file mode 100644 index 0000000..a320923 --- /dev/null +++ b/content/effects.toml @@ -0,0 +1,29 @@ +[effect."small explosion"] +sprite = "particle::explosion::small" +lifetime = "inherit" +inherit_velocity = "target" +size = 3.0 + +[effect."huge explosion"] +sprite = "particle::explosion::huge" +lifetime = "inherit" +inherit_velocity = "target" +size = 3.0 + +[effect."blaster expire"] +sprite = "particle::blaster" +lifetime = "inherit" +inherit_velocity = "projectile" +size = 3.0 + + +# TODO: +# inherit velocity scale +# absolute velocity/angle (no inherit) +# random lifetime, velocity, angle, spin +# bullet bounce effect: inherit and change velocity +# effect probabilities & variants +# multiple particles in one effect +# fade +# better physics +# document:effect vs particle diff --git a/content/guns.toml b/content/guns.toml index a3e20f9..9efcc2b 100644 --- a/content/guns.toml +++ b/content/guns.toml @@ -29,13 +29,9 @@ projectile.force = 0.0 projectile.collider.ball.radius = 2.0 -projectile.impact.sprite = "particle::explosion" -projectile.impact.lifetime = "inherit" -projectile.impact.inherit_velocity = "target" -projectile.impact.size = 3.0 +projectile.impact_effect = "small explosion" - -projectile.expire.sprite = "particle::blaster" -projectile.expire.lifetime = "inherit" -projectile.expire.inherit_velocity = "projectile" -projectile.expire.size = 3.0 +projectile.expire_effect.sprite = "particle::blaster" +projectile.expire_effect.lifetime = "inherit" +projectile.expire_effect.inherit_velocity = "projectile" +projectile.expire_effect.size = 3.0 diff --git a/content/sprite.toml b/content/sprite.toml index 7ce88b8..ce05826 100644 --- a/content/sprite.toml +++ b/content/sprite.toml @@ -1,3 +1,9 @@ +# TODO: +# random start frame +# repeat once: stay on last frame +# blending mode: alpha / half-alpha / additive + + [sprite."starfield"] file = "starfield.png" @@ -20,8 +26,9 @@ file = "projectile/blaster.png" file = "ship/gypsum.png" [sprite."ship::peregrine"] -duration = 1.3 +timing.duration = 1.3 repeat = "reverse" +random_start_frame = true frames = [ "ship/peregrine/01.png", "ship/peregrine/02.png", @@ -52,7 +59,7 @@ file = "ui/radarframe.png" file = "ui/center-arrow.png" [sprite."particle::blaster"] -duration = 0.15 +timing.duration = 0.15 repeat = "once" frames = [ "particle/blaster/01.png", @@ -62,8 +69,48 @@ frames = [ ] -[sprite."particle::explosion"] -duration = 0.4 +[sprite."particle::explosion::tiny"] +timing.fps = 15 +repeat = "once" +frames = [ + "particle/explosion-tiny/01.png", + "particle/explosion-tiny/02.png", + "particle/explosion-tiny/03.png", + "particle/explosion-tiny/04.png", + "particle/explosion-tiny/05.png", + "particle/explosion-tiny/06.png", +] + +[sprite."particle::explosion::small"] +timing.fps = 15 +repeat = "once" +frames = [ + "particle/explosion-small/01.png", + "particle/explosion-small/02.png", + "particle/explosion-small/03.png", + "particle/explosion-small/04.png", + "particle/explosion-small/05.png", + "particle/explosion-small/06.png", + "particle/explosion-small/07.png", +] + +[sprite."particle::explosion::medium"] +timing.fps = 15 +repeat = "once" +frames = [ + "particle/explosion-medium/01.png", + "particle/explosion-medium/02.png", + "particle/explosion-medium/03.png", + "particle/explosion-medium/04.png", + "particle/explosion-medium/05.png", + "particle/explosion-medium/06.png", + "particle/explosion-medium/07.png", + "particle/explosion-medium/08.png", +] + + +[sprite."particle::explosion::large"] +timing.fps = 15 repeat = "once" frames = [ "particle/explosion-large/01.png", @@ -76,3 +123,19 @@ frames = [ "particle/explosion-large/08.png", "particle/explosion-large/09.png", ] + +[sprite."particle::explosion::huge"] +timing.fps = 15 +repeat = "once" +frames = [ + "particle/explosion-huge/01.png", + "particle/explosion-huge/02.png", + "particle/explosion-huge/03.png", + "particle/explosion-huge/04.png", + "particle/explosion-huge/05.png", + "particle/explosion-huge/06.png", + "particle/explosion-huge/07.png", + "particle/explosion-huge/08.png", + "particle/explosion-huge/09.png", + "particle/explosion-huge/10.png", +] diff --git a/crates/content/src/handle.rs b/crates/content/src/handle.rs index be0fdf3..3fd7680 100644 --- a/crates/content/src/handle.rs +++ b/crates/content/src/handle.rs @@ -10,17 +10,20 @@ use std::{cmp::Eq, hash::Hash}; /// A lightweight representation of a sprite #[derive(Debug, Clone, Copy)] pub struct SpriteHandle { - /// The index of this sprite in content.sprites - /// This must be public, since render uses this to - /// select sprites. - /// - /// This is a u32 for that same reason, too. - pub index: u32, + pub(crate) index: usize, /// The aspect ratio of this sprite (width / height) pub aspect: f32, } +impl SpriteHandle { + /// The index of this sprite in content's sprite array. + /// Render uses this to build its buffers. + pub fn get_index(&self) -> u32 { + self.index as u32 + } +} + impl Hash for SpriteHandle { fn hash(&self, state: &mut H) { self.index.hash(state) @@ -37,7 +40,7 @@ impl PartialEq for SpriteHandle { /// A lightweight representation of an outfit #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct OutfitHandle { - /// TODO + /// TODO: pub in crate, currently for debug (same with all other handles) pub index: usize, } @@ -65,7 +68,12 @@ pub struct SystemHandle { /// A lightweight representation of a faction #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct FactionHandle { - /// The index of this faction in content.factions - /// TODO: pub in crate, currently for debug (same with all other handles) + /// TODO pub index: usize, } + +/// A lightweight representation of an effect +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct EffectHandle { + pub(crate) index: usize, +} diff --git a/crates/content/src/lib.rs b/crates/content/src/lib.rs index b9197c0..43614f6 100644 --- a/crates/content/src/lib.rs +++ b/crates/content/src/lib.rs @@ -18,10 +18,12 @@ use std::{ use toml; use walkdir::WalkDir; -pub use handle::{FactionHandle, GunHandle, OutfitHandle, ShipHandle, SpriteHandle, SystemHandle}; +pub use handle::{ + EffectHandle, FactionHandle, GunHandle, OutfitHandle, ShipHandle, SpriteHandle, SystemHandle, +}; pub use part::{ - EnginePoint, Faction, Gun, GunPoint, ImpactInheritVelocity, Outfit, OutfitSpace, Projectile, - ProjectileCollider, ProjectileParticle, Relationship, RepeatMode, Ship, Sprite, System, + Effect, EnginePoint, Faction, Gun, GunPoint, ImpactInheritVelocity, Outfit, OutfitSpace, + Projectile, ProjectileCollider, Relationship, RepeatMode, Ship, Sprite, System, }; mod syntax { @@ -29,7 +31,7 @@ mod syntax { use serde::Deserialize; use std::{collections::HashMap, fmt::Display, hash::Hash}; - use crate::part::{faction, gun, outfit, ship, sprite, system}; + use crate::part::{effect, faction, gun, outfit, ship, sprite, system}; #[derive(Debug, Deserialize)] pub struct Root { @@ -39,6 +41,7 @@ mod syntax { pub outfit: Option>, pub sprite: Option>, pub faction: Option>, + pub effect: Option>, } fn merge_hashmap( @@ -75,6 +78,7 @@ mod syntax { outfit: None, sprite: None, faction: None, + effect: None, } } @@ -89,6 +93,8 @@ mod syntax { .with_context(|| "while merging sprites")?; merge_hashmap(&mut self.faction, other.faction) .with_context(|| "while merging factions")?; + merge_hashmap(&mut self.effect, other.effect) + .with_context(|| "while merging effects")?; return Ok(()); } } @@ -98,11 +104,29 @@ trait Build { type InputSyntaxType; /// Build a processed System struct from raw serde data - fn build(root: Self::InputSyntaxType, ct: &mut Content) -> Result<()> + fn build( + root: Self::InputSyntaxType, + build_context: &mut ContentBuildContext, + content: &mut Content, + ) -> Result<()> where Self: Sized; } +/// Stores temporary data while building context objects +#[derive(Debug)] +pub(crate) struct ContentBuildContext { + pub effect_index: HashMap, +} + +impl ContentBuildContext { + fn new() -> Self { + Self { + effect_index: HashMap::new(), + } + } +} + /// Represents static game content #[derive(Debug)] pub struct Content { @@ -113,30 +137,22 @@ pub struct Content { starfield_sprite_name: String, /// Sprites - pub sprites: Vec, + pub sprites: Vec, /// 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, + sprite_index: HashMap, /// The texture to use for starfield stars - starfield_handle: Option, + starfield_handle: Option, /// Keeps track of which images are in which texture sprite_atlas: SpriteAtlas, - /// Outfits - outfits: Vec, - - /// Ship guns - guns: Vec, - - /// Ship bodies - ships: Vec, - - /// Star systems - systems: Vec, - - /// Factions - factions: Vec, + outfits: Vec, + guns: Vec, // TODO: merge with outfit + ships: Vec, + systems: Vec, + factions: Vec, + effects: Vec, } // Loading methods @@ -189,6 +205,8 @@ impl Content { toml::from_str(&file_string)? }; + let mut build_context = ContentBuildContext::new(); + let mut content = Self { sprite_atlas: atlas, systems: Vec::new(), @@ -197,33 +215,58 @@ impl Content { outfits: Vec::new(), sprites: Vec::new(), factions: Vec::new(), + effects: Vec::new(), sprite_index: HashMap::new(), starfield_handle: None, image_root: texture_root, starfield_sprite_name: starfield_texture_name, }; - // Order here matters, usually - if root.sprite.is_some() { - part::sprite::Sprite::build(root.sprite.take().unwrap(), &mut content)?; - } - // TODO: enforce sprite and image limits + // Order matters. Some content types require another to be fully initialized + if root.sprite.is_some() { + part::sprite::Sprite::build( + root.sprite.take().unwrap(), + &mut build_context, + &mut content, + )?; + } + if root.effect.is_some() { + part::effect::Effect::build( + root.effect.take().unwrap(), + &mut build_context, + &mut content, + )?; + } + + // Order below this line does not matter if root.ship.is_some() { - part::ship::Ship::build(root.ship.take().unwrap(), &mut content)?; + part::ship::Ship::build(root.ship.take().unwrap(), &mut build_context, &mut content)?; } if root.gun.is_some() { - part::gun::Gun::build(root.gun.take().unwrap(), &mut content)?; + part::gun::Gun::build(root.gun.take().unwrap(), &mut build_context, &mut content)?; } if root.outfit.is_some() { - part::outfit::Outfit::build(root.outfit.take().unwrap(), &mut content)?; + part::outfit::Outfit::build( + root.outfit.take().unwrap(), + &mut build_context, + &mut content, + )?; } if root.system.is_some() { - part::system::System::build(root.system.take().unwrap(), &mut content)?; + part::system::System::build( + root.system.take().unwrap(), + &mut build_context, + &mut content, + )?; } if root.faction.is_some() { - part::faction::Faction::build(root.faction.take().unwrap(), &mut content)?; + part::faction::Faction::build( + root.faction.take().unwrap(), + &mut build_context, + &mut content, + )?; } return Ok(content); @@ -289,4 +332,9 @@ impl Content { pub fn get_faction(&self, h: FactionHandle) -> &Faction { return &self.factions[h.index]; } + + /// Get an effect from a handle + pub fn get_effect(&self, h: EffectHandle) -> &Effect { + return &self.effects[h.index]; + } } diff --git a/crates/content/src/part/effect.rs b/crates/content/src/part/effect.rs new file mode 100644 index 0000000..1d9a078 --- /dev/null +++ b/crates/content/src/part/effect.rs @@ -0,0 +1,154 @@ +use anyhow::{Context, Result}; +use serde::Deserialize; +use std::collections::HashMap; + +use crate::{handle::SpriteHandle, Content, ContentBuildContext, EffectHandle}; + +pub(crate) mod syntax { + use anyhow::{bail, Result}; + use serde::Deserialize; + + use crate::{Content, ContentBuildContext, EffectHandle}; + // Raw serde syntax structs. + // These are never seen by code outside this crate. + + #[derive(Debug, Deserialize)] + pub struct Effect { + pub sprite: String, + pub lifetime: EffectLifetime, + pub inherit_velocity: super::ImpactInheritVelocity, + pub size: f32, + } + + // We implement building here instead of in super::Effect because + // effects may be defined inline (see EffectReference). + impl Effect { + pub fn add_to( + self, + _build_context: &mut ContentBuildContext, + content: &mut Content, + ) -> Result { + let sprite = match content.sprite_index.get(&self.sprite) { + None => bail!("sprite `{}` doesn't exist", self.sprite), + Some(t) => *t, + }; + + let lifetime = match self.lifetime { + EffectLifetime::Seconds(s) => s, + EffectLifetime::Inherit(s) => { + if s == "inherit" { + let sprite = content.get_sprite(sprite); + sprite.fps * sprite.frames.len() as f32 + } else { + bail!("bad effect lifetime, must be float or \"inherit\"",) + } + } + }; + + let handle = EffectHandle { + index: content.effects.len(), + }; + content.effects.push(super::Effect { + sprite, + lifetime, + handle, + inherit_velocity: self.inherit_velocity, + size: self.size, + }); + + return Ok(handle); + } + } + + #[derive(Debug, Deserialize)] + #[serde(untagged)] + pub enum EffectLifetime { + Inherit(String), + Seconds(f32), + } + + // This isn't used here, but is pulled in by other content items. + /// A reference to an effect by name, or an inline definition. + #[derive(Debug, Deserialize)] + #[serde(untagged)] + pub enum EffectReference { + Label(String), + Effect(Effect), + } + + impl EffectReference { + pub fn to_handle( + self, + build_context: &mut ContentBuildContext, + content: &mut Content, + ) -> Result { + // We do not insert anything into build_context here, + // since inline effects cannot be referenced by name. + Ok(match self { + Self::Effect(e) => e.add_to(build_context, content)?, + Self::Label(l) => match build_context.effect_index.get(&l) { + Some(h) => *h, + None => bail!("no effect named `{}`", l), + }, + }) + } + } +} + +/// How we should set an effect's velocity +#[derive(Debug, Deserialize, Clone)] +pub enum ImpactInheritVelocity { + /// Don't inherit any velocity. + /// This impact particle will be still. + #[serde(rename = "don't")] + Dont, + + /// Inherit target velocity. + /// This impact particle will stick to the object it hits. + #[serde(rename = "target")] + Target, + + /// Inherit projectile velocity. + /// This impact particle will continue on its projectile's path. + #[serde(rename = "projectile")] + Projectile, +} + +/// The particle a projectile will spawn when it hits something +#[derive(Debug, Clone)] +pub struct Effect { + /// The sprite to use for this particle. + /// This is most likely animated. + pub sprite: SpriteHandle, + + /// This effect's handle + pub handle: EffectHandle, + + /// How many seconds this particle should live + pub lifetime: f32, + + /// How we should set this particle's velocity + pub inherit_velocity: ImpactInheritVelocity, + + /// The height of this particle, in game units. + pub size: f32, +} + +impl crate::Build for Effect { + type InputSyntaxType = HashMap; + + fn build( + effects: Self::InputSyntaxType, + build_context: &mut ContentBuildContext, + content: &mut Content, + ) -> Result<()> { + for (effect_name, effect) in effects { + let h = effect + .add_to(build_context, content) + .with_context(|| format!("while evaluating effect `{}`", effect_name))?; + build_context.effect_index.insert(effect_name, h); + } + + return Ok(()); + } +} diff --git a/crates/content/src/part/faction.rs b/crates/content/src/part/faction.rs index 851674e..418b930 100644 --- a/crates/content/src/part/faction.rs +++ b/crates/content/src/part/faction.rs @@ -2,7 +2,7 @@ use anyhow::{bail, Result}; use serde::Deserialize; use std::collections::HashMap; -use crate::{handle::FactionHandle, Content}; +use crate::{handle::FactionHandle, Content, ContentBuildContext}; pub(crate) mod syntax { use std::collections::HashMap; @@ -61,13 +61,17 @@ pub struct Faction { impl crate::Build for Faction { type InputSyntaxType = HashMap; - fn build(factions: Self::InputSyntaxType, ct: &mut Content) -> Result<()> { + fn build( + factions: Self::InputSyntaxType, + _build_context: &mut ContentBuildContext, + content: &mut Content, + ) -> Result<()> { // Keeps track of position in faction array. // This lets us build FactionHandles before finishing all factions. let faction_names: Vec = factions.keys().map(|x| x.to_owned()).collect(); // Indexing will break if this is false. - assert!(ct.factions.len() == 0); + assert!(content.factions.len() == 0); for f_idx in 0..faction_names.len() { let faction_name = &faction_names[f_idx]; @@ -112,7 +116,7 @@ impl crate::Build for Faction { ); } - ct.factions.push(Self { + content.factions.push(Self { name: faction_name.to_owned(), handle: h, relationships, diff --git a/crates/content/src/part/gun.rs b/crates/content/src/part/gun.rs index 56bc56b..8bb97b9 100644 --- a/crates/content/src/part/gun.rs +++ b/crates/content/src/part/gun.rs @@ -5,10 +5,11 @@ use std::collections::HashMap; use crate::{handle::SpriteHandle, Content}; -use crate::OutfitSpace; +use crate::{ContentBuildContext, EffectHandle, OutfitSpace}; pub(crate) mod syntax { - use crate::part::shared; + use crate::part::effect; + use crate::part::outfitspace; use serde::Deserialize; // Raw serde syntax structs. // These are never seen by code outside this crate. @@ -18,7 +19,7 @@ pub(crate) mod syntax { pub projectile: Projectile, pub rate: f32, pub rate_rng: f32, - pub space: shared::syntax::OutfitSpace, + pub space: outfitspace::syntax::OutfitSpace, } #[derive(Debug, Deserialize)] @@ -32,26 +33,11 @@ pub(crate) mod syntax { pub lifetime_rng: f32, pub damage: f32, pub angle_rng: f32, - pub impact: Option, - pub expire: Option, + pub impact_effect: Option, + pub expire_effect: Option, pub collider: super::ProjectileCollider, pub force: f32, } - - #[derive(Debug, Deserialize)] - pub struct ProjectileParticle { - pub sprite: String, - pub lifetime: ParticleLifetime, - pub inherit_velocity: super::ImpactInheritVelocity, - pub size: f32, - } - - #[derive(Debug, Deserialize)] - #[serde(untagged)] - pub enum ParticleLifetime { - Inherit(String), - Seconds(f32), - } } /// Defines a projectile's collider @@ -67,25 +53,6 @@ pub struct BallCollider { pub radius: f32, } -/// How we should set an impact particle's velocity -#[derive(Debug, Deserialize, Clone)] -pub enum ImpactInheritVelocity { - /// Don't inherit any velocity. - /// This impact particle will be still. - #[serde(rename = "don't")] - Dont, - - /// Inherit target velocity. - /// This impact particle will stick to the object it hits. - #[serde(rename = "target")] - Target, - - /// Inherit projectile velocity. - /// This impact particle will continue on its projectile's path. - #[serde(rename = "projectile")] - Projectile, -} - /// Represents a gun outfit. #[derive(Debug, Clone)] pub struct Gun { @@ -143,86 +110,50 @@ pub struct Projectile { pub angle_rng: Deg, /// The particle this projectile will spawn when it hits something - pub impact_particle: Option, + pub impact_effect: Option, /// The particle this projectile will spawn when it expires - pub expire_particle: Option, + pub expire_effect: Option, /// Collider parameters for this projectile pub collider: ProjectileCollider, } -/// The particle a projectile will spawn when it hits something -#[derive(Debug, Clone)] -pub struct ProjectileParticle { - /// The sprite to use for this particle. - /// This is most likely animated. - pub sprite: SpriteHandle, - - /// How many seconds this particle should live - pub lifetime: f32, - - /// How we should set this particle's velocity - pub inherit_velocity: ImpactInheritVelocity, - - /// The height of this particle, in game units. - pub size: f32, -} - -fn parse_projectile_particle( - ct: &Content, - p: Option, -) -> Result> { - if let Some(impact) = p { - let impact_sprite_handle = match ct.sprite_index.get(&impact.sprite) { - None => bail!("impact sprite `{}` doesn't exist", impact.sprite), - Some(t) => *t, - }; - - let impact_lifetime = match impact.lifetime { - syntax::ParticleLifetime::Seconds(s) => s, - syntax::ParticleLifetime::Inherit(s) => { - if s == "inherit" { - let sprite = ct.get_sprite(impact_sprite_handle); - sprite.fps * sprite.frames.len() as f32 - } else { - bail!("bad impact lifetime, must be float or \"inherit\"",) - } - } - }; - - Ok(Some(ProjectileParticle { - sprite: impact_sprite_handle, - lifetime: impact_lifetime, - inherit_velocity: impact.inherit_velocity, - size: impact.size, - })) - } else { - Ok(None) - } -} - impl crate::Build for Gun { type InputSyntaxType = HashMap; - fn build(gun: Self::InputSyntaxType, ct: &mut Content) -> Result<()> { + fn build( + gun: Self::InputSyntaxType, + build_context: &mut ContentBuildContext, + content: &mut Content, + ) -> Result<()> { for (gun_name, gun) in gun { - let projectile_sprite_handle = match ct.sprite_index.get(&gun.projectile.sprite) { + let projectile_sprite_handle = match content.sprite_index.get(&gun.projectile.sprite) { None => bail!( - "In gun `{}`: projectile sprite `{}` doesn't exist", + "projectile sprite `{}` doesn't exist in gun `{}`", + gun.projectile.sprite, gun_name, - gun.projectile.sprite ), Some(t) => *t, }; - let impact_particle = parse_projectile_particle(ct, gun.projectile.impact) - .with_context(|| format!("In gun `{}`", gun_name))?; + let impact_effect = match gun.projectile.impact_effect { + Some(e) => Some( + e.to_handle(build_context, content) + .with_context(|| format!("while loading gun `{}`", gun_name))?, + ), + None => None, + }; - let expire_particle = parse_projectile_particle(ct, gun.projectile.expire) - .with_context(|| format!("In gun `{}`", gun_name))?; + let expire_effect = match gun.projectile.expire_effect { + Some(e) => Some( + e.to_handle(build_context, content) + .with_context(|| format!("while loading gun `{}`", gun_name))?, + ), + None => None, + }; - ct.guns.push(Self { + content.guns.push(Self { name: gun_name, space: gun.space.into(), rate: gun.rate, @@ -238,8 +169,8 @@ impl crate::Build for Gun { lifetime_rng: gun.projectile.lifetime_rng, damage: gun.projectile.damage, angle_rng: Deg(gun.projectile.angle_rng), - impact_particle, - expire_particle, + impact_effect, + expire_effect, collider: gun.projectile.collider, }, }); diff --git a/crates/content/src/part/mod.rs b/crates/content/src/part/mod.rs index 0da4d35..dc6280c 100644 --- a/crates/content/src/part/mod.rs +++ b/crates/content/src/part/mod.rs @@ -1,17 +1,19 @@ //! Content parts +pub mod effect; pub mod faction; pub mod gun; pub mod outfit; -mod shared; +pub mod outfitspace; pub mod ship; pub mod sprite; pub mod system; +pub use effect::{Effect, ImpactInheritVelocity}; pub use faction::{Faction, Relationship}; -pub use gun::{Gun, ImpactInheritVelocity, Projectile, ProjectileCollider, ProjectileParticle}; +pub use gun::{Gun, Projectile, ProjectileCollider}; pub use outfit::Outfit; -pub use shared::OutfitSpace; +pub use outfitspace::OutfitSpace; pub use ship::{EnginePoint, GunPoint, Ship}; pub use sprite::{RepeatMode, Sprite}; pub use system::{Object, System}; diff --git a/crates/content/src/part/outfit.rs b/crates/content/src/part/outfit.rs index 3478bc3..e3ef793 100644 --- a/crates/content/src/part/outfit.rs +++ b/crates/content/src/part/outfit.rs @@ -2,10 +2,10 @@ use std::collections::HashMap; use anyhow::{bail, Result}; -use crate::{handle::SpriteHandle, Content, OutfitSpace}; +use crate::{handle::SpriteHandle, Content, ContentBuildContext, OutfitSpace}; pub(crate) mod syntax { - use crate::part::shared; + use crate::part::outfitspace; use serde::Deserialize; // Raw serde syntax structs. // These are never seen by code outside this crate. @@ -14,7 +14,7 @@ pub(crate) mod syntax { pub struct Outfit { pub engine: Option, pub steering: Option, - pub space: shared::syntax::OutfitSpace, + pub space: outfitspace::syntax::OutfitSpace, } #[derive(Debug, Deserialize)] @@ -53,7 +53,11 @@ pub struct Outfit { impl crate::Build for Outfit { type InputSyntaxType = HashMap; - fn build(outfits: Self::InputSyntaxType, ct: &mut Content) -> Result<()> { + fn build( + outfits: Self::InputSyntaxType, + _build_context: &mut ContentBuildContext, + content: &mut Content, + ) -> Result<()> { for (outfit_name, outfit) in outfits { let mut o = Self { name: outfit_name.clone(), @@ -65,7 +69,7 @@ impl crate::Build for Outfit { // Engine stats if let Some(engine) = outfit.engine { - let th = match ct.sprite_index.get(&engine.flare_sprite) { + let th = match content.sprite_index.get(&engine.flare_sprite) { None => bail!( "In outfit `{}`: flare sprite `{}` doesn't exist", outfit_name, @@ -82,7 +86,7 @@ impl crate::Build for Outfit { o.steer_power = steer.power; } - ct.outfits.push(o); + content.outfits.push(o); } return Ok(()); diff --git a/crates/content/src/part/shared.rs b/crates/content/src/part/outfitspace.rs similarity index 100% rename from crates/content/src/part/shared.rs rename to crates/content/src/part/outfitspace.rs diff --git a/crates/content/src/part/ship.rs b/crates/content/src/part/ship.rs index b80a90c..412dd7a 100644 --- a/crates/content/src/part/ship.rs +++ b/crates/content/src/part/ship.rs @@ -4,10 +4,10 @@ use anyhow::{bail, Result}; use cgmath::Point2; use nalgebra::{point, Point}; -use crate::{handle::SpriteHandle, Content, OutfitSpace}; +use crate::{handle::SpriteHandle, Content, ContentBuildContext, OutfitSpace}; pub(crate) mod syntax { - use crate::part::shared; + use crate::part::outfitspace; use serde::Deserialize; // Raw serde syntax structs. @@ -24,7 +24,7 @@ pub(crate) mod syntax { pub collision: Collision, pub angular_drag: f32, pub linear_drag: f32, - pub space: shared::syntax::OutfitSpace, + pub space: outfitspace::syntax::OutfitSpace, } #[derive(Debug, Deserialize)] @@ -125,9 +125,13 @@ pub struct GunPoint { impl crate::Build for Ship { type InputSyntaxType = HashMap; - fn build(ship: Self::InputSyntaxType, ct: &mut Content) -> Result<()> { + fn build( + ship: Self::InputSyntaxType, + _build_context: &mut ContentBuildContext, + content: &mut Content, + ) -> Result<()> { for (ship_name, ship) in ship { - let handle = match ct.sprite_index.get(&ship.sprite) { + let handle = match content.sprite_index.get(&ship.sprite) { None => bail!( "In ship `{}`: sprite `{}` doesn't exist", ship_name, @@ -137,9 +141,9 @@ impl crate::Build for Ship { }; let size = ship.size; - let aspect = ct.get_sprite(handle).aspect; + let aspect = content.get_sprite(handle).aspect; - ct.ships.push(Self { + content.ships.push(Self { aspect, name: ship_name, sprite: handle, diff --git a/crates/content/src/part/sprite.rs b/crates/content/src/part/sprite.rs index 2b901d8..2283809 100644 --- a/crates/content/src/part/sprite.rs +++ b/crates/content/src/part/sprite.rs @@ -3,7 +3,7 @@ use image::io::Reader; use serde::Deserialize; use std::{collections::HashMap, path::PathBuf}; -use crate::{handle::SpriteHandle, Content}; +use crate::{handle::SpriteHandle, Content, ContentBuildContext}; pub(crate) mod syntax { use serde::Deserialize; @@ -29,8 +29,18 @@ pub(crate) mod syntax { #[derive(Debug, Deserialize)] pub struct FrameSprite { pub frames: Vec, - pub duration: f32, + pub timing: Timing, pub repeat: RepeatMode, + pub random_start_frame: Option, + } + + #[derive(Debug, Deserialize)] + pub enum Timing { + #[serde(rename = "duration")] + Duration(f32), + + #[serde(rename = "fps")] + Fps(f32), } } @@ -84,16 +94,23 @@ pub struct Sprite { /// Aspect ratio of this sprite (width / height) pub aspect: f32, + + /// If true, start on a random frame of this sprite. + pub random_start_frame: bool, } impl crate::Build for Sprite { type InputSyntaxType = HashMap; - fn build(sprites: Self::InputSyntaxType, ct: &mut Content) -> Result<()> { + fn build( + sprites: Self::InputSyntaxType, + _build_context: &mut ContentBuildContext, + content: &mut Content, + ) -> Result<()> { for (sprite_name, t) in sprites { match t { syntax::Sprite::Static(t) => { - let file = ct.image_root.join(&t.file); + let file = content.image_root.join(&t.file); let reader = Reader::open(&file).with_context(|| { format!( "Failed to read file `{}` in sprite `{}`", @@ -110,34 +127,35 @@ impl crate::Build for Sprite { })?; let h = SpriteHandle { - index: ct.sprites.len() as u32, + index: content.sprites.len(), aspect: dim.0 as f32 / dim.1 as f32, }; - if sprite_name == ct.starfield_sprite_name { - if ct.starfield_handle.is_none() { - ct.starfield_handle = Some(h) + if sprite_name == content.starfield_sprite_name { + 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.") } } - ct.sprite_index.insert(sprite_name.clone(), h); + content.sprite_index.insert(sprite_name.clone(), h); - ct.sprites.push(Self { + content.sprites.push(Self { name: sprite_name, frames: vec![t.file], fps: 0.0, handle: h, repeat: RepeatMode::Once, aspect: dim.0 as f32 / dim.1 as f32, + random_start_frame: false, }); } syntax::Sprite::Frames(t) => { let mut dim = None; for f in &t.frames { - let file = ct.image_root.join(f); + let file = content.image_root.join(f); let reader = Reader::open(&file).with_context(|| { format!( "Failed to read file `{}` in sprite `{}`", @@ -167,33 +185,37 @@ impl crate::Build for Sprite { let dim = dim.unwrap(); let h = SpriteHandle { - index: ct.sprites.len() as u32, + index: content.sprites.len(), aspect: dim.0 as f32 / dim.1 as f32, }; - if sprite_name == ct.starfield_sprite_name { + if sprite_name == content.starfield_sprite_name { unreachable!("Starfield texture may not be animated") } - let fps = t.duration / t.frames.len() as f32; + let fps = match t.timing { + syntax::Timing::Duration(d) => d / t.frames.len() as f32, + syntax::Timing::Fps(f) => 1.0 / f, + }; - ct.sprite_index.insert(sprite_name.clone(), h); - ct.sprites.push(Self { + content.sprite_index.insert(sprite_name.clone(), h); + content.sprites.push(Self { name: sprite_name, frames: t.frames, fps, handle: h, repeat: t.repeat, aspect: dim.0 as f32 / dim.1 as f32, + random_start_frame: t.random_start_frame.unwrap_or(false), }); } } } - if ct.starfield_handle.is_none() { + if content.starfield_handle.is_none() { bail!( "Could not find a starfield texture (name: `{}`)", - ct.starfield_sprite_name + content.starfield_sprite_name ) } diff --git a/crates/content/src/part/system.rs b/crates/content/src/part/system.rs index cf186bc..c62ee6f 100644 --- a/crates/content/src/part/system.rs +++ b/crates/content/src/part/system.rs @@ -2,7 +2,7 @@ use anyhow::{bail, Context, Result}; use cgmath::{Deg, Point3}; use std::collections::{HashMap, HashSet}; -use crate::{handle::SpriteHandle, util::Polar, Content}; +use crate::{handle::SpriteHandle, util::Polar, Content, ContentBuildContext}; pub(crate) mod syntax { use serde::Deserialize; @@ -177,7 +177,11 @@ fn resolve_position( impl crate::Build for System { type InputSyntaxType = HashMap; - fn build(system: Self::InputSyntaxType, ct: &mut Content) -> Result<()> { + fn build( + system: Self::InputSyntaxType, + _build_context: &mut ContentBuildContext, + content: &mut Content, + ) -> Result<()> { for (system_name, system) in system { let mut objects = Vec::new(); @@ -185,7 +189,7 @@ impl crate::Build for System { let mut cycle_detector = HashSet::new(); cycle_detector.insert(label.clone()); - let handle = match ct.sprite_index.get(&obj.sprite) { + let handle = match content.sprite_index.get(&obj.sprite) { None => bail!( "In system `{}`: sprite `{}` doesn't exist", system_name, @@ -203,7 +207,7 @@ impl crate::Build for System { }); } - ct.systems.push(Self { + content.systems.push(Self { name: system_name, objects, }); diff --git a/crates/render/shaders/include/animate.wgsl b/crates/render/shaders/include/animate.wgsl index 507ae79..c2eaab6 100644 --- a/crates/render/shaders/include/animate.wgsl +++ b/crates/render/shaders/include/animate.wgsl @@ -11,11 +11,11 @@ fn animate(instance: InstanceInput, age: f32) -> u32 { var frame: u32 = u32(0); - // Repeat + // Once if rep == u32(1) { frame = u32(min( - (age / fps), + age / fps, f32(len) - 1.0 )); diff --git a/crates/render/src/gpustate.rs b/crates/render/src/gpustate.rs index f93d47f..a808c0b 100644 --- a/crates/render/src/gpustate.rs +++ b/crates/render/src/gpustate.rs @@ -380,7 +380,7 @@ impl GPUState { instances.push(ObjectInstance { transform: t.into(), - sprite_index: s.sprite.index, + sprite_index: s.sprite.get_index(), }); // Add children @@ -429,7 +429,7 @@ impl GPUState { instances.push(ObjectInstance { transform: t.into(), - sprite_index: s.sprite.index, + sprite_index: s.sprite.get_index(), }); } @@ -481,7 +481,7 @@ impl GPUState { instances.push(UiInstance { transform: (OPENGL_TO_WGPU_MATRIX * translate * screen_aspect * rotate * scale).into(), - sprite_index: s.sprite.index, + sprite_index: s.sprite.get_index(), color: s.color.unwrap_or([1.0, 1.0, 1.0, 1.0]), }); } @@ -613,7 +613,7 @@ impl GPUState { self.window_size.height as f32, ], window_aspect: [self.window_aspect, 0.0], - starfield_sprite: [s.index, 0], + starfield_sprite: [s.get_index(), 0], starfield_tile_size: [galactica_constants::STARFIELD_SIZE as f32, 0.0], starfield_size_limits: [ galactica_constants::STARFIELD_SIZE_MIN, @@ -633,7 +633,7 @@ impl GPUState { velocity: i.velocity.into(), rotation: Matrix2::from_angle(i.angle).into(), size: i.size, - sprite_index: i.sprite.index, + sprite_index: i.sprite.get_index(), created: state.current_time, expires: state.current_time + i.lifetime, }]), diff --git a/crates/world/src/world.rs b/crates/world/src/world.rs index a70bf50..8f6e641 100644 --- a/crates/world/src/world.rs +++ b/crates/world/src/world.rs @@ -169,9 +169,10 @@ impl<'a> World { .angle(Vector2 { x: 1.0, y: 0.0 }) .into(); - match &projectile.projectile.content.impact_particle { + match &projectile.projectile.content.impact_effect { None => {} Some(x) => { + let x = ct.get_effect(*x); let velocity = match x.inherit_velocity { content::ImpactInheritVelocity::Dont => Vector2 { x: 0.0, y: 0.0 }, content::ImpactInheritVelocity::Projectile => util::rigidbody_velocity(pr), @@ -318,9 +319,10 @@ impl<'a> World { for c in to_remove { let (pr, p) = self.remove_projectile(c).unwrap(); - match &p.projectile.content.expire_particle { + match &p.projectile.content.expire_effect { None => {} Some(x) => { + let x = ct.get_effect(*x); let pos = util::rigidbody_position(&pr); let angle: Deg = util::rigidbody_rotation(&pr) .angle(Vector2 { x: 1.0, y: 0.0 })