use cgmath::{Deg, EuclideanSpace, InnerSpace, Matrix2, Rad, Vector2, Zero}; use nalgebra::{point, vector}; use object::GameShipHandle; use rand::{rngs::ThreadRng, Rng}; use rapier2d::{ dynamics::{RigidBody, RigidBodyHandle}, geometry::{Collider, ColliderHandle}, }; use crate::{behavior::ShipBehavior, util, ParticleBuilder, StepResources}; use galactica_content as content; use galactica_gameobject as object; /// A ship's controls #[derive(Debug, Clone)] pub struct ShipControls { /// True if turning left pub left: bool, /// True if turning right pub right: bool, /// True if foward thrust pub thrust: bool, /// True if firing guns 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, time_elapsed: 0.0, rng: rand::thread_rng(), } } /// Has this sequence been fully played out? fn is_done(&self) -> bool { self.time_elapsed >= self.total_length } /// Pick a random points inside a ship's collider fn random_in_ship( &mut self, ship_content: &content::Ship, collider: &Collider, ) -> Vector2 { 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 } } /// Step this sequence `t` seconds fn step( &mut self, res: &mut StepResources, ship: GameShipHandle, rigid_body: &mut RigidBody, collider: &mut Collider, ) { let h = ship.content_handle(); let ship_content = res.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 + res.t) || (event.time == 0.0 && self.time_elapsed == 0.0) { for spawner in &event.effects { let effect = res.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]); res.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 = res.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 = (res.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]); res.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 += res.t; } } /// A ship instance in the physics system pub struct ShipWorldObject { /// This ship's physics handle pub rigid_body: RigidBodyHandle, /// This ship's collider pub collider: ColliderHandle, /// This ship's game data pub data_handle: GameShipHandle, /// This ship's controls pub(crate) controls: ShipControls, /// This ship's behavior behavior: Box, /// This ship's collapse sequence collapse_sequence: ShipCollapseSequence, } impl ShipWorldObject { /// Make a new ship pub fn new( ct: &content::Content, data_handle: GameShipHandle, behavior: Box, rigid_body: RigidBodyHandle, collider: ColliderHandle, ) -> Self { let ship_content = ct.get_ship(data_handle.content_handle()); ShipWorldObject { rigid_body, collider, data_handle, behavior, controls: ShipControls::new(), collapse_sequence: ShipCollapseSequence::new(ship_content.collapse.length), } } /// Compute this ship's controls using its behavior pub fn update_controls(&mut self, res: &StepResources) { self.controls = self.behavior.update_controls(res); } /// Step this ship's state by t seconds pub fn step( &mut self, res: &mut StepResources, rigid_body: &mut RigidBody, collider: &mut Collider, ) { let ship = res.dt.get_ship(self.data_handle).unwrap(); let ship_content = res.ct.get_ship(self.data_handle.content_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 ship.get_hull() <= ship_content.damage.hull { for e in &ship_content.damage.effects { if rng.gen_range(0.0..=1.0) <= res.t / e.frequency { let effect = res.ct.get_effect(e.effect); 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]); res.particles.push(ParticleBuilder::from_content( effect, pos, Rad::zero(), Vector2 { x: velocity.x, y: velocity.y, }, Vector2::zero(), )) } } } let engine_force = ship_rot * res.t; if self.controls.thrust { rigid_body.apply_impulse( vector![engine_force.x, engine_force.y] * ship.get_outfits().get_engine_thrust(), true, ); } if self.controls.right { rigid_body .apply_torque_impulse(ship.get_outfits().get_steer_power() * -100.0 * res.t, true); } if self.controls.left { rigid_body .apply_torque_impulse(ship.get_outfits().get_steer_power() * 100.0 * res.t, true); } //for i in self.ship.outfits.iter_guns() { // i.cooldown -= t; //} } }