Added projectile parameters

master
Mark 2024-01-03 13:19:10 -08:00
parent 67f19e91b6
commit e344c344ad
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
8 changed files with 297 additions and 77 deletions

View File

@ -1,5 +1,4 @@
## Specific Jobs ## Specific Jobs
- Define projectile colliders & particles
- Particle variation - Particle variation
- Animated sprites - Animated sprites
- UI: health, shield, fuel, heat, energy bars - UI: health, shield, fuel, heat, energy bars

View File

@ -4,10 +4,10 @@ space.weapon = 10
# Average delay between shots # Average delay between shots
rate = 0.2 rate = 0.2
# Random rate variation (+- this in both directions) # Random rate variation (each cooldown is +- this)
rate_rng = 0.1 rate_rng = 0.1
# TODO: apply force on fire
projectile.sprite_texture = "projectile::blaster" projectile.sprite_texture = "projectile::blaster"
# Height of projectile in game units # Height of projectile in game units
projectile.size = 6 projectile.size = 6
@ -24,3 +24,18 @@ projectile.damage = 10.0
# If this is 10, projectiles will form a cone of 10 degrees # If this is 10, projectiles will form a cone of 10 degrees
# ( 5 degrees on each side ) # ( 5 degrees on each side )
projectile.angle_rng = 2 projectile.angle_rng = 2
# How much force this projectile will apply to an object it hits
projectile.force = 0.0
projectile.collider.ball.radius = 2.0
projectile.impact.texture = "particle::blaster"
projectile.impact.lifetime = "inherit"
projectile.impact.inherit_velocity = "target"
projectile.impact.size = 3.0
projectile.expire.texture = "particle::blaster"
projectile.expire.lifetime = "inherit"
projectile.expire.inherit_velocity = "projectile"
projectile.expire.size = 3.0

View File

@ -17,7 +17,7 @@ file = "planet/luna.png"
file = "projectile/blaster.png" file = "projectile/blaster.png"
[texture."ship::gypsum"] [texture."ship::gypsum"]
file = "ship/gypsum2.png" file = "ship/gypsum.png"
[texture."ui::radar"] [texture."ui::radar"]
file = "ui/radar.png" file = "ui/radar.png"

View File

