445 lines
10 KiB
Rust
Raw Normal View History

2024-01-12 22:47:40 -08:00
use galactica_content::{Content, FactionHandle, GunPoint, Outfit, ShipHandle, SystemObjectHandle};
use nalgebra::{Point2, Point3};
use rand::{rngs::ThreadRng, Rng};
2024-01-13 18:57:21 -08:00
use rapier2d::math::Isometry;
2024-01-09 17:23:54 -08:00
use std::{collections::HashMap, time::Instant};
2024-01-08 22:38:36 -08:00
2024-01-08 23:05:07 -08:00
use super::{OutfitSet, ShipPersonality};
2024-01-08 22:38:36 -08:00
2024-01-13 18:57:21 -08:00
/// 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,
},
}
2024-01-11 22:10:36 -08:00
/// 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 {
2024-01-12 17:38:33 -08:00
/// This ship is dead, and should be removed from the game.
Dead,
2024-01-11 22:10:36 -08:00
/// This ship is alive and well in open space
2024-01-13 18:57:21 -08:00
Flying {
/// The autopilot we're using.
/// Overrides ship controller.
autopilot: ShipAutoPilot,
},
2024-01-11 22:10:36 -08:00
/// This ship has been destroyed, and is playing its collapse sequence.
2024-01-11 22:28:02 -08:00
Collapsing {
/// Total collapse sequence length, in seconds
total: f32,
/// How many seconds of the collapse sequence we've played
elapsed: f32,
},
2024-01-12 14:34:31 -08:00
/// This ship is landed on a planet
Landed {
/// The planet this ship is landed on
target: SystemObjectHandle,
},
2024-01-11 22:10:36 -08:00
2024-01-12 17:38:33 -08:00
/// This ship is landing on a planet
/// (playing the animation)
Landing {
/// The planet we're landing on
target: SystemObjectHandle,
2024-01-11 22:10:36 -08:00
2024-01-13 18:57:21 -08:00
/// Our current z-coordinate
current_z: f32,
2024-01-12 18:16:20 -08:00
},
/// This ship is taking off from a planet
/// (playing the animation)
UnLanding {
/// The point, in world coordinates, to which we're going
2024-01-13 18:57:21 -08:00
to_position: Isometry<f32>,
2024-01-12 18:16:20 -08:00
/// 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
2024-01-12 17:38:33 -08:00
elapsed: f32,
},
}
impl ShipState {
2024-01-12 14:34:31 -08:00
/// What planet is this ship landed on?
pub fn landed_on(&self) -> Option<SystemObjectHandle> {
2024-01-11 22:10:36 -08:00
match self {
2024-01-12 14:34:31 -08:00
Self::Landed { target } => Some(*target),
_ => None,
2024-01-11 22:10:36 -08:00
}
}
/// 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,
}
}
2024-01-12 17:38:33 -08:00
2024-01-12 18:16:20 -08:00
/// Compute position of this ship's sprite during its unlanding sequence
pub fn unlanding_position(&self, ct: &Content) -> Option<Point3<f32>> {
match self {
Self::UnLanding {
to_position,
from,
total,
elapsed,
..
} => Some({
let from = ct.get_system_object(*from);
2024-01-13 18:57:21 -08:00
let t = Point2::new(to_position.translation.x, to_position.translation.y);
let diff = t - Point2::new(from.pos.x, from.pos.y);
2024-01-12 18:16:20 -08:00
//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
2024-01-12 22:47:40 -08:00
let pos = Point2::new(from.pos.x, from.pos.y) + (diff * (elapsed / total));
2024-01-12 18:16:20 -08:00
2024-01-12 22:47:40 -08:00
Point3::new(
pos.x,
pos.y,
from.pos.z + ((1.0 - from.pos.z) * (elapsed / total)),
)
2024-01-12 18:16:20 -08:00
}),
_ => None,
}
}
2024-01-11 22:10:36 -08:00
}
2024-01-11 22:28:02 -08:00
/// Represents all attributes of a single ship
2024-01-11 11:28:11 -08:00
#[derive(Debug, Clone)]
2024-01-11 22:28:02 -08:00
pub struct ShipData {
2024-01-08 22:38:36 -08:00
// Metadata values
2024-01-09 21:45:30 -08:00
ct_handle: ShipHandle,
faction: FactionHandle,
2024-01-08 23:05:07 -08:00
outfits: OutfitSet,
personality: ShipPersonality,
2024-01-08 22:38:36 -08:00
2024-01-11 22:10:36 -08:00
/// Ship state machine. Keeps track of all possible ship state.
/// TODO: document this, draw a graph
state: ShipState,
2024-01-08 22:38:36 -08:00
// State values
2024-01-08 23:05:07 -08:00
// TODO: unified ship stats struct, like outfit space
hull: f32,
shields: f32,
2024-01-09 17:23:54 -08:00
gun_cooldowns: HashMap<GunPoint, f32>,
rng: ThreadRng,
2024-01-08 22:38:36 -08:00
// Utility values
/// The last time this ship was damaged
2024-01-08 23:05:07 -08:00
last_hit: Instant,
2024-01-08 22:38:36 -08:00
}
2024-01-11 22:28:02 -08:00
impl ShipData {
/// Create a new ShipData
pub(crate) fn new(
2024-01-09 21:45:30 -08:00
ct: &Content,
ct_handle: ShipHandle,
faction: FactionHandle,
2024-01-08 23:05:07 -08:00
personality: ShipPersonality,
2024-01-08 22:38:36 -08:00
) -> Self {
let s = ct.get_ship(ct_handle);
2024-01-11 22:28:02 -08:00
ShipData {
2024-01-08 22:38:36 -08:00
ct_handle,
faction,
2024-01-09 17:23:54 -08:00
outfits: OutfitSet::new(s.space, &s.guns),
2024-01-08 23:05:07 -08:00
personality,
2024-01-08 22:38:36 -08:00
last_hit: Instant::now(),
2024-01-09 17:23:54 -08:00
rng: rand::thread_rng(),
2024-01-08 22:38:36 -08:00
2024-01-11 22:10:36 -08:00
// TODO: ships must always start landed on planets
2024-01-13 18:57:21 -08:00
state: ShipState::Flying {
autopilot: ShipAutoPilot::None,
},
2024-01-11 22:10:36 -08:00
2024-01-08 22:38:36 -08:00
// Initial stats
hull: s.hull,
2024-01-09 17:23:54 -08:00
shields: 0.0,
gun_cooldowns: s.guns.iter().map(|x| (x.clone(), 0.0)).collect(),
2024-01-08 22:38:36 -08:00
}
}
2024-01-13 18:57:21 -08:00
/// 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!")
}
};
}
2024-01-12 14:34:31 -08:00
/// Land this ship on `target`
2024-01-13 18:57:21 -08:00
/// 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) {
2024-01-12 17:38:33 -08:00
match self.state {
2024-01-13 18:57:21 -08:00
ShipState::Flying { .. } => {
2024-01-12 17:38:33 -08:00
self.state = ShipState::Landing {
2024-01-13 18:57:21 -08:00
target: target_handle,
current_z: 1.0,
2024-01-12 17:38:33 -08:00
};
}
_ => {
2024-01-13 18:57:21 -08:00
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 flying!")
2024-01-12 17:38:33 -08:00
}
};
2024-01-12 14:34:31 -08:00
}
/// Take off from `target`
2024-01-13 18:57:21 -08:00
pub fn unland(&mut self, to_position: Isometry<f32>) {
2024-01-12 17:38:33 -08:00
match self.state {
2024-01-12 18:16:20 -08:00
ShipState::Landed { target } => {
self.state = ShipState::UnLanding {
to_position,
from: target,
2024-01-13 18:57:21 -08:00
total: 2.0,
2024-01-12 18:16:20 -08:00
elapsed: 0.0,
};
2024-01-12 17:38:33 -08:00
}
_ => {
unreachable!("Called `unland` on a ship that isn't landed!")
}
};
2024-01-12 14:34:31 -08:00
}
2024-01-09 17:23:54 -08:00
/// Add an outfit to this ship
2024-01-09 21:45:30 -08:00
pub fn add_outfit(&mut self, o: &Outfit) -> super::OutfitAddResult {
2024-01-09 20:51:28 -08:00
let r = self.outfits.add(o);
self.shields = self.outfits.get_shield_strength();
return r;
2024-01-09 17:23:54 -08:00
}
/// Remove an outfit from this ship
2024-01-09 21:45:30 -08:00
pub fn remove_outfit(&mut self, o: &Outfit) -> super::OutfitRemoveResult {
2024-01-09 17:23:54 -08:00
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.
2024-01-11 22:28:02 -08:00
pub(crate) fn fire_gun(&mut self, ct: &Content, which: &GunPoint) -> bool {
2024-01-09 17:23:54 -08:00
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;
}
}
2024-01-08 22:38:36 -08:00
/// Hit this ship with the given amount of damage
2024-01-11 22:28:02 -08:00
pub(crate) fn apply_damage(&mut self, ct: &Content, mut d: f32) {
2024-01-12 17:38:33 -08:00
match self.state {
2024-01-13 18:57:21 -08:00
ShipState::Flying { .. } => {}
2024-01-12 17:38:33 -08:00
_ => {
unreachable!("Cannot apply damage to a ship that is not flying!")
}
2024-01-08 22:38:36 -08:00
}
2024-01-12 17:38:33 -08:00
2024-01-08 22:38:36 -08:00
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();
2024-01-11 22:10:36 -08:00
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,
}
}
2024-01-08 22:38:36 -08:00
}
/// Update this ship's state by `t` seconds
2024-01-11 22:28:02 -08:00
pub(crate) fn step(&mut self, t: f32) {
2024-01-11 22:10:36 -08:00
match self.state {
2024-01-13 18:57:21 -08:00
ShipState::Landing { .. } => {}
2024-01-09 17:23:54 -08:00
2024-01-12 18:16:20 -08:00
ShipState::UnLanding {
ref mut elapsed,
total,
..
} => {
*elapsed += t;
if *elapsed >= total {
2024-01-13 18:57:21 -08:00
self.state = ShipState::Flying {
autopilot: ShipAutoPilot::None,
};
2024-01-12 18:16:20 -08:00
}
}
2024-01-12 14:34:31 -08:00
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();
}
}
2024-01-13 18:57:21 -08:00
ShipState::Flying { .. } => {
2024-01-11 22:10:36 -08:00
// 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;
}
}
2024-01-08 22:38:36 -08:00
}
}
}
2024-01-12 17:38:33 -08:00
ShipState::Collapsing {
ref mut elapsed,
total,
} => {
*elapsed += t;
if *elapsed >= total {
self.state = ShipState::Dead
}
}
ShipState::Dead => {}
2024-01-08 22:38:36 -08:00
}
}
}
2024-01-08 23:05:07 -08:00
// Misc getters, so internal state is untouchable
2024-01-11 22:28:02 -08:00
impl ShipData {
2024-01-11 22:10:36 -08:00
/// Get this ship's state
pub fn get_state(&self) -> &ShipState {
&self.state
2024-01-09 11:34:54 -08:00
}
/// Get a handle to this ship's content
2024-01-09 21:45:30 -08:00
pub fn get_content(&self) -> ShipHandle {
2024-01-09 11:34:54 -08:00
self.ct_handle
}
2024-01-08 23:05:07 -08:00
/// Get this ship's current hull.
/// Use content handle to get maximum hull
pub fn get_hull(&self) -> f32 {
2024-01-09 11:34:54 -08:00
self.hull
2024-01-08 23:05:07 -08:00
}
/// 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
2024-01-09 21:45:30 -08:00
pub fn get_faction(&self) -> FactionHandle {
2024-01-08 23:05:07 -08:00
self.faction
}
/// Get this ship's content handle
2024-01-09 21:45:30 -08:00
pub fn get_ship(&self) -> ShipHandle {
2024-01-08 23:05:07 -08:00
self.ct_handle
}
}