From 27c8bf6093afc3808f2ceaeb38aed1d3439a13bd Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 5 Jan 2024 18:04:30 -0800 Subject: [PATCH] Added ship collapse sequence --- assets | 2 +- content/effects.toml | 13 ++- content/ship.toml | 57 ++++++++++ crates/content/src/lib.rs | 5 +- crates/content/src/part/gun.rs | 2 + crates/content/src/part/mod.rs | 21 ++-- crates/content/src/part/ship.rs | 149 ++++++++++++++++++++++++-- crates/galactica/src/game.rs | 5 +- crates/gameobject/src/ship.rs | 13 ++- crates/render/src/gpustate.rs | 2 + crates/world/src/objects/ship.rs | 174 +++++++++++++++++++++++++++++-- crates/world/src/world.rs | 18 ++-- 12 files changed, 416 insertions(+), 45 deletions(-) diff --git a/assets b/assets index 38fd676..74ddbde 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 38fd6766762ce90bb699f98e30e46b17c4eda50c +Subproject commit 74ddbde9e1cec1418c17844bc7336324ece88d15 diff --git a/content/effects.toml b/content/effects.toml index a320923..69e5d1a 100644 --- a/content/effects.toml +++ b/content/effects.toml @@ -2,13 +2,19 @@ sprite = "particle::explosion::small" lifetime = "inherit" inherit_velocity = "target" -size = 3.0 +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 = 3.0 +size = 50.0 [effect."blaster expire"] sprite = "particle::blaster" @@ -25,5 +31,4 @@ size = 3.0 # effect probabilities & variants # multiple particles in one effect # fade -# better physics -# document:effect vs particle +# document: effect vs particle diff --git a/content/ship.toml b/content/ship.toml index fcb43c1..103824b 100644 --- a/content/ship.toml +++ b/content/ship.toml @@ -6,6 +6,9 @@ hull = 200 linear_drag = 0.2 angular_drag = 0.2 +# TODO: disable +# TODO: damage effects + space.outfit = 200 space.engine = 50 space.weapon = 50 @@ -13,6 +16,19 @@ space.weapon = 50 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 }] + +# 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], [0.53921, 1.0000], @@ -37,3 +53,44 @@ collision = [ [-0.53921, 0.29343], [-0.53921, 1.0000], ] + + +# Scripted explosion +[[ship."Gypsum".collapse.event]] +time = 5.0 +effects = [ + #[rustfmt:skip], + { effect = "small explosion", count = 8 }, + { effect = "large explosion", count = 5 }, + { effect = "huge explosion", count = 1, pos = [0, 0] }, + { effect = "huge explosion", count = 4 }, +] + +# 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 }] diff --git a/crates/content/src/lib.rs b/crates/content/src/lib.rs index 43614f6..a95660e 100644 --- a/crates/content/src/lib.rs +++ b/crates/content/src/lib.rs @@ -21,10 +21,7 @@ use walkdir::WalkDir; pub use handle::{ EffectHandle, FactionHandle, GunHandle, OutfitHandle, ShipHandle, SpriteHandle, SystemHandle, }; -pub use part::{ - Effect, EnginePoint, Faction, Gun, GunPoint, ImpactInheritVelocity, Outfit, OutfitSpace, - Projectile, ProjectileCollider, Relationship, RepeatMode, Ship, Sprite, System, -}; +pub use part::*; mod syntax { use anyhow::{bail, Context, Result}; diff --git a/crates/content/src/part/gun.rs b/crates/content/src/part/gun.rs index 8bb97b9..5f927f7 100644 --- a/crates/content/src/part/gun.rs +++ b/crates/content/src/part/gun.rs @@ -48,8 +48,10 @@ pub enum ProjectileCollider { Ball(BallCollider), } +/// A simple ball-shaped collider, centered at the object's position #[derive(Debug, Deserialize, Clone)] pub struct BallCollider { + /// The radius of this ball pub radius: f32, } diff --git a/crates/content/src/part/mod.rs b/crates/content/src/part/mod.rs index dc6280c..2debf4c 100644 --- a/crates/content/src/part/mod.rs +++ b/crates/content/src/part/mod.rs @@ -1,19 +1,22 @@ //! Content parts -pub mod effect; -pub mod faction; -pub mod gun; -pub mod outfit; -pub mod outfitspace; -pub mod ship; -pub mod sprite; -pub mod system; +pub(crate) mod effect; +pub(crate) mod faction; +pub(crate) mod gun; +pub(crate) mod outfit; +pub(crate) mod outfitspace; +pub(crate) mod ship; +pub(crate) mod sprite; +pub(crate) mod system; pub use effect::{Effect, ImpactInheritVelocity}; pub use faction::{Faction, Relationship}; pub use gun::{Gun, Projectile, ProjectileCollider}; pub use outfit::Outfit; 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 system::{Object, System}; diff --git a/crates/content/src/part/ship.rs b/crates/content/src/part/ship.rs index 87930a4..9d49fd0 100644 --- a/crates/content/src/part/ship.rs +++ b/crates/content/src/part/ship.rs @@ -1,13 +1,13 @@ use std::collections::HashMap; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use cgmath::Point2; use nalgebra::{point, Point}; -use crate::{handle::SpriteHandle, Content, ContentBuildContext, OutfitSpace}; +use crate::{handle::SpriteHandle, Content, ContentBuildContext, EffectHandle, OutfitSpace}; pub(crate) mod syntax { - use crate::part::outfitspace; + use crate::part::{effect::syntax::EffectReference, outfitspace}; use serde::Deserialize; // Raw serde syntax structs. @@ -25,6 +25,7 @@ pub(crate) mod syntax { pub angular_drag: f32, pub linear_drag: f32, pub space: outfitspace::syntax::OutfitSpace, + pub collapse: Option, } #[derive(Debug, Deserialize)] @@ -39,6 +40,34 @@ pub(crate) mod syntax { pub x: f32, pub y: f32, } + + // TODO: + // plural or not? document! + #[derive(Debug, Deserialize)] + pub struct Collapse { + pub length: f32, + pub effects: Vec, + pub event: Vec, + } + + #[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, + } } // Processed data structs. @@ -86,6 +115,9 @@ pub struct Ship { /// Outfit space in this ship pub space: OutfitSpace, + + /// Ship collapse sequence + pub collapse: ShipCollapse, } /// Collision shape for this ship @@ -116,16 +148,60 @@ pub struct GunPoint { pub pos: Point2, } +/// 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, + + /// Scripted events during ship collapse + pub events: Vec, +} + +/// 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>, +} + +/// 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, +} + impl crate::Build for Ship { type InputSyntaxType = HashMap; fn build( ship: Self::InputSyntaxType, - _build_context: &mut ContentBuildContext, - content: &mut Content, + build_context: &mut ContentBuildContext, + ct: &mut Content, ) -> Result<()> { for (ship_name, ship) in ship { - let handle = match content.sprite_index.get(&ship.sprite) { + let handle = match ct.sprite_index.get(&ship.sprite) { None => bail!( "In ship `{}`: sprite `{}` doesn't exist", ship_name, @@ -135,10 +211,67 @@ impl crate::Build for Ship { }; let size = ship.size; - let aspect = content.get_sprite(handle).aspect; + let aspect = ct.get_sprite(handle).aspect; - content.ships.push(Self { + 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 { aspect, + collapse, name: ship_name, sprite: handle, mass: ship.mass, diff --git a/crates/galactica/src/game.rs b/crates/galactica/src/game.rs index cc95916..391de5e 100644 --- a/crates/galactica/src/game.rs +++ b/crates/galactica/src/game.rs @@ -41,6 +41,7 @@ impl Game { 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 }); let s = object::Ship::new( &ct, @@ -57,14 +58,14 @@ impl Game { // This method of specifying factions is non-deterministic, // but that's ok since this is for debug. // TODO: fix - content::FactionHandle { index: 0 }, + content::FactionHandle { index: 1 }, object::OutfitSet::new(ss), ); let h2 = physics.add_ship(&ct, s, Point2 { x: 300.0, y: 300.0 }); let mut o1 = object::OutfitSet::new(ss); 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( &ct, diff --git a/crates/gameobject/src/ship.rs b/crates/gameobject/src/ship.rs index 7d9fa6b..40ad71c 100644 --- a/crates/gameobject/src/ship.rs +++ b/crates/gameobject/src/ship.rs @@ -30,8 +30,7 @@ impl Ship { } } - /// Has this ship been destroyed? - pub fn is_destroyed(&self) -> bool { + pub fn is_dead(&self) -> bool { self.hull <= 0.0 } @@ -42,8 +41,10 @@ impl Ship { let r = f.relationships.get(&p.faction).unwrap(); match r { content::Relationship::Hostile => { - // TODO: implement death and spawning, and enable damage - //s.hull -= p.damage; + if self.is_dead() { + return true; + } + self.hull -= p.content.damage; return true; } _ => return false, @@ -51,6 +52,10 @@ impl Ship { } pub fn fire_guns(&mut self) -> Vec<(Projectile, content::GunPoint)> { + if self.is_dead() { + return vec![]; + } + self.outfits .iter_guns_points() .filter(|(g, _)| g.cooldown <= 0.0) diff --git a/crates/render/src/gpustate.rs b/crates/render/src/gpustate.rs index a808c0b..c232399 100644 --- a/crates/render/src/gpustate.rs +++ b/crates/render/src/gpustate.rs @@ -2,6 +2,7 @@ use anyhow::Result; use bytemuck; use cgmath::{Deg, EuclideanSpace, Matrix2, Matrix4, Point2, Vector3}; use galactica_constants; +use rand::seq::SliceRandom; use std::{iter, rc::Rc}; use wgpu; use winit::{self, dpi::LogicalSize, window::Window}; @@ -624,6 +625,7 @@ impl GPUState { ); // Write all new particles to GPU buffer + state.new_particles.shuffle(&mut rand::thread_rng()); for i in state.new_particles.iter() { self.queue.write_buffer( &self.vertex_buffers.particle.instances, diff --git a/crates/world/src/objects/ship.rs b/crates/world/src/objects/ship.rs index ad1ff15..047fcda 100644 --- a/crates/world/src/objects/ship.rs +++ b/crates/world/src/objects/ship.rs @@ -1,12 +1,13 @@ -use cgmath::{Deg, InnerSpace, Vector2}; -use nalgebra::vector; +use cgmath::{Deg, EuclideanSpace, InnerSpace, Matrix2, Rad, Vector2, Zero}; +use nalgebra::{point, vector}; -use rapier2d::dynamics::RigidBody; +use rand::{rngs::ThreadRng, Rng}; +use rapier2d::{dynamics::RigidBody, geometry::Collider}; use crate::{util, ShipPhysicsHandle}; use galactica_content as content; use galactica_gameobject as object; -use galactica_render::ObjectSprite; +use galactica_render::{ObjectSprite, ParticleBuilder}; pub struct ShipControls { 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 { + // 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, + 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 pub struct ShipWorldObject { /// This ship's physics handle @@ -36,20 +172,46 @@ pub struct ShipWorldObject { /// This ship's controls pub controls: ShipControls, + + collapse_sequence: ShipCollapseSequence, } impl ShipWorldObject { /// 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 { physics_handle, ship, 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 - pub fn step(&mut self, r: &mut RigidBody, t: f32) { + pub fn step( + &mut self, + ct: &content::Content, + particles: &mut Vec, + 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 engine_force = ship_rot * t; diff --git a/crates/world/src/world.rs b/crates/world/src/world.rs index 8f6e641..7f9bb0f 100644 --- a/crates/world/src/world.rs +++ b/crates/world/src/world.rs @@ -253,7 +253,8 @@ impl<'a> World { ); 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; } @@ -264,16 +265,19 @@ impl<'a> World { let mut projectiles = Vec::new(); let mut to_remove = Vec::new(); 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]; - 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 { 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 { self.add_projectiles(s, p);