Compare commits

...

5 Commits

Author SHA1 Message Date
Mark 7565f09e3b
Updated TODO 2024-01-05 18:14:00 -08:00
Mark 27c8bf6093
Added ship collapse sequence 2024-01-05 18:04:30 -08:00
Mark 46313b4880
Generate collision indices 2024-01-05 13:25:44 -08:00
Mark 9ee22d3618
Added margin 2024-01-05 12:17:00 -08:00
Mark eabc1ebd37
Improved effect definitions 2024-01-05 12:09:59 -08:00
25 changed files with 918 additions and 279 deletions

View File

@ -1,9 +1,11 @@
## Specific Jobs ## Specific Jobs
- Particle variation - Particle variation
- Particle physics
- UI: health, shield, fuel, heat, energy bars - UI: health, shield, fuel, heat, energy bars
- UI: text arranger - UI: text arranger
- Sound system - Sound system
- Ship death animation & debris - Ship death debris
- Sprite reels
---------------------------------- ----------------------------------

2
assets

@ -1 +1 @@
Subproject commit 38fd6766762ce90bb699f98e30e46b17c4eda50c Subproject commit 74ddbde9e1cec1418c17844bc7336324ece88d15

34
content/effects.toml Normal file
View File

@ -0,0 +1,34 @@
[effect."small explosion"]
sprite = "particle::explosion::small"
lifetime = "inherit"
inherit_velocity = "target"
size = 8.0
[effect."large explosion"]
sprite = "particle::explosion::large"
lifetime = "inherit"
inherit_velocity = "target"
size = 25.0
[effect."huge explosion"]
sprite = "particle::explosion::huge"
lifetime = "inherit"
inherit_velocity = "target"
size = 50.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
# document: effect vs particle

View File

@ -29,13 +29,9 @@ projectile.force = 0.0
projectile.collider.ball.radius = 2.0 projectile.collider.ball.radius = 2.0
projectile.impact.sprite = "particle::explosion" projectile.impact_effect = "small explosion"
projectile.impact.lifetime = "inherit"
projectile.impact.inherit_velocity = "target"
projectile.impact.size = 3.0
projectile.expire_effect.sprite = "particle::blaster"
projectile.expire.sprite = "particle::blaster" projectile.expire_effect.lifetime = "inherit"
projectile.expire.lifetime = "inherit" projectile.expire_effect.inherit_velocity = "projectile"
projectile.expire.inherit_velocity = "projectile" projectile.expire_effect.size = 3.0
projectile.expire.size = 3.0

View File

