use galactica_content::{CollapseEvent, Ship}; use nalgebra::{Point2, Vector2}; use rand::{rngs::ThreadRng, Rng}; use rapier2d::{ dynamics::RigidBodyHandle, geometry::{Collider, ColliderHandle}, }; use crate::{ data::ShipData, phys::{objects::PhysEffect, physsim::NewObjects, PhysStepResources, PhysWrapper}, }; #[derive(Debug, Clone)] pub(super) struct ShipCollapseSequence { rng: ThreadRng, /// The total length of this collapse sequence total_length: f32, /// How many seconds we've spent playing this sequence elapsed: f32, } impl ShipCollapseSequence { pub(super) fn new(total_length: f32) -> Self { Self { rng: rand::thread_rng(), total_length, elapsed: 0.0, } } /// Has this collapse sequence fully played out? pub fn is_done(&self) -> bool { self.elapsed >= self.total_length } /// Pick a random points inside a ship's collider fn random_in_ship(&mut self, ship: &Ship, collider: &Collider) -> Vector2 { let mut y = 0.0; let mut x = 0.0; let mut a = false; while !a { x = self.rng.gen_range(-1.0..=1.0) * ship.size / 2.0; y = self.rng.gen_range(-1.0..=1.0) * ship.size * ship.sprite.aspect / 2.0; a = collider.shape().contains_local_point(&Point2::new(x, y)); } Vector2::new(x, y) } /// Step this sequence `t` seconds pub(super) fn step( &mut self, res: &mut PhysStepResources, wrapper: &mut PhysWrapper, new: &mut NewObjects, ship_data: &ShipData, rigid_body_handle: RigidBodyHandle, collider_handle: ColliderHandle, ) { let rigid_body = wrapper.get_rigid_body(rigid_body_handle).unwrap().clone(); let collider = wrapper.get_collider(collider_handle).unwrap().clone(); let ship_content = ship_data.get_content(); let ship_pos = rigid_body.translation(); let ship_rot = rigid_body.rotation(); // The fraction of this collapse sequence that has been played let frac_done = self.elapsed / self.total_length; // TODO: slight random offset for event effects // Trigger collapse events for event in &ship_content.collapse.events { match event { CollapseEvent::Effect(event) => { if (event.time > self.elapsed && event.time <= self.elapsed + res.t) || (event.time == 0.0 && self.elapsed == 0.0) // ^^ Don't miss events scheduled at the very start of the sequence! { for spawner in &event.effects { for _ in 0..spawner.count as usize { let pos: Vector2 = if let Some(pos) = spawner.pos { Vector2::new(pos.x, pos.y) } else { self.random_in_ship(&ship_content, &collider) }; let pos = ship_pos + (ship_rot * pos); new.effects.push(PhysEffect::new( wrapper, spawner.effect.clone(), pos, rigid_body_handle, None, )); } } } } } } // Create collapse effects for spawner in &ship_content.collapse.effects { // Probability of adding an effect instance 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 { Vector2::new(pos.x, pos.y) } else { self.random_in_ship(&ship_content, &collider) }; // Position, adjusted for ship rotation let pos = ship_pos + (ship_rot * pos); new.effects.push(PhysEffect::new( wrapper, spawner.effect.clone(), pos, rigid_body_handle, None, )); } } self.elapsed += res.t; } }