401 lines
10 KiB
Rust
401 lines
10 KiB
Rust
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<Engine>,
|
|
pub steering: Option<Steering>,
|
|
pub space: outfitspace::syntax::OutfitSpace,
|
|
pub shield: Option<Shield>,
|
|
pub gun: Option<Gun>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct Shield {
|
|
pub strength: Option<f32>,
|
|
pub generation: Option<f32>,
|
|
pub delay: Option<f32>,
|
|
// 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<SectionEdge>,
|
|
pub on_stop: Option<SectionEdge>,
|
|
}
|
|
|
|
#[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<f32>,
|
|
}
|
|
|
|
impl Gun {
|
|
pub fn build(
|
|
self,
|
|
build_context: &mut ContentBuildContext,
|
|
content: &mut crate::Content,
|
|
) -> Result<super::Gun> {
|
|
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<effect::syntax::EffectReference>,
|
|
pub expire_effect: Option<effect::syntax::EffectReference>,
|
|
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<SpriteHandle>,
|
|
|
|
/// Jump to this edge when engines turn on
|
|
pub engine_flare_on_start: Option<SectionEdge>,
|
|
|
|
/// Jump to this edge when engines turn off
|
|
pub engine_flare_on_stop: Option<SectionEdge>,
|
|
|
|
/// 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<Gun>,
|
|
}
|
|
|
|
/// 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<EffectHandle>,
|
|
|
|
/// The effect this projectile will spawn when it expires
|
|
pub expire_effect: Option<EffectHandle>,
|
|
|
|
/// Collider parameters for this projectile
|
|
pub collider: ProjectileCollider,
|
|
}
|
|
|
|
impl crate::Build for Outfit {
|
|
type InputSyntaxType = HashMap<String, syntax::Outfit>;
|
|
|
|
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(());
|
|
}
|
|
}
|