@ -6,6 +6,8 @@ hull = 200
linear_drag = 0.2 linear_drag = 0.2
angular_drag = 0.2 angular_drag = 0.2
# TODO: disable
# TODO: damage effects
space.outfit = 200 space.outfit = 200
space.engine = 50 space.engine = 50
@ -14,7 +16,20 @@ space.weapon = 50
engines = [{ x = 0.0, y = -1.05, size = 50.0 }] engines = [{ x = 0.0, y = -1.05, size = 50.0 }]
guns = [{ x = 0.0, y = 1 }, { x = 0.1, y = 0.80 }, { x = -0.1, y = 0.80 }] guns = [{ x = 0.0, y = 1 }, { x = 0.1, y = 0.80 }, { x = -0.1, y = 0.80 }]
collision.points = [
# Length of death sequence, in seconds
collapse.length = 5.0
# Effects to create during the collapse sequence.
# On average, `count` will be spawned over the sequence,
# with a distribution of (x^2 + 0.1)
collapse.effects = [
{ effect = "small explosion", count = 30 },
{ effect = "large explosion", count = 5 },
]
collision = [
#[rustfmt:skip], #[rustfmt:skip],
[0.53921, 1.0000], [0.53921, 1.0000],
[0.53921, 0.29343], [0.53921, 0.29343],
@ -39,27 +54,43 @@ collision.points = [
[-0.53921, 1.0000], [-0.53921, 1.0000],
] ]
# TODO: generate this automatically
collision.indices = [ # Scripted explosion
[[ship."Gypsum".collapse.event]]
time = 5.0
effects = [
#[rustfmt:skip], #[rustfmt:skip],
[0, 1], { effect = "small explosion", count = 8 },
[1, 2], { effect = "large explosion", count = 5 },
[2, 3], { effect = "huge explosion", count = 1, pos = [0, 0] },
[3, 4], { effect = "huge explosion", count = 4 },
[4, 5],
[5, 6],
[6, 7],
[7, 8],
[8, 9],
[9, 10],
[10, 11],
[11, 12],
[12, 13],
[13, 14],
[14, 15],
[15, 16],
[16, 17],
[17, 18],
[18, 19],
[19, 0],
] ]
# Scripted explosion
[[ship."Gypsum".collapse.event]]
time = 0.0
effects = [
#[rustfmt:skip],
{ effect = "small explosion", count = 3 },
{ effect = "large explosion", count = 1 },
]
# Play a sprite reel (or change sprite)
#[[ship."Gypsum".death.collapse.event]]
#time = 10.0
#reel = "gypsum post-death"
# Create debris
#[[ship."Gypsum".death.collapse.event]]
#time = "end"
#physics = "inherit"
# OR (relative to original rigidbody)
#physics.position = [0, 0]
#physics.velocity = [0, 0]
#physics.angle = 0
#physics.angvel = 0
#debris = "debris"
# Burning, interactable, destructible debris
#[debris]
#effects = [{ type = "small explosion", count = 10 }]

View File

@ -1,3 +1,9 @@
# TODO:
# random start frame
# repeat once: stay on last frame
# blending mode: alpha / half-alpha / additive
[sprite."starfield"] [sprite."starfield"]
file = "starfield.png" file = "starfield.png"
@ -20,8 +26,9 @@ file = "projectile/blaster.png"
file = "ship/gypsum.png" file = "ship/gypsum.png"
[sprite."ship::peregrine"] [sprite."ship::peregrine"]
duration = 1.3 timing.duration = 1.3
repeat = "reverse" repeat = "reverse"
random_start_frame = true
frames = [ frames = [
"ship/peregrine/01.png", "ship/peregrine/01.png",
"ship/peregrine/02.png", "ship/peregrine/02.png",
@ -52,7 +59,7 @@ file = "ui/radarframe.png"
file = "ui/center-arrow.png" file = "ui/center-arrow.png"
[sprite."particle::blaster"] [sprite."particle::blaster"]
duration = 0.15 timing.duration = 0.15
repeat = "once" repeat = "once"
frames = [ frames = [
"particle/blaster/01.png", "particle/blaster/01.png",
@ -62,8 +69,48 @@ frames = [
] ]
[sprite."particle::explosion"] [sprite."particle::explosion::tiny"]
duration = 0.4 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" repeat = "once"
frames = [ frames = [
"particle/explosion-large/01.png", "particle/explosion-large/01.png",
@ -76,3 +123,19 @@ frames = [
"particle/explosion-large/08.png", "particle/explosion-large/08.png",
"particle/explosion-large/09.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",
]

View File

@ -10,17 +10,20 @@ use std::{cmp::Eq, hash::Hash};
/// A lightweight representation of a sprite /// A lightweight representation of a sprite
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct SpriteHandle { pub struct SpriteHandle {
/// The index of this sprite in content.sprites pub(crate) index: usize,
/// This must be public, since render uses this to
/// select sprites.
///
/// This is a u32 for that same reason, too.
pub index: u32,
/// The aspect ratio of this sprite (width / height) /// The aspect ratio of this sprite (width / height)
pub aspect: f32, 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 { impl Hash for SpriteHandle {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.index.hash(state) self.index.hash(state)
@ -37,7 +40,7 @@ impl PartialEq for SpriteHandle {
/// A lightweight representation of an outfit /// A lightweight representation of an outfit
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OutfitHandle { pub struct OutfitHandle {
/// TODO /// TODO: pub in crate, currently for debug (same with all other handles)
pub index: usize, pub index: usize,
} }
@ -65,7 +68,12 @@ pub struct SystemHandle {
/// A lightweight representation of a faction /// A lightweight representation of a faction
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FactionHandle { pub struct FactionHandle {
/// The index of this faction in content.factions /// TODO
/// TODO: pub in crate, currently for debug (same with all other handles)
pub index: usize, pub index: usize,
} }
/// A lightweight representation of an effect
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct EffectHandle {
pub(crate) index: usize,
}

View File

@ -18,18 +18,17 @@ use std::{
use toml; use toml;
use walkdir::WalkDir; use walkdir::WalkDir;
pub use handle::{FactionHandle, GunHandle, OutfitHandle, ShipHandle, SpriteHandle, SystemHandle}; pub use handle::{
pub use part::{ EffectHandle, FactionHandle, GunHandle, OutfitHandle, ShipHandle, SpriteHandle, SystemHandle,
EnginePoint, Faction, Gun, GunPoint, ImpactInheritVelocity, Outfit, OutfitSpace, Projectile,
ProjectileCollider, ProjectileParticle, Relationship, RepeatMode, Ship, Sprite, System,
}; };
pub use part::*;
mod syntax { mod syntax {
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use serde::Deserialize; use serde::Deserialize;
use std::{collections::HashMap, fmt::Display, hash::Hash}; 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)] #[derive(Debug, Deserialize)]
pub struct Root { pub struct Root {
@ -39,6 +38,7 @@ mod syntax {
pub outfit: Option<HashMap<String, outfit::syntax::Outfit>>, pub outfit: Option<HashMap<String, outfit::syntax::Outfit>>,
pub sprite: Option<HashMap<String, sprite::syntax::Sprite>>, pub sprite: Option<HashMap<String, sprite::syntax::Sprite>>,
pub faction: Option<HashMap<String, faction::syntax::Faction>>, pub faction: Option<HashMap<String, faction::syntax::Faction>>,
pub effect: Option<HashMap<String, effect::syntax::Effect>>,
} }
fn merge_hashmap<K, V>( fn merge_hashmap<K, V>(
@ -75,6 +75,7 @@ mod syntax {
outfit: None, outfit: None,
sprite: None, sprite: None,
faction: None, faction: None,
effect: None,
} }
} }
@ -89,6 +90,8 @@ mod syntax {
.with_context(|| "while merging sprites")?; .with_context(|| "while merging sprites")?;
merge_hashmap(&mut self.faction, other.faction) merge_hashmap(&mut self.faction, other.faction)
.with_context(|| "while merging factions")?; .with_context(|| "while merging factions")?;
merge_hashmap(&mut self.effect, other.effect)
.with_context(|| "while merging effects")?;
return Ok(()); return Ok(());
} }
} }
@ -98,11 +101,29 @@ trait Build {
type InputSyntaxType; type InputSyntaxType;
/// Build a processed System struct from raw serde data /// 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 where
Self: Sized; Self: Sized;
} }
/// Stores temporary data while building context objects
#[derive(Debug)]
pub(crate) struct ContentBuildContext {
pub effect_index: HashMap<String, EffectHandle>,
}
impl ContentBuildContext {
fn new() -> Self {
Self {
effect_index: HashMap::new(),
}
}
}
/// Represents static game content /// Represents static game content
#[derive(Debug)] #[derive(Debug)]
pub struct Content { pub struct Content {
@ -113,30 +134,22 @@ pub struct Content {
starfield_sprite_name: String, starfield_sprite_name: String,
/// Sprites /// Sprites
pub sprites: Vec<part::sprite::Sprite>, pub sprites: Vec<Sprite>,
/// Map strings to texture names. /// Map strings to texture names.
/// This is only necessary because we need to hard-code a few texture names for UI elements. /// This is only necessary because we need to hard-code a few texture names for UI elements.
sprite_index: HashMap<String, handle::SpriteHandle>, sprite_index: HashMap<String, SpriteHandle>,
/// The texture to use for starfield stars /// The texture to use for starfield stars
starfield_handle: Option<handle::SpriteHandle>, starfield_handle: Option<SpriteHandle>,
/// Keeps track of which images are in which texture /// Keeps track of which images are in which texture
sprite_atlas: SpriteAtlas, sprite_atlas: SpriteAtlas,
/// Outfits outfits: Vec<Outfit>,
outfits: Vec<part::outfit::Outfit>, guns: Vec<Gun>, // TODO: merge with outfit
ships: Vec<Ship>,
/// Ship guns systems: Vec<System>,
guns: Vec<part::gun::Gun>, factions: Vec<Faction>,
effects: Vec<Effect>,
/// Ship bodies
ships: Vec<part::ship::Ship>,
/// Star systems
systems: Vec<part::system::System>,
/// Factions
factions: Vec<part::faction::Faction>,
} }
// Loading methods // Loading methods
@ -189,6 +202,8 @@ impl Content {
toml::from_str(&file_string)? toml::from_str(&file_string)?
}; };
let mut build_context = ContentBuildContext::new();
let mut content = Self { let mut content = Self {
sprite_atlas: atlas, sprite_atlas: atlas,
systems: Vec::new(), systems: Vec::new(),
@ -197,33 +212,58 @@ impl Content {
outfits: Vec::new(), outfits: Vec::new(),
sprites: Vec::new(), sprites: Vec::new(),
factions: Vec::new(), factions: Vec::new(),
effects: Vec::new(),
sprite_index: HashMap::new(), sprite_index: HashMap::new(),
starfield_handle: None, starfield_handle: None,
image_root: texture_root, image_root: texture_root,
starfield_sprite_name: starfield_texture_name, 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 // 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() { 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() { 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() { 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() { 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() { 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); return Ok(content);
@ -289,4 +329,9 @@ impl Content {
pub fn get_faction(&self, h: FactionHandle) -> &Faction { pub fn get_faction(&self, h: FactionHandle) -> &Faction {
return &self.factions[h.index]; return &self.factions[h.index];
} }
/// Get an effect from a handle
pub fn get_effect(&self, h: EffectHandle) -> &Effect {
return &self.effects[h.index];
}
} }

View File

@ -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<EffectHandle> {
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<EffectHandle> {
// 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<String, syntax::Effect>;
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(());
}
}

View File

@ -2,7 +2,7 @@ use anyhow::{bail, Result};
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use crate::{handle::FactionHandle, Content}; use crate::{handle::FactionHandle, Content, ContentBuildContext};
pub(crate) mod syntax { pub(crate) mod syntax {
use std::collections::HashMap; use std::collections::HashMap;
@ -61,13 +61,17 @@ pub struct Faction {
impl crate::Build for Faction { impl crate::Build for Faction {
type InputSyntaxType = HashMap<String, syntax::Faction>; type InputSyntaxType = HashMap<String, syntax::Faction>;
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. // Keeps track of position in faction array.
// This lets us build FactionHandles before finishing all factions. // This lets us build FactionHandles before finishing all factions.
let faction_names: Vec<String> = factions.keys().map(|x| x.to_owned()).collect(); let faction_names: Vec<String> = factions.keys().map(|x| x.to_owned()).collect();
// Indexing will break if this is false. // 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() { for f_idx in 0..faction_names.len() {
let faction_name = &faction_names[f_idx]; 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(), name: faction_name.to_owned(),
handle: h, handle: h,
relationships, relationships,

View File

@ -5,10 +5,11 @@ use std::collections::HashMap;
use crate::{handle::SpriteHandle, Content}; use crate::{handle::SpriteHandle, Content};
use crate::OutfitSpace; use crate::{ContentBuildContext, EffectHandle, OutfitSpace};
pub(crate) mod syntax { pub(crate) mod syntax {
use crate::part::shared; use crate::part::effect;
use crate::part::outfitspace;
use serde::Deserialize; use serde::Deserialize;
// Raw serde syntax structs. // Raw serde syntax structs.
// These are never seen by code outside this crate. // These are never seen by code outside this crate.
@ -18,7 +19,7 @@ pub(crate) mod syntax {
pub projectile: Projectile, pub projectile: Projectile,
pub rate: f32, pub rate: f32,
pub rate_rng: f32, pub rate_rng: f32,
pub space: shared::syntax::OutfitSpace, pub space: outfitspace::syntax::OutfitSpace,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -32,26 +33,11 @@ pub(crate) mod syntax {
pub lifetime_rng: f32, pub lifetime_rng: f32,
pub damage: f32, pub damage: f32,
pub angle_rng: f32, pub angle_rng: f32,
pub impact: Option<ProjectileParticle>, pub impact_effect: Option<effect::syntax::EffectReference>,
pub expire: Option<ProjectileParticle>, pub expire_effect: Option<effect::syntax::EffectReference>,
pub collider: super::ProjectileCollider, pub collider: super::ProjectileCollider,
pub force: f32, 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 /// Defines a projectile's collider
@ -62,30 +48,13 @@ pub enum ProjectileCollider {
Ball(BallCollider), Ball(BallCollider),
} }
/// A simple ball-shaped collider, centered at the object's position
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct BallCollider { pub struct BallCollider {
/// The radius of this ball
pub radius: f32, 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. /// Represents a gun outfit.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Gun { pub struct Gun {
@ -143,86 +112,50 @@ pub struct Projectile {
pub angle_rng: Deg<f32>, pub angle_rng: Deg<f32>,
/// The particle this projectile will spawn when it hits something /// The particle this projectile will spawn when it hits something
pub impact_particle: Option<ProjectileParticle>, pub impact_effect: Option<EffectHandle>,
/// The particle this projectile will spawn when it expires /// The particle this projectile will spawn when it expires
pub expire_particle: Option<ProjectileParticle>, pub expire_effect: Option<EffectHandle>,
/// Collider parameters for this projectile /// Collider parameters for this projectile
pub collider: ProjectileCollider, 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<syntax::ProjectileParticle>,
) -> Result<Option<ProjectileParticle>> {
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 { impl crate::Build for Gun {
type InputSyntaxType = HashMap<String, syntax::Gun>; type InputSyntaxType = HashMap<String, syntax::Gun>;
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 { 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!( None => bail!(
"In gun `{}`: projectile sprite `{}` doesn't exist", "projectile sprite `{}` doesn't exist in gun `{}`",
gun.projectile.sprite,
gun_name, gun_name,
gun.projectile.sprite
), ),
Some(t) => *t, Some(t) => *t,
}; };
let impact_particle = parse_projectile_particle(ct, gun.projectile.impact) let impact_effect = match gun.projectile.impact_effect {
.with_context(|| format!("In gun `{}`", gun_name))?; 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) let expire_effect = match gun.projectile.expire_effect {
.with_context(|| format!("In gun `{}`", gun_name))?; 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, name: gun_name,
space: gun.space.into(), space: gun.space.into(),
rate: gun.rate, rate: gun.rate,
@ -238,8 +171,8 @@ impl crate::Build for Gun {
lifetime_rng: gun.projectile.lifetime_rng, lifetime_rng: gun.projectile.lifetime_rng,
damage: gun.projectile.damage, damage: gun.projectile.damage,
angle_rng: Deg(gun.projectile.angle_rng), angle_rng: Deg(gun.projectile.angle_rng),
impact_particle, impact_effect,
expire_particle, expire_effect,
collider: gun.projectile.collider, collider: gun.projectile.collider,
}, },
}); });

View File

@ -1,17 +1,22 @@
//! Content parts //! Content parts
pub mod faction; pub(crate) mod effect;
pub mod gun; pub(crate) mod faction;
pub mod outfit; pub(crate) mod gun;
mod shared; pub(crate) mod outfit;
pub mod ship; pub(crate) mod outfitspace;
pub mod sprite; pub(crate) mod ship;
pub mod system; pub(crate) mod sprite;
pub(crate) mod system;
pub use effect::{Effect, ImpactInheritVelocity};
pub use faction::{Faction, Relationship}; 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 outfit::Outfit;
pub use shared::OutfitSpace; pub use outfitspace::OutfitSpace;
pub use ship::{EnginePoint, GunPoint, Ship}; pub use ship::{
CollapseEffectSpawner, CollapseEvent, EffectCollapseEvent, EnginePoint, GunPoint, Ship,
ShipCollapse,
};
pub use sprite::{RepeatMode, Sprite}; pub use sprite::{RepeatMode, Sprite};
pub use system::{Object, System}; pub use system::{Object, System};

View File

@ -2,10 +2,10 @@ use std::collections::HashMap;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use crate::{handle::SpriteHandle, Content, OutfitSpace}; use crate::{handle::SpriteHandle, Content, ContentBuildContext, OutfitSpace};
pub(crate) mod syntax { pub(crate) mod syntax {
use crate::part::shared; use crate::part::outfitspace;
use serde::Deserialize; use serde::Deserialize;
// Raw serde syntax structs. // Raw serde syntax structs.
// These are never seen by code outside this crate. // These are never seen by code outside this crate.
@ -14,7 +14,7 @@ pub(crate) mod syntax {
pub struct Outfit { pub struct Outfit {
pub engine: Option<Engine>, pub engine: Option<Engine>,
pub steering: Option<Steering>, pub steering: Option<Steering>,
pub space: shared::syntax::OutfitSpace, pub space: outfitspace::syntax::OutfitSpace,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -53,7 +53,11 @@ pub struct Outfit {
impl crate::Build for Outfit { impl crate::Build for Outfit {
type InputSyntaxType = HashMap<String, syntax::Outfit>; type InputSyntaxType = HashMap<String, syntax::Outfit>;
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 { for (outfit_name, outfit) in outfits {
let mut o = Self { let mut o = Self {
name: outfit_name.clone(), name: outfit_name.clone(),
@ -65,7 +69,7 @@ impl crate::Build for Outfit {
// Engine stats // Engine stats
if let Some(engine) = outfit.engine { 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!( None => bail!(
"In outfit `{}`: flare sprite `{}` doesn't exist", "In outfit `{}`: flare sprite `{}` doesn't exist",
outfit_name, outfit_name,
@ -82,7 +86,7 @@ impl crate::Build for Outfit {
o.steer_power = steer.power; o.steer_power = steer.power;
} }
ct.outfits.push(o); content.outfits.push(o);
} }
return Ok(()); return Ok(());

View File

@ -1,13 +1,13 @@
use std::collections::HashMap; use std::collections::HashMap;
use anyhow::{bail, Result}; use anyhow::{bail, Context, Result};
use cgmath::Point2; use cgmath::Point2;
use nalgebra::{point, Point}; use nalgebra::{point, Point};
use crate::{handle::SpriteHandle, Content, OutfitSpace}; use crate::{handle::SpriteHandle, Content, ContentBuildContext, EffectHandle, OutfitSpace};
pub(crate) mod syntax { pub(crate) mod syntax {
use crate::part::shared; use crate::part::{effect::syntax::EffectReference, outfitspace};
use serde::Deserialize; use serde::Deserialize;
// Raw serde syntax structs. // Raw serde syntax structs.
@ -21,16 +21,11 @@ pub(crate) mod syntax {
pub guns: Vec<Gun>, pub guns: Vec<Gun>,
pub hull: f32, pub hull: f32,
pub mass: f32, pub mass: f32,
pub collision: Collision, pub collision: Vec<[f32; 2]>,
pub angular_drag: f32, pub angular_drag: f32,
pub linear_drag: f32, pub linear_drag: f32,
pub space: shared::syntax::OutfitSpace, pub space: outfitspace::syntax::OutfitSpace,
} pub collapse: Option<Collapse>,
#[derive(Debug, Deserialize)]
pub struct Collision {
pub points: Vec<[f32; 2]>,
pub indices: Vec<[u32; 2]>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -45,6 +40,34 @@ pub(crate) mod syntax {
pub x: f32, pub x: f32,
pub y: f32, pub y: f32,
} }
// TODO:
// plural or not? document!
#[derive(Debug, Deserialize)]
pub struct Collapse {
pub length: f32,
pub effects: Vec<CollapseEffectSpawner>,
pub event: Vec<CollapseEvent>,
}
#[derive(Debug, Deserialize)]
pub struct CollapseEffectSpawner {
pub effect: EffectReference,
pub count: f32,
pub pos: Option<[f32; 2]>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum CollapseEvent {
Effect(EffectCollapseEvent),
}
#[derive(Debug, Deserialize)]
pub struct EffectCollapseEvent {
pub time: f32,
pub effects: Vec<CollapseEffectSpawner>,
}
} }
// Processed data structs. // Processed data structs.
@ -92,6 +115,9 @@ pub struct Ship {
/// Outfit space in this ship /// Outfit space in this ship
pub space: OutfitSpace, pub space: OutfitSpace,
/// Ship collapse sequence
pub collapse: ShipCollapse,
} }
/// Collision shape for this ship /// Collision shape for this ship
@ -122,10 +148,58 @@ pub struct GunPoint {
pub pos: Point2<f32>, pub pos: Point2<f32>,
} }
/// Parameters for a ship's collapse sequence
#[derive(Debug, Clone)]
pub struct ShipCollapse {
/// Collapse sequence length, in seconds
pub length: f32,
/// Effects to create during collapse
pub effects: Vec<CollapseEffectSpawner>,
/// Scripted events during ship collapse
pub events: Vec<CollapseEvent>,
}
/// A scripted event during a ship collapse sequence
#[derive(Debug, Clone)]
pub struct CollapseEffectSpawner {
/// The effect to create
pub effect: EffectHandle,
/// How many effects to create
pub count: f32,
/// Where to create these effects.
/// Position is random if None.
pub pos: Option<Point2<f32>>,
}
/// A scripted event during a ship collapse sequence
#[derive(Debug, Clone)]
pub enum CollapseEvent {
/// A scripted effect during a ship collapse sequence
Effect(EffectCollapseEvent),
}
/// A scripted effect during a ship collapse sequence
#[derive(Debug, Clone)]
pub struct EffectCollapseEvent {
/// When to trigger this event
pub time: f32,
/// The effect to create
pub effects: Vec<CollapseEffectSpawner>,
}
impl crate::Build for Ship { impl crate::Build for Ship {
type InputSyntaxType = HashMap<String, syntax::Ship>; type InputSyntaxType = HashMap<String, syntax::Ship>;
fn build(ship: Self::InputSyntaxType, ct: &mut Content) -> Result<()> { fn build(
ship: Self::InputSyntaxType,
build_context: &mut ContentBuildContext,
ct: &mut Content,
) -> Result<()> {
for (ship_name, ship) in ship { for (ship_name, ship) in ship {
let handle = match ct.sprite_index.get(&ship.sprite) { let handle = match ct.sprite_index.get(&ship.sprite) {
None => bail!( None => bail!(
@ -139,8 +213,65 @@ impl crate::Build for Ship {
let size = ship.size; let size = ship.size;
let aspect = ct.get_sprite(handle).aspect; let aspect = ct.get_sprite(handle).aspect;
let collapse = if let Some(c) = ship.collapse {
let mut effects = Vec::new();
for e in c.effects {
effects.push(CollapseEffectSpawner {
effect: e
.effect
.to_handle(build_context, ct)
.with_context(|| format!("while loading ship `{}`", ship_name))?,
count: e.count,
pos: e.pos.map(|p| Point2 {
x: p[0] * (size / 2.0) * aspect,
y: p[1] * size / 2.0,
}),
});
}
let mut events = Vec::new();
for e in c.event {
match e {
syntax::CollapseEvent::Effect(e) => {
let mut effects = Vec::new();
for g in e.effects {
effects.push(CollapseEffectSpawner {
effect: g.effect.to_handle(build_context, ct).with_context(
|| format!("while loading ship `{}`", ship_name),
)?,
count: g.count,
pos: g.pos.map(|p| Point2 {
x: p[0] * (size / 2.0) * aspect,
y: p[1] * size / 2.0,
}),
})
}
events.push(CollapseEvent::Effect(EffectCollapseEvent {
time: e.time,
effects,
}))
}
}
}
ShipCollapse {
length: c.length,
effects,
events,
}
} else {
// Default collapse sequence
ShipCollapse {
length: 0.0,
effects: vec![],
events: vec![],
}
};
ct.ships.push(Self { ct.ships.push(Self {
aspect, aspect,
collapse,
name: ship_name, name: ship_name,
sprite: handle, sprite: handle,
mass: ship.mass, mass: ship.mass,
@ -149,6 +280,7 @@ impl crate::Build for Ship {
linear_drag: ship.linear_drag, linear_drag: ship.linear_drag,
size, size,
hull: ship.hull, hull: ship.hull,
engines: ship engines: ship
.engines .engines
.iter() .iter()
@ -160,6 +292,7 @@ impl crate::Build for Ship {
size: e.size, size: e.size,
}) })
.collect(), .collect(),
guns: ship guns: ship
.guns .guns
.iter() .iter()
@ -170,11 +303,22 @@ impl crate::Build for Ship {
}, },
}) })
.collect(), .collect(),
collision: Collision { collision: Collision {
indices: ship.collision.indices, indices: (0..ship.collision.len())
.map(|x| {
// Auto-generate mesh lines:
// [ [0, 1], [1, 2], ..., [n, 0] ]
let next = if x == ship.collision.len() - 1 {
0
} else {
x + 1
};
[x as u32, next as u32]
})
.collect(),
points: ship points: ship
.collision .collision
.points
.iter() .iter()
.map(|x| point![x[0] * (size / 2.0) * aspect, x[1] * size / 2.0]) .map(|x| point![x[0] * (size / 2.0) * aspect, x[1] * size / 2.0])
.collect(), .collect(),

View File

@ -3,7 +3,7 @@ use image::io::Reader;
use serde::Deserialize; use serde::Deserialize;
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};
use crate::{handle::SpriteHandle, Content}; use crate::{handle::SpriteHandle, Content, ContentBuildContext};
pub(crate) mod syntax { pub(crate) mod syntax {
use serde::Deserialize; use serde::Deserialize;
@ -29,8 +29,18 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct FrameSprite { pub struct FrameSprite {
pub frames: Vec<PathBuf>, pub frames: Vec<PathBuf>,
pub duration: f32, pub timing: Timing,
pub repeat: RepeatMode, pub repeat: RepeatMode,
pub random_start_frame: Option<bool>,
}
#[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) /// Aspect ratio of this sprite (width / height)
pub aspect: f32, pub aspect: f32,
/// If true, start on a random frame of this sprite.
pub random_start_frame: bool,
} }
impl crate::Build for Sprite { impl crate::Build for Sprite {
type InputSyntaxType = HashMap<String, syntax::Sprite>; type InputSyntaxType = HashMap<String, syntax::Sprite>;
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 { for (sprite_name, t) in sprites {
match t { match t {
syntax::Sprite::Static(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(|| { let reader = Reader::open(&file).with_context(|| {
format!( format!(
"Failed to read file `{}` in sprite `{}`", "Failed to read file `{}` in sprite `{}`",
@ -110,34 +127,35 @@ impl crate::Build for Sprite {
})?; })?;
let h = SpriteHandle { let h = SpriteHandle {
index: ct.sprites.len() as u32, index: content.sprites.len(),
aspect: dim.0 as f32 / dim.1 as f32, aspect: dim.0 as f32 / dim.1 as f32,
}; };
if sprite_name == ct.starfield_sprite_name { if sprite_name == content.starfield_sprite_name {
if ct.starfield_handle.is_none() { if content.starfield_handle.is_none() {
ct.starfield_handle = Some(h) content.starfield_handle = Some(h)
} else { } else {
// This can't happen, since this is a hashmap. // This can't happen, since this is a hashmap.
unreachable!("Found two starfield sprites! Something is very wrong.") 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, name: sprite_name,
frames: vec![t.file], frames: vec![t.file],
fps: 0.0, fps: 0.0,
handle: h, handle: h,
repeat: RepeatMode::Once, repeat: RepeatMode::Once,
aspect: dim.0 as f32 / dim.1 as f32, aspect: dim.0 as f32 / dim.1 as f32,
random_start_frame: false,
}); });
} }
syntax::Sprite::Frames(t) => { syntax::Sprite::Frames(t) => {
let mut dim = None; let mut dim = None;
for f in &t.frames { 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(|| { let reader = Reader::open(&file).with_context(|| {
format!( format!(
"Failed to read file `{}` in sprite `{}`", "Failed to read file `{}` in sprite `{}`",
@ -167,33 +185,37 @@ impl crate::Build for Sprite {
let dim = dim.unwrap(); let dim = dim.unwrap();
let h = SpriteHandle { let h = SpriteHandle {
index: ct.sprites.len() as u32, index: content.sprites.len(),
aspect: dim.0 as f32 / dim.1 as f32, 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") 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); content.sprite_index.insert(sprite_name.clone(), h);
ct.sprites.push(Self { content.sprites.push(Self {
name: sprite_name, name: sprite_name,
frames: t.frames, frames: t.frames,
fps, fps,
handle: h, handle: h,
repeat: t.repeat, repeat: t.repeat,
aspect: dim.0 as f32 / dim.1 as f32, 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!( bail!(
"Could not find a starfield texture (name: `{}`)", "Could not find a starfield texture (name: `{}`)",
ct.starfield_sprite_name content.starfield_sprite_name
) )
} }

View File

@ -2,7 +2,7 @@ use anyhow::{bail, Context, Result};
use cgmath::{Deg, Point3}; use cgmath::{Deg, Point3};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use crate::{handle::SpriteHandle, util::Polar, Content}; use crate::{handle::SpriteHandle, util::Polar, Content, ContentBuildContext};
pub(crate) mod syntax { pub(crate) mod syntax {
use serde::Deserialize; use serde::Deserialize;
@ -177,7 +177,11 @@ fn resolve_position(
impl crate::Build for System { impl crate::Build for System {
type InputSyntaxType = HashMap<String, syntax::System>; type InputSyntaxType = HashMap<String, syntax::System>;
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 { for (system_name, system) in system {
let mut objects = Vec::new(); let mut objects = Vec::new();
@ -185,7 +189,7 @@ impl crate::Build for System {
let mut cycle_detector = HashSet::new(); let mut cycle_detector = HashSet::new();
cycle_detector.insert(label.clone()); 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!( None => bail!(
"In system `{}`: sprite `{}` doesn't exist", "In system `{}`: sprite `{}` doesn't exist",
system_name, system_name,
@ -203,7 +207,7 @@ impl crate::Build for System {
}); });
} }
ct.systems.push(Self { content.systems.push(Self {
name: system_name, name: system_name,
objects, objects,
}); });

View File

@ -41,6 +41,7 @@ impl Game {
o1.add(&ct, content::OutfitHandle { index: 0 }); o1.add(&ct, content::OutfitHandle { index: 0 });
o1.add_gun(&ct, content::GunHandle { index: 0 }); o1.add_gun(&ct, content::GunHandle { index: 0 });
o1.add_gun(&ct, content::GunHandle { index: 0 }); o1.add_gun(&ct, content::GunHandle { index: 0 });
o1.add_gun(&ct, content::GunHandle { index: 0 });
let s = object::Ship::new( let s = object::Ship::new(
&ct, &ct,
@ -57,14 +58,14 @@ impl Game {
// This method of specifying factions is non-deterministic, // This method of specifying factions is non-deterministic,
// but that's ok since this is for debug. // but that's ok since this is for debug.
// TODO: fix // TODO: fix
content::FactionHandle { index: 0 }, content::FactionHandle { index: 1 },
object::OutfitSet::new(ss), object::OutfitSet::new(ss),
); );
let h2 = physics.add_ship(&ct, s, Point2 { x: 300.0, y: 300.0 }); let h2 = physics.add_ship(&ct, s, Point2 { x: 300.0, y: 300.0 });
let mut o1 = object::OutfitSet::new(ss); let mut o1 = object::OutfitSet::new(ss);
o1.add(&ct, content::OutfitHandle { index: 0 }); o1.add(&ct, content::OutfitHandle { index: 0 });
o1.add_gun(&ct, content::GunHandle { index: 0 }); //o1.add_gun(&ct, content::GunHandle { index: 0 });
let s = object::Ship::new( let s = object::Ship::new(
&ct, &ct,

View File

@ -30,8 +30,7 @@ impl Ship {
} }
} }
/// Has this ship been destroyed? pub fn is_dead(&self) -> bool {
pub fn is_destroyed(&self) -> bool {
self.hull <= 0.0 self.hull <= 0.0
} }
@ -42,8 +41,10 @@ impl Ship {
let r = f.relationships.get(&p.faction).unwrap(); let r = f.relationships.get(&p.faction).unwrap();
match r { match r {
content::Relationship::Hostile => { content::Relationship::Hostile => {
// TODO: implement death and spawning, and enable damage if self.is_dead() {
//s.hull -= p.damage; return true;
}
self.hull -= p.content.damage;
return true; return true;
} }
_ => return false, _ => return false,
@ -51,6 +52,10 @@ impl Ship {
} }
pub fn fire_guns(&mut self) -> Vec<(Projectile, content::GunPoint)> { pub fn fire_guns(&mut self) -> Vec<(Projectile, content::GunPoint)> {
if self.is_dead() {
return vec![];
}
self.outfits self.outfits
.iter_guns_points() .iter_guns_points()
.filter(|(g, _)| g.cooldown <= 0.0) .filter(|(g, _)| g.cooldown <= 0.0)

View File

@ -1,6 +1,6 @@
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use galactica_packer::{SpriteAtlas, SpriteAtlasImage}; use galactica_packer::{SpriteAtlas, SpriteAtlasImage};
use image::{imageops, ImageBuffer, Rgba, RgbaImage}; use image::{imageops, GenericImageView, ImageBuffer, Rgba, RgbaImage};
use std::{ use std::{
fs::File, fs::File,
io::{Read, Write}, io::{Read, Write},
@ -51,6 +51,9 @@ pub struct AtlasSet {
/// The root directory that contains all image files. /// The root directory that contains all image files.
/// Files outside this directory will not be packed. /// Files outside this directory will not be packed.
asset_root: PathBuf, asset_root: PathBuf,
/// Leave an empty border this many pixels wide around each image
image_margin: u32,
} }
impl AtlasSet { impl AtlasSet {
@ -59,6 +62,7 @@ impl AtlasSet {
texture_height: u32, texture_height: u32,
texture_limit: usize, texture_limit: usize,
asset_root: &Path, asset_root: &Path,
image_margin: u32,
) -> Self { ) -> Self {
Self { Self {
asset_root: asset_root.to_path_buf(), asset_root: asset_root.to_path_buf(),
@ -71,11 +75,12 @@ impl AtlasSet {
index: SpriteAtlas::new(), index: SpriteAtlas::new(),
used_area: 0f64, used_area: 0f64,
image_y_start: Vec::new(), image_y_start: Vec::new(),
image_margin,
} }
} }
/// Add a sprite to this atlas set /// Add a sprite to this atlas set
pub fn write_image(&mut self, path: &Path, dim: [u32; 2]) -> Result<usize> { pub fn write_image(&mut self, path: &Path) -> Result<usize> {
let mut f = File::open(&path)?; let mut f = File::open(&path)?;
let mut bytes = Vec::new(); let mut bytes = Vec::new();
f.read_to_end(&mut bytes) f.read_to_end(&mut bytes)
@ -83,6 +88,13 @@ impl AtlasSet {
let img = image::load_from_memory(&bytes) let img = image::load_from_memory(&bytes)
.with_context(|| format!("While loading file `{}`", path.display()))?; .with_context(|| format!("While loading file `{}`", path.display()))?;
let image_dim = img.dimensions();
let dim = [
image_dim.0 + 2 * self.image_margin,
image_dim.1 + 2 * self.image_margin,
];
let mut x = 0; let mut x = 0;
let mut y = 0; let mut y = 0;
let mut final_atlas_idx = None; let mut final_atlas_idx = None;
@ -113,8 +125,6 @@ impl AtlasSet {
if dim[0] >= sd[0] || dim[1] >= sd[1] { if dim[0] >= sd[0] || dim[1] >= sd[1] {
y = sy; y = sy;
} }
} else {
self.image_y_start.push((0, [u32::MAX, u32::MAX]));
} }
let mut free = false; let mut free = false;
@ -156,8 +166,12 @@ impl AtlasSet {
}; };
// We found a spot for this image, write it. // We found a spot for this image, write it.
//let img = RgbaImage::from_pixel(dim[0], dim[1], Rgba([0, 0, 0, 255])); imageops::overlay(
imageops::overlay(&mut self.texture_list[atlas_idx], &img, x.into(), y.into()); &mut self.texture_list[atlas_idx],
&img,
(x + self.image_margin).into(),
(y + self.image_margin).into(),
);
self.used_regions[atlas_idx].push(([x, y], dim)); self.used_regions[atlas_idx].push(([x, y], dim));
self.used_area += dim[0] as f64 * dim[1] as f64; self.used_area += dim[0] as f64 * dim[1] as f64;
@ -182,10 +196,10 @@ impl AtlasSet {
p.to_path_buf(), p.to_path_buf(),
SpriteAtlasImage { SpriteAtlasImage {
atlas: atlas_idx as u32, atlas: atlas_idx as u32,
x: x as f32 / self.texture_width as f32, x: (x + self.image_margin) as f32 / self.texture_width as f32,
y: y as f32 / self.texture_height as f32, y: (y + self.image_margin) as f32 / self.texture_height as f32,
w: dim[0] as f32 / self.texture_width as f32, w: image_dim.0 as f32 / self.texture_width as f32,
h: dim[1] as f32 / self.texture_height as f32, h: image_dim.1 as f32 / self.texture_height as f32,
}, },
); );

View File

@ -64,13 +64,13 @@ fn main() -> Result<()> {
} }
// Create atlas set // Create atlas set
let mut atlas_set = AtlasSet::new(8192, 8192, 16, &asset_root); let mut atlas_set = AtlasSet::new(8192, 8192, 16, &asset_root, 2);
let total = files.len(); let total = files.len();
let mut i = 0; let mut i = 0;
let mut peak_efficiency = 0f64; let mut peak_efficiency = 0f64;
for (path, dim) in files { for (path, _) in files {
i += 1; i += 1;
let atlas_idx = atlas_set.write_image(&path, dim)?; let atlas_idx = atlas_set.write_image(&path)?;
println!( println!(
"({i} of {total}, atlas {atlas_idx}, efficiency {:.02}%) Added {}", "({i} of {total}, atlas {atlas_idx}, efficiency {:.02}%) Added {}",
100.0 * atlas_set.get_efficiency(), 100.0 * atlas_set.get_efficiency(),

View File

@ -11,11 +11,11 @@ fn animate(instance: InstanceInput, age: f32) -> u32 {
var frame: u32 = u32(0); var frame: u32 = u32(0);
// Repeat // Once
if rep == u32(1) { if rep == u32(1) {
frame = u32(min( frame = u32(min(
(age / fps), age / fps,
f32(len) - 1.0 f32(len) - 1.0
)); ));

View File

@ -2,6 +2,7 @@ use anyhow::Result;
use bytemuck; use bytemuck;
use cgmath::{Deg, EuclideanSpace, Matrix2, Matrix4, Point2, Vector3}; use cgmath::{Deg, EuclideanSpace, Matrix2, Matrix4, Point2, Vector3};
use galactica_constants; use galactica_constants;
use rand::seq::SliceRandom;
use std::{iter, rc::Rc}; use std::{iter, rc::Rc};
use wgpu; use wgpu;
use winit::{self, dpi::LogicalSize, window::Window}; use winit::{self, dpi::LogicalSize, window::Window};
@ -380,7 +381,7 @@ impl GPUState {
instances.push(ObjectInstance { instances.push(ObjectInstance {
transform: t.into(), transform: t.into(),
sprite_index: s.sprite.index, sprite_index: s.sprite.get_index(),
}); });
// Add children // Add children
@ -429,7 +430,7 @@ impl GPUState {
instances.push(ObjectInstance { instances.push(ObjectInstance {
transform: t.into(), transform: t.into(),
sprite_index: s.sprite.index, sprite_index: s.sprite.get_index(),
}); });
} }
@ -481,7 +482,7 @@ impl GPUState {
instances.push(UiInstance { instances.push(UiInstance {
transform: (OPENGL_TO_WGPU_MATRIX * translate * screen_aspect * rotate * scale).into(), 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]), color: s.color.unwrap_or([1.0, 1.0, 1.0, 1.0]),
}); });
} }
@ -613,7 +614,7 @@ impl GPUState {
self.window_size.height as f32, self.window_size.height as f32,
], ],
window_aspect: [self.window_aspect, 0.0], 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_tile_size: [galactica_constants::STARFIELD_SIZE as f32, 0.0],
starfield_size_limits: [ starfield_size_limits: [
galactica_constants::STARFIELD_SIZE_MIN, galactica_constants::STARFIELD_SIZE_MIN,
@ -624,6 +625,7 @@ impl GPUState {
); );
// Write all new particles to GPU buffer // Write all new particles to GPU buffer
state.new_particles.shuffle(&mut rand::thread_rng());
for i in state.new_particles.iter() { for i in state.new_particles.iter() {
self.queue.write_buffer( self.queue.write_buffer(
&self.vertex_buffers.particle.instances, &self.vertex_buffers.particle.instances,
@ -633,7 +635,7 @@ impl GPUState {
velocity: i.velocity.into(), velocity: i.velocity.into(),
rotation: Matrix2::from_angle(i.angle).into(), rotation: Matrix2::from_angle(i.angle).into(),
size: i.size, size: i.size,
sprite_index: i.sprite.index, sprite_index: i.sprite.get_index(),
created: state.current_time, created: state.current_time,
expires: state.current_time + i.lifetime, expires: state.current_time + i.lifetime,
}]), }]),

View File

@ -1,12 +1,13 @@
use cgmath::{Deg, InnerSpace, Vector2}; use cgmath::{Deg, EuclideanSpace, InnerSpace, Matrix2, Rad, Vector2, Zero};
use nalgebra::vector; use nalgebra::{point, vector};
use rapier2d::dynamics::RigidBody; use rand::{rngs::ThreadRng, Rng};
use rapier2d::{dynamics::RigidBody, geometry::Collider};
use crate::{util, ShipPhysicsHandle}; use crate::{util, ShipPhysicsHandle};
use galactica_content as content; use galactica_content as content;
use galactica_gameobject as object; use galactica_gameobject as object;
use galactica_render::ObjectSprite; use galactica_render::{ObjectSprite, ParticleBuilder};
pub struct ShipControls { pub struct ShipControls {
pub left: bool, pub left: bool,
@ -26,6 +27,141 @@ impl ShipControls {
} }
} }
struct ShipCollapseSequence {
total_length: f32,
time_elapsed: f32,
rng: ThreadRng,
}
impl ShipCollapseSequence {
fn new(total_length: f32) -> Self {
Self {
total_length: total_length,
time_elapsed: 0.0,
rng: rand::thread_rng(),
}
}
fn is_done(&self) -> bool {
self.time_elapsed >= self.total_length
}
fn random_in_ship(
&mut self,
ship_content: &content::Ship,
collider: &Collider,
) -> Vector2<f32> {
// Pick a random point inside this ship's collider
let mut y = 0.0;
let mut x = 0.0;
let mut a = false;
while !a {
y = self.rng.gen_range(-1.0..=1.0) * ship_content.size / 2.0;
x = self.rng.gen_range(-1.0..=1.0) * ship_content.size * ship_content.sprite.aspect
/ 2.0;
a = collider.shape().contains_local_point(&point![x, y]);
}
Vector2 { x, y }
}
fn step(
&mut self,
ship: &object::Ship,
ct: &content::Content,
particles: &mut Vec<ParticleBuilder>,
rigid_body: &mut RigidBody,
collider: &mut Collider,
t: f32,
) {
let h = ship.handle;
let ship_content = ct.get_ship(h);
let ship_pos = util::rigidbody_position(rigid_body);
let ship_rot = util::rigidbody_rotation(rigid_body);
let ship_ang = ship_rot.angle(Vector2 { x: 1.0, y: 0.0 });
// The fraction of this collapse sequence that has been played
let frac_done = self.time_elapsed / self.total_length;
// Trigger collapse events
for event in &ship_content.collapse.events {
match event {
content::CollapseEvent::Effect(event) => {
if (event.time > self.time_elapsed && event.time <= self.time_elapsed + t)
|| (event.time == 0.0 && self.time_elapsed == 0.0)
{
for spawner in &event.effects {
let effect = ct.get_effect(spawner.effect);
for _ in 0..spawner.count as usize {
let pos = if let Some(pos) = spawner.pos {
pos.to_vec()
} else {
self.random_in_ship(ship_content, collider)
};
// Position, adjusted for ship rotation
let pos =
Matrix2::from_angle(-ship_ang - Rad::from(Deg(90.0))) * pos;
particles.push(ParticleBuilder {
sprite: effect.sprite,
pos: ship_pos + pos,
velocity: Vector2::zero(),
angle: Deg::zero(),
lifetime: effect.lifetime,
size: effect.size,
});
}
}
}
}
}
}
// Create collapse effects
for spawner in &ship_content.collapse.effects {
let effect = ct.get_effect(spawner.effect);
// Probability of adding a particle this frame.
// The area of this function over [0, 1] should be 1.
let pdf = |x: f32| {
let f = 0.2;
let y = if x < (1.0 - f) {
let x = x / (1.0 - f);
(x * x + 0.2) * 1.8 - f
} else {
1.0
};
return y;
};
let p_add = (t / self.total_length) * pdf(frac_done) * spawner.count;
if self.rng.gen_range(0.0..=1.0) <= p_add {
let pos = if let Some(pos) = spawner.pos {
pos.to_vec()
} else {
self.random_in_ship(ship_content, collider)
};
// Position, adjusted for ship rotation
let pos = Matrix2::from_angle(-ship_ang - Rad::from(Deg(90.0))) * pos;
particles.push(ParticleBuilder {
sprite: effect.sprite,
pos: ship_pos + pos,
velocity: Vector2::zero(),
angle: Deg::zero(),
lifetime: effect.lifetime,
size: effect.size,
});
}
}
self.time_elapsed += t;
}
}
/// A ship instance in the physics system /// A ship instance in the physics system
pub struct ShipWorldObject { pub struct ShipWorldObject {
/// This ship's physics handle /// This ship's physics handle
@ -36,20 +172,46 @@ pub struct ShipWorldObject {
/// This ship's controls /// This ship's controls
pub controls: ShipControls, pub controls: ShipControls,
collapse_sequence: ShipCollapseSequence,
} }
impl ShipWorldObject { impl ShipWorldObject {
/// Make a new ship /// Make a new ship
pub fn new(ship: object::Ship, physics_handle: ShipPhysicsHandle) -> Self { pub fn new(
ct: &content::Content,
ship: object::Ship,
physics_handle: ShipPhysicsHandle,
) -> Self {
let ship_content = ct.get_ship(ship.handle);
ShipWorldObject { ShipWorldObject {
physics_handle, physics_handle,
ship, ship,
controls: ShipControls::new(), controls: ShipControls::new(),
collapse_sequence: ShipCollapseSequence::new(ship_content.collapse.length),
} }
} }
/// Should this ship should be removed from the world?
pub fn remove_from_world(&self) -> bool {
return self.ship.is_dead() && self.collapse_sequence.is_done();
}
/// Step this ship's state by t seconds /// Step this ship's state by t seconds
pub fn step(&mut self, r: &mut RigidBody, t: f32) { pub fn step(
&mut self,
ct: &content::Content,
particles: &mut Vec<ParticleBuilder>,
r: &mut RigidBody,
c: &mut Collider,
t: f32,
) {
if self.ship.is_dead() {
return self
.collapse_sequence
.step(&self.ship, ct, particles, r, c, t);
}
let ship_rot = util::rigidbody_rotation(r); let ship_rot = util::rigidbody_rotation(r);
let engine_force = ship_rot * t; let engine_force = ship_rot * t;

View File

@ -169,9 +169,10 @@ impl<'a> World {
.angle(Vector2 { x: 1.0, y: 0.0 }) .angle(Vector2 { x: 1.0, y: 0.0 })
.into(); .into();
match &projectile.projectile.content.impact_particle { match &projectile.projectile.content.impact_effect {
None => {} None => {}
Some(x) => { Some(x) => {
let x = ct.get_effect(*x);
let velocity = match x.inherit_velocity { let velocity = match x.inherit_velocity {
content::ImpactInheritVelocity::Dont => Vector2 { x: 0.0, y: 0.0 }, content::ImpactInheritVelocity::Dont => Vector2 { x: 0.0, y: 0.0 },
content::ImpactInheritVelocity::Projectile => util::rigidbody_velocity(pr), content::ImpactInheritVelocity::Projectile => util::rigidbody_velocity(pr),
@ -252,7 +253,8 @@ impl<'a> World {
); );
let h = ShipPhysicsHandle(r, c); let h = ShipPhysicsHandle(r, c);
self.ships.insert(c, objects::ShipWorldObject::new(ship, h)); self.ships
.insert(c, objects::ShipWorldObject::new(ct, ship, h));
return h; return h;
} }
@ -263,16 +265,19 @@ impl<'a> World {
let mut projectiles = Vec::new(); let mut projectiles = Vec::new();
let mut to_remove = Vec::new(); let mut to_remove = Vec::new();
for (_, s) in &mut self.ships { for (_, s) in &mut self.ships {
if s.ship.is_destroyed() {
to_remove.push(s.physics_handle);
continue;
}
let r = &mut self.wrapper.rigid_body_set[s.physics_handle.0]; let r = &mut self.wrapper.rigid_body_set[s.physics_handle.0];
s.step(r, t); let c = &mut self.wrapper.collider_set[s.physics_handle.1];
// TODO: unified step info struct
s.step(ct, particles, r, c, t);
if s.controls.guns { if s.controls.guns {
projectiles.push((s.physics_handle, s.ship.fire_guns())); projectiles.push((s.physics_handle, s.ship.fire_guns()));
} }
if s.remove_from_world() {
to_remove.push(s.physics_handle);
continue;
}
} }
for (s, p) in projectiles { for (s, p) in projectiles {
self.add_projectiles(s, p); self.add_projectiles(s, p);
@ -318,9 +323,10 @@ impl<'a> World {
for c in to_remove { for c in to_remove {
let (pr, p) = self.remove_projectile(c).unwrap(); let (pr, p) = self.remove_projectile(c).unwrap();
match &p.projectile.content.expire_particle { match &p.projectile.content.expire_effect {
None => {} None => {}
Some(x) => { Some(x) => {
let x = ct.get_effect(*x);
let pos = util::rigidbody_position(&pr); let pos = util::rigidbody_position(&pr);
let angle: Deg<f32> = util::rigidbody_rotation(&pr) let angle: Deg<f32> = util::rigidbody_rotation(&pr)
.angle(Vector2 { x: 1.0, y: 0.0 }) .angle(Vector2 { x: 1.0, y: 0.0 })