use galactica_content::{Content, FactionHandle, GunPoint, Outfit, ShipHandle, SystemObjectHandle}; use nalgebra::Isometry2; use rand::{rngs::ThreadRng, Rng}; use rapier2d::math::Isometry; use std::{collections::HashMap, time::Instant}; use super::{OutfitSet, ShipPersonality}; /// A ship autopilot. /// An autopilot is a lightweight ShipController that /// temporarily has control over a ship. #[derive(Debug, Clone)] pub enum ShipAutoPilot { /// No autopilot, use usual behavior. None, /// Automatically arrange for landing on the given object Landing { /// The body we want to land on target: SystemObjectHandle, }, } /// Ship state machine. /// Any ship we keep track of is in one of these states. /// Dead ships don't exist---they removed once their collapse /// sequence fully plays out. #[derive(Debug, Clone)] pub enum ShipState { /// This ship is dead, and should be removed from the game. Dead, /// This ship is alive and well in open space Flying { /// The autopilot we're using. /// Overrides ship controller. autopilot: ShipAutoPilot, }, /// This ship has been destroyed, and is playing its collapse sequence. Collapsing, /// This ship is landed on a planet Landed { /// The planet this ship is landed on target: SystemObjectHandle, }, /// This ship is landing on a planet /// (playing the animation) Landing { /// The planet we're landing on target: SystemObjectHandle, /// Our current z-coordinate current_z: f32, }, /// This ship is taking off from a planet /// (playing the animation) UnLanding { /// The point to which we're going, in world coordinates to_position: Isometry, /// The planet we're taking off from from: SystemObjectHandle, /// Our current z-coordinate current_z: f32, }, } impl ShipState { /// What planet is this ship landed on? pub fn landed_on(&self) -> Option { match self { Self::Landed { target } => Some(*target), _ => None, } } } /// Represents all attributes of a single ship #[derive(Debug, Clone)] pub struct ShipData { // Metadata values ct_handle: ShipHandle, faction: FactionHandle, outfits: OutfitSet, personality: ShipPersonality, /// Ship state machine. Keeps track of all possible ship state. /// TODO: document this, draw a graph state: ShipState, // State values // TODO: unified ship stats struct, like outfit space hull: f32, shields: f32, gun_cooldowns: HashMap, rng: ThreadRng, // Utility values /// The last time this ship was damaged last_hit: Instant, } impl ShipData { /// Create a new ShipData pub(crate) fn new( ct: &Content, ct_handle: ShipHandle, faction: FactionHandle, personality: ShipPersonality, ) -> Self { let s = ct.get_ship(ct_handle); ShipData { ct_handle, faction, outfits: OutfitSet::new(s.space, &s.guns), personality, last_hit: Instant::now(), rng: rand::thread_rng(), // TODO: ships must always start landed on planets state: ShipState::Flying { autopilot: ShipAutoPilot::None, }, // Initial stats hull: s.hull, shields: 0.0, gun_cooldowns: s.guns.iter().map(|x| (x.clone(), 0.0)).collect(), } } /// Set this ship's autopilot. /// Panics if we're not flying. pub fn set_autopilot(&mut self, autopilot: ShipAutoPilot) { match self.state { ShipState::Flying { autopilot: ref mut pilot, } => { *pilot = autopilot; } _ => { unreachable!("Called `set_autopilot` on a ship that isn't flying!") } }; } /// Land this ship on `target` /// This does NO checks (speed, range, etc). /// That is the simulation's responsiblity. /// /// Will panic if we're not flying. pub fn start_land_on(&mut self, target_handle: SystemObjectHandle) { match self.state { ShipState::Flying { .. } => { self.state = ShipState::Landing { target: target_handle, current_z: 1.0, }; } _ => { unreachable!("Called `start_land_on` on a ship that isn't flying!") } }; } /// When landing, update z position. /// Will panic if we're not landing pub fn set_landing_z(&mut self, z: f32) { match &mut self.state { ShipState::Landing { ref mut current_z, .. } => *current_z = z, _ => unreachable!("Called `set_landing_z` on a ship that isn't landing!"), } } /// Finish landing sequence /// Will panic if we're not landing pub fn finish_land_on(&mut self) { match self.state { ShipState::Landing { target, .. } => { self.state = ShipState::Landed { target }; } _ => { unreachable!("Called `finish_land_on` on a ship that isn't landing!") } }; } /// Land this ship on `target` /// This does NO checks (speed, range, etc). /// That is the simulation's responsiblity. /// /// Will panic if we're not flying. pub fn start_unland_to(&mut self, ct: &Content, to_position: Isometry2) { match self.state { ShipState::Landed { target } => { let obj = ct.get_system_object(target); self.state = ShipState::UnLanding { to_position, from: target, current_z: obj.pos.z, }; } _ => { unreachable!("Called `start_unland_to` on a ship that isn't landed!") } }; } /// When unlanding, update z position. /// Will panic if we're not unlanding pub fn set_unlanding_z(&mut self, z: f32) { match &mut self.state { ShipState::UnLanding { ref mut current_z, .. } => *current_z = z, _ => unreachable!("Called `set_unlanding_z` on a ship that isn't unlanding!"), } } /// Finish unlanding sequence /// Will panic if we're not unlanding pub fn finish_unland_to(&mut self) { match self.state { ShipState::UnLanding { .. } => { self.state = ShipState::Flying { autopilot: ShipAutoPilot::None, } } _ => { unreachable!("Called `finish_unland_to` on a ship that isn't unlanding!") } }; } /// Called when collapse sequence is finished. /// Will panic if we're not collapsing pub fn finish_collapse(&mut self) { match self.state { ShipState::Collapsing => self.state = ShipState::Dead, _ => { unreachable!("Called `finish_collapse` on a ship that isn't collapsing!") } }; } /// Add an outfit to this ship pub fn add_outfit(&mut self, o: &Outfit) -> super::OutfitAddResult { let r = self.outfits.add(o); self.shields = self.outfits.get_shield_strength(); return r; } /// Remove an outfit from this ship pub fn remove_outfit(&mut self, o: &Outfit) -> super::OutfitRemoveResult { self.outfits.remove(o) } /// Try to fire a gun. /// Will panic if `which` isn't a point on this ship. /// Returns `true` if this gun was fired, /// and `false` if it is on cooldown or empty. pub(crate) fn fire_gun(&mut self, ct: &Content, which: &GunPoint) -> bool { let c = self.gun_cooldowns.get_mut(which).unwrap(); if *c > 0.0 { return false; } let g = self.outfits.get_gun(which); if g.is_some() { let g = ct.get_outfit(g.unwrap()); let gun = g.gun.as_ref().unwrap(); *c = 0f32.max(gun.rate + self.rng.gen_range(-gun.rate_rng..=gun.rate_rng)); return true; } else { return false; } } /// Hit this ship with the given amount of damage pub(crate) fn apply_damage(&mut self, mut d: f32) { match self.state { ShipState::Flying { .. } => {} _ => { unreachable!("Cannot apply damage to a ship that is not flying!") } } if self.shields >= d { self.shields -= d } else { d -= self.shields; self.shields = 0.0; self.hull = 0f32.max(self.hull - d); } self.last_hit = Instant::now(); if self.hull <= 0.0 { // This ship has been destroyed, update state self.state = ShipState::Collapsing } } /// Update this ship's state by `t` seconds pub(crate) fn step(&mut self, t: f32) { match self.state { ShipState::UnLanding { .. } | ShipState::Landing { .. } => {} ShipState::Landed { .. } => { // Cooldown guns for (_, c) in &mut self.gun_cooldowns { if *c > 0.0 { *c = 0.0; } } // Regenerate shields if self.shields != self.outfits.get_shield_strength() { self.shields = self.outfits.get_shield_strength(); } } ShipState::Flying { .. } => { // Cooldown guns for (_, c) in &mut self.gun_cooldowns { if *c > 0.0 { *c -= t; } } // Regenerate shields let time_since = self.last_hit.elapsed().as_secs_f32(); if self.shields != self.outfits.get_shield_strength() { for g in self.outfits.iter_shield_generators() { if time_since >= g.delay { self.shields += g.generation * t; if self.shields > self.outfits.get_shield_strength() { self.shields = self.outfits.get_shield_strength(); break; } } } } } ShipState::Collapsing {} | ShipState::Dead => {} } } } // Misc getters, so internal state is untouchable impl ShipData { /// Get this ship's state pub fn get_state(&self) -> &ShipState { &self.state } /// Get a handle to this ship's content pub fn get_content(&self) -> ShipHandle { self.ct_handle } /// Get this ship's current hull. /// Use content handle to get maximum hull pub fn get_hull(&self) -> f32 { self.hull } /// Get this ship's current shields. /// Use get_outfits() for maximum shields pub fn get_shields(&self) -> f32 { self.shields } /// Get all outfits on this ship pub fn get_outfits(&self) -> &OutfitSet { &self.outfits } /// Get this ship's personality pub fn get_personality(&self) -> ShipPersonality { self.personality } /// Get this ship's faction pub fn get_faction(&self) -> FactionHandle { self.faction } /// Get this ship's content handle pub fn get_ship(&self) -> ShipHandle { self.ct_handle } }