use std::{collections::HashMap, time::Instant}; use super::{OutfitSet, ShipPersonality}; use galactica_content::{Content, FactionHandle, GunPoint, Outfit, ShipHandle, SystemObjectHandle}; use rand::{rngs::ThreadRng, Rng}; /// 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 alive and well in open space Flying, // TODO: system, position (also in collapse)? /// This ship has been destroyed, and is playing its collapse sequence. Collapsing { /// Total collapse sequence length, in seconds total: f32, /// How many seconds of the collapse sequence we've played elapsed: f32, }, /// This ship is landed on a planet Landed { /// The planet this ship is landed on target: SystemObjectHandle, }, } impl ShipState { /// Is this ship playing its collapse sequence? pub fn is_collapsing(&self) -> bool { match self { Self::Collapsing { .. } => true, _ => false, } } /// Is this ship flying in open space? pub fn is_flying(&self) -> bool { match self { Self::Flying => true, _ => false, } } /// Is this ship landed on a planet? pub fn is_landed(&self) -> bool { match self { Self::Landed { .. } => true, _ => false, } } /// True if this ship has been destroyed and has finished it's collapse sequence. /// Ships are deleted once this is true. pub fn should_be_removed(&self) -> bool { match self { Self::Collapsing { elapsed, total } => elapsed >= total, _ => false, } } /// What planet is this ship landed on? pub fn landed_on(&self) -> Option { match self { Self::Landed { target } => Some(*target), _ => None, } } /// If this ship is collapsing, return total collapse time and remaining collapse time. /// Otherwise, return None pub fn collapse_state(&self) -> Option<(f32, f32)> { match self { Self::Collapsing { total, elapsed: remaining, } => Some((*total, *remaining)), _ => 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, // Initial stats hull: s.hull, shields: 0.0, gun_cooldowns: s.guns.iter().map(|x| (x.clone(), 0.0)).collect(), } } /// Land this ship on `target` pub fn land_on(&mut self, target: SystemObjectHandle) -> bool { if self.state.is_flying() { self.state = ShipState::Landed { target }; } else { return false; } return true; } /// Take off from `target` pub fn unland(&mut self) -> bool { if self.state.is_landed() { self.state = ShipState::Flying; } else { return false; } return true; } /// 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, ct: &Content, mut d: f32) { if self.state.is_collapsing() { return; } 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 { total: ct.get_ship(self.ct_handle).collapse.length, elapsed: 0.0, } } } /// Update this ship's state by `t` seconds pub(crate) fn step(&mut self, t: f32) { match self.state { ShipState::Collapsing { ref mut elapsed, .. } => { *elapsed += t; } 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; } } } } } } } } // 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 } }