2024-01-08 20:43:58 -08:00

294 lines
7.1 KiB
Rust

use cgmath::{Deg, EuclideanSpace, InnerSpace, Matrix2, Rad, Vector2, Zero};
use nalgebra::{point, vector};
use rand::{rngs::ThreadRng, Rng};
use rapier2d::{dynamics::RigidBody, geometry::Collider};
use crate::{util, ParticleBuilder, ShipPhysicsHandle};
use galactica_content as content;
use galactica_gameobject as object;
pub struct ShipControls {
pub left: bool,
pub right: bool,
pub thrust: bool,
pub guns: bool,
}
impl ShipControls {
pub fn new() -> Self {
ShipControls {
left: false,
right: false,
thrust: false,
guns: false,
}
}
}
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)
};
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(),
))
}
}
}
}
}
}
// 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 = ship_pos + Matrix2::from_angle(-ship_ang - Rad::from(Deg(90.0))) * pos;
let vel = rigid_body.velocity_at_point(&point![pos.x, pos.y]);
particles.push(ParticleBuilder {
sprite: effect.sprite,
pos,
velocity: Vector2 { x: vel.x, y: vel.y },
angle: Rad::zero(),
angvel: Rad::zero(),
lifetime: effect.lifetime,
size: effect.size,
fade: 0.0,
});
}
}
self.time_elapsed += t;
}
}
/// A ship instance in the physics system
pub struct ShipWorldObject {
/// This ship's physics handle
pub physics_handle: ShipPhysicsHandle,
/// This ship's game data
pub ship: object::Ship,
/// This ship's controls
pub controls: ShipControls,
collapse_sequence: ShipCollapseSequence,
}
impl ShipWorldObject {
/// Make a new ship
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,
ct: &content::Content,
particles: &mut Vec<ParticleBuilder>,
rigid_body: &mut RigidBody,
collider: &mut Collider,
t: f32,
) {
if self.ship.is_dead() {
return self
.collapse_sequence
.step(&self.ship, ct, particles, rigid_body, collider, t);
}
self.ship.step(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 engine_force = ship_rot * t;
if self.controls.thrust {
rigid_body.apply_impulse(
vector![engine_force.x, engine_force.y]
* self.ship.outfits.stat_sum().engine_thrust,
true,
);
}
if self.controls.right {
rigid_body
.apply_torque_impulse(self.ship.outfits.stat_sum().steer_power * -100.0 * t, true);
}
if self.controls.left {
rigid_body
.apply_torque_impulse(self.ship.outfits.stat_sum().steer_power * 100.0 * t, true);
}
for i in self.ship.outfits.iter_guns() {
i.cooldown -= t;
}
}
}