diff --git a/crates/content/src/part/outfitspace.rs b/crates/content/src/part/outfitspace.rs index c12aaf5..790febf 100644 --- a/crates/content/src/part/outfitspace.rs +++ b/crates/content/src/part/outfitspace.rs @@ -1,3 +1,5 @@ +use std::ops::{Add, AddAssign, Sub, SubAssign}; + pub(crate) mod syntax { use serde::Deserialize; @@ -9,6 +11,8 @@ pub(crate) mod syntax { } } +// TODO: user-defined space values + /// Represents outfit space, either that available in a ship /// or that used by an outfit. #[derive(Debug, Clone, Copy)] @@ -25,7 +29,7 @@ pub struct OutfitSpace { } impl OutfitSpace { - /// Make a new, zero OutfitSpace + /// Make a new, zeroed OutfitSpace pub fn new() -> Self { Self { outfit: 0, @@ -34,28 +38,54 @@ impl OutfitSpace { } } - /// Does this outfit contain `smaller`? + /// Can this outfit contain `smaller`? pub fn can_contain(&self, smaller: &Self) -> bool { - self.outfit >= (smaller.outfit + smaller.weapon + smaller.engine) + self.outfit >= smaller.outfit && self.weapon >= smaller.weapon && self.engine >= smaller.engine } +} - /// Free outfit space - pub fn free(&mut self, rhs: &Self) { - self.outfit += rhs.outfit + rhs.weapon + rhs.engine; - self.weapon += rhs.weapon; - self.engine += rhs.engine; +impl Sub for OutfitSpace { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + OutfitSpace { + outfit: self.outfit - rhs.outfit, + weapon: self.weapon - rhs.weapon, + engine: self.engine - rhs.engine, + } } +} - /// Occupy outfit space - pub fn occupy(&mut self, rhs: &Self) { - self.outfit -= rhs.outfit + rhs.weapon + rhs.engine; +impl SubAssign for OutfitSpace { + fn sub_assign(&mut self, rhs: Self) { + self.outfit -= rhs.outfit; self.weapon -= rhs.weapon; self.engine -= rhs.engine; } } +impl Add for OutfitSpace { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + OutfitSpace { + outfit: self.outfit + rhs.outfit, + weapon: self.weapon + rhs.weapon, + engine: self.engine + rhs.engine, + } + } +} + +impl AddAssign for OutfitSpace { + fn add_assign(&mut self, rhs: Self) { + self.outfit += rhs.outfit; + self.weapon += rhs.weapon; + self.engine += rhs.engine; + } +} + impl From for OutfitSpace { fn from(value: syntax::OutfitSpace) -> Self { Self { diff --git a/crates/content/src/part/ship.rs b/crates/content/src/part/ship.rs index cdd931d..4458146 100644 --- a/crates/content/src/part/ship.rs +++ b/crates/content/src/part/ship.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, hash::Hash}; use anyhow::{bail, Context, Result}; use cgmath::Point2; @@ -160,11 +160,28 @@ pub struct EnginePoint { /// A gun point on a ship. #[derive(Debug, Clone)] pub struct GunPoint { + /// This gun point's index in this ship + pub idx: u32, + /// This gun point's position, in game units, /// relative to the ship's center. pub pos: Point2, } +impl Eq for GunPoint {} +impl PartialEq for GunPoint { + fn eq(&self, other: &Self) -> bool { + self.idx == other.idx + } +} + +// We use a hashmap of these in OutfitSet +impl Hash for GunPoint { + fn hash(&self, state: &mut H) { + self.idx.hash(state); + } +} + /// Parameters for a ship's collapse sequence #[derive(Debug, Clone)] pub struct ShipCollapse { @@ -345,6 +362,18 @@ impl crate::Build for Ship { } }; + // TODO: document this + let mut guns = Vec::new(); + for g in ship.guns { + guns.push(GunPoint { + idx: guns.len() as u32, + pos: Point2 { + x: g.x * size * aspect / 2.0, + y: g.y * size / 2.0, + }, + }) + } + ct.ships.push(Self { aspect, collapse, @@ -370,16 +399,7 @@ impl crate::Build for Ship { }) .collect(), - guns: ship - .guns - .iter() - .map(|e| GunPoint { - pos: Point2 { - x: e.x * size * aspect / 2.0, - y: e.y * size / 2.0, - }, - }) - .collect(), + guns, collision: Collision { indices: (0..ship.collision.len()) diff --git a/crates/gameobject/src/lib.rs b/crates/gameobject/src/lib.rs index 17a5192..003dfcd 100644 --- a/crates/gameobject/src/lib.rs +++ b/crates/gameobject/src/lib.rs @@ -4,14 +4,10 @@ //! of every ship in the game, but it has no understanding of physics. //! That is done in `galactica_world`. -mod outfits; mod projectile; mod ship; mod system; -mod systemobject; -pub use outfits::{OutfitSet, OutfitStatSum, ShipGun}; pub use projectile::Projectile; pub use ship::Ship; -pub use system::System; -pub use systemobject::SystemObject; +pub use system::{System, SystemObject}; diff --git a/crates/gameobject/src/outfits.rs b/crates/gameobject/src/outfits.rs deleted file mode 100644 index e48f88f..0000000 --- a/crates/gameobject/src/outfits.rs +++ /dev/null @@ -1,203 +0,0 @@ -use content::{EnginePoint, OutfitHandle, SpriteHandle}; -use galactica_content as content; - -/// Represents a gun attached to a specific ship at a certain gunpoint. -#[derive(Debug)] -pub struct ShipGun { - /// The kind of gun this is - pub kind: content::Gun, - - /// How many seconds we must wait before this gun can fire - pub cooldown: f32, - - /// Index of the gunpoint this gun is attached to - pub point: usize, -} - -impl ShipGun { - /// Make a new shipgun - pub fn new(kind: &content::Gun, point: usize) -> Self { - Self { - kind: kind.clone(), - point, - cooldown: 0.0, - } - } -} - -/// This struct keeps track of the combined stats of a set of outfits. -/// It does NOT check for sanity (e.g, removing an outfit that was never added) -/// That is handled by ShipOutfits. -#[derive(Debug)] -pub struct OutfitStatSum { - pub engine_thrust: f32, - pub steer_power: f32, - pub engine_flare_sprites: Vec, - pub shield_strength: f32, - - // Delay, generation - pub shield_generators: Vec<(OutfitHandle, f32, f32)>, -} - -impl OutfitStatSum { - pub fn new() -> Self { - Self { - engine_thrust: 0.0, - steer_power: 0.0, - engine_flare_sprites: Vec::new(), - shield_strength: 0.0, - shield_generators: Vec::new(), - } - } - - pub fn add(&mut self, o: &content::Outfit) { - self.engine_thrust += o.engine_thrust; - if let Some(t) = o.engine_flare_sprite { - self.engine_flare_sprites.push(t); - }; - self.steer_power += o.steer_power; - self.shield_strength += o.shield_strength; - self.shield_generators - .push((o.handle, o.shield_delay, o.shield_generation)); - } - - pub fn remove(&mut self, o: &content::Outfit) { - self.engine_thrust -= o.engine_thrust; - if let Some(t) = o.engine_flare_sprite { - self.engine_flare_sprites.remove( - self.engine_flare_sprites - .iter() - .position(|x| *x == t) - .unwrap(), - ); - } - self.steer_power -= o.steer_power; - self.shield_strength -= o.shield_strength; - { - // Assume such an index exists - let index = self - .shield_generators - .iter() - .position(|(h, _, _)| *h == o.handle) - .unwrap(); - self.shield_generators.remove(index); - } - } -} - -/// Represents a set of outfits attached to a ship. -/// Keeps track of the sum of their stats, so it mustn't be re-computed every frame. -#[derive(Debug)] -pub struct OutfitSet { - /// The total sum of the stats this set of outfits provides - stats: OutfitStatSum, - - //pub total_space: content::OutfitSpace, - available_space: content::OutfitSpace, - outfits: Vec, - guns: Vec, - enginepoints: Vec, - gunpoints: Vec, -} - -impl<'a> OutfitSet { - /// Make a new outfit array - pub fn new(content: &content::Ship) -> Self { - Self { - stats: OutfitStatSum::new(), - outfits: Vec::new(), - guns: Vec::new(), - available_space: content.space.clone(), - //total_space: content.space.clone(), - enginepoints: content.engines.clone(), - gunpoints: content.guns.clone(), - } - } - - /// Does this outfit set contain the specified outfit? - pub fn contains_outfit(&self, o: content::OutfitHandle) -> bool { - match self.outfits.iter().position(|x| *x == o) { - Some(_) => true, - None => false, - } - } - - pub fn stat_sum(&self) -> &OutfitStatSum { - &self.stats - } - - /// Add an outfit to this ship. - /// Returns true on success, and false on failure - /// TODO: failure reason enum - pub fn add(&mut self, ct: &content::Content, o: content::OutfitHandle) -> bool { - let outfit = ct.get_outfit(o); - if !self.available_space.can_contain(&outfit.space) { - return false; - } - self.available_space.occupy(&outfit.space); - self.stats.add(&outfit); - self.outfits.push(o); - return true; - } - - /// Remove an outfit from this set - pub fn remove(&mut self, ct: &content::Content, o: content::OutfitHandle) { - let outfit = ct.get_outfit(o); - let i = match self.outfits.iter().position(|x| *x == o) { - Some(i) => i, - None => panic!("removed non-existing outfit"), - }; - self.available_space.free(&outfit.space); - self.outfits.remove(i); - } - - /// Add a gun to this outfit set. - /// This automatically attaches the gun to the first available gunpoint, - /// and returns false (applying no changes) if no points are available. - pub fn add_gun(&mut self, ct: &content::Content, g: content::GunHandle) -> bool { - // Find first unused point - let mut p = None; - 'outer: for i in 0..self.gunpoints.len() { - for g in &self.guns { - if g.point == i { - continue 'outer; - } - } - p = Some(i); - break; - } - let gun = ct.get_gun(g); - - // All points are taken - if p.is_none() { - return false; - } - - let sg = ShipGun::new(gun, p.unwrap()); - self.guns.push(sg); - return true; - } - - /// Iterate over all guns in this outfitset - pub fn iter_guns(&mut self) -> impl Iterator { - self.guns.iter_mut() - } - - /// Iterate over all guns and the gunpoints they're attached to - pub fn iter_guns_points(&mut self) -> impl Iterator { - self.guns - .iter_mut() - .map(|x| (&self.gunpoints[x.point], x)) - .map(|(a, b)| (b, a)) - } - - // TODO: move to ship - /// Iterate over all ships in this physics system - pub fn iter_enginepoints(&self) -> impl Iterator + '_ { - self.enginepoints.iter() - } - - pub fn get_flare_sprite(&self) -> Option { - self.stats.engine_flare_sprites.iter().next().map(|x| *x) - } -} diff --git a/crates/gameobject/src/ship.rs b/crates/gameobject/src/ship.rs deleted file mode 100644 index 4563ea2..0000000 --- a/crates/gameobject/src/ship.rs +++ /dev/null @@ -1,112 +0,0 @@ -use rand::Rng; -use std::time::Instant; - -use crate::{OutfitSet, Projectile}; -use galactica_content as content; - -#[derive(Debug)] -pub struct Ship { - pub handle: content::ShipHandle, - pub faction: content::FactionHandle, - pub outfits: OutfitSet, - pub last_hit: Instant, - - // TODO: unified ship stats struct, like space - // TODO: unified outfit stats struct: check - pub hull: f32, - pub shields: f32, -} - -impl Ship { - pub fn new( - ct: &content::Content, - handle: content::ShipHandle, - faction: content::FactionHandle, - outfits: OutfitSet, - ) -> Self { - let s = ct.get_ship(handle); - let shields = outfits.stat_sum().shield_strength; - Ship { - handle: handle, - faction, - outfits, - hull: s.hull, - shields, - last_hit: Instant::now(), - } - } - - pub fn is_dead(&self) -> bool { - self.hull <= 0.0 - } - - /// Called whenever a projectile hits this ship. - /// Returns true if that projectile should be destroyed and false otherwise. - pub fn handle_projectile_collision(&mut self, ct: &content::Content, p: &Projectile) -> bool { - let f = ct.get_faction(self.faction); - let r = f.relationships.get(&p.faction).unwrap(); - match r { - content::Relationship::Hostile => { - let mut d = p.content.damage; - if self.is_dead() { - return true; - } - if self.shields >= d { - self.shields -= d - } else { - d -= self.shields; - self.shields = 0.0; - self.hull -= d; - } - self.last_hit = Instant::now(); - - return true; - } - _ => return false, - } - } - - pub fn fire_guns(&mut self) -> Vec<(Projectile, content::GunPoint)> { - if self.is_dead() { - return vec![]; - } - - self.outfits - .iter_guns_points() - .filter(|(g, _)| g.cooldown <= 0.0) - .map(|(g, p)| { - let mut rng = rand::thread_rng(); - g.cooldown = g.kind.rate + &rng.gen_range(-g.kind.rate_rng..=g.kind.rate_rng); - - let lifetime = g.kind.projectile.lifetime - + rng.gen_range( - -g.kind.projectile.lifetime_rng..=g.kind.projectile.lifetime_rng, - ); - - ( - Projectile { - content: g.kind.projectile.clone(), - lifetime: 0f32.max(lifetime), - faction: self.faction, - }, - p.clone(), - ) - }) - .collect() - } - - pub fn step(&mut self, t: f32) { - let time_since = self.last_hit.elapsed().as_secs_f32(); - if self.shields != self.outfits.stat_sum().shield_strength { - for (_, d, g) in &self.outfits.stat_sum().shield_generators { - if time_since >= *d { - self.shields += g * t; - if self.shields > self.outfits.stat_sum().shield_strength { - self.shields = self.outfits.stat_sum().shield_strength; - break; - } - } - } - } - } -} diff --git a/crates/gameobject/src/ship/mod.rs b/crates/gameobject/src/ship/mod.rs new file mode 100644 index 0000000..9f8e1c6 --- /dev/null +++ b/crates/gameobject/src/ship/mod.rs @@ -0,0 +1,5 @@ +mod outfitset; +mod ship; + +pub use outfitset::*; +pub use ship::*; diff --git a/crates/gameobject/src/ship/outfitset.rs b/crates/gameobject/src/ship/outfitset.rs new file mode 100644 index 0000000..d614a28 --- /dev/null +++ b/crates/gameobject/src/ship/outfitset.rs @@ -0,0 +1,187 @@ +use std::collections::HashMap; + +use content::{GunPoint, OutfitHandle, OutfitSpace}; +use galactica_content as content; + +/// Possible outcomes when adding an outfit +pub enum OutfitAddResult { + /// An outfit was successfully added + Ok, + + /// An outfit could not be added because we don't have enough free space. + /// The string tells us what kind of space we need: + /// `outfit,` `weapon,` `engine,` etc. Note that these sometimes overlap: + /// outfits may need outfit AND weapon space. In these cases, this result + /// should name the "most specific" kind of space we lack. + NotEnoughSpace(String), +} + +/// Possible outcomes when removing an outfit +pub enum OutfitRemoveResult { + /// This outfit was successfully removed + Ok, + + /// This outfit isn't in this set + NotExist, + // TODO: + // This is where we'll add non-removable outfits, + // outfits that provide space, etc +} + +/// A simple data class, used to keep track of delayed shield generators +#[derive(Debug)] +pub(crate) struct ShieldGenerator { + pub outfit: OutfitHandle, + pub delay: f32, + pub generation: f32, +} + +/// This struct keeps track of a ship's outfit loadout. +#[derive(Debug)] +pub struct OutfitSet { + /// What outfits does this statsum contain? + outfits: HashMap, + + /// Space available in this outfitset. + /// set at creation and never changes. + total_space: OutfitSpace, + + /// Space used by the outfits in this set. + /// This may be negative if certain outfits provide space! + used_space: OutfitSpace, + + /// The gun points available in this ship. + /// If value is None, this point is free. + /// if value is Some, this point is taken. + gun_points: HashMap>, + + // Outfit values + // This isn't strictly necessary, but we don't want to + // re-compute this on each frame. + engine_thrust: f32, + steer_power: f32, + shield_strength: f32, + + // Delay, generation + // TODO: struct + shield_generators: Vec, +} + +impl OutfitSet { + pub fn new(available_space: OutfitSpace, gun_points: &[GunPoint]) -> Self { + Self { + outfits: HashMap::new(), + total_space: available_space, + used_space: OutfitSpace::new(), + gun_points: gun_points.iter().map(|x| (x.clone(), None)).collect(), + + engine_thrust: 0.0, + steer_power: 0.0, + shield_strength: 0.0, + shield_generators: Vec::new(), + } + } + + pub fn add(&mut self, o: &content::Outfit) -> OutfitAddResult { + if !(self.total_space - self.used_space).can_contain(&o.space) { + return OutfitAddResult::NotEnoughSpace("TODO".to_string()); + } + + self.used_space += o.space; + + self.engine_thrust += o.engine_thrust; + self.steer_power += o.steer_power; + self.shield_strength += o.shield_strength; + self.shield_generators.push(ShieldGenerator { + outfit: o.handle, + delay: o.shield_delay, + generation: o.shield_generation, + }); + + return OutfitAddResult::Ok; + } + + pub fn remove(&mut self, o: &content::Outfit) -> OutfitRemoveResult { + if !self.outfits.contains_key(&o.handle) { + return OutfitRemoveResult::NotExist; + } else { + let n = *self.outfits.get(&o.handle).unwrap(); + if n == 1u32 { + self.outfits.remove(&o.handle); + } else { + *self.outfits.get_mut(&o.handle).unwrap() -= 1; + } + } + + self.used_space -= o.space; + + self.engine_thrust -= o.engine_thrust; + self.steer_power -= o.steer_power; + self.shield_strength -= o.shield_strength; + + { + // This index will exist, since we checked the hashmap + let index = self + .shield_generators + .iter() + .position(|g| g.outfit == o.handle) + .unwrap(); + self.shield_generators.remove(index); + } + + return OutfitRemoveResult::Ok; + } +} + +// Simple getters to make sure nobody meddles with our internal state +impl OutfitSet { + /// The number of outfits in this set + pub fn len(&self) -> u32 { + self.outfits.iter().map(|(_, x)| x).sum() + } + + /// Iterate over all outfits + pub fn iter_outfits(&self) -> impl Iterator { + self.outfits.iter() + } + + /// Iterate over all gun points + pub fn iter_gun_points(&self) -> impl Iterator)> { + self.gun_points.iter() + } + + /// Iterate over all shield generators + pub(crate) fn iter_shield_generators(&self) -> impl Iterator { + self.shield_generators.iter() + } + + /// Get maximum possible shield regen + pub fn get_max_shield_regen(&self) -> f32 { + self.shield_generators.iter().map(|x| x.generation).sum() + } + + /// Total available outfit space + pub fn get_total_space(&self) -> &OutfitSpace { + &self.total_space + } + + /// Used outfit space + pub fn get_used_space(&self) -> &OutfitSpace { + &self.used_space + } + + /// Total foward thrust + pub fn get_engine_thrust(&self) -> f32 { + self.engine_thrust + } + + /// Total steer power + pub fn get_steer_power(&self) -> f32 { + self.steer_power + } + + /// Total shield strength + pub fn get_shield_strength(&self) -> f32 { + self.shield_strength + } +} diff --git a/crates/gameobject/src/ship/ship.rs b/crates/gameobject/src/ship/ship.rs new file mode 100644 index 0000000..5f76f08 --- /dev/null +++ b/crates/gameobject/src/ship/ship.rs @@ -0,0 +1,79 @@ +use std::time::Instant; + +use super::OutfitSet; +use galactica_content as content; + +#[derive(Debug)] +pub struct Ship { + // Metadata values + pub ct_handle: content::ShipHandle, + pub faction: content::FactionHandle, + pub outfits: OutfitSet, + + // State values + // TODO: unified ship stats struct, like space + pub hull: f32, + pub shields: f32, + + // Utility values + /// The last time this ship was damaged + pub last_hit: Instant, +} + +impl Ship { + pub fn new( + ct: &content::Content, + ct_handle: content::ShipHandle, + faction: content::FactionHandle, + outfits: OutfitSet, + ) -> Self { + let s = ct.get_ship(ct_handle); + let shields = outfits.get_shield_strength(); + Ship { + ct_handle, + faction, + outfits, + last_hit: Instant::now(), + + // Initial stats + hull: s.hull, + shields, + } + } + + /// If this ship is dead, it will be removed from the game. + pub fn is_dead(&self) -> bool { + self.hull <= 0.0 + } + + /// Hit this ship with the given amount of damage + pub fn apply_damage(&mut self, mut d: f32) { + if self.is_dead() { + 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(); + } + + /// Update this ship's state by `t` seconds + pub fn step(&mut self, t: f32) { + 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; + } + } + } + } + } +} diff --git a/crates/gameobject/src/system.rs b/crates/gameobject/src/system.rs index 10ffebb..08348b2 100644 --- a/crates/gameobject/src/system.rs +++ b/crates/gameobject/src/system.rs @@ -1,6 +1,8 @@ -use crate::SystemObject; +use cgmath::{Point3, Rad}; use galactica_content as content; +// TODO: rework + pub struct System { pub name: String, pub bodies: Vec, @@ -25,8 +27,11 @@ impl System { return s; } - - //pub fn get_sprites(&self) -> Vec { - // return self.bodies.iter().map(|x| x.get_sprite()).collect(); - //} +} + +pub struct SystemObject { + pub sprite: content::SpriteHandle, + pub pos: Point3, + pub size: f32, + pub angle: Rad, } diff --git a/crates/gameobject/src/systemobject.rs b/crates/gameobject/src/systemobject.rs deleted file mode 100644 index 5415f57..0000000 --- a/crates/gameobject/src/systemobject.rs +++ /dev/null @@ -1,10 +0,0 @@ -use cgmath::{Point3, Rad}; - -use galactica_content as content; - -pub struct SystemObject { - pub sprite: content::SpriteHandle, - pub pos: Point3, - pub size: f32, - pub angle: Rad, -}