Compare commits

..

5 Commits

Author SHA1 Message Date
Mark 6b5588b061
Minor effect tweaks 2024-01-07 12:39:03 -08:00
Mark 49f4473963
Removed engine flare transparency 2024-01-07 12:38:02 -08:00
Mark 1e07632e39
Updated TODO 2024-01-07 12:23:08 -08:00
Mark 94d7724611
Renames and minor cleanup 2024-01-07 12:16:07 -08:00
Mark 60f84a4c8e
Added ship damage effects and effect fade 2024-01-07 12:15:34 -08:00
24 changed files with 500 additions and 215 deletions

28
TODO.md
View File

@ -1,13 +1,21 @@
## Specific Jobs ## Specific Jobs
- Particle variation
- 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 debris - Ship death debris
- Sprite reels - Sprite reels
- random start frame
- ship leaks
- Passive engine glow - Passive engine glow
- Ship death damage and force events - Ship death damage and force events
- Fix particle inherit velocity - Gun fire effect
- Sprite color variation
- Multi-particle effects
- Better loading
- incremental?
- Higher texture limit (16 x 8096 x 8096 isn't enough)
- GPU limits? (texture size, texture number)
- Particles when a ship is damaged (events)
---------------------------------- ----------------------------------
@ -41,10 +49,11 @@
- Orbiting debris (asteroids) - Orbiting debris (asteroids)
- Collectibles (flotsam) - Collectibles (flotsam)
- UI - UI
- health, shields (cpu by tinyskia?)
- loading screen, menus
- Indicators (planet names, enemy ship stats)
- Landable planets - Landable planets
- Back arrow: reverse thruster or reverse ship - Back arrow: reverse thruster or reverse ship
- Loading screen, incremental texturearray loading
- Indicators (planet names, enemy ship stats)
- Multiplayer? (how does that fit into gameplay?) - Multiplayer? (how does that fit into gameplay?)
- On-screen text - On-screen text
- Controller input & key bindings - Controller input & key bindings
@ -59,10 +68,6 @@
- how to target them - how to target them
- where to go - where to go
- etc, extra flags - etc, extra flags
- Higher texture limit (16 x 8096 x 8096 isn't enough)
- Fast-load menu, progress bar for the rest
- Only load what is needed?
- GPU limits? (texture size, texture number)
- Advanced particle physics (must move to cpu. Maybe both?) - Advanced particle physics (must move to cpu. Maybe both?)
@ -82,14 +87,11 @@
- Clear all `// TODO:` comments littered in the source - Clear all `// TODO:` comments littered in the source
- CLI options (debug, save location, content location, check content) - CLI options (debug, save location, content location, check content)
- Config file and compile options, remove all those consts. - Config file and compile options, remove all those consts.
- Engine flares shouldn't be centered
- Sprite optimization: do we need to allocate a new `Vec` every frame? Probably not. - Sprite optimization: do we need to allocate a new `Vec` every frame? Probably not.
- Better error when run outside of directory - Better error when run outside of directory
- Documentation site & front page - Documentation site & front page
- Random animation age for objects * ui
- Random animation delay/fps? - Random animation delay/fps?
- Fade between animation frames - Better WGSL preprocessor
- Better WGSL preprocessor (warning when including a bad file!)
## Content ## Content
- Angled engines - Angled engines
@ -121,10 +123,8 @@
- Zoom parallax (?) - Zoom parallax (?)
- Background haze - Background haze
- Nova dust parallax - Nova dust parallax
- Ship outlines in radar
- Engine flare ease in/out - Engine flare ease in/out
- Lens flare - Lens flare
- Particles when a ship is damaged
## Write and Document ## Write and Document
- Parallax - Parallax

2
assets

@ -1 +1 @@
Subproject commit 74ddbde9e1cec1418c17844bc7336324ece88d15 Subproject commit 95558076be1819b10d5a56c62274cdf7f61ea9a8

View File

@ -6,6 +6,8 @@ size = 8.0
size_rng = 1.6 size_rng = 1.6
angle_rng = 360 angle_rng = 360
velocity_scale_parent = 1.0 velocity_scale_parent = 1.0
fade = 0.2
fade_rng = 0.1
[effect."large explosion"] [effect."large explosion"]
@ -16,6 +18,8 @@ size = 25.0
size_rng = 5.0 size_rng = 5.0
angle_rng = 360 angle_rng = 360
velocity_scale_parent = 1.0 velocity_scale_parent = 1.0
fade = 0.2
fade_rng = 0.1
[effect."huge explosion"] [effect."huge explosion"]
@ -26,6 +30,46 @@ size = 50.0
size_rng = 10.0 size_rng = 10.0
angle_rng = 360 angle_rng = 360
velocity_scale_parent = 1.0 velocity_scale_parent = 1.0
fade = 0.2
fade_rng = 0.1
[effect."blue spark"]
sprite = "particle::spark::blue"
lifetime = 0.5
lifetime_rng = 0.5
inherit_velocity = "parent"
size = 4.0
size_rng = 2.0
angle_rng = 360
angvel_rng = 0.0
velocity_scale_parent = 1.0
fade = 0.2
fade_rng = 0.1
[effect."yellow spark"]
sprite = "particle::spark::yellow"
lifetime = "inherit"
inherit_velocity = "parent"
size = 4.0
size_rng = 2.0
angle_rng = 360
angvel_rng = 0.0
velocity_scale_parent = 1.0
fade = 0.2
fade_rng = 0.1
[effect."red spark"]
sprite = "particle::spark::red"
lifetime = "inherit"
inherit_velocity = "parent"
size = 4.0
size_rng = 1.0
angle_rng = 360
angvel_rng = 0.0
velocity_scale_parent = 1.0
fade = 0.2
fade_rng = 0.1
# Every effect has a parent, some effects have a target # Every effect has a parent, some effects have a target
@ -38,7 +82,7 @@ size = 3.0 # sprite size, in game units
size_rng = 1.0 # random size variation size_rng = 1.0 # random size variation
angle = 0.0 # absolute starting angle. always added to parent angle. angle = 0.0 # absolute starting angle. always added to parent angle.
angle_rng = 90.0 # Starting angle randomness (up to this value) angle_rng = 0.0 # Starting angle randomness (up to this value)
# Does not affect velocity, only sprite angle # Does not affect velocity, only sprite angle
angvel_rng = 0.0 # Angvel randomness, applied to angvel angvel_rng = 0.0 # Angvel randomness, applied to angvel
@ -51,8 +95,10 @@ velocity_scale_parent_rng = 0.0 # random variation of scale
velocity_scale_target = 1.0 velocity_scale_target = 1.0
velocity_scale_target_rng = 1.0 velocity_scale_target_rng = 1.0
direction_rng = 1.0 # Random variation of travel direction, in degrees, applied to velocity vector (/2 each side?) direction_rng = 0.0 # Random variation of travel direction, in degrees, applied to velocity vector (/2 each side?)
fade = 0.2
fade_rng = 0.1
# TODO: # TODO:
# effect probabilities & variants # effect probabilities & variants

View File

@ -13,10 +13,19 @@ space.outfit = 200
space.engine = 50 space.engine = 50
space.weapon = 50 space.weapon = 50
engines = [{ x = 0.0, y = -1.05, size = 50.0 }] engines = [{ x = 0.0, y = -1, size = 25.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 }]
# Show these once we've been reduced to this much hull
damage.hull = 190
# Spawn this effect once every n seconds, on average
damage.effects = [
{ effect = "blue spark", frequency = 3 },
{ effect = "yellow spark", frequency = 1 },
]
# Length of death sequence, in seconds # Length of death sequence, in seconds
collapse.length = 5.0 collapse.length = 5.0

View File

@ -139,3 +139,39 @@ frames = [
"particle/explosion-huge/09.png", "particle/explosion-huge/09.png",
"particle/explosion-huge/10.png", "particle/explosion-huge/10.png",
] ]
[sprite."particle::spark::blue"]
timing.duration = 0.3
#timing.rng = 0.2 # each frame will be independently sped up/slowed by this factor
#timing.uniform_rng = 0.2 # one factor for all frames
repeat = "reverse"
frames = [
"particle/spark-blue/01.png",
"particle/spark-blue/02.png",
"particle/spark-blue/03.png",
"particle/spark-blue/04.png",
"particle/spark-blue/05.png",
]
[sprite."particle::spark::yellow"]
timing.duration = 0.3
timing.rng = 0.2
repeat = "once"
frames = [
"particle/spark-yellow/01.png",
"particle/spark-yellow/02.png",
"particle/spark-yellow/03.png",
"particle/spark-yellow/04.png",
"particle/spark-yellow/05.png",
]
[sprite."particle::spark::red"]
timing.duration = 0.3
timing.rng = 0.2
repeat = "once"
frames = [
"particle/spark-red/01.png",
"particle/spark-red/02.png",
"particle/spark-red/03.png",
]

View File

@ -29,6 +29,8 @@ pub(crate) mod syntax {
pub velocity_scale_target: Option<f32>, pub velocity_scale_target: Option<f32>,
pub velocity_scale_target_rng: Option<f32>, pub velocity_scale_target_rng: Option<f32>,
pub direction_rng: Option<f32>, pub direction_rng: Option<f32>,
pub fade: Option<f32>,
pub fade_rng: Option<f32>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -56,7 +58,7 @@ pub(crate) mod syntax {
TextOrFloat::Text(s) => { TextOrFloat::Text(s) => {
if s == "inherit" { if s == "inherit" {
let sprite = content.get_sprite(sprite); let sprite = content.get_sprite(sprite);
sprite.fps * sprite.frames.len() as f32 sprite.frame_duration * sprite.frames.len() as f32
} else { } else {
bail!("bad effect lifetime, must be float or \"inherit\"",) bail!("bad effect lifetime, must be float or \"inherit\"",)
} }
@ -74,14 +76,16 @@ pub(crate) mod syntax {
lifetime, lifetime,
lifetime_rng: self.lifetime_rng.unwrap_or(0.0), lifetime_rng: self.lifetime_rng.unwrap_or(0.0),
angle: Deg(self.angle.unwrap_or(0.0) / 2.0).into(), angle: Deg(self.angle.unwrap_or(0.0) / 2.0).into(),
angle_rng: Deg(self.angle_rng.unwrap_or(0.0) / 2.0).into(), angle_rng: self.angle_rng.unwrap_or(0.0) / 2.0,
angvel: Deg(self.angvel.unwrap_or(0.0)).into(), angvel: Deg(self.angvel.unwrap_or(0.0)).into(),
angvel_rng: Deg(self.angle_rng.unwrap_or(0.0)).into(), angvel_rng: self.angvel_rng.unwrap_or(0.0),
velocity_scale_parent: self.velocity_scale_parent.unwrap_or(0.0), velocity_scale_parent: self.velocity_scale_parent.unwrap_or(0.0),
velocity_scale_parent_rng: self.velocity_scale_parent_rng.unwrap_or(0.0), velocity_scale_parent_rng: self.velocity_scale_parent_rng.unwrap_or(0.0),
velocity_scale_target: self.velocity_scale_target.unwrap_or(0.0), velocity_scale_target: self.velocity_scale_target.unwrap_or(0.0),
velocity_scale_target_rng: self.velocity_scale_target_rng.unwrap_or(0.0), velocity_scale_target_rng: self.velocity_scale_target_rng.unwrap_or(0.0),
direction_rng: Deg(self.direction_rng.unwrap_or(0.0) / 2.0).into(), direction_rng: self.direction_rng.unwrap_or(0.0) / 2.0,
fade: self.fade.unwrap_or(0.0),
fade_rng: self.fade_rng.unwrap_or(0.0),
}); });
return Ok(handle); return Ok(handle);
@ -141,13 +145,13 @@ pub struct Effect {
pub angle: Rad<f32>, pub angle: Rad<f32>,
/// Random angle variation /// Random angle variation
pub angle_rng: Rad<f32>, pub angle_rng: f32,
/// How fast this particle spins /// How fast this particle spins
pub angvel: Rad<f32>, pub angvel: Rad<f32>,
/// Random angvel variation /// Random angvel variation
pub angvel_rng: Rad<f32>, pub angvel_rng: f32,
/// The amount of this particle's parent's velocity to inherit /// The amount of this particle's parent's velocity to inherit
pub velocity_scale_parent: f32, pub velocity_scale_parent: f32,
@ -163,7 +167,13 @@ pub struct Effect {
pub velocity_scale_target_rng: f32, pub velocity_scale_target_rng: f32,
/// Travel direction random variation /// Travel direction random variation
pub direction_rng: Rad<f32>, pub direction_rng: f32,
/// Fade this effect out over this many seconds as it ends
pub fade: f32,
/// Random fade ariation
pub fade_rng: f32,
} }
impl crate::Build for Effect { impl crate::Build for Effect {

View File

@ -170,7 +170,10 @@ impl crate::Build for Gun {
lifetime: gun.projectile.lifetime, lifetime: gun.projectile.lifetime,
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),
// Divide by 2, so the angle matches the angle of the fire cone.
// This should ALWAYS be done in the content parser.
angle_rng: Deg(gun.projectile.angle_rng / 2.0),
impact_effect, impact_effect,
expire_effect, expire_effect,
collider: gun.projectile.collider, collider: gun.projectile.collider,

View File

@ -26,6 +26,7 @@ pub(crate) mod syntax {
pub linear_drag: f32, pub linear_drag: f32,
pub space: outfitspace::syntax::OutfitSpace, pub space: outfitspace::syntax::OutfitSpace,
pub collapse: Option<Collapse>, pub collapse: Option<Collapse>,
pub damage: Option<Damage>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -41,6 +42,19 @@ pub(crate) mod syntax {
pub y: f32, pub y: f32,
} }
#[derive(Debug, Deserialize)]
pub struct Damage {
pub hull: f32,
pub effects: Vec<DamageEffectSpawner>,
}
#[derive(Debug, Deserialize)]
pub struct DamageEffectSpawner {
pub effect: EffectReference,
pub frequency: f32,
pub pos: Option<[f32; 2]>,
}
// TODO: // TODO:
// plural or not? document! // plural or not? document!
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -118,6 +132,9 @@ pub struct Ship {
/// Ship collapse sequence /// Ship collapse sequence
pub collapse: ShipCollapse, pub collapse: ShipCollapse,
/// Damaged ship effects
pub damage: ShipDamage,
} }
/// Collision shape for this ship /// Collision shape for this ship
@ -161,7 +178,31 @@ pub struct ShipCollapse {
pub events: Vec<CollapseEvent>, pub events: Vec<CollapseEvent>,
} }
/// A scripted event during a ship collapse sequence /// Parameters for damaged ship effects
#[derive(Debug, Clone)]
pub struct ShipDamage {
/// Show damaged ship effects if hull is below this value
pub hull: f32,
/// Effects to create during collapse
pub effects: Vec<DamageEffectSpawner>,
}
/// An effect shown when a ship is damaged
#[derive(Debug, Clone)]
pub struct DamageEffectSpawner {
/// The effect to create
pub effect: EffectHandle,
/// How often to create this effect
pub frequency: f32,
/// Where to create is effect.
/// Position is random if None.
pub pos: Option<Point2<f32>>,
}
/// An effect shown during a ship collapse sequence
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CollapseEffectSpawner { pub struct CollapseEffectSpawner {
/// The effect to create /// The effect to create
@ -170,7 +211,7 @@ pub struct CollapseEffectSpawner {
/// How many effects to create /// How many effects to create
pub count: f32, pub count: f32,
/// Where to create these effects. /// Where to create this effect.
/// Position is random if None. /// Position is random if None.
pub pos: Option<Point2<f32>>, pub pos: Option<Point2<f32>>,
} }
@ -213,7 +254,8 @@ 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 collapse = {
if let Some(c) = ship.collapse {
let mut effects = Vec::new(); let mut effects = Vec::new();
for e in c.effects { for e in c.effects {
effects.push(CollapseEffectSpawner { effects.push(CollapseEffectSpawner {
@ -236,9 +278,12 @@ impl crate::Build for Ship {
let mut effects = Vec::new(); let mut effects = Vec::new();
for g in e.effects { for g in e.effects {
effects.push(CollapseEffectSpawner { effects.push(CollapseEffectSpawner {
effect: g.effect.to_handle(build_context, ct).with_context( effect: g
|| format!("while loading ship `{}`", ship_name), .effect
)?, .to_handle(build_context, ct)
.with_context(|| {
format!("while loading ship `{}`", ship_name)
})?,
count: g.count, count: g.count,
pos: g.pos.map(|p| Point2 { pos: g.pos.map(|p| Point2 {
x: p[0] * (size / 2.0) * aspect, x: p[0] * (size / 2.0) * aspect,
@ -267,11 +312,43 @@ impl crate::Build for Ship {
effects: vec![], effects: vec![],
events: vec![], events: vec![],
} }
}
};
let damage = {
if let Some(c) = ship.damage {
let mut effects = Vec::new();
for e in c.effects {
effects.push(DamageEffectSpawner {
effect: e
.effect
.to_handle(build_context, ct)
.with_context(|| format!("while loading ship `{}`", ship_name))?,
frequency: e.frequency,
pos: e.pos.map(|p| Point2 {
x: p[0] * (size / 2.0) * aspect,
y: p[1] * size / 2.0,
}),
});
}
ShipDamage {
hull: c.hull,
effects: effects,
}
} else {
// Default damage effects
ShipDamage {
hull: 0.0,
effects: vec![],
}
}
}; };
ct.ships.push(Self { ct.ships.push(Self {
aspect, aspect,
collapse, collapse,
damage,
name: ship_name, name: ship_name,
sprite: handle, sprite: handle,
mass: ship.mass, mass: ship.mass,

View File

@ -35,13 +35,20 @@ pub(crate) mod syntax {
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub enum Timing { pub enum TimingVariant {
#[serde(rename = "duration")] #[serde(rename = "duration")]
Duration(f32), Duration(f32),
#[serde(rename = "fps")] #[serde(rename = "fps")]
Fps(f32), Fps(f32),
} }
#[derive(Debug, Deserialize)]
pub struct Timing {
#[serde(flatten)]
pub variant: TimingVariant,
//pub uniform_rng: Option<f32>,
}
} }
/// How to replay a texture's animation /// How to replay a texture's animation
@ -86,8 +93,11 @@ pub struct Sprite {
pub frames: Vec<PathBuf>, pub frames: Vec<PathBuf>,
/// The speed of this sprite's animation. /// The speed of this sprite's animation.
/// unanimated sprites have zero fps. /// This is zero for unanimate sprites.
pub fps: f32, pub frame_duration: f32,
/// All frames will be sped up/slowed by this factor.
//pub frame_uniform_rng: f32,
/// How to replay this sprite's animation /// How to replay this sprite's animation
pub repeat: RepeatMode, pub repeat: RepeatMode,
@ -145,7 +155,8 @@ impl crate::Build for Sprite {
content.sprites.push(Self { content.sprites.push(Self {
name: sprite_name, name: sprite_name,
frames: vec![t.file], frames: vec![t.file],
fps: 0.0, frame_duration: 0.0,
//frame_uniform_rng: 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,
@ -193,16 +204,17 @@ impl crate::Build for Sprite {
unreachable!("Starfield texture may not be animated") unreachable!("Starfield texture may not be animated")
} }
let fps = match t.timing { let frame_duration = match t.timing.variant {
syntax::Timing::Duration(d) => d / t.frames.len() as f32, syntax::TimingVariant::Duration(d) => d / t.frames.len() as f32,
syntax::Timing::Fps(f) => 1.0 / f, syntax::TimingVariant::Fps(f) => 1.0 / f,
}; };
content.sprite_index.insert(sprite_name.clone(), h); content.sprite_index.insert(sprite_name.clone(), h);
content.sprites.push(Self { content.sprites.push(Self {
name: sprite_name, name: sprite_name,
frames: t.frames, frames: t.frames,
fps, frame_duration,
//frame_uniform_rng: t.timing.uniform_rng.unwrap_or(0.0),
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,

View File

@ -197,7 +197,7 @@ impl<'a> OutfitSet {
.map(|p| ObjectSubSprite { .map(|p| ObjectSubSprite {
pos: Point3 { pos: Point3 {
x: p.pos.x, x: p.pos.x,
y: p.pos.y, y: p.pos.y - p.size / 2.0,
z: 1.0, z: 1.0,
}, },
sprite: *s, sprite: *s,

View File

@ -63,13 +63,15 @@ impl Ship {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
g.cooldown = g.kind.rate + &rng.gen_range(-g.kind.rate_rng..=g.kind.rate_rng); g.cooldown = g.kind.rate + &rng.gen_range(-g.kind.rate_rng..=g.kind.rate_rng);
let lifetime = g.kind.projectile.lifetime
+ rng.gen_range(
-g.kind.projectile.lifetime_rng..=g.kind.projectile.lifetime_rng,
);
( (
Projectile { Projectile {
content: g.kind.projectile.clone(), content: g.kind.projectile.clone(),
lifetime: g.kind.projectile.lifetime lifetime: 0f32.max(lifetime),
+ rng.gen_range(
-g.kind.projectile.lifetime_rng..=g.kind.projectile.lifetime_rng,
),
faction: self.faction, faction: self.faction,
}, },
p.clone(), p.clone(),

View File

@ -90,6 +90,21 @@ impl AtlasSet {
let image_dim = img.dimensions(); let image_dim = img.dimensions();
/*
TODO: seperate CLI argument to check these.
don't check this every run,because sometimes it's necessary. (for animated sprites!)
let mut transparent_border = true;
for (x, y, c) in img.pixels() {
if (x == 0 || y == 0 || x == img.width() - 1 || y == img.height() - 1) && c[3] != 0 {
transparent_border = false;
break;
}
}
if transparent_border {
println!("[WARNING] {} wastes space with a transmparent border",);
}
*/
let dim = [ let dim = [
image_dim.0 + 2 * self.image_margin, image_dim.0 + 2 * self.image_margin,
image_dim.1 + 2 * self.image_margin, image_dim.1 + 2 * self.image_margin,

View File

@ -64,7 +64,7 @@ fn main() -> Result<()> {
} }
// Create atlas set // Create atlas set
let mut atlas_set = AtlasSet::new(8192, 8192, 16, &asset_root, 2); let mut atlas_set = AtlasSet::new(8192, 8192, 16, &asset_root, 1);
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;
@ -88,8 +88,8 @@ fn main() -> Result<()> {
println!("Saving files..."); println!("Saving files...");
atlas_set.save_files( atlas_set.save_files(
|x| PathBuf::from(format!("atlas-{x:0.2}.bmp")), |x| PathBuf::from(format!("cache/atlas-{x:0.2}.bmp")),
&PathBuf::from("spriteatlas.toml"), &PathBuf::from("cache/spriteatlas.toml"),
)?; )?;
return Ok(()); return Ok(());

View File

@ -3,20 +3,20 @@ fn animate(sprite_index: u32, age: f32, offset: f32) -> f32 {
let len = global_sprites[sprite_index].frame_count; let len = global_sprites[sprite_index].frame_count;
let rep = global_sprites[sprite_index].repeatmode; let rep = global_sprites[sprite_index].repeatmode;
let fps = global_sprites[sprite_index].fps; let frame_duration = global_sprites[sprite_index].frame_duration;
var frame: f32 = 0.0; var frame: f32 = 0.0;
// Once // Once
if rep == u32(1) { if rep == u32(1) {
frame = min( frame = min(
age / fps + offset, age / frame_duration + offset,
f32(len) - 1.0 f32(len) - 1.0
); );
// Reverse // Reverse
} else if rep == u32(2) { } else if rep == u32(2) {
let x = age / fps + offset; let x = age / frame_duration + offset;
let m = f32(len) * 2.0 - 1.0; let m = f32(len) * 2.0 - 1.0;
// x fmod m // x fmod m
frame = x - floor(x / m) * m; frame = x - floor(x / m) * m;
@ -32,7 +32,7 @@ fn animate(sprite_index: u32, age: f32, offset: f32) -> f32 {
// Repeat (default) // Repeat (default)
} else { } else {
let x = age / fps + offset; let x = age / frame_duration + offset;
let m = f32(len); let m = f32(len);
frame = x - floor(x / m) * m; frame = x - floor(x / m) * m;
} }

View File

@ -8,7 +8,8 @@ struct InstanceInput {
@location(6) size: f32, @location(6) size: f32,
@location(7) created: f32, @location(7) created: f32,
@location(8) expires: f32, @location(8) expires: f32,
@location(9) sprite_index: u32, @location(9) fade: f32,
@location(10) sprite_index: u32,
}; };
struct VertexInput { struct VertexInput {
@ -19,10 +20,11 @@ struct VertexInput {
struct VertexOutput { struct VertexOutput {
@builtin(position) position: vec4<f32>, @builtin(position) position: vec4<f32>,
@location(0) tween: f32, @location(0) tween: f32,
@location(1) texture_index_a: u32, @location(1) fade: f32,
@location(2) texture_coords_a: vec2<f32>, @location(2) texture_index_a: u32,
@location(3) texture_index_b: u32, @location(3) texture_coords_a: vec2<f32>,
@location(4) texture_coords_b: vec2<f32>, @location(4) texture_index_b: u32,
@location(5) texture_coords_b: vec2<f32>,
} }
@ -84,6 +86,11 @@ fn vertex_main(
out.position = vec4(pos, 0.0, 1.0); out.position = vec4(pos, 0.0, 1.0);
if instance.expires - global_data.current_time.x <= instance.fade {
out.fade = (instance.expires - global_data.current_time.x) / instance.fade;
} else {
out.fade = 1.0;
}
// Compute texture coordinates // Compute texture coordinates
let frame = animate(instance.sprite_index, age, 0.0); let frame = animate(instance.sprite_index, age, 0.0);
@ -131,5 +138,5 @@ fn fragment_main(in: VertexOutput) -> @location(0) vec4<f32> {
0.0 0.0
).rgba, ).rgba,
in.tween in.tween
); ) * vec4(1.0, 1.0, 1.0, in.fade);
} }

View File

@ -83,7 +83,7 @@ impl GlobalUniform {
frame_count: u32, frame_count: u32,
repeatmode: u32, repeatmode: u32,
aspect: f32, aspect: f32,
fps: f32, frame_duration: f32,
first_frame: u32, first_frame: u32,
padding_a: f32, padding_a: f32,

View File

@ -10,7 +10,7 @@ pub struct SpriteData {
pub frame_count: u32, pub frame_count: u32,
pub repeatmode: u32, pub repeatmode: u32,
pub aspect: f32, pub aspect: f32,
pub fps: f32, pub frame_duration: f32,
// Index of first frame in ImageLocationArray // Index of first frame in ImageLocationArray
pub first_frame: u32, pub first_frame: u32,

View File

@ -586,6 +586,7 @@ impl GPUState {
sprite_index: i.sprite.get_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,
fade: i.fade,
}]), }]),
); );
self.vertex_buffers.particle_array_head += 1; self.vertex_buffers.particle_array_head += 1;

View File

@ -1,5 +1,6 @@
use crate::content; use crate::content;
use cgmath::{Deg, Point2, Point3, Rad, Vector2}; use cgmath::{Deg, Matrix2, Point2, Point3, Rad, Vector2};
use rand::Rng;
/// Instructions to create a new particle /// Instructions to create a new particle
pub struct ParticleBuilder { pub struct ParticleBuilder {
@ -24,6 +25,65 @@ pub struct ParticleBuilder {
/// The size of this particle, /// The size of this particle,
/// given as height in world units. /// given as height in world units.
pub size: f32, pub size: f32,
/// Fade this particle out over this many seconds as it expires
pub fade: f32,
}
impl ParticleBuilder {
/// Create a ParticleBuilder from an Effect
pub fn from_content(
effect: &content::Effect,
pos: Point2<f32>,
parent_angle: Rad<f32>,
parent_velocity: Vector2<f32>,
target_velocity: Vector2<f32>,
) -> Self {
let mut rng = rand::thread_rng();
let velocity = {
let a =
rng.gen_range(-effect.velocity_scale_parent_rng..=effect.velocity_scale_parent_rng);
let b =
rng.gen_range(-effect.velocity_scale_target_rng..=effect.velocity_scale_target_rng);
let velocity = ((effect.velocity_scale_parent + a) * parent_velocity)
+ ((effect.velocity_scale_target + b) * target_velocity);
Matrix2::from_angle(Rad(
rng.gen_range(-effect.direction_rng..=effect.direction_rng)
)) * velocity
};
// Rad has odd behavior when its angle is zero, so we need extra checks here
let angvel = if effect.angvel_rng == 0.0 {
effect.angvel
} else {
Rad(effect.angvel.0 + rng.gen_range(-effect.angvel_rng..=effect.angvel_rng))
};
let angle = if effect.angle_rng == 0.0 {
parent_angle + effect.angle
} else {
parent_angle + effect.angle + Rad(rng.gen_range(-effect.angle_rng..=effect.angle_rng))
};
ParticleBuilder {
sprite: effect.sprite,
pos,
velocity,
angle,
angvel,
lifetime: 0f32
.max(effect.lifetime + rng.gen_range(-effect.lifetime_rng..=effect.lifetime_rng)),
// Make sure size isn't negative. This check should be on EVERY rng!
size: 0f32.max(effect.size + rng.gen_range(-effect.size_rng..=effect.size_rng)),
fade: 0f32.max(effect.fade + rng.gen_range(-effect.fade_rng..=effect.fade_rng)),
}
}
} }
/// The location of a UI element, in one of a few /// The location of a UI element, in one of a few

View File

@ -77,7 +77,7 @@ impl RawTexture {
pub struct Texture { pub struct Texture {
pub index: u32, // Index in texture array pub index: u32, // Index in texture array
pub len: u32, // Number of frames pub len: u32, // Number of frames
pub fps: f32, // Frames per second pub frame_duration: f32, // Frames per second
pub aspect: f32, // width / height pub aspect: f32, // width / height
pub repeat: u32, // How to re-play this texture pub repeat: u32, // How to re-play this texture
pub location: Vec<SpriteAtlasImage>, pub location: Vec<SpriteAtlasImage>,
@ -120,7 +120,7 @@ impl TextureArray {
frame_count: t.frames.len() as u32, frame_count: t.frames.len() as u32,
repeatmode: t.repeat.as_int(), repeatmode: t.repeat.as_int(),
aspect: t.aspect, aspect: t.aspect,
fps: t.fps, frame_duration: t.frame_duration,
first_frame: image_counter, first_frame: image_counter,
_padding: Default::default(), _padding: Default::default(),
}; };

View File

@ -206,6 +206,9 @@ pub struct ParticleInstance {
/// Time is kept by a variable in the global uniform. /// Time is kept by a variable in the global uniform.
pub expires: f32, pub expires: f32,
/// Fade this particle out over this many seconds as it expires
pub fade: f32,
/// What sprite to use for this particle /// What sprite to use for this particle
pub sprite_index: u32, pub sprite_index: u32,
} }
@ -258,10 +261,16 @@ impl BufferObject for ParticleInstance {
shader_location: 8, shader_location: 8,
format: wgpu::VertexFormat::Float32, format: wgpu::VertexFormat::Float32,
}, },
// Sprite // Fade
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 9]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 9]>() as wgpu::BufferAddress,
shader_location: 9, shader_location: 9,
format: wgpu::VertexFormat::Float32,
},
// Sprite
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 10]>() as wgpu::BufferAddress,
shader_location: 10,
format: wgpu::VertexFormat::Uint32, format: wgpu::VertexFormat::Uint32,
}, },
], ],

View File

@ -1,4 +1,5 @@
use cgmath::{Deg, InnerSpace, Point3, Vector2}; use cgmath::{Deg, InnerSpace, Point3, Vector2};
use rand::Rng;
use rapier2d::{ use rapier2d::{
dynamics::{RigidBody, RigidBodyHandle}, dynamics::{RigidBody, RigidBodyHandle},
geometry::ColliderHandle, geometry::ColliderHandle,
@ -19,6 +20,9 @@ pub struct ProjectileWorldObject {
/// This projectile's collider /// This projectile's collider
pub collider: ColliderHandle, pub collider: ColliderHandle,
/// This projectile's size variation
pub size_rng: f32,
} }
impl ProjectileWorldObject { impl ProjectileWorldObject {
@ -28,10 +32,13 @@ impl ProjectileWorldObject {
rigid_body: RigidBodyHandle, rigid_body: RigidBodyHandle,
collider: ColliderHandle, collider: ColliderHandle,
) -> Self { ) -> Self {
let mut rng = rand::thread_rng();
let size_rng = projectile.content.size_rng;
ProjectileWorldObject { ProjectileWorldObject {
rigid_body, rigid_body,
collider, collider,
projectile, projectile,
size_rng: rng.gen_range(-size_rng..=size_rng),
} }
} }
@ -50,7 +57,7 @@ impl ProjectileWorldObject {
y: pos.y, y: pos.y,
z: 1.0, z: 1.0,
}, },
size: self.projectile.content.size, size: 0f32.max(self.projectile.content.size + self.size_rng),
angle: -ang, angle: -ang,
children: None, children: None,
} }

View File

@ -98,51 +98,21 @@ impl ShipCollapseSequence {
} else { } else {
self.random_in_ship(ship_content, collider) self.random_in_ship(ship_content, collider)
}; };
// Position, adjusted for ship rotation
let pos = ship_pos let pos = ship_pos
+ Matrix2::from_angle(-ship_ang - Rad::from(Deg(90.0))) * pos; + (Matrix2::from_angle(-ship_ang - Rad::from(Deg(90.0))) * pos);
let velocity = { let velocity = rigid_body.velocity_at_point(&point![pos.x, pos.y]);
let a = self.rng.gen_range(
-effect.velocity_scale_parent_rng
..=effect.velocity_scale_parent_rng,
);
let velocity = (effect.velocity_scale_parent + a) particles.push(ParticleBuilder::from_content(
* rigid_body.velocity_at_point(&point![pos.x, pos.y]); effect,
pos,
Matrix2::from_angle(Rad(self.rng.gen_range( Rad::zero(),
-effect.direction_rng.0..=effect.direction_rng.0, Vector2 {
))) * Vector2 {
x: velocity.x, x: velocity.x,
y: velocity.y, y: velocity.y,
} },
}; Vector2::zero(),
))
particles.push(ParticleBuilder {
sprite: effect.sprite,
pos,
velocity,
angle: effect.angle
+ Rad(self
.rng
.gen_range(-effect.angle_rng.0..=effect.angle_rng.0)),
angvel: Rad(effect.angvel.0
+ self
.rng
.gen_range(-effect.angvel_rng.0..=effect.angvel_rng.0)),
lifetime: effect.lifetime
+ self
.rng
.gen_range(-effect.lifetime_rng..=effect.lifetime_rng),
size: effect.size
+ self.rng.gen_range(-effect.size_rng..=effect.size_rng),
});
} }
} }
} }
@ -188,6 +158,7 @@ impl ShipCollapseSequence {
angvel: Rad::zero(), angvel: Rad::zero(),
lifetime: effect.lifetime, lifetime: effect.lifetime,
size: effect.size, size: effect.size,
fade: 0.0,
}); });
} }
} }
@ -236,21 +207,68 @@ impl ShipWorldObject {
&mut self, &mut self,
ct: &content::Content, ct: &content::Content,
particles: &mut Vec<ParticleBuilder>, particles: &mut Vec<ParticleBuilder>,
r: &mut RigidBody, rigid_body: &mut RigidBody,
c: &mut Collider, collider: &mut Collider,
t: f32, t: f32,
) { ) {
if self.ship.is_dead() { if self.ship.is_dead() {
return self return self
.collapse_sequence .collapse_sequence
.step(&self.ship, ct, particles, r, c, t); .step(&self.ship, ct, particles, rigid_body, collider, t);
}
let ship_content = ct.get_ship(self.ship.handle);
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 });
let mut rng = rand::thread_rng();
if self.ship.hull <= ship_content.damage.hull {
for e in &ship_content.damage.effects {
if rng.gen_range(0.0..=1.0) <= t / e.frequency {
let effect = ct.get_effect(e.effect);
let ship_content = ct.get_ship(self.ship.handle);
let pos = if let Some(pos) = e.pos {
pos.to_vec()
} else {
// 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 = rng.gen_range(-1.0..=1.0) * ship_content.size / 2.0;
x = 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 }
};
let pos =
ship_pos + (Matrix2::from_angle(-ship_ang - Rad::from(Deg(90.0))) * pos);
let velocity = rigid_body.velocity_at_point(&point![pos.x, pos.y]);
particles.push(ParticleBuilder::from_content(
effect,
pos,
Rad::zero(),
Vector2 {
x: velocity.x,
y: velocity.y,
},
Vector2::zero(),
))
}
}
} }
let ship_rot = util::rigidbody_rotation(r);
let engine_force = ship_rot * t; let engine_force = ship_rot * t;
if self.controls.thrust { if self.controls.thrust {
r.apply_impulse( rigid_body.apply_impulse(
vector![engine_force.x, engine_force.y] vector![engine_force.x, engine_force.y]
* self.ship.outfits.stat_sum().engine_thrust, * self.ship.outfits.stat_sum().engine_thrust,
true, true,
@ -258,11 +276,13 @@ impl ShipWorldObject {
} }
if self.controls.right { if self.controls.right {
r.apply_torque_impulse(self.ship.outfits.stat_sum().steer_power * -100.0 * t, true); rigid_body
.apply_torque_impulse(self.ship.outfits.stat_sum().steer_power * -100.0 * t, true);
} }
if self.controls.left { if self.controls.left {
r.apply_torque_impulse(self.ship.outfits.stat_sum().steer_power * 100.0 * t, true); rigid_body
.apply_torque_impulse(self.ship.outfits.stat_sum().steer_power * 100.0 * t, true);
} }
for i in self.ship.outfits.iter_guns() { for i in self.ship.outfits.iter_guns() {

View File

@ -1,4 +1,4 @@
use cgmath::{Deg, EuclideanSpace, InnerSpace, Matrix2, Point2, Rad, Vector2}; use cgmath::{Deg, EuclideanSpace, InnerSpace, Matrix2, Point2, Rad, Vector2, Zero};
use crossbeam::channel::Receiver; use crossbeam::channel::Receiver;
use nalgebra::{point, vector}; use nalgebra::{point, vector};
use rand::Rng; use rand::Rng;
@ -80,9 +80,9 @@ impl<'a> World {
let pos = ship_pos + (Matrix2::from_angle(-ship_ang) * point.pos.to_vec()); let pos = ship_pos + (Matrix2::from_angle(-ship_ang) * point.pos.to_vec());
let spread: Rad<f32> = Deg(rng.gen_range( let spread: Rad<f32> = Deg(
-(projectile.content.angle_rng.0 / 2.0)..=projectile.content.angle_rng.0 / 2.0, rng.gen_range(-projectile.content.angle_rng.0..=projectile.content.angle_rng.0)
)) )
.into(); .into();
let vel = ship_vel let vel = ship_vel
@ -117,11 +117,7 @@ impl<'a> World {
self.projectiles.insert( self.projectiles.insert(
collider.clone(), collider.clone(),
ProjectileWorldObject { ProjectileWorldObject::new(projectile, rigid_body, collider),
projectile,
rigid_body,
collider,
},
); );
} }
} }
@ -133,7 +129,6 @@ impl<'a> World {
projectile_h: ColliderHandle, projectile_h: ColliderHandle,
ship_h: ColliderHandle, ship_h: ColliderHandle,
) { ) {
let mut rng = rand::thread_rng();
let projectile = self.projectiles.get(&projectile_h); let projectile = self.projectiles.get(&projectile_h);
let ship = self.ships.get_mut(&ship_h); let ship = self.ships.get_mut(&ship_h);
if projectile.is_none() || ship.is_none() { if projectile.is_none() || ship.is_none() {
@ -173,40 +168,22 @@ impl<'a> World {
match &projectile.projectile.content.impact_effect { match &projectile.projectile.content.impact_effect {
None => {} None => {}
Some(x) => { Some(x) => {
let x = ct.get_effect(*x); let effect = ct.get_effect(*x);
let velocity = {
let (_, sr) = self.get_ship_body(s).unwrap(); let (_, sr) = self.get_ship_body(s).unwrap();
let parent_velocity = util::rigidbody_velocity(pr);
let target_velocity = let target_velocity =
sr.velocity_at_point(&nalgebra::Point2::new(pos.x, pos.y)); sr.velocity_at_point(&nalgebra::Point2::new(pos.x, pos.y));
let a = rng
.gen_range(-x.velocity_scale_parent_rng..=x.velocity_scale_parent_rng);
let b = rng
.gen_range(-x.velocity_scale_target_rng..=x.velocity_scale_target_rng);
let velocity = ((x.velocity_scale_parent + a) particles.push(ParticleBuilder::from_content(
* util::rigidbody_velocity(pr)) effect,
+ ((x.velocity_scale_target + b) pos,
* Vector2 { Rad::from(-angle),
parent_velocity,
Vector2 {
x: target_velocity.x, x: target_velocity.x,
y: target_velocity.y, y: target_velocity.y,
}); },
));
Matrix2::from_angle(Rad(
rng.gen_range(-x.direction_rng.0..=x.direction_rng.0)
)) * velocity
};
particles.push(ParticleBuilder {
sprite: x.sprite,
pos: Point2 { x: pos.x, y: pos.y },
velocity,
angle: Rad::from(-angle)
+ Rad(rng.gen_range(-x.angle_rng.0..=x.angle_rng.0)),
angvel: Rad(x.angvel.0 + rng.gen_range(-x.angvel_rng.0..=x.angvel_rng.0)),
lifetime: x.lifetime + rng.gen_range(-x.lifetime_rng..=x.lifetime_rng),
size: x.size + rng.gen_range(-x.size_rng..=x.size_rng),
});
} }
}; };
@ -352,21 +329,15 @@ impl<'a> World {
let velocity = (x.velocity_scale_parent + a) * vel; let velocity = (x.velocity_scale_parent + a) * vel;
velocity velocity
//Matrix2::from_angle(Rad(
// rng.gen_range(-x.direction_rng.0..=x.direction_rng.0)
//)) * velocity
}; };
particles.push(ParticleBuilder { particles.push(ParticleBuilder::from_content(
sprite: x.sprite, x,
pos: Point2 { x: pos.x, y: pos.y }, pos,
Rad::from(-angle),
velocity, velocity,
angle: Rad::from(-angle) Vector2::zero(),
+ x.angle + Rad(rng.gen_range(-x.angle_rng.0..=x.angle_rng.0)), ));
angvel: Rad(x.angvel.0 + rng.gen_range(-x.angvel_rng.0..=x.angvel_rng.0)),
lifetime: x.lifetime + rng.gen_range(-x.lifetime_rng..=x.lifetime_rng),
size: x.size + rng.gen_range(-x.size_rng..=x.size_rng),
});
} }
}; };
} }