From d5e2820f7debd15029a29d9328c2f054b43be3fc Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 13 Jan 2024 19:05:00 -0800 Subject: [PATCH] Split systemsim into a module --- crates/system/src/phys/systemsim.rs | 636 ------------------- crates/system/src/phys/systemsim/mod.rs | 182 ++++++ crates/system/src/phys/systemsim/step.rs | 283 +++++++++ crates/system/src/phys/systemsim/steputil.rs | 196 ++++++ 4 files changed, 661 insertions(+), 636 deletions(-) delete mode 100644 crates/system/src/phys/systemsim.rs create mode 100644 crates/system/src/phys/systemsim/mod.rs create mode 100644 crates/system/src/phys/systemsim/step.rs create mode 100644 crates/system/src/phys/systemsim/steputil.rs diff --git a/crates/system/src/phys/systemsim.rs b/crates/system/src/phys/systemsim.rs deleted file mode 100644 index ca6de77..0000000 --- a/crates/system/src/phys/systemsim.rs +++ /dev/null @@ -1,636 +0,0 @@ -use galactica_content::{ - Content, FactionHandle, GunPoint, OutfitHandle, ProjectileCollider, Relationship, ShipHandle, - SystemHandle, SystemObjectHandle, -}; -use galactica_playeragent::PlayerAgent; -use nalgebra::{Isometry2, Point2, Rotation2, Vector2}; -use rand::Rng; -use rapier2d::{ - dynamics::{RigidBody, RigidBodyBuilder, RigidBodyHandle, RigidBodySet}, - geometry::{ColliderBuilder, ColliderHandle, ColliderSet}, - pipeline::ActiveEvents, -}; -use std::collections::HashMap; - -use crate::data::{ShipAutoPilot, ShipPersonality, ShipState}; - -use super::{ - controller::{autopilot, ShipController}, - objects::{PhysProjectile, PhysSimShip}, - ParticleBuilder, PhysStepResources, -}; - -// TODO: replace with a more generic handle -/// A handle for a ship in this simulation -/// This lets other crates reference ships -/// without including `rapier2d`. -pub struct PhysSimShipHandle(pub ColliderHandle); - -/// Manages the physics state of one system -#[derive(Clone)] -pub struct PhysSim { - /// The system this sim is attached to - _system: SystemHandle, - - rigid_body_set: RigidBodySet, - collider_set: ColliderSet, - - projectiles: HashMap, - ships: HashMap, - ship_behaviors: HashMap, -} - -// Private methods -impl<'a> PhysSim { - fn remove_projectile( - &mut self, - res: &mut PhysStepResources, - c: ColliderHandle, - ) -> Option<(RigidBody, PhysProjectile)> { - let p = match self.projectiles.remove(&c) { - Some(p) => p, - None => return None, - }; - let r = self - .rigid_body_set - .remove( - p.rigid_body, - &mut res.wrapper.im, - &mut self.collider_set, - &mut res.wrapper.ij, - &mut res.wrapper.mj, - true, - ) - .unwrap(); - - return Some((r, p)); - } - - /// Try to land the given ship on the given planet. - /// State changes on success, and nothing happens on failure. - /// returns `true` if landing was successful. - fn try_land_ship( - &mut self, - ct: &Content, - collider: ColliderHandle, - target_handle: SystemObjectHandle, - ) -> bool { - let ship = self.ships.get_mut(&collider).unwrap(); - let r = self.rigid_body_set.get(ship.rigid_body).unwrap(); - - let target = ct.get_system_object(target_handle); - let t_pos = Vector2::new(target.pos.x, target.pos.y); - let s_pos = Vector2::new(r.position().translation.x, r.position().translation.y); - - // TODO: deactivate collider when landing. - // Can't just set_active(false), since we still need that collider's mass. - - // We're in land range... - if (t_pos - s_pos).magnitude() > target.size / 2.0 { - return false; - } - - // And we'll stay in land range long enough. - if (t_pos - (s_pos + r.velocity_at_point(r.center_of_mass()) * 2.0)).magnitude() - > target.size / 2.0 - { - return false; - } - - ship.data.start_land_on(target_handle); - return true; - } - - /// Finish landing this ship on a planet. - /// Called after the landing animation finishes. - fn finish_land_ship( - &mut self, - _ct: &Content, - collider: ColliderHandle, - _target: SystemObjectHandle, - ) { - let ship = self.ships.get_mut(&collider).unwrap(); - ship.data.finish_land_on(); - let r = self.rigid_body_set.get_mut(ship.rigid_body).unwrap(); - r.set_enabled(false); - r.set_angvel(0.0, false); - r.set_linvel(nalgebra::Vector2::new(0.0, 0.0), false); - } - - fn unland_ship(&mut self, ct: &Content, collider: ColliderHandle) { - let ship = self.ships.get_mut(&collider).unwrap(); - let obj = ship.data.get_state().landed_on().unwrap(); - let obj = ct.get_system_object(obj); - - let target_pos = Isometry2::new(Vector2::new(obj.pos.x + 100.0, obj.pos.y + 100.0), 1.0); - - ship.data.unland(target_pos); - - let r = self.rigid_body_set.get_mut(ship.rigid_body).unwrap(); - r.set_enabled(true); - r.set_position(target_pos, true); - - self.collider_set - .get_mut(ship.collider) - .unwrap() - .set_enabled(true); - } - - fn remove_ship(&mut self, res: &mut PhysStepResources, colliderhandle: ColliderHandle) { - let ship = match self.ships.get(&colliderhandle) { - None => return, - Some(s) => s, - }; - - self.rigid_body_set.remove( - ship.rigid_body, - &mut res.wrapper.im, - &mut self.collider_set, - &mut res.wrapper.ij, - &mut res.wrapper.mj, - true, - ); - self.ships.remove(&colliderhandle).unwrap(); - self.ship_behaviors.remove(&colliderhandle); - } - - fn collide_projectile_ship( - &mut self, - res: &mut PhysStepResources, - 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 f = res.ct.get_faction(projectile.faction); - let r = f.relationships.get(&ship.data.get_faction()).unwrap(); - let destory_projectile = match r { - Relationship::Hostile => match ship.data.get_state() { - ShipState::Flying { .. } => { - ship.data.apply_damage(res.ct, projectile.content.damage); - true - } - ShipState::Collapsing { .. } => true, - _ => false, - }, - _ => false, - }; - - if destory_projectile { - let pr = self.rigid_body_set.get(projectile.rigid_body).unwrap(); - let v = - pr.velocity_at_point(pr.center_of_mass()).normalize() * projectile.content.force; - let pos = *pr.translation(); - let _ = pr; - - let r = self.rigid_body_set.get_mut(ship.rigid_body).unwrap(); - r.apply_impulse_at_point(Vector2::new(v.x, v.y), Point2::new(pos.x, pos.y), true); - - // Borrow again, we can only have one at a time - let pr = self.rigid_body_set.get(projectile.rigid_body).unwrap(); - let pos = *pr.translation(); - let angle = pr.rotation().angle(); - - match &projectile.content.impact_effect { - None => {} - Some(x) => { - let effect = res.ct.get_effect(*x); - let r = ship.rigid_body; - let sr = self.get_rigid_body(r).unwrap(); - let parent_velocity = pr.velocity_at_point(pr.center_of_mass()); - let target_velocity = - sr.velocity_at_point(&nalgebra::Point2::new(pos.x, pos.y)); - - res.particles.push(ParticleBuilder::from_content( - effect, - pos.into(), - -angle, - parent_velocity, - target_velocity, - )); - } - }; - - self.remove_projectile(res, projectile.collider); - } - } -} - -// Public methods -impl PhysSim { - /// Create a new physics system - pub fn new(_ct: &Content, system: SystemHandle) -> Self { - Self { - _system: system, - rigid_body_set: RigidBodySet::new(), - collider_set: ColliderSet::new(), - projectiles: HashMap::new(), - ships: HashMap::new(), - ship_behaviors: HashMap::new(), - } - } - - /// Add a ship to this physics system - pub fn add_ship( - &mut self, - ct: &Content, - handle: ShipHandle, - faction: FactionHandle, - personality: ShipPersonality, - position: Point2, - ) -> PhysSimShipHandle { - let ship_content = ct.get_ship(handle); - let cl = ship_content.collider.0.clone(); - // TODO: additonal ship mass from outfits and cargo - - let rb = RigidBodyBuilder::dynamic() - .angular_damping(ship_content.angular_drag) - .linear_damping(ship_content.linear_drag) - .translation(Vector2::new(position.x, position.y)) - .can_sleep(false); - - let ridid_body = self.rigid_body_set.insert(rb.build()); - let collider = - self.collider_set - .insert_with_parent(cl, ridid_body, &mut self.rigid_body_set); - - self.ship_behaviors.insert( - collider, - match personality { - ShipPersonality::Dummy | ShipPersonality::Player => ShipController::new_null(), - ShipPersonality::Point => ShipController::new_point(), - }, - ); - - self.ships.insert( - collider, - PhysSimShip::new(ct, handle, personality, faction, ridid_body, collider), - ); - - return PhysSimShipHandle(collider); - } - - /// Update a player ship's controls - pub fn update_player_controls(&mut self, ct: &Content, player: &PlayerAgent) { - if player.ship.is_none() { - return; - } - let ship_object = self.ships.get_mut(&player.ship.unwrap()); - if let Some(ship_object) = ship_object { - match ship_object.data.get_state() { - ShipState::Landing { .. } - | ShipState::UnLanding { .. } - | ShipState::Collapsing { .. } - | ShipState::Dead => {} - - ShipState::Flying { - autopilot: ShipAutoPilot::None, - } => { - ship_object.controls.guns = player.input.pressed_guns(); - ship_object.controls.left = player.input.pressed_left(); - ship_object.controls.right = player.input.pressed_right(); - ship_object.controls.thrust = player.input.pressed_thrust(); - - if player.input.pressed_land() { - ship_object.data.set_autopilot(ShipAutoPilot::Landing { - target: player.selection.get_planet().unwrap(), - }) - } - } - - ShipState::Flying { .. } => { - // Any input automatically releases autopilot - if player.input.pressed_left() - || player.input.pressed_right() - || player.input.pressed_thrust() - || player.input.pressed_guns() - { - ship_object.data.set_autopilot(ShipAutoPilot::None); - } - } - - ShipState::Landed { .. } => { - if player.input.pressed_land() { - self.unland_ship(ct, player.ship.unwrap()); - } - } - }; - } - } - - /// Run ship updates: - /// - updates ship controls (runs behaviors) - /// - applies ship controls - /// - creates projectiles - fn step_ships(&mut self, res: &mut PhysStepResources) { - // We can't apply these right away since self is borrowed - // by the iterator - // TODO: don't allocate! - let mut projectiles = Vec::new(); - let mut to_remove = Vec::new(); - // Again, borrow checker hack. TODO: fix - let keys: Vec<_> = self.ships.keys().map(|c| *c).collect(); - for collider in keys { - // Borrow immutably for now... - // (required to compute controls) - let ship = self.ships.get(&collider).unwrap(); - match ship.data.get_state() { - ShipState::Dead => { - to_remove.push(collider); - } - - ShipState::UnLanding { .. } - | ShipState::Landed { .. } - | ShipState::Collapsing { .. } => { - let ship = self.ships.get_mut(&collider).unwrap(); - ship.step( - res, - &mut self.rigid_body_set[ship.rigid_body], - &mut self.collider_set[ship.collider], - ); - } - - ShipState::Landing { target, current_z } => { - let target_obj = res.ct.get_system_object(*target); - let controls = autopilot::auto_landing( - &res, - &self.rigid_body_set, - &self.ships, - ship.collider, - *target, - ); - - let current_z = *current_z; - let target = *target; - let ship = self.ships.get_mut(&collider).unwrap(); - let r = &mut self.rigid_body_set[ship.rigid_body]; - let zdist = target_obj.pos.z - 1.0; - - if current_z >= target_obj.pos.z { - self.finish_land_ship(res.ct, collider, target); - } else { - ship.data.set_landing_z(current_z + zdist * res.t / 2.0); - - if let Some(controls) = controls { - ship.controls = controls; - } - ship.step(res, r, &mut self.collider_set[ship.collider]) - }; - } - - ShipState::Flying { autopilot } => { - // Compute new controls - // This is why we borrow immutably first - let controls = match autopilot { - ShipAutoPilot::Landing { - target: target_handle, - } => { - let controls = autopilot::auto_landing( - &res, - &self.rigid_body_set, - &self.ships, - ship.collider, - *target_handle, - ); - - let landed = self.try_land_ship(res.ct, collider, *target_handle); - if landed { - None - } else { - controls - } - } - - ShipAutoPilot::None => { - let b = self.ship_behaviors.get_mut(&collider).unwrap(); - b.update_controls( - &res, - &self.rigid_body_set, - &self.ships, - ship.collider, - ) - } - }; - - // Re-borrow mutably to apply changes - let ship = self.ships.get_mut(&collider).unwrap(); - - if let Some(controls) = controls { - ship.controls = controls; - } - ship.step( - res, - &mut self.rigid_body_set[ship.rigid_body], - &mut self.collider_set[ship.collider], - ); - - // If we're firing, try to fire each gun - if ship.controls.guns { - // TODO: don't allocate here. This is a hack to satisfy the borrow checker, - // convert this to a refcell or do the replace dance. - let pairs: Vec<(GunPoint, Option)> = ship - .data - .get_outfits() - .iter_gun_points() - .map(|(p, o)| (p.clone(), o.clone())) - .collect(); - - for (gun, outfit) in pairs { - if ship.data.fire_gun(res.ct, &gun) { - projectiles.push((ship.collider, gun.clone(), outfit.unwrap())); - } - } - } - } - } - } - - // Remove ships that don't exist - for c in to_remove { - self.remove_ship(res, c); - } - - // Create projectiles - for (collider, gun_point, outfit) in projectiles { - let mut rng = rand::thread_rng(); - - let ship = self.ships.get(&collider).unwrap(); - let rigid_body = self.get_rigid_body(ship.rigid_body).unwrap(); - let ship_pos = rigid_body.translation(); - let ship_rot = rigid_body.rotation(); - let ship_ang = ship_rot.angle(); - let ship_vel = rigid_body.velocity_at_point(rigid_body.center_of_mass()); - - let pos = ship_pos + (ship_rot * gun_point.pos); - - let outfit = res.ct.get_outfit(outfit); - let outfit = outfit.gun.as_ref().unwrap(); - - let spread = rng.gen_range(-outfit.projectile.angle_rng..=outfit.projectile.angle_rng); - let vel = ship_vel - + (Rotation2::new(ship_ang + spread) - * Vector2::new( - outfit.projectile.speed - + rng.gen_range( - -outfit.projectile.speed_rng..=outfit.projectile.speed_rng, - ), - 0.0, - )); - - let rigid_body = RigidBodyBuilder::kinematic_velocity_based() - .translation(pos) - .rotation(ship_ang) - .linvel(vel) - .build(); - - let collider = match &outfit.projectile.collider { - ProjectileCollider::Ball(b) => ColliderBuilder::ball(b.radius) - .sensor(true) - .active_events(ActiveEvents::COLLISION_EVENTS) - .build(), - }; - - let rigid_body = self.rigid_body_set.insert(rigid_body); - let collider = self.collider_set.insert_with_parent( - collider, - rigid_body, - &mut self.rigid_body_set, - ); - - self.projectiles.insert( - collider.clone(), - PhysProjectile::new( - outfit.projectile.clone(), - rigid_body, - ship.data.get_faction(), - collider, - ), - ); - } - } - - /// Step this physics system by `t` seconds - pub fn step(&mut self, mut res: PhysStepResources) { - res.timing.start_physics_ships(); - self.step_ships(&mut res); - res.timing.mark_physics_ships(); - - res.timing.start_physics_sim(); - - // Update physics - res.wrapper - .step(res.t, &mut self.rigid_body_set, &mut self.collider_set); - - // Handle collision events - while let Ok(event) = &res.wrapper.collision_queue.try_recv() { - if event.started() { - let a = event.collider1(); - let b = event.collider2(); - - // If projectiles are part of this collision, make sure - // `a` is one of them. - let (a, b) = if self.projectiles.contains_key(&b) { - (b, a) - } else { - (a, b) - }; - - let p = self.projectiles.get(&a); - let s = self.ships.get_mut(&b); - if p.is_none() || s.is_none() { - continue; - } - self.collide_projectile_ship(&mut res, a, b); - } - } - - // Delete projectiles - let mut to_remove = Vec::new(); - for (c, p) in &mut self.projectiles { - p.tick(res.t); - if p.is_expired() { - to_remove.push(*c); - } - } - - let mut rng = rand::thread_rng(); - for c in to_remove { - let (pr, p) = self.remove_projectile(&mut res, c).unwrap(); - - match &p.content.expire_effect { - None => {} - Some(x) => { - let x = res.ct.get_effect(*x); - let pos = *pr.translation(); - let vel = pr.velocity_at_point(pr.center_of_mass()); - let angle = pr.rotation().angle(); - - let velocity = { - let a = rng - .gen_range(-x.velocity_scale_parent_rng..=x.velocity_scale_parent_rng); - - let velocity = (x.velocity_scale_parent + a) * vel; - - velocity - }; - - res.particles.push(ParticleBuilder::from_content( - x, - pos.into(), - -angle, - velocity, - Vector2::new(0.0, 0.0), - )); - } - }; - } - - res.timing.mark_physics_sim(); - } -} - -// Public getters -impl PhysSim { - /// Get a ship physics object - pub fn get_ship(&self, ship: &PhysSimShipHandle) -> Option<&PhysSimShip> { - self.ships.get(&ship.0) - } - - /// Get a ship physics object - pub fn get_ship_mut(&mut self, ship: &PhysSimShipHandle) -> Option<&mut PhysSimShip> { - self.ships.get_mut(&ship.0) - } - - /// Get a rigid body from a handle - pub fn get_rigid_body(&self, r: RigidBodyHandle) -> Option<&RigidBody> { - self.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.rigid_body_set.get_mut(r) - } - - /// Iterate over all ships in this physics system - pub fn iter_ship_body(&self) -> impl Iterator + '_ { - self.ships - .values() - .map(|x| (x, self.rigid_body_set.get(x.rigid_body).unwrap())) - } - - /// Iterate over all ships in this physics system - pub fn iter_ships(&self) -> impl Iterator + '_ { - self.ships.values() - } - - /// Iterate over all ships in this physics system - pub fn iter_projectiles(&self) -> impl Iterator + '_ { - self.projectiles.values() - } -} diff --git a/crates/system/src/phys/systemsim/mod.rs b/crates/system/src/phys/systemsim/mod.rs new file mode 100644 index 0000000..55fa8d9 --- /dev/null +++ b/crates/system/src/phys/systemsim/mod.rs @@ -0,0 +1,182 @@ +use galactica_content::{Content, FactionHandle, ShipHandle, SystemHandle}; +use galactica_playeragent::PlayerAgent; +use nalgebra::{Point2, Vector2}; + +use rapier2d::{ + dynamics::{RigidBody, RigidBodyBuilder, RigidBodyHandle, RigidBodySet}, + geometry::{ColliderHandle, ColliderSet}, +}; +use std::collections::HashMap; + +use crate::data::{ShipAutoPilot, ShipPersonality, ShipState}; + +use super::{ + controller::ShipController, + objects::{PhysProjectile, PhysSimShip}, +}; + +// TODO: replace with a more generic handle +/// A handle for a ship in this simulation +/// This lets other crates reference ships +/// without including `rapier2d`. +pub struct PhysSimShipHandle(pub ColliderHandle); + +/// Manages the physics state of one system +#[derive(Clone)] +pub struct PhysSim { + /// The system this sim is attached to + _system: SystemHandle, + + rigid_body_set: RigidBodySet, + collider_set: ColliderSet, + + projectiles: HashMap, + ships: HashMap, + ship_behaviors: HashMap, +} + +mod step; +mod steputil; + +// Public methods +impl PhysSim { + /// Create a new physics system + pub fn new(_ct: &Content, system: SystemHandle) -> Self { + Self { + _system: system, + rigid_body_set: RigidBodySet::new(), + collider_set: ColliderSet::new(), + projectiles: HashMap::new(), + ships: HashMap::new(), + ship_behaviors: HashMap::new(), + } + } + + /// Add a ship to this physics system + pub fn add_ship( + &mut self, + ct: &Content, + handle: ShipHandle, + faction: FactionHandle, + personality: ShipPersonality, + position: Point2, + ) -> PhysSimShipHandle { + let ship_content = ct.get_ship(handle); + let cl = ship_content.collider.0.clone(); + // TODO: additonal ship mass from outfits and cargo + + let rb = RigidBodyBuilder::dynamic() + .angular_damping(ship_content.angular_drag) + .linear_damping(ship_content.linear_drag) + .translation(Vector2::new(position.x, position.y)) + .can_sleep(false); + + let ridid_body = self.rigid_body_set.insert(rb.build()); + let collider = + self.collider_set + .insert_with_parent(cl, ridid_body, &mut self.rigid_body_set); + + self.ship_behaviors.insert( + collider, + match personality { + ShipPersonality::Dummy | ShipPersonality::Player => ShipController::new_null(), + ShipPersonality::Point => ShipController::new_point(), + }, + ); + + self.ships.insert( + collider, + PhysSimShip::new(ct, handle, personality, faction, ridid_body, collider), + ); + + return PhysSimShipHandle(collider); + } + + /// Update a player ship's controls + pub fn update_player_controls(&mut self, ct: &Content, player: &PlayerAgent) { + if player.ship.is_none() { + return; + } + let ship_object = self.ships.get_mut(&player.ship.unwrap()); + if let Some(ship_object) = ship_object { + match ship_object.data.get_state() { + ShipState::Landing { .. } + | ShipState::UnLanding { .. } + | ShipState::Collapsing { .. } + | ShipState::Dead => {} + + ShipState::Flying { + autopilot: ShipAutoPilot::None, + } => { + ship_object.controls.guns = player.input.pressed_guns(); + ship_object.controls.left = player.input.pressed_left(); + ship_object.controls.right = player.input.pressed_right(); + ship_object.controls.thrust = player.input.pressed_thrust(); + + if player.input.pressed_land() { + ship_object.data.set_autopilot(ShipAutoPilot::Landing { + target: player.selection.get_planet().unwrap(), + }) + } + } + + ShipState::Flying { .. } => { + // Any input automatically releases autopilot + if player.input.pressed_left() + || player.input.pressed_right() + || player.input.pressed_thrust() + || player.input.pressed_guns() + { + ship_object.data.set_autopilot(ShipAutoPilot::None); + } + } + + ShipState::Landed { .. } => { + if player.input.pressed_land() { + self.unland_ship(ct, player.ship.unwrap()); + } + } + }; + } + } +} + +// Public getters +impl PhysSim { + /// Get a ship physics object + pub fn get_ship(&self, ship: &PhysSimShipHandle) -> Option<&PhysSimShip> { + self.ships.get(&ship.0) + } + + /// Get a ship physics object + pub fn get_ship_mut(&mut self, ship: &PhysSimShipHandle) -> Option<&mut PhysSimShip> { + self.ships.get_mut(&ship.0) + } + + /// Get a rigid body from a handle + pub fn get_rigid_body(&self, r: RigidBodyHandle) -> Option<&RigidBody> { + self.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.rigid_body_set.get_mut(r) + } + + /// Iterate over all ships in this physics system + pub fn iter_ship_body(&self) -> impl Iterator + '_ { + self.ships + .values() + .map(|x| (x, self.rigid_body_set.get(x.rigid_body).unwrap())) + } + + /// Iterate over all ships in this physics system + pub fn iter_ships(&self) -> impl Iterator + '_ { + self.ships.values() + } + + /// Iterate over all ships in this physics system + pub fn iter_projectiles(&self) -> impl Iterator + '_ { + self.projectiles.values() + } +} diff --git a/crates/system/src/phys/systemsim/step.rs b/crates/system/src/phys/systemsim/step.rs new file mode 100644 index 0000000..37e4992 --- /dev/null +++ b/crates/system/src/phys/systemsim/step.rs @@ -0,0 +1,283 @@ +use galactica_content::{GunPoint, OutfitHandle, ProjectileCollider}; +use nalgebra::{Rotation2, Vector2}; +use rand::Rng; +use rapier2d::{dynamics::RigidBodyBuilder, geometry::ColliderBuilder, pipeline::ActiveEvents}; + +use crate::{ + data::{ShipAutoPilot, ShipState}, + phys::{controller::autopilot, objects::PhysProjectile, ParticleBuilder, PhysStepResources}, +}; + +use super::PhysSim; + +impl PhysSim { + /// Run ship updates: + /// - updates ship controls (runs behaviors) + /// - applies ship controls + /// - creates projectiles + fn step_ships(&mut self, res: &mut PhysStepResources) { + // We can't apply these right away since self is borrowed + // by the iterator + // TODO: don't allocate! + let mut projectiles = Vec::new(); + let mut to_remove = Vec::new(); + // Again, borrow checker hack. TODO: fix + let keys: Vec<_> = self.ships.keys().map(|c| *c).collect(); + for collider in keys { + // Borrow immutably for now... + // (required to compute controls) + let ship = self.ships.get(&collider).unwrap(); + match ship.data.get_state() { + ShipState::Dead => { + to_remove.push(collider); + } + + ShipState::UnLanding { .. } + | ShipState::Landed { .. } + | ShipState::Collapsing { .. } => { + let ship = self.ships.get_mut(&collider).unwrap(); + ship.step( + res, + &mut self.rigid_body_set[ship.rigid_body], + &mut self.collider_set[ship.collider], + ); + } + + ShipState::Landing { target, current_z } => { + let target_obj = res.ct.get_system_object(*target); + let controls = autopilot::auto_landing( + &res, + &self.rigid_body_set, + &self.ships, + ship.collider, + *target, + ); + + let current_z = *current_z; + let target = *target; + let ship = self.ships.get_mut(&collider).unwrap(); + let r = &mut self.rigid_body_set[ship.rigid_body]; + let zdist = target_obj.pos.z - 1.0; + + if current_z >= target_obj.pos.z { + self.finish_land_ship(res.ct, collider, target); + } else { + ship.data.set_landing_z(current_z + zdist * res.t / 2.0); + + if let Some(controls) = controls { + ship.controls = controls; + } + ship.step(res, r, &mut self.collider_set[ship.collider]) + }; + } + + ShipState::Flying { autopilot } => { + // Compute new controls + // This is why we borrow immutably first + let controls = match autopilot { + ShipAutoPilot::Landing { + target: target_handle, + } => { + let controls = autopilot::auto_landing( + &res, + &self.rigid_body_set, + &self.ships, + ship.collider, + *target_handle, + ); + + let landed = self.try_land_ship(res.ct, collider, *target_handle); + if landed { + None + } else { + controls + } + } + + ShipAutoPilot::None => { + let b = self.ship_behaviors.get_mut(&collider).unwrap(); + b.update_controls( + &res, + &self.rigid_body_set, + &self.ships, + ship.collider, + ) + } + }; + + // Re-borrow mutably to apply changes + let ship = self.ships.get_mut(&collider).unwrap(); + + if let Some(controls) = controls { + ship.controls = controls; + } + ship.step( + res, + &mut self.rigid_body_set[ship.rigid_body], + &mut self.collider_set[ship.collider], + ); + + // If we're firing, try to fire each gun + if ship.controls.guns { + // TODO: don't allocate here. This is a hack to satisfy the borrow checker, + // convert this to a refcell or do the replace dance. + let pairs: Vec<(GunPoint, Option)> = ship + .data + .get_outfits() + .iter_gun_points() + .map(|(p, o)| (p.clone(), o.clone())) + .collect(); + + for (gun, outfit) in pairs { + if ship.data.fire_gun(res.ct, &gun) { + projectiles.push((ship.collider, gun.clone(), outfit.unwrap())); + } + } + } + } + } + } + + // Remove ships that don't exist + for c in to_remove { + self.remove_ship(res, c); + } + + // Create projectiles + for (collider, gun_point, outfit) in projectiles { + let mut rng = rand::thread_rng(); + + let ship = self.ships.get(&collider).unwrap(); + let rigid_body = self.get_rigid_body(ship.rigid_body).unwrap(); + let ship_pos = rigid_body.translation(); + let ship_rot = rigid_body.rotation(); + let ship_ang = ship_rot.angle(); + let ship_vel = rigid_body.velocity_at_point(rigid_body.center_of_mass()); + + let pos = ship_pos + (ship_rot * gun_point.pos); + + let outfit = res.ct.get_outfit(outfit); + let outfit = outfit.gun.as_ref().unwrap(); + + let spread = rng.gen_range(-outfit.projectile.angle_rng..=outfit.projectile.angle_rng); + let vel = ship_vel + + (Rotation2::new(ship_ang + spread) + * Vector2::new( + outfit.projectile.speed + + rng.gen_range( + -outfit.projectile.speed_rng..=outfit.projectile.speed_rng, + ), + 0.0, + )); + + let rigid_body = RigidBodyBuilder::kinematic_velocity_based() + .translation(pos) + .rotation(ship_ang) + .linvel(vel) + .build(); + + let collider = match &outfit.projectile.collider { + ProjectileCollider::Ball(b) => ColliderBuilder::ball(b.radius) + .sensor(true) + .active_events(ActiveEvents::COLLISION_EVENTS) + .build(), + }; + + let rigid_body = self.rigid_body_set.insert(rigid_body); + let collider = self.collider_set.insert_with_parent( + collider, + rigid_body, + &mut self.rigid_body_set, + ); + + self.projectiles.insert( + collider.clone(), + PhysProjectile::new( + outfit.projectile.clone(), + rigid_body, + ship.data.get_faction(), + collider, + ), + ); + } + } + + /// Step this physics system by `t` seconds + pub fn step(&mut self, mut res: PhysStepResources) { + res.timing.start_physics_ships(); + self.step_ships(&mut res); + res.timing.mark_physics_ships(); + + res.timing.start_physics_sim(); + + // Update physics + res.wrapper + .step(res.t, &mut self.rigid_body_set, &mut self.collider_set); + + // Handle collision events + while let Ok(event) = &res.wrapper.collision_queue.try_recv() { + if event.started() { + let a = event.collider1(); + let b = event.collider2(); + + // If projectiles are part of this collision, make sure + // `a` is one of them. + let (a, b) = if self.projectiles.contains_key(&b) { + (b, a) + } else { + (a, b) + }; + + let p = self.projectiles.get(&a); + let s = self.ships.get_mut(&b); + if p.is_none() || s.is_none() { + continue; + } + self.collide_projectile_ship(&mut res, a, b); + } + } + + // Delete projectiles + let mut to_remove = Vec::new(); + for (c, p) in &mut self.projectiles { + p.tick(res.t); + if p.is_expired() { + to_remove.push(*c); + } + } + + let mut rng = rand::thread_rng(); + for c in to_remove { + let (pr, p) = self.remove_projectile(&mut res, c).unwrap(); + + match &p.content.expire_effect { + None => {} + Some(x) => { + let x = res.ct.get_effect(*x); + let pos = *pr.translation(); + let vel = pr.velocity_at_point(pr.center_of_mass()); + let angle = pr.rotation().angle(); + + let velocity = { + let a = rng + .gen_range(-x.velocity_scale_parent_rng..=x.velocity_scale_parent_rng); + + let velocity = (x.velocity_scale_parent + a) * vel; + + velocity + }; + + res.particles.push(ParticleBuilder::from_content( + x, + pos.into(), + -angle, + velocity, + Vector2::new(0.0, 0.0), + )); + } + }; + } + + res.timing.mark_physics_sim(); + } +} diff --git a/crates/system/src/phys/systemsim/steputil.rs b/crates/system/src/phys/systemsim/steputil.rs new file mode 100644 index 0000000..c22bc56 --- /dev/null +++ b/crates/system/src/phys/systemsim/steputil.rs @@ -0,0 +1,196 @@ +use galactica_content::{Content, Relationship, SystemObjectHandle}; +use nalgebra::{Isometry2, Point2, Vector2}; +use rapier2d::{dynamics::RigidBody, geometry::ColliderHandle}; + +use crate::{ + data::ShipState, + phys::{objects::PhysProjectile, ParticleBuilder, PhysStepResources}, +}; + +use super::PhysSim; + +// Private methods +impl PhysSim { + pub(super) fn remove_projectile( + &mut self, + res: &mut PhysStepResources, + c: ColliderHandle, + ) -> Option<(RigidBody, PhysProjectile)> { + let p = match self.projectiles.remove(&c) { + Some(p) => p, + None => return None, + }; + let r = self + .rigid_body_set + .remove( + p.rigid_body, + &mut res.wrapper.im, + &mut self.collider_set, + &mut res.wrapper.ij, + &mut res.wrapper.mj, + true, + ) + .unwrap(); + + return Some((r, p)); + } + + /// Try to land the given ship on the given planet. + /// State changes on success, and nothing happens on failure. + /// returns `true` if landing was successful. + pub(super) fn try_land_ship( + &mut self, + ct: &Content, + collider: ColliderHandle, + target_handle: SystemObjectHandle, + ) -> bool { + let ship = self.ships.get_mut(&collider).unwrap(); + let r = self.rigid_body_set.get(ship.rigid_body).unwrap(); + + let target = ct.get_system_object(target_handle); + let t_pos = Vector2::new(target.pos.x, target.pos.y); + let s_pos = Vector2::new(r.position().translation.x, r.position().translation.y); + + // TODO: deactivate collider when landing. + // Can't just set_active(false), since we still need that collider's mass. + + // We're in land range... + if (t_pos - s_pos).magnitude() > target.size / 2.0 { + return false; + } + + // And we'll stay in land range long enough. + if (t_pos - (s_pos + r.velocity_at_point(r.center_of_mass()) * 2.0)).magnitude() + > target.size / 2.0 + { + return false; + } + + ship.data.start_land_on(target_handle); + return true; + } + + /// Finish landing this ship on a planet. + /// Called after the landing animation finishes. + pub(super) fn finish_land_ship( + &mut self, + _ct: &Content, + collider: ColliderHandle, + _target: SystemObjectHandle, + ) { + let ship = self.ships.get_mut(&collider).unwrap(); + ship.data.finish_land_on(); + let r = self.rigid_body_set.get_mut(ship.rigid_body).unwrap(); + r.set_enabled(false); + r.set_angvel(0.0, false); + r.set_linvel(nalgebra::Vector2::new(0.0, 0.0), false); + } + + pub(super) fn unland_ship(&mut self, ct: &Content, collider: ColliderHandle) { + let ship = self.ships.get_mut(&collider).unwrap(); + let obj = ship.data.get_state().landed_on().unwrap(); + let obj = ct.get_system_object(obj); + + let target_pos = Isometry2::new(Vector2::new(obj.pos.x + 100.0, obj.pos.y + 100.0), 1.0); + + ship.data.unland(target_pos); + + let r = self.rigid_body_set.get_mut(ship.rigid_body).unwrap(); + r.set_enabled(true); + r.set_position(target_pos, true); + + self.collider_set + .get_mut(ship.collider) + .unwrap() + .set_enabled(true); + } + + pub(super) fn remove_ship( + &mut self, + res: &mut PhysStepResources, + colliderhandle: ColliderHandle, + ) { + let ship = match self.ships.get(&colliderhandle) { + None => return, + Some(s) => s, + }; + + self.rigid_body_set.remove( + ship.rigid_body, + &mut res.wrapper.im, + &mut self.collider_set, + &mut res.wrapper.ij, + &mut res.wrapper.mj, + true, + ); + self.ships.remove(&colliderhandle).unwrap(); + self.ship_behaviors.remove(&colliderhandle); + } + + pub(super) fn collide_projectile_ship( + &mut self, + res: &mut PhysStepResources, + 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 f = res.ct.get_faction(projectile.faction); + let r = f.relationships.get(&ship.data.get_faction()).unwrap(); + let destory_projectile = match r { + Relationship::Hostile => match ship.data.get_state() { + ShipState::Flying { .. } => { + ship.data.apply_damage(res.ct, projectile.content.damage); + true + } + ShipState::Collapsing { .. } => true, + _ => false, + }, + _ => false, + }; + + if destory_projectile { + let pr = self.rigid_body_set.get(projectile.rigid_body).unwrap(); + let v = + pr.velocity_at_point(pr.center_of_mass()).normalize() * projectile.content.force; + let pos = *pr.translation(); + let _ = pr; + + let r = self.rigid_body_set.get_mut(ship.rigid_body).unwrap(); + r.apply_impulse_at_point(Vector2::new(v.x, v.y), Point2::new(pos.x, pos.y), true); + + // Borrow again, we can only have one at a time + let pr = self.rigid_body_set.get(projectile.rigid_body).unwrap(); + let pos = *pr.translation(); + let angle = pr.rotation().angle(); + + match &projectile.content.impact_effect { + None => {} + Some(x) => { + let effect = res.ct.get_effect(*x); + let r = ship.rigid_body; + let sr = self.get_rigid_body(r).unwrap(); + let parent_velocity = pr.velocity_at_point(pr.center_of_mass()); + let target_velocity = + sr.velocity_at_point(&nalgebra::Point2::new(pos.x, pos.y)); + + res.particles.push(ParticleBuilder::from_content( + effect, + pos.into(), + -angle, + parent_velocity, + target_velocity, + )); + } + }; + + self.remove_projectile(res, projectile.collider); + } + } +}