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 { // 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) }; 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, 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; } } }