use std::{collections::HashMap, time::Instant}; use super::{OutfitSet, ShipPersonality}; use cgmath::{InnerSpace, Point2, Point3, Rad}; 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 dead, and should be removed from the game. Dead, /// 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, }, /// This ship is landing on a planet /// (playing the animation) Landing { /// The point, in world coordinates, where we started from_position: Point2, /// The ship's angle when we started landing from_angle: Rad, /// The planet we're landing on target: SystemObjectHandle, /// The total amount of time, in seconds, we will spend landing total: f32, /// The amount of time we've already spent playing this landing sequence elapsed: f32, }, /// This ship is taking off from a planet /// (playing the animation) UnLanding { /// The point, in world coordinates, to which we're going to_position: Point2, /// The angle we'll be at when we arrive to_angle: Rad, /// The planet we're taking off from from: SystemObjectHandle, /// The total amount of time, in seconds, we will spend taking off total: f32, /// The amount of time we've already spent playing this unlanding sequence elapsed: f32, }, } impl ShipState { /// 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, } } /// Compute position of this ship's sprite during its landing sequence pub fn landing_position(&self, ct: &Content) -> Option> { match self { Self::Landing { from_position, target, total, elapsed, .. } => Some({ let target = ct.get_system_object(*target); let diff = Point2 { x: target.pos.x, y: target.pos.y, } - from_position; let diff = diff - diff.normalize() * (target.size / 2.0) * 0.8; // TODO: improve animation // TODO: fade // TODO: atmosphere burn // TODO: land at random point // TODO: don't jump camera // TODO: time by distance // TODO: keep momentum let pos = from_position + (diff * (elapsed / total)); Point3 { x: pos.x, y: pos.y, z: 1.0 + ((target.pos.z - 1.0) * (elapsed / total)), } }), _ => None, } } /// Compute position of this ship's sprite during its unlanding sequence pub fn unlanding_position(&self, ct: &Content) -> Option> { match self { Self::UnLanding { to_position, from, total, elapsed, .. } => Some({ let from = ct.get_system_object(*from); let diff = to_position - Point2 { x: from.pos.x, y: from.pos.y, }; //let diff = diff - diff.normalize() * (target.size / 2.0) * 0.8; // TODO: improve animation // TODO: fade // TODO: atmosphere burn // TODO: land at random point // TODO: don't jump camera // TODO: time by distance // TODO: keep momentum let pos = Point2 { x: from.pos.x, y: from.pos.y, } + (diff * (elapsed / total)); Point3 { x: pos.x, y: pos.y, z: from.pos.z + ((1.0 - from.pos.z) * (elapsed / total)), } }), _ => 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(), } } // TODO: position in data? /// Land this ship on `target` pub fn land_on( &mut self, target: SystemObjectHandle, from_position: Point2, from_angle: Rad, ) -> bool { match self.state { ShipState::Flying => { self.state = ShipState::Landing { elapsed: 0.0, total: 5.0, target, from_position, from_angle, }; return true; } _ => { unreachable!("Called `land_on` on a ship that isn't flying!") } }; } /// Take off from `target` pub fn unland(&mut self, to_position: Point2) { match self.state { ShipState::Landed { target } => { self.state = ShipState::UnLanding { to_position, to_angle: Rad(1.0), from: target, total: 5.0, elapsed: 0.0, }; } _ => { unreachable!("Called `unland` on a ship that isn't landed!") } }; } /// 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) { 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 { 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::Landing { ref mut elapsed, total, target, .. } => { *elapsed += t; if *elapsed >= total { self.state = ShipState::Landed { target }; } } ShipState::UnLanding { ref mut elapsed, total, .. } => { *elapsed += t; if *elapsed >= total { self.state = ShipState::Flying; } } 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 { ref mut elapsed, total, } => { *elapsed += t; if *elapsed >= total { self.state = ShipState::Dead } } 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 } }