Reworked game data

master
Mark 2024-01-08 22:38:36 -08:00
parent acb3315392
commit 9e0551ae12
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
10 changed files with 354 additions and 357 deletions

View File

@ -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<syntax::OutfitSpace> for OutfitSpace {
fn from(value: syntax::OutfitSpace) -> Self {
Self {

View File

@ -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<f32>,
}
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<H: std::hash::Hasher>(&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())

View File

@ -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};

View File

@ -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<content::SpriteHandle>,
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<content::OutfitHandle>,
guns: Vec<ShipGun>,
enginepoints: Vec<content::EnginePoint>,
gunpoints: Vec<content::GunPoint>,
}
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<Item = &mut ShipGun> {
self.guns.iter_mut()
}
/// Iterate over all guns and the gunpoints they're attached to
pub fn iter_guns_points(&mut self) -> impl Iterator<Item = (&mut ShipGun, &content::GunPoint)> {
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<Item = &EnginePoint> + '_ {
self.enginepoints.iter()
}
pub fn get_flare_sprite(&self) -> Option<SpriteHandle> {
self.stats.engine_flare_sprites.iter().next().map(|x| *x)
}
}

View File

@ -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;
}
}
}
}
}
}

View File

@ -0,0 +1,5 @@
mod outfitset;
mod ship;
pub use outfitset::*;
pub use ship::*;

View File

@ -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<OutfitHandle, u32>,
/// 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<GunPoint, Option<OutfitHandle>>,
// 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<ShieldGenerator>,
}
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<Item = (&OutfitHandle, &u32)> {
self.outfits.iter()
}
/// Iterate over all gun points
pub fn iter_gun_points(&self) -> impl Iterator<Item = (&GunPoint, &Option<OutfitHandle>)> {
self.gun_points.iter()
}
/// Iterate over all shield generators
pub(crate) fn iter_shield_generators(&self) -> impl Iterator<Item = &ShieldGenerator> {
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
}
}

View File

@ -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;
}
}
}
}
}
}

View File

@ -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<SystemObject>,
@ -25,8 +27,11 @@ impl System {
return s;
}
//pub fn get_sprites(&self) -> Vec<ObjectSprite> {
// return self.bodies.iter().map(|x| x.get_sprite()).collect();
//}
}
pub struct SystemObject {
pub sprite: content::SpriteHandle,
pub pos: Point3<f32>,
pub size: f32,
pub angle: Rad<f32>,
}

View File

@ -1,10 +0,0 @@
use cgmath::{Point3, Rad};
use galactica_content as content;
pub struct SystemObject {
pub sprite: content::SpriteHandle,
pub pos: Point3<f32>,
pub size: f32,
pub angle: Rad<f32>,
}