use anyhow::{bail, Context, Result}; use cgmath::Deg; use serde::Deserialize; use std::collections::HashMap; use crate::{handle::TextureHandle, Content}; use crate::OutfitSpace; pub(crate) mod syntax { use crate::part::shared; use serde::Deserialize; // Raw serde syntax structs. // These are never seen by code outside this crate. #[derive(Debug, Deserialize)] pub struct Gun { pub projectile: Projectile, pub rate: f32, pub rate_rng: f32, pub space: shared::syntax::OutfitSpace, } #[derive(Debug, Deserialize)] pub struct Projectile { pub sprite_texture: 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: Option, pub expire: Option, pub collider: super::ProjectileCollider, pub force: f32, } #[derive(Debug, Deserialize)] pub struct ProjectileParticle { pub texture: 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 #[derive(Debug, Deserialize, Clone)] pub enum ProjectileCollider { /// A ball collider #[serde(rename = "ball")] Ball(BallCollider), } #[derive(Debug, Deserialize, Clone)] 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 { /// The name of this gun pub name: String, /// 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, /// How much space this gun uses pub space: OutfitSpace, } /// Represents a projectile that a [`Gun`] produces. #[derive(Debug, Clone)] pub struct Projectile { /// The projectile sprite pub sprite_texture: TextureHandle, /// 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. /// Projectiles can be off center up to /// `spread / 2` degrees in both directions. /// /// (Forming a "fire cone" of `spread` degrees) pub angle_rng: Deg, /// The particle this projectile will spawn when it hits something pub impact_particle: Option, /// The particle this projectile will spawn when it expires pub expire_particle: 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 texture to use for this particle. /// This is most likely animated. pub texture: TextureHandle, /// 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_texture = match ct.texture_index.get(&impact.texture) { None => bail!("impact texture `{}` doesn't exist", impact.texture), Some(t) => *t, }; let impact_lifetime = match impact.lifetime { syntax::ParticleLifetime::Seconds(s) => s, syntax::ParticleLifetime::Inherit(s) => { if s == "inherit" { let t = ct.get_texture(impact_texture); t.fps * t.frames.len() as f32 } else { bail!("bad impact lifetime, must be float or \"inherit\"",) } } }; Ok(Some(ProjectileParticle { texture: impact_texture, lifetime: impact_lifetime, inherit_velocity: impact.inherit_velocity, size: impact.size, })) } else { Ok(None) } } impl crate::Build for Gun { type InputSyntax = HashMap; fn build(gun: Self::InputSyntax, ct: &mut Content) -> Result<()> { for (gun_name, gun) in gun { let projectile_texture = match ct.texture_index.get(&gun.projectile.sprite_texture) { None => bail!( "In gun `{}`: projectile texture `{}` doesn't exist", gun_name, gun.projectile.sprite_texture ), Some(t) => *t, }; let impact_particle = parse_projectile_particle(ct, gun.projectile.impact) .with_context(|| format!("In gun `{}`", gun_name))?; let expire_particle = parse_projectile_particle(ct, gun.projectile.expire) .with_context(|| format!("In gun `{}`", gun_name))?; ct.guns.push(Self { name: gun_name, space: gun.space.into(), rate: gun.rate, rate_rng: gun.rate_rng, projectile: Projectile { force: gun.projectile.force, sprite_texture: projectile_texture, size: gun.projectile.size, size_rng: gun.projectile.size_rng, speed: gun.projectile.speed, speed_rng: gun.projectile.speed_rng, lifetime: gun.projectile.lifetime, lifetime_rng: gun.projectile.lifetime_rng, damage: gun.projectile.damage, angle_rng: Deg(gun.projectile.angle_rng), impact_particle, expire_particle, collider: gun.projectile.collider, }, }); } return Ok(()); } }