use anyhow::{anyhow, Context, Result}; use serde::Deserialize; use std::collections::HashMap; use crate::{ handle::SpriteHandle, resolve_edge_as_edge, Content, ContentBuildContext, EffectHandle, OutfitHandle, OutfitSpace, SectionEdge, }; pub(crate) mod syntax { use crate::{effect, part::outfitspace, sprite::syntax::SectionEdge, ContentBuildContext}; use anyhow::{bail, Result}; use galactica_util::to_radians; use serde::Deserialize; // Raw serde syntax structs. // These are never seen by code outside this crate. #[derive(Debug, Deserialize)] pub struct Outfit { pub thumbnail: String, pub engine: Option, pub steering: Option, pub space: outfitspace::syntax::OutfitSpace, pub shield: Option, pub gun: Option, } #[derive(Debug, Deserialize)] pub struct Shield { pub strength: Option, pub generation: Option, pub delay: Option, // more stats: permiability, shield armor, ramp, etc } #[derive(Debug, Deserialize)] pub struct Engine { pub thrust: f32, pub flare: EngineFlare, } #[derive(Debug, Deserialize)] pub struct EngineFlare { pub sprite: String, pub on_start: Option, pub on_stop: Option, } #[derive(Debug, Deserialize)] pub struct Steering { pub power: f32, } #[derive(Debug, Deserialize)] pub struct Gun { pub projectile: Projectile, pub rate: f32, pub rate_rng: Option, } impl Gun { pub fn build( self, build_context: &mut ContentBuildContext, content: &mut crate::Content, ) -> Result { let projectile_sprite_handle = match content.sprite_index.get(&self.projectile.sprite) { None => bail!( "projectile sprite `{}` doesn't exist", self.projectile.sprite, ), Some(t) => *t, }; let impact_effect = match self.projectile.impact_effect { Some(e) => Some(e.to_handle(build_context, content)?), None => None, }; let expire_effect = match self.projectile.expire_effect { Some(e) => Some(e.to_handle(build_context, content)?), None => None, }; return Ok(super::Gun { rate: self.rate, rate_rng: self.rate_rng.unwrap_or(0.0), projectile: super::Projectile { force: self.projectile.force, sprite: projectile_sprite_handle, size: self.projectile.size, size_rng: self.projectile.size_rng, speed: self.projectile.speed, speed_rng: self.projectile.speed_rng, lifetime: self.projectile.lifetime, lifetime_rng: self.projectile.lifetime_rng, damage: self.projectile.damage, // Divide by 2, so the angle matches the angle of the fire cone. // This should ALWAYS be done in the content parser. angle_rng: to_radians(self.projectile.angle_rng / 2.0).into(), impact_effect, expire_effect, collider: self.projectile.collider, }, }); } } #[derive(Debug, Deserialize)] pub struct Projectile { pub sprite: String, pub size: f32, pub size_rng: f32, pub speed: f32, pub speed_rng: f32, pub lifetime: f32, pub lifetime_rng: f32, pub damage: f32, pub angle_rng: f32, pub impact_effect: Option, pub expire_effect: Option, pub collider: super::ProjectileCollider, pub force: f32, } } /// Represents an outfit that may be attached to a ship. #[derive(Debug, Clone)] pub struct Outfit { /// This outfit's thumbnail pub thumbnail: SpriteHandle, /// How much space this outfit requires pub space: OutfitSpace, /// This outfit's handle pub handle: OutfitHandle, /// The name of this outfit pub name: String, /// How much engine thrust this outfit produces pub engine_thrust: f32, /// How much steering power this outfit provids pub steer_power: f32, /// The engine flare sprite this outfit creates. /// Its location and size is determined by a ship's /// engine points. pub engine_flare_sprite: Option, /// Jump to this edge when engines turn on pub engine_flare_on_start: Option, /// Jump to this edge when engines turn off pub engine_flare_on_stop: Option, /// Shield hit points pub shield_strength: f32, /// Shield regeneration rate, per second pub shield_generation: f32, /// Wait this many seconds after taking damage before regenerating shields pub shield_delay: f32, /// This outfit's gun stats. /// If this is some, this outfit requires a gun point. pub gun: Option, } /// Defines a projectile's collider #[derive(Debug, Deserialize, Clone)] pub enum ProjectileCollider { /// A ball collider #[serde(rename = "ball")] Ball(BallCollider), } /// A simple ball-shaped collider, centered at the object's position #[derive(Debug, Deserialize, Clone)] pub struct BallCollider { /// The radius of this ball pub radius: f32, } /// Represents gun stats of an outfit. /// If an outfit has this value, it requires a gun point. #[derive(Debug, Clone)] pub struct Gun { /// The projectile this gun produces pub projectile: Projectile, /// Average delay between projectiles, in seconds. pub rate: f32, /// Random variation of projectile delay, in seconds. /// Each shot waits (rate += rate_rng). pub rate_rng: f32, } /// Represents a projectile that a [`Gun`] produces. #[derive(Debug, Clone)] pub struct Projectile { /// The projectile sprite pub sprite: SpriteHandle, /// The average size of this projectile /// (height in game units) pub size: f32, /// Random size variation pub size_rng: f32, /// The speed of this projectile, in game units / second pub speed: f32, /// Random speed variation pub speed_rng: f32, /// The lifespan of this projectile. /// It will vanish if it lives this long without hitting anything. pub lifetime: f32, /// Random lifetime variation pub lifetime_rng: f32, /// The damage this projectile does pub damage: f32, /// The force this projectile applies pub force: f32, /// The angle variation of this projectile, in radians pub angle_rng: f32, /// The effect this projectile will spawn when it hits something pub impact_effect: Option, /// The effect this projectile will spawn when it expires pub expire_effect: Option, /// Collider parameters for this projectile pub collider: ProjectileCollider, } impl crate::Build for Outfit { type InputSyntaxType = HashMap; fn build( outfits: Self::InputSyntaxType, build_context: &mut ContentBuildContext, content: &mut Content, ) -> Result<()> { for (outfit_name, outfit) in outfits { let handle = OutfitHandle { index: content.outfits.len(), }; let gun = match outfit.gun { None => None, Some(g) => Some( g.build(build_context, content) .with_context(|| format!("in outfit {}", outfit_name))?, ), }; let thumb_handle = match content.sprite_index.get(&outfit.thumbnail) { None => { return Err(anyhow!( "thumbnail sprite `{}` doesn't exist", outfit.thumbnail )) .with_context(|| format!("in outfit `{}`", outfit_name)); } Some(t) => *t, }; let mut o = Self { thumbnail: thumb_handle, gun, handle, name: outfit_name.clone(), engine_thrust: 0.0, steer_power: 0.0, engine_flare_sprite: None, engine_flare_on_start: None, engine_flare_on_stop: None, space: OutfitSpace::from(outfit.space), shield_delay: 0.0, shield_generation: 0.0, shield_strength: 0.0, }; // Engine stats if let Some(engine) = outfit.engine { let sprite_handle = match content.sprite_index.get(&engine.flare.sprite) { None => { return Err(anyhow!( "flare sprite `{}` doesn't exist", engine.flare.sprite )) .with_context(|| format!("in outfit `{}`", outfit_name)); } Some(t) => *t, }; o.engine_thrust = engine.thrust; 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 = { let x = engine.flare.on_start; if x.is_none() { None } else { let x = x.unwrap(); let mut e = resolve_edge_as_edge(&x.val, 0.0, |x| { sprite.get_section_handle_by_name(x) }) .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 = { let x = engine.flare.on_stop; if x.is_none() { None } else { let x = x.unwrap(); let mut e = resolve_edge_as_edge(&x.val, 0.0, |x| { sprite.get_section_handle_by_name(x) }) .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) } }; } // Steering stats if let Some(steer) = outfit.steering { o.steer_power = steer.power; } // Shield stats if let Some(shield) = outfit.shield { o.shield_delay = shield.delay.unwrap_or(0.0); o.shield_generation = shield.generation.unwrap_or(0.0); o.shield_strength = shield.strength.unwrap_or(0.0); } content.outfits.push(o); } return Ok(()); } }