@ -19,8 +19,8 @@ use walkdir::WalkDir;
pub use handle::{FactionHandle, GunHandle, OutfitHandle, ShipHandle, SystemHandle, TextureHandle}; pub use handle::{FactionHandle, GunHandle, OutfitHandle, ShipHandle, SystemHandle, TextureHandle};
pub use part::{ pub use part::{
EnginePoint, Faction, Gun, GunPoint, Outfit, OutfitSpace, Projectile, Relationship, RepeatMode, EnginePoint, Faction, Gun, GunPoint, ImpactInheritVelocity, Outfit, OutfitSpace, Projectile,
Ship, System, Texture, ProjectileCollider, ProjectileParticle, Relationship, RepeatMode, Ship, System, Texture,
}; };
mod syntax { mod syntax {

View File

@ -1,7 +1,7 @@
use std::collections::HashMap; use anyhow::{bail, Context, Result};
use anyhow::{bail, Result};
use cgmath::Deg; use cgmath::Deg;
use serde::Deserialize;
use std::collections::HashMap;
use crate::{handle::TextureHandle, Content}; use crate::{handle::TextureHandle, Content};
@ -32,7 +32,58 @@ 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 expire: Option<ProjectileParticle>,
pub collider: super::ProjectileCollider,
pub force: f32,
} }
#[derive(Debug, Deserialize)]
pub struct ProjectileParticle {
pub texture: String,
pub lifetime: ParticleLifetime,
pub inherit_velocity: super::ImpactInheritVelocity,
pub size: f32,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum ParticleLifetime {
Inherit(String),
Seconds(f32),
}
}
/// Defines a projectile's collider
#[derive(Debug, Deserialize, Clone)]
pub enum ProjectileCollider {
/// A ball collider
#[serde(rename = "ball")]
Ball(BallCollider),
}
#[derive(Debug, Deserialize, Clone)]
pub struct BallCollider {
pub radius: f32,
}
/// How we should set an impact particle's velocity
#[derive(Debug, Deserialize, Clone)]
pub enum ImpactInheritVelocity {
/// Don't inherit any velocity.
/// This impact particle will be still.
#[serde(rename = "don't")]
Dont,
/// Inherit target velocity.
/// This impact particle will stick to the object it hits.
#[serde(rename = "target")]
Target,
/// Inherit projectile velocity.
/// This impact particle will continue on its projectile's path.
#[serde(rename = "projectile")]
Projectile,
} }
/// Represents a gun outfit. /// Represents a gun outfit.
@ -81,12 +132,74 @@ pub struct Projectile {
/// The damage this projectile does /// The damage this projectile does
pub damage: f32, pub damage: f32,
/// The force this projectile applies
pub force: f32,
/// The angle variation of this projectile. /// The angle variation of this projectile.
/// Projectiles can be off center up to /// Projectiles can be off center up to
/// `spread / 2` degrees in both directions. /// `spread / 2` degrees in both directions.
/// ///
/// (Forming a "fire cone" of `spread` degrees) /// (Forming a "fire cone" of `spread` degrees)
pub angle_rng: Deg<f32>, pub angle_rng: Deg<f32>,
/// The particle this projectile will spawn when it hits something
pub impact_particle: Option<ProjectileParticle>,
/// The particle this projectile will spawn when it expires
pub expire_particle: Option<ProjectileParticle>,
/// Collider parameters for this projectile
pub collider: ProjectileCollider,
}
/// The particle a projectile will spawn when it hits something
#[derive(Debug, Clone)]
pub struct ProjectileParticle {
/// The texture to use for this particle.
/// This is most likely animated.
pub texture: TextureHandle,
/// How many seconds this particle should live
pub lifetime: f32,
/// How we should set this particle's velocity
pub inherit_velocity: ImpactInheritVelocity,
/// The height of this particle, in game units.
pub size: f32,
}
fn parse_projectile_particle(
ct: &Content,
p: Option<syntax::ProjectileParticle>,
) -> Result<Option<ProjectileParticle>> {
if let Some(impact) = p {
let impact_texture = match ct.texture_index.get(&impact.texture) {
None => bail!("impact texture `{}` doesn't exist", impact.texture),
Some(t) => *t,
};
let impact_lifetime = match impact.lifetime {
syntax::ParticleLifetime::Seconds(s) => s,
syntax::ParticleLifetime::Inherit(s) => {
if s == "inherit" {
let t = ct.get_texture(impact_texture);
t.fps * t.frames.len() as f32
} else {
bail!("bad impact lifetime, must be float or \"inherit\"",)
}
}
};
Ok(Some(ProjectileParticle {
texture: impact_texture,
lifetime: impact_lifetime,
inherit_velocity: impact.inherit_velocity,
size: impact.size,
}))
} else {
Ok(None)
}
} }
impl crate::Build for Gun { impl crate::Build for Gun {
@ -94,22 +207,29 @@ impl crate::Build for Gun {
fn build(gun: Self::InputSyntax, ct: &mut Content) -> Result<()> { fn build(gun: Self::InputSyntax, ct: &mut Content) -> Result<()> {
for (gun_name, gun) in gun { for (gun_name, gun) in gun {
let th = match ct.texture_index.get(&gun.projectile.sprite_texture) { let projectile_texture = match ct.texture_index.get(&gun.projectile.sprite_texture) {
None => bail!( None => bail!(
"In gun `{}`: texture `{}` doesn't exist", "In gun `{}`: projectile texture `{}` doesn't exist",
gun_name, gun_name,
gun.projectile.sprite_texture gun.projectile.sprite_texture
), ),
Some(t) => *t, Some(t) => *t,
}; };
let impact_particle = parse_projectile_particle(ct, gun.projectile.impact)
.with_context(|| format!("In gun `{}`", gun_name))?;
let expire_particle = parse_projectile_particle(ct, gun.projectile.expire)
.with_context(|| format!("In gun `{}`", gun_name))?;
ct.guns.push(Self { ct.guns.push(Self {
name: gun_name, name: gun_name,
space: gun.space.into(), space: gun.space.into(),
rate: gun.rate, rate: gun.rate,
rate_rng: gun.rate_rng, rate_rng: gun.rate_rng,
projectile: Projectile { projectile: Projectile {
sprite_texture: th, force: gun.projectile.force,
sprite_texture: projectile_texture,
size: gun.projectile.size, size: gun.projectile.size,
size_rng: gun.projectile.size_rng, size_rng: gun.projectile.size_rng,
speed: gun.projectile.speed, speed: gun.projectile.speed,
@ -118,6 +238,9 @@ 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,
expire_particle,
collider: gun.projectile.collider,
}, },
}); });
} }

View File

@ -9,7 +9,7 @@ pub mod system;
pub mod texture; pub mod texture;
pub use faction::{Faction, Relationship}; pub use faction::{Faction, Relationship};
pub use gun::{Gun, Projectile}; pub use gun::{Gun, ImpactInheritVelocity, Projectile, ProjectileCollider, ProjectileParticle};
pub use outfit::Outfit; pub use outfit::Outfit;
pub use shared::OutfitSpace; pub use shared::OutfitSpace;
pub use ship::{EnginePoint, GunPoint, Ship}; pub use ship::{EnginePoint, GunPoint, Ship};

View File

@ -168,7 +168,7 @@ impl Game {
.get_ship_mut(&self.player) .get_ship_mut(&self.player)
.unwrap() .unwrap()
.physics_handle; .physics_handle;
let r = self.world.get_rigid_body(r.0); // TODO: r.0 shouldn't be public let r = self.world.get_rigid_body(r.0).unwrap(); // TODO: r.0 shouldn't be public
let ship_pos = util::rigidbody_position(r); let ship_pos = util::rigidbody_position(r);
self.camera.pos = ship_pos; self.camera.pos = ship_pos;
self.last_update = Instant::now(); self.last_update = Instant::now();

View File

@ -1,6 +1,6 @@
use cgmath::{Deg, EuclideanSpace, InnerSpace, Matrix2, Point2, Rad, Vector2}; use cgmath::{Deg, EuclideanSpace, InnerSpace, Matrix2, Point2, Rad, Vector2};
use crossbeam::channel::Receiver; use crossbeam::channel::Receiver;
use nalgebra::vector; use nalgebra::{point, vector};
use rand::Rng; use rand::Rng;
use rapier2d::{ use rapier2d::{
dynamics::{RigidBody, RigidBodyBuilder, RigidBodyHandle}, dynamics::{RigidBody, RigidBodyBuilder, RigidBodyHandle},
@ -27,19 +27,28 @@ pub struct World {
// Private methods // Private methods
impl<'a> World { impl<'a> World {
fn remove_projectile(&mut self, c: ColliderHandle) { fn remove_projectile(
&mut self,
c: ColliderHandle,
) -> Option<(RigidBody, ProjectileWorldObject)> {
let p = match self.projectiles.remove(&c) { let p = match self.projectiles.remove(&c) {
Some(p) => p, Some(p) => p,
None => return, None => return None,
}; };
self.wrapper.rigid_body_set.remove( let r = self
.wrapper
.rigid_body_set
.remove(
p.rigid_body, p.rigid_body,
&mut self.wrapper.im, &mut self.wrapper.im,
&mut self.wrapper.collider_set, &mut self.wrapper.collider_set,
&mut self.wrapper.ij, &mut self.wrapper.ij,
&mut self.wrapper.mj, &mut self.wrapper.mj,
true, true,
); )
.unwrap();
return Some((r, p));
} }
fn remove_ship(&mut self, h: ShipPhysicsHandle) { fn remove_ship(&mut self, h: ShipPhysicsHandle) {
@ -92,10 +101,12 @@ impl<'a> World {
.linvel(vector![vel.x, vel.y]) .linvel(vector![vel.x, vel.y])
.build(); .build();
let collider = ColliderBuilder::ball(1.0) let collider = match &projectile.content.collider {
content::ProjectileCollider::Ball(b) => ColliderBuilder::ball(b.radius)
.sensor(true) .sensor(true)
.active_events(ActiveEvents::COLLISION_EVENTS) .active_events(ActiveEvents::COLLISION_EVENTS)
.build(); .build(),
};
let rigid_body = self.wrapper.rigid_body_set.insert(rigid_body); let rigid_body = self.wrapper.rigid_body_set.insert(rigid_body);
let collider = self.wrapper.collider_set.insert_with_parent( let collider = self.wrapper.collider_set.insert_with_parent(
@ -114,6 +125,83 @@ impl<'a> World {
); );
} }
} }
fn collide_projectile_ship(
&mut self,
ct: &content::Content,
particles: &mut Vec<ParticleBuilder>,
projectile_h: ColliderHandle,
ship_h: ColliderHandle,
) {
let projectile = self.projectiles.get(&projectile_h);
let ship = self.ships.get_mut(&ship_h);
if projectile.is_none() || ship.is_none() {
return;
}
let projectile = projectile.unwrap();
let ship = ship.unwrap();
let hit = ship
.ship
.handle_projectile_collision(ct, &projectile.projectile);
let s = ship.physics_handle;
if hit {
let pr = self
.wrapper
.rigid_body_set
.get(projectile.rigid_body)
.unwrap();
let v = util::rigidbody_velocity(pr).normalize() * projectile.projectile.content.force;
let pos = util::rigidbody_position(pr);
let _ = pr;
let r = self.wrapper.rigid_body_set.get_mut(s.0).unwrap();
r.apply_impulse_at_point(vector![v.x, v.y], point![pos.x, pos.y], true);
// Borrow again, we can only have one at a time
let pr = self
.wrapper
.rigid_body_set
.get(projectile.rigid_body)
.unwrap();
let pos = util::rigidbody_position(pr);
let angle: Deg<f32> = util::rigidbody_rotation(pr)
.angle(Vector2 { x: 1.0, y: 0.0 })
.into();
match &projectile.projectile.content.impact_particle {
None => {}
Some(x) => {
let velocity = match x.inherit_velocity {
content::ImpactInheritVelocity::Dont => Vector2 { x: 0.0, y: 0.0 },
content::ImpactInheritVelocity::Projectile => util::rigidbody_velocity(pr),
content::ImpactInheritVelocity::Target => {
// Match target ship velocity.
// Particles will fly off if the ship is spinning fast, but I
// haven't found a good way to fix that.
let (_, sr) = self.get_ship_body(s).unwrap();
let velocity =
sr.velocity_at_point(&nalgebra::Point2::new(pos.x, pos.y));
Vector2 {
x: velocity.x,
y: velocity.y,
}
}
};
particles.push(ParticleBuilder {
texture: x.texture,
pos: Point2 { x: pos.x, y: pos.y },
velocity,
angle: -angle,
lifetime: x.lifetime,
size: x.size,
});
}
};
self.remove_projectile(projectile.collider);
}
}
} }
// Public methods // Public methods
@ -199,76 +287,71 @@ impl<'a> World {
// Handle collision events // Handle collision events
while let Ok(event) = &self.collision_queue.try_recv() { while let Ok(event) = &self.collision_queue.try_recv() {
if event.started() { if event.started() {
let a = &event.collider1(); let a = event.collider1();
let b = &event.collider2(); let b = event.collider2();
// If projectiles are part of this collision, make sure // If projectiles are part of this collision, make sure
// `a` is one of them. // `a` is one of them.
let (a, b) = if self.projectiles.contains_key(b) { let (a, b) = if self.projectiles.contains_key(&b) {
(b, a) (b, a)
} else { } else {
(a, b) (a, b)
}; };
if let Some(p) = self.projectiles.get(a) { let p = self.projectiles.get(&a);
let hit = if let Some(s) = self.ships.get_mut(b) { let s = self.ships.get_mut(&b);
s.ship.handle_projectile_collision(ct, &p.projectile) if p.is_none() || s.is_none() {
} else { continue;
false
};
// Stupid re-borrow trick.
// We can't have s be mutable inside this block.
if let Some(s) = self.ships.get(b) {
if hit {
let pr = self.get_rigid_body(p.rigid_body);
let pos = util::rigidbody_position(pr);
let angle: Deg<f32> = util::rigidbody_rotation(pr)
.angle(Vector2 { x: 1.0, y: 0.0 })
.into();
// Match target ship velocity, so impact particles are "sticky".
// Particles will fly off if the ship is spinning fast, but I
// haven't found a good way to fix that.
let (_, sr) = self.get_ship_body(s.physics_handle).unwrap();
let velocity =
sr.velocity_at_point(&nalgebra::Point2::new(pos.x, pos.y));
let velocity = Vector2 {
x: velocity.x,
y: velocity.y,
};
particles.push(ParticleBuilder {
texture: ct.get_texture_handle("particle::blaster"),
pos: Point2 { x: pos.x, y: pos.y },
velocity,
angle: -angle,
lifetime: 0.15,
size: 5.0,
});
self.remove_projectile(*a);
}
}
} }
self.collide_projectile_ship(ct, particles, a, b);
} }
} }
// Delete projectiles // Delete projectiles
let mut to_remove = Vec::new(); let mut to_remove = Vec::new();
for (_, p) in &mut self.projectiles { for (c, p) in &mut self.projectiles {
p.projectile.tick(t); p.projectile.tick(t);
if p.projectile.is_expired() { if p.projectile.is_expired() {
to_remove.push(p.collider); to_remove.push(*c);
} }
} }
for i in to_remove { for c in to_remove {
self.remove_projectile(i); let (pr, p) = self.remove_projectile(c).unwrap();
match &p.projectile.content.expire_particle {
None => {}
Some(x) => {
let pos = util::rigidbody_position(&pr);
let angle: Deg<f32> = util::rigidbody_rotation(&pr)
.angle(Vector2 { x: 1.0, y: 0.0 })
.into();
let velocity = match x.inherit_velocity {
content::ImpactInheritVelocity::Target
| content::ImpactInheritVelocity::Dont => Vector2 { x: 0.0, y: 0.0 },
content::ImpactInheritVelocity::Projectile => util::rigidbody_velocity(&pr),
};
particles.push(ParticleBuilder {
texture: x.texture,
pos: Point2 { x: pos.x, y: pos.y },
velocity,
angle: -angle,
lifetime: x.lifetime,
size: x.size,
});
}
};
} }
} }
/// Get a rigid body from a handle /// Get a rigid body from a handle
pub fn get_rigid_body(&self, r: RigidBodyHandle) -> &RigidBody { pub fn get_rigid_body(&self, r: RigidBodyHandle) -> Option<&RigidBody> {
&self.wrapper.rigid_body_set[r] self.wrapper.rigid_body_set.get(r)
}
/// Get a rigid body from a handle
pub fn get_rigid_body_mut(&mut self, r: RigidBodyHandle) -> Option<&mut RigidBody> {
self.wrapper.rigid_body_set.get_mut(r)
} }
/// Get a ship from a handle /// Get a ship from a handle