Compare commits

..

2 Commits

Author SHA1 Message Date
Mark 07ec685c13
Reworked guns and projectile spawning 2024-01-09 17:23:54 -08:00
Mark 0095a23dd6
Merged guns and outfits 2024-01-09 17:22:52 -08:00
13 changed files with 447 additions and 392 deletions

View File

@ -1,37 +0,0 @@
[gun."blaster"]
space.weapon = 10
# Average delay between shots
rate = 0.2
# Random rate variation (each cooldown is +- this)
rate_rng = 0.1
# TODO: apply force to ship on fire
projectile.sprite = "projectile::blaster"
# Height of projectile in game units
projectile.size = 6
projectile.size_rng = 0.0
# Speed of projectile, in game units/second
projectile.speed = 300
projectile.speed_rng = 10.0
# Lifetime of projectile, in seconds
projectile.lifetime = 2.0
projectile.lifetime_rng = 0.2
projectile.damage = 10.0
# Random variation of firing angle.
# If this is 0, the projectile always goes straight.
# If this is 10, projectiles will form a cone of 10 degrees
# ( 5 degrees on each side )
projectile.angle_rng = 2
# How much force this projectile will apply to an object it hits
projectile.force = 0.0
projectile.collider.ball.radius = 2.0
projectile.impact_effect = "blaster impact"
projectile.expire_effect.sprite = "particle::blaster"
projectile.expire_effect.lifetime = "inherit"
projectile.expire_effect.size = 3.0
projectile.expire_effect.velocity_scale_parent = 1.0

View File

@ -14,3 +14,42 @@ space.outfit = 5
shield.generation = 10 shield.generation = 10
shield.strength = 500 shield.strength = 500
shield.delay = 2.0 shield.delay = 2.0
[outfit."blaster"]
space.weapon = 10
# Average delay between shots
gun.rate = 0.2
# Random rate variation (each cooldown is +- this)
gun.rate_rng = 0.1
# TODO: apply force to ship on fire
gun.projectile.sprite = "projectile::blaster"
# Height of projectile in game units
gun.projectile.size = 6
gun.projectile.size_rng = 0.0
# Speed of projectile, in game units/second
gun.projectile.speed = 300
gun.projectile.speed_rng = 10.0
# Lifetime of projectile, in seconds
gun.projectile.lifetime = 2.0
gun.projectile.lifetime_rng = 0.2
gun.projectile.damage = 10.0
# Random variation of firing angle.
# If this is 0, the projectile always goes straight.
# If this is 10, projectiles will form a cone of 10 degrees
# ( 5 degrees on each side )
gun.projectile.angle_rng = 2
# How much force this projectile will apply to an object it hits
gun.projectile.force = 0.0
gun.projectile.collider.ball.radius = 2.0
gun.projectile.impact_effect = "blaster impact"
gun.projectile.expire_effect.sprite = "particle::blaster"
gun.projectile.expire_effect.lifetime = "inherit"
gun.projectile.expire_effect.size = 3.0
gun.projectile.expire_effect.velocity_scale_parent = 1.0

View File

@ -28,11 +28,10 @@ mod syntax {
use serde::Deserialize; use serde::Deserialize;
use std::{collections::HashMap, fmt::Display, hash::Hash}; use std::{collections::HashMap, fmt::Display, hash::Hash};
use crate::part::{effect, faction, gun, outfit, ship, sprite, system}; use crate::part::{effect, faction, outfit, ship, sprite, system};
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Root { pub struct Root {
pub gun: Option<HashMap<String, gun::syntax::Gun>>,
pub ship: Option<HashMap<String, ship::syntax::Ship>>, pub ship: Option<HashMap<String, ship::syntax::Ship>>,
pub system: Option<HashMap<String, system::syntax::System>>, pub system: Option<HashMap<String, system::syntax::System>>,
pub outfit: Option<HashMap<String, outfit::syntax::Outfit>>, pub outfit: Option<HashMap<String, outfit::syntax::Outfit>>,
@ -69,7 +68,6 @@ mod syntax {
impl Root { impl Root {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
gun: None,
ship: None, ship: None,
system: None, system: None,
outfit: None, outfit: None,
@ -80,7 +78,6 @@ mod syntax {
} }
pub fn merge(&mut self, other: Root) -> Result<()> { pub fn merge(&mut self, other: Root) -> Result<()> {
merge_hashmap(&mut self.gun, other.gun).with_context(|| "while merging guns")?;
merge_hashmap(&mut self.ship, other.ship).with_context(|| "while merging ships")?; merge_hashmap(&mut self.ship, other.ship).with_context(|| "while merging ships")?;
merge_hashmap(&mut self.system, other.system) merge_hashmap(&mut self.system, other.system)
.with_context(|| "while merging systems")?; .with_context(|| "while merging systems")?;
@ -241,9 +238,6 @@ impl Content {
if root.ship.is_some() { if root.ship.is_some() {
part::ship::Ship::build(root.ship.take().unwrap(), &mut build_context, &mut content)?; part::ship::Ship::build(root.ship.take().unwrap(), &mut build_context, &mut content)?;
} }
if root.gun.is_some() {
part::gun::Gun::build(root.gun.take().unwrap(), &mut build_context, &mut content)?;
}
if root.outfit.is_some() { if root.outfit.is_some() {
part::outfit::Outfit::build( part::outfit::Outfit::build(
root.outfit.take().unwrap(), root.outfit.take().unwrap(),

View File

@ -1,186 +0,0 @@
use anyhow::{bail, Context, Result};
use cgmath::{Deg, Rad};
use serde::Deserialize;
use std::collections::HashMap;
use crate::{handle::SpriteHandle, Content};
use crate::{ContentBuildContext, EffectHandle, OutfitSpace};
pub(crate) mod syntax {
use crate::part::effect;
use crate::part::outfitspace;
use serde::Deserialize;
// Raw serde syntax structs.
// These are never seen by code outside this crate.
#[derive(Debug, Deserialize)]
pub struct Gun {
pub projectile: Projectile,
pub rate: f32,
pub rate_rng: f32,
pub space: outfitspace::syntax::OutfitSpace,
}
#[derive(Debug, Deserialize)]
pub struct Projectile {
pub sprite: String,
pub size: f32,
pub size_rng: f32,
pub speed: f32,
pub speed_rng: f32,
pub lifetime: f32,
pub lifetime_rng: f32,
pub damage: f32,
pub angle_rng: f32,
pub impact_effect: Option<effect::syntax::EffectReference>,
pub expire_effect: Option<effect::syntax::EffectReference>,
pub collider: super::ProjectileCollider,
pub force: f32,
}
}
/// Defines a projectile's collider
#[derive(Debug, Deserialize, Clone)]
pub enum ProjectileCollider {
/// A ball collider
#[serde(rename = "ball")]
Ball(BallCollider),
}
/// A simple ball-shaped collider, centered at the object's position
#[derive(Debug, Deserialize, Clone)]
pub struct BallCollider {
/// The radius of this ball
pub radius: f32,
}
/// Represents a gun outfit.
#[derive(Debug, Clone)]
pub struct Gun {
/// The name of this gun
pub name: String,
/// The projectile this gun produces
pub projectile: Projectile,
/// Average delay between projectiles, in seconds.
pub rate: f32,
/// Random variation of projectile delay, in seconds.
/// Each shot waits (rate += rate_rng).
pub rate_rng: f32,
/// How much space this gun uses
pub space: OutfitSpace,
}
/// Represents a projectile that a [`Gun`] produces.
#[derive(Debug, Clone)]
pub struct Projectile {
/// The projectile sprite
pub sprite: SpriteHandle,
/// The average size of this projectile
/// (height in game units)
pub size: f32,
/// Random size variation
pub size_rng: f32,
/// The speed of this projectile, in game units / second
pub speed: f32,
/// Random speed variation
pub speed_rng: f32,
/// The lifespan of this projectile.
/// It will vanish if it lives this long without hitting anything.
pub lifetime: f32,
/// Random lifetime variation
pub lifetime_rng: f32,
/// The damage this projectile does
pub damage: f32,
/// The force this projectile applies
pub force: f32,
/// The angle variation of this projectile.
/// Projectiles can be off center up to
/// `spread / 2` degrees in both directions.
///
/// (Forming a "fire cone" of `spread` degrees)
pub angle_rng: Rad<f32>,
/// The particle this projectile will spawn when it hits something
pub impact_effect: Option<EffectHandle>,
/// The particle this projectile will spawn when it expires
pub expire_effect: Option<EffectHandle>,
/// Collider parameters for this projectile
pub collider: ProjectileCollider,
}
impl crate::Build for Gun {
type InputSyntaxType = HashMap<String, syntax::Gun>;
fn build(
gun: Self::InputSyntaxType,
build_context: &mut ContentBuildContext,
content: &mut Content,
) -> Result<()> {
for (gun_name, gun) in gun {
let projectile_sprite_handle = match content.sprite_index.get(&gun.projectile.sprite) {
None => bail!(
"projectile sprite `{}` doesn't exist in gun `{}`",
gun.projectile.sprite,
gun_name,
),
Some(t) => *t,
};
let impact_effect = match gun.projectile.impact_effect {
Some(e) => Some(
e.to_handle(build_context, content)
.with_context(|| format!("while loading gun `{}`", gun_name))?,
),
None => None,
};
let expire_effect = match gun.projectile.expire_effect {
Some(e) => Some(
e.to_handle(build_context, content)
.with_context(|| format!("while loading gun `{}`", gun_name))?,
),
None => None,
};
content.guns.push(Self {
name: gun_name,
space: gun.space.into(),
rate: gun.rate,
rate_rng: gun.rate_rng,
projectile: Projectile {
force: gun.projectile.force,
sprite: projectile_sprite_handle,
size: gun.projectile.size,
size_rng: gun.projectile.size_rng,
speed: gun.projectile.speed,
speed_rng: gun.projectile.speed_rng,
lifetime: gun.projectile.lifetime,
lifetime_rng: gun.projectile.lifetime_rng,
damage: gun.projectile.damage,
// Divide by 2, so the angle matches the angle of the fire cone.
// This should ALWAYS be done in the content parser.
angle_rng: Deg(gun.projectile.angle_rng / 2.0).into(),
impact_effect,
expire_effect,
collider: gun.projectile.collider,
},
});
}
return Ok(());
}
}

View File

@ -2,7 +2,6 @@
pub(crate) mod effect; pub(crate) mod effect;
pub(crate) mod faction; pub(crate) mod faction;
pub(crate) mod gun;
pub(crate) mod outfit; pub(crate) mod outfit;
pub(crate) mod outfitspace; pub(crate) mod outfitspace;
pub(crate) mod ship; pub(crate) mod ship;
@ -11,8 +10,7 @@ pub(crate) mod system;
pub use effect::Effect; pub use effect::Effect;
pub use faction::{Faction, Relationship}; pub use faction::{Faction, Relationship};
pub use gun::{Gun, Projectile, ProjectileCollider}; pub use outfit::{Gun, Outfit, Projectile, ProjectileCollider};
pub use outfit::Outfit;
pub use outfitspace::OutfitSpace; pub use outfitspace::OutfitSpace;
pub use ship::{ pub use ship::{
CollapseEffectSpawner, CollapseEvent, EffectCollapseEvent, EnginePoint, GunPoint, Ship, CollapseEffectSpawner, CollapseEvent, EffectCollapseEvent, EnginePoint, GunPoint, Ship,

View File

@ -1,11 +1,17 @@
use std::collections::HashMap; use std::collections::HashMap;
use anyhow::{bail, Result}; use anyhow::{bail, Context, Result};
use cgmath::Rad;
use serde::Deserialize;
use crate::{handle::SpriteHandle, Content, ContentBuildContext, OutfitHandle, OutfitSpace}; use crate::{
handle::SpriteHandle, Content, ContentBuildContext, EffectHandle, OutfitHandle, OutfitSpace,
};
pub(crate) mod syntax { pub(crate) mod syntax {
use crate::part::outfitspace; use crate::{effect, part::outfitspace, ContentBuildContext};
use anyhow::{bail, Result};
use cgmath::Deg;
use serde::Deserialize; use serde::Deserialize;
// Raw serde syntax structs. // Raw serde syntax structs.
// These are never seen by code outside this crate. // These are never seen by code outside this crate.
@ -16,6 +22,7 @@ pub(crate) mod syntax {
pub steering: Option<Steering>, pub steering: Option<Steering>,
pub space: outfitspace::syntax::OutfitSpace, pub space: outfitspace::syntax::OutfitSpace,
pub shield: Option<Shield>, pub shield: Option<Shield>,
pub gun: Option<Gun>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -36,6 +43,79 @@ pub(crate) mod syntax {
pub struct Steering { pub struct Steering {
pub power: f32, pub power: f32,
} }
#[derive(Debug, Deserialize)]
pub struct Gun {
pub projectile: Projectile,
pub rate: f32,
pub rate_rng: Option<f32>,
}
impl Gun {
pub fn build(
self,
build_context: &mut ContentBuildContext,
content: &mut crate::Content,
) -> Result<super::Gun> {
let projectile_sprite_handle = match content.sprite_index.get(&self.projectile.sprite) {
None => bail!(
"projectile sprite `{}` doesn't exist",
self.projectile.sprite,
),
Some(t) => *t,
};
let impact_effect = match self.projectile.impact_effect {
Some(e) => Some(e.to_handle(build_context, content)?),
None => None,
};
let expire_effect = match self.projectile.expire_effect {
Some(e) => Some(e.to_handle(build_context, content)?),
None => None,
};
return Ok(super::Gun {
rate: self.rate,
rate_rng: self.rate_rng.unwrap_or(0.0),
projectile: super::Projectile {
force: self.projectile.force,
sprite: projectile_sprite_handle,
size: self.projectile.size,
size_rng: self.projectile.size_rng,
speed: self.projectile.speed,
speed_rng: self.projectile.speed_rng,
lifetime: self.projectile.lifetime,
lifetime_rng: self.projectile.lifetime_rng,
damage: self.projectile.damage,
// Divide by 2, so the angle matches the angle of the fire cone.
// This should ALWAYS be done in the content parser.
angle_rng: Deg(self.projectile.angle_rng / 2.0).into(),
impact_effect,
expire_effect,
collider: self.projectile.collider,
},
});
}
}
#[derive(Debug, Deserialize)]
pub struct Projectile {
pub sprite: String,
pub size: f32,
pub size_rng: f32,
pub speed: f32,
pub speed_rng: f32,
pub lifetime: f32,
pub lifetime_rng: f32,
pub damage: f32,
pub angle_rng: f32,
pub impact_effect: Option<effect::syntax::EffectReference>,
pub expire_effect: Option<effect::syntax::EffectReference>,
pub collider: super::ProjectileCollider,
pub force: f32,
}
} }
/// Represents an outfit that may be attached to a ship. /// Represents an outfit that may be attached to a ship.
@ -69,6 +149,82 @@ pub struct Outfit {
/// Wait this many seconds after taking damage before regenerating shields /// Wait this many seconds after taking damage before regenerating shields
pub shield_delay: f32, pub shield_delay: f32,
/// This outfit's gun stats.
/// If this is some, this outfit requires a gun point.
pub gun: Option<Gun>,
}
/// Defines a projectile's collider
#[derive(Debug, Deserialize, Clone)]
pub enum ProjectileCollider {
/// A ball collider
#[serde(rename = "ball")]
Ball(BallCollider),
}
/// A simple ball-shaped collider, centered at the object's position
#[derive(Debug, Deserialize, Clone)]
pub struct BallCollider {
/// The radius of this ball
pub radius: f32,
}
/// Represents gun stats of an outfit.
/// If an outfit has this value, it requires a gun point.
#[derive(Debug, Clone)]
pub struct Gun {
/// The projectile this gun produces
pub projectile: Projectile,
/// Average delay between projectiles, in seconds.
pub rate: f32,
/// Random variation of projectile delay, in seconds.
/// Each shot waits (rate += rate_rng).
pub rate_rng: f32,
}
/// Represents a projectile that a [`Gun`] produces.
#[derive(Debug, Clone)]
pub struct Projectile {
/// The projectile sprite
pub sprite: SpriteHandle,
/// The average size of this projectile
/// (height in game units)
pub size: f32,
/// Random size variation
pub size_rng: f32,
/// The speed of this projectile, in game units / second
pub speed: f32,
/// Random speed variation
pub speed_rng: f32,
/// The lifespan of this projectile.
/// It will vanish if it lives this long without hitting anything.
pub lifetime: f32,
/// Random lifetime variation
pub lifetime_rng: f32,
/// The damage this projectile does
pub damage: f32,
/// The force this projectile applies
pub force: f32,
/// The angle variation of this projectile.
pub angle_rng: Rad<f32>,
/// The particle this projectile will spawn when it hits something
pub impact_effect: Option<EffectHandle>,
/// The particle this projectile will spawn when it expires
pub expire_effect: Option<EffectHandle>,
/// Collider parameters for this projectile
pub collider: ProjectileCollider,
} }
impl crate::Build for Outfit { impl crate::Build for Outfit {
@ -76,7 +232,7 @@ impl crate::Build for Outfit {
fn build( fn build(
outfits: Self::InputSyntaxType, outfits: Self::InputSyntaxType,
_build_context: &mut ContentBuildContext, build_context: &mut ContentBuildContext,
content: &mut Content, content: &mut Content,
) -> Result<()> { ) -> Result<()> {
for (outfit_name, outfit) in outfits { for (outfit_name, outfit) in outfits {
@ -84,7 +240,16 @@ impl crate::Build for Outfit {
index: content.outfits.len(), index: content.outfits.len(),
}; };
let gun = match outfit.gun {
None => None,
Some(g) => Some(
g.build(build_context, content)
.with_context(|| format!("in outfit {}", outfit_name))?,
),
};
let mut o = Self { let mut o = Self {
gun,
handle, handle,
name: outfit_name.clone(), name: outfit_name.clone(),
engine_thrust: 0.0, engine_thrust: 0.0,

View File

@ -1,7 +1,4 @@
use object::{ use object::{ship::ShipPersonality, GameData, GameShipHandle};
ship::{OutfitSet, ShipPersonality},
GameData, GameShipHandle,
};
use std::time::Instant; use std::time::Instant;
use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode}; use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode};
@ -12,7 +9,7 @@ use galactica_constants;
use galactica_content as content; use galactica_content as content;
use galactica_gameobject as object; use galactica_gameobject as object;
use galactica_render::RenderState; use galactica_render::RenderState;
use galactica_world::{objects::ShipControls, ParticleBuilder, StepResources, World}; use galactica_world::{objects::ShipControls, util, ParticleBuilder, StepResources, World};
pub struct Game { pub struct Game {
input: InputStatus, input: InputStatus,
@ -36,45 +33,35 @@ impl Game {
pub fn new(ct: content::Content) -> Self { pub fn new(ct: content::Content) -> Self {
let mut gamedata = GameData::new(&ct); let mut gamedata = GameData::new(&ct);
let ss = ct.get_ship(content::ShipHandle { index: 0 });
let mut o1 = OutfitSet::new(ss.space, &[]);
o1.add(&ct.get_outfit(content::OutfitHandle { index: 0 }));
o1.add(&ct.get_outfit(content::OutfitHandle { index: 1 }));
//o1.add_gun(&ct, content::GunHandle { index: 0 });
//o1.add_gun(&ct, content::GunHandle { index: 0 });
//o1.add_gun(&ct, content::GunHandle { index: 0 });
let player = gamedata.create_ship( let player = gamedata.create_ship(
&ct, &ct,
content::ShipHandle { index: 0 }, content::ShipHandle { index: 0 },
content::FactionHandle { index: 0 }, content::FactionHandle { index: 0 },
ShipPersonality::Player, ShipPersonality::Player,
o1.clone(),
&content::SystemHandle { index: 0 }, &content::SystemHandle { index: 0 },
); );
let s = gamedata.get_ship_mut(player).unwrap();
s.add_outfit(&ct.get_outfit(content::OutfitHandle { index: 0 }));
s.add_outfit(&ct.get_outfit(content::OutfitHandle { index: 1 }));
s.add_outfit(&ct.get_outfit(content::OutfitHandle { index: 2 }));
gamedata.create_ship( gamedata.create_ship(
&ct, &ct,
content::ShipHandle { index: 0 }, content::ShipHandle { index: 0 },
content::FactionHandle { index: 1 }, content::FactionHandle { index: 1 },
ShipPersonality::Dummy, ShipPersonality::Dummy,
OutfitSet::new(ss.space, &[]),
&content::SystemHandle { index: 0 }, &content::SystemHandle { index: 0 },
); );
let mut o1 = OutfitSet::new(ss.space, &[]); let a = gamedata.create_ship(
o1.add(&ct.get_outfit(content::OutfitHandle { index: 0 }));
//o1.add_gun(&ct, content::GunHandle { index: 0 });
gamedata.create_ship(
&ct, &ct,
content::ShipHandle { index: 0 }, content::ShipHandle { index: 0 },
content::FactionHandle { index: 1 }, content::FactionHandle { index: 1 },
ShipPersonality::Point, ShipPersonality::Point,
o1,
&content::SystemHandle { index: 0 }, &content::SystemHandle { index: 0 },
); );
let s = gamedata.get_ship_mut(a).unwrap();
s.add_outfit(&ct.get_outfit(content::OutfitHandle { index: 0 }));
let physics = World::new(&ct, &gamedata, content::SystemHandle { index: 0 }); let physics = World::new(&ct, &gamedata, content::SystemHandle { index: 0 });
@ -127,7 +114,9 @@ impl Game {
pub fn update(&mut self) { pub fn update(&mut self) {
let t: f32 = self.last_update.elapsed().as_secs_f32() * self.time_scale; let t: f32 = self.last_update.elapsed().as_secs_f32() * self.time_scale;
let world_output = self.world.step(StepResources { self.gamedata.step(t);
self.world.step(StepResources {
player: self.player, player: self.player,
player_controls: ShipControls { player_controls: ShipControls {
left: self.input.key_left, left: self.input.key_left,
@ -147,7 +136,12 @@ impl Game {
self.input.v_scroll = 0.0; self.input.v_scroll = 0.0;
} }
self.camera.pos = world_output.player_position; self.camera.pos = {
let o = self.world.get_ship(self.player).unwrap();
let r = self.world.get_rigid_body(o.rigid_body).unwrap();
util::rigidbody_position(r)
};
self.last_update = Instant::now(); self.last_update = Instant::now();
} }

View File

@ -1,10 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::{ use crate::{handles::GameShipHandle, ship::Ship, ship::ShipPersonality};
handles::GameShipHandle,
ship::Ship,
ship::{OutfitSet, ShipPersonality},
};
use galactica_content as content; use galactica_content as content;
/// Keeps track of all objects in the galaxy. /// Keeps track of all objects in the galaxy.
@ -44,7 +40,6 @@ impl GameData {
ship: content::ShipHandle, ship: content::ShipHandle,
faction: content::FactionHandle, faction: content::FactionHandle,
personality: ShipPersonality, personality: ShipPersonality,
outfits: OutfitSet,
system: &content::SystemHandle, system: &content::SystemHandle,
) -> GameShipHandle { ) -> GameShipHandle {
let handle = GameShipHandle { let handle = GameShipHandle {
@ -53,15 +48,19 @@ impl GameData {
}; };
self.index += 1; self.index += 1;
self.ships.insert( self.ships
handle, .insert(handle, Ship::new(ct, handle, ship, faction, personality));
Ship::new(ct, handle, ship, faction, personality, outfits),
);
self.system_ship_table.get_mut(system).unwrap().push(handle); self.system_ship_table.get_mut(system).unwrap().push(handle);
self.ship_system_table.insert(handle, *system); self.ship_system_table.insert(handle, *system);
return handle; return handle;
} }
pub fn step(&mut self, t: f32) {
for (_, s) in &mut self.ships {
s.step(t);
}
}
} }
// Public getters // Public getters

View File

@ -14,6 +14,10 @@ pub enum OutfitAddResult {
/// outfits may need outfit AND weapon space. In these cases, this result /// outfits may need outfit AND weapon space. In these cases, this result
/// should name the "most specific" kind of space we lack. /// should name the "most specific" kind of space we lack.
NotEnoughSpace(String), NotEnoughSpace(String),
/// An outfit couldn't be added because there weren't enough points for it
/// (e.g, gun points, turret points, etc)
NotEnoughPoints(String),
} }
/// Possible outcomes when removing an outfit /// Possible outcomes when removing an outfit
@ -37,6 +41,9 @@ pub(crate) struct ShieldGenerator {
} }
/// This struct keeps track of a ship's outfit loadout. /// This struct keeps track of a ship's outfit loadout.
/// This is a fairly static data structure: it does not keep track of cooldowns,
/// shield damage, etc. It only provides an interface for static stats which are
/// then used elsewhere.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct OutfitSet { pub struct OutfitSet {
/// What outfits does this statsum contain? /// What outfits does this statsum contain?
@ -68,7 +75,7 @@ pub struct OutfitSet {
} }
impl OutfitSet { impl OutfitSet {
pub fn new(available_space: OutfitSpace, gun_points: &[GunPoint]) -> Self { pub(super) fn new(available_space: OutfitSpace, gun_points: &[GunPoint]) -> Self {
Self { Self {
outfits: HashMap::new(), outfits: HashMap::new(),
total_space: available_space, total_space: available_space,
@ -82,11 +89,26 @@ impl OutfitSet {
} }
} }
pub fn add(&mut self, o: &content::Outfit) -> OutfitAddResult { pub(super) fn add(&mut self, o: &content::Outfit) -> OutfitAddResult {
if !(self.total_space - self.used_space).can_contain(&o.space) { if !(self.total_space - self.used_space).can_contain(&o.space) {
return OutfitAddResult::NotEnoughSpace("TODO".to_string()); return OutfitAddResult::NotEnoughSpace("TODO".to_string());
} }
// Check and return as fast as possible,
// BEFORE we make any changes.
if o.gun.is_some() {
let mut added = false;
for (_, outfit) in &mut self.gun_points {
if outfit.is_none() {
*outfit = Some(o.handle);
added = true;
}
}
if !added {
return OutfitAddResult::NotEnoughPoints("gun".to_string());
}
}
self.used_space += o.space; self.used_space += o.space;
self.engine_thrust += o.engine_thrust; self.engine_thrust += o.engine_thrust;
@ -107,7 +129,7 @@ impl OutfitSet {
return OutfitAddResult::Ok; return OutfitAddResult::Ok;
} }
pub fn remove(&mut self, o: &content::Outfit) -> OutfitRemoveResult { pub(super) fn remove(&mut self, o: &content::Outfit) -> OutfitRemoveResult {
if !self.outfits.contains_key(&o.handle) { if !self.outfits.contains_key(&o.handle) {
return OutfitRemoveResult::NotExist; return OutfitRemoveResult::NotExist;
} else { } else {
@ -177,6 +199,12 @@ impl OutfitSet {
self.shield_generators.iter().map(|x| x.generation).sum() self.shield_generators.iter().map(|x| x.generation).sum()
} }
/// Get the outfit attached to the given gun point
/// Will panic if this gunpoint is not in this outfit set.
pub fn get_gun(&self, point: &GunPoint) -> Option<OutfitHandle> {
self.gun_points.get(point).unwrap().clone()
}
/// Total available outfit space /// Total available outfit space
pub fn get_total_space(&self) -> &OutfitSpace { pub fn get_total_space(&self) -> &OutfitSpace {
&self.total_space &self.total_space

View File

@ -1,9 +1,11 @@
use std::time::Instant; use std::{collections::HashMap, time::Instant};
use crate::GameShipHandle; use crate::GameShipHandle;
use super::{OutfitSet, ShipPersonality}; use super::{OutfitSet, ShipPersonality};
use content::GunPoint;
use galactica_content as content; use galactica_content as content;
use rand::{rngs::ThreadRng, Rng};
#[derive(Debug)] #[derive(Debug)]
pub struct Ship { pub struct Ship {
@ -19,6 +21,8 @@ pub struct Ship {
// TODO: unified ship stats struct, like outfit space // TODO: unified ship stats struct, like outfit space
hull: f32, hull: f32,
shields: f32, shields: f32,
gun_cooldowns: HashMap<GunPoint, f32>,
rng: ThreadRng,
// Utility values // Utility values
/// The last time this ship was damaged /// The last time this ship was damaged
@ -32,29 +36,61 @@ impl Ship {
ct_handle: content::ShipHandle, ct_handle: content::ShipHandle,
faction: content::FactionHandle, faction: content::FactionHandle,
personality: ShipPersonality, personality: ShipPersonality,
outfits: OutfitSet,
) -> Self { ) -> Self {
let s = ct.get_ship(ct_handle); let s = ct.get_ship(ct_handle);
let shields = outfits.get_shield_strength();
Ship { Ship {
handle, handle,
ct_handle, ct_handle,
faction, faction,
outfits, outfits: OutfitSet::new(s.space, &s.guns),
personality, personality,
last_hit: Instant::now(), last_hit: Instant::now(),
rng: rand::thread_rng(),
// Initial stats // Initial stats
hull: s.hull, hull: s.hull,
shields, shields: 0.0,
gun_cooldowns: s.guns.iter().map(|x| (x.clone(), 0.0)).collect(),
} }
} }
/// Add an outfit to this ship
pub fn add_outfit(&mut self, o: &content::Outfit) -> super::OutfitAddResult {
self.outfits.add(o)
}
/// Remove an outfit from this ship
pub fn remove_outfit(&mut self, o: &content::Outfit) -> super::OutfitRemoveResult {
self.outfits.remove(o)
}
/// If this ship is dead, it will be removed from the game. /// If this ship is dead, it will be removed from the game.
pub fn is_dead(&self) -> bool { pub fn is_dead(&self) -> bool {
self.hull <= 0.0 self.hull <= 0.0
} }
/// 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 fn fire_gun(&mut self, ct: &content::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 /// Hit this ship with the given amount of damage
pub fn apply_damage(&mut self, mut d: f32) { pub fn apply_damage(&mut self, mut d: f32) {
if self.is_dead() { if self.is_dead() {
@ -72,6 +108,14 @@ impl Ship {
/// Update this ship's state by `t` seconds /// Update this ship's state by `t` seconds
pub fn step(&mut self, t: f32) { pub fn step(&mut self, t: f32) {
// 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(); let time_since = self.last_hit.elapsed().as_secs_f32();
if self.shields != self.outfits.get_shield_strength() { if self.shields != self.outfits.get_shield_strength() {
for g in self.outfits.iter_shield_generators() { for g in self.outfits.iter_shield_generators() {

View File

@ -298,10 +298,6 @@ impl ShipWorldObject {
rigid_body rigid_body
.apply_torque_impulse(ship.get_outfits().get_steer_power() * 100.0 * res.t, true); .apply_torque_impulse(ship.get_outfits().get_steer_power() * 100.0 * res.t, true);
} }
//for i in self.ship.outfits.iter_guns() {
// i.cooldown -= t;
//}
} }
} }

View File

@ -1,5 +1,4 @@
use crate::{objects::ShipControls, ParticleBuilder}; use crate::{objects::ShipControls, ParticleBuilder};
use cgmath::Point2;
use galactica_content::Content; use galactica_content::Content;
use galactica_gameobject::{GameData, GameShipHandle}; use galactica_gameobject::{GameData, GameShipHandle};
@ -24,10 +23,3 @@ pub struct StepResources<'a> {
/// The ship that the player controls /// The ship that the player controls
pub player: GameShipHandle, pub player: GameShipHandle,
} }
/// Return values after computing time steps
#[derive(Debug)]
pub struct StepOutput {
/// The player's position in world coordinates
pub player_position: Point2<f32>,
}

View File

@ -1,4 +1,5 @@
use cgmath::{Deg, EuclideanSpace, InnerSpace, Matrix2, Point2, Rad, Vector2, Zero}; use cgmath::{Deg, EuclideanSpace, InnerSpace, Matrix2, Point2, Rad, Vector2, Zero};
use content::{GunPoint, OutfitHandle};
use crossbeam::channel::Receiver; use crossbeam::channel::Receiver;
use nalgebra::{point, vector}; use nalgebra::{point, vector};
use object::{ship::Ship, GameData, GameShipHandle}; use object::{ship::Ship, GameData, GameShipHandle};
@ -15,7 +16,7 @@ use crate::{
objects::{ProjectileWorldObject, ShipWorldObject}, objects::{ProjectileWorldObject, ShipWorldObject},
util, util,
wrapper::Wrapper, wrapper::Wrapper,
ParticleBuilder, StepOutput, StepResources, ParticleBuilder, StepResources,
}; };
use galactica_content as content; use galactica_content as content;
use galactica_gameobject as object; use galactica_gameobject as object;
@ -23,7 +24,7 @@ use galactica_gameobject as object;
/// Manages the physics state of one system /// Manages the physics state of one system
pub struct World { pub struct World {
/// The system this world is attached to /// The system this world is attached to
system: content::SystemHandle, _system: content::SystemHandle,
wrapper: Wrapper, wrapper: Wrapper,
projectiles: HashMap<ColliderHandle, objects::ProjectileWorldObject>, projectiles: HashMap<ColliderHandle, objects::ProjectileWorldObject>,
@ -73,67 +74,6 @@ impl<'a> World {
self.ships.remove(&h); self.ships.remove(&h);
} }
/*
/// Add a projectile fired from a ship
fn add_projectiles(
&mut self,
s: ShipPhysicsHandle,
p: Vec<(ProjectileWorldObject, content::GunPoint)>,
) {
let mut rng = rand::thread_rng();
for (projectile, point) in p {
let r = self.get_ship_body(s).unwrap().1;
let ship_pos = util::rigidbody_position(r);
let ship_rot = util::rigidbody_rotation(r);
let ship_vel = util::rigidbody_velocity(r);
let ship_ang = ship_rot.angle(Vector2 { x: 0.0, y: 1.0 });
let pos = ship_pos + (Matrix2::from_angle(-ship_ang) * point.pos.to_vec());
let spread: Rad<f32> = Deg(
rng.gen_range(-projectile.content.angle_rng.0..=projectile.content.angle_rng.0)
)
.into();
let vel = ship_vel
+ (Matrix2::from_angle(-ship_ang + spread)
* Vector2 {
x: 0.0,
y: projectile.content.speed
+ rng.gen_range(
-projectile.content.speed_rng..=projectile.content.speed_rng,
),
});
let rigid_body = RigidBodyBuilder::kinematic_velocity_based()
.translation(vector![pos.x, pos.y])
.rotation(-ship_ang.0)
.linvel(vector![vel.x, vel.y])
.build();
let collider = match &projectile.content.collider {
content::ProjectileCollider::Ball(b) => ColliderBuilder::ball(b.radius)
.sensor(true)
.active_events(ActiveEvents::COLLISION_EVENTS)
.build(),
};
let rigid_body = self.wrapper.rigid_body_set.insert(rigid_body);
let collider = self.wrapper.collider_set.insert_with_parent(
collider,
rigid_body,
&mut self.wrapper.rigid_body_set,
);
self.projectiles.insert(
collider.clone(),
ProjectileWorldObject::new(projectile, rigid_body, collider),
);
}
}
*/
fn collide_projectile_ship( fn collide_projectile_ship(
&mut self, &mut self,
res: &mut StepResources, res: &mut StepResources,
@ -152,10 +92,17 @@ impl<'a> World {
let ship_d = res.dt.get_ship_mut(ship.data_handle).unwrap(); let ship_d = res.dt.get_ship_mut(ship.data_handle).unwrap();
// TODO: check faction let f = res.ct.get_faction(projectile.faction);
ship_d.apply_damage(projectile.content.damage); let r = f.relationships.get(&ship_d.get_faction()).unwrap();
let destory_projectile = match r {
content::Relationship::Hostile => {
ship_d.apply_damage(projectile.content.damage);
true
}
_ => false,
};
if true { if destory_projectile {
let pr = self let pr = self
.wrapper .wrapper
.rigid_body_set .rigid_body_set
@ -210,14 +157,14 @@ impl<'a> World {
} }
// Public methods // Public methods
impl<'a> World { impl World {
/// Create a new physics system /// Create a new physics system
pub fn new(ct: &content::Content, dt: &GameData, system: content::SystemHandle) -> Self { pub fn new(ct: &content::Content, dt: &GameData, system: content::SystemHandle) -> Self {
let (collision_send, collision_queue) = crossbeam::channel::unbounded(); let (collision_send, collision_queue) = crossbeam::channel::unbounded();
let (contact_force_send, _) = crossbeam::channel::unbounded(); let (contact_force_send, _) = crossbeam::channel::unbounded();
let mut w = Self { let mut w = Self {
system, _system: system,
wrapper: Wrapper::new(), wrapper: Wrapper::new(),
projectiles: HashMap::new(), projectiles: HashMap::new(),
ships: HashMap::new(), ships: HashMap::new(),
@ -283,45 +230,125 @@ impl<'a> World {
); );
} }
/// Step this physics system by `t` seconds /// Run ship updates:
pub fn step(&mut self, mut res: StepResources) -> StepOutput { /// - updates ship controls (runs behaviors)
let mut output = StepOutput { /// - applies ship controls
player_position: Point2 { x: 0.0, y: 0.0 }, /// - creates projectiles
}; fn step_ships(&mut self, res: &mut StepResources) {
let mut projectiles = Vec::new();
// Run ship updates
// TODO: maybe reorganize projectile creation?
//let mut projectiles = Vec::new();
let mut to_remove = Vec::new(); let mut to_remove = Vec::new();
for (_, s) in &mut self.ships { for (_, ship_object) in &mut self.ships {
let r = &mut self.wrapper.rigid_body_set[s.rigid_body];
let c = &mut self.wrapper.collider_set[s.collider];
if s.data_handle == res.player {
s.controls = res.player_controls.clone();
output.player_position = util::rigidbody_position(r);
} else {
s.update_controls(&res);
}
// TODO: unified step info struct
s.step(&mut res, r, c);
//if s.controls.guns {
// projectiles.push((s.physics_handle, s.ship.fire_guns()));
//}
//if s.remove_from_world() { //if s.remove_from_world() {
// to_remove.push(s.physics_handle); // to_remove.push(s.physics_handle);
// continue; // continue;
//} //}
let rigid_body = &mut self.wrapper.rigid_body_set[ship_object.rigid_body];
let collider = &mut self.wrapper.collider_set[ship_object.collider];
if ship_object.data_handle == res.player {
ship_object.controls = res.player_controls.clone();
} else {
ship_object.update_controls(&res);
}
// TODO: unified step info struct
ship_object.step(res, rigid_body, collider);
if ship_object.controls.guns {
let ship_data = res.dt.get_ship_mut(ship_object.data_handle).unwrap();
// TODO: don't allocate here. This is a hack to satisfy the borrow checker,
// convert this to a refcell or do the replace dance.
let pairs: Vec<(GunPoint, Option<OutfitHandle>)> = ship_data
.get_outfits()
.iter_gun_points()
.map(|(p, o)| (p.clone(), o.clone()))
.collect();
for (gun, outfit) in pairs {
if ship_data.fire_gun(res.ct, &gun) {
projectiles.push((
ship_object.data_handle,
ship_object.rigid_body,
gun.clone(),
outfit.unwrap(),
));
}
}
}
} }
//for (s, p) in projectiles {
// self.add_projectiles(s, p); // Remove ships that don't exist
//}
for s in to_remove { for s in to_remove {
self.remove_ship(s); self.remove_ship(s);
} }
// Create projectiles
for (ship_dat, rigid_body, gun_point, outfit) in projectiles {
let mut rng = rand::thread_rng();
let rigid_body = self.get_rigid_body(rigid_body).unwrap();
let ship_dat = res.dt.get_ship(ship_dat).unwrap();
let ship_pos = util::rigidbody_position(rigid_body);
let ship_rot = util::rigidbody_rotation(rigid_body);
let ship_vel = util::rigidbody_velocity(rigid_body);
let ship_ang = ship_rot.angle(Vector2 { x: 0.0, y: 1.0 });
let pos = ship_pos + (Matrix2::from_angle(-ship_ang) * gun_point.pos.to_vec());
let outfit = res.ct.get_outfit(outfit);
let outfit = outfit.gun.as_ref().unwrap();
let spread: Rad<f32> =
Deg(rng.gen_range(-outfit.projectile.angle_rng.0..=outfit.projectile.angle_rng.0))
.into();
let vel = ship_vel
+ (Matrix2::from_angle(-ship_ang + spread)
* Vector2 {
x: 0.0,
y: outfit.projectile.speed
+ rng.gen_range(
-outfit.projectile.speed_rng..=outfit.projectile.speed_rng,
),
});
let rigid_body = RigidBodyBuilder::kinematic_velocity_based()
.translation(vector![pos.x, pos.y])
.rotation(-ship_ang.0)
.linvel(vector![vel.x, vel.y])
.build();
let collider = match &outfit.projectile.collider {
content::ProjectileCollider::Ball(b) => ColliderBuilder::ball(b.radius)
.sensor(true)
.active_events(ActiveEvents::COLLISION_EVENTS)
.build(),
};
let rigid_body = self.wrapper.rigid_body_set.insert(rigid_body);
let collider = self.wrapper.collider_set.insert_with_parent(
collider,
rigid_body,
&mut self.wrapper.rigid_body_set,
);
self.projectiles.insert(
collider.clone(),
ProjectileWorldObject::new(
outfit.projectile.clone(),
rigid_body,
ship_dat.get_faction(),
collider,
),
);
}
}
/// Step this physics system by `t` seconds
pub fn step(&mut self, mut res: StepResources) {
self.step_ships(&mut res);
// Update physics // Update physics
self.wrapper.step(res.t, &self.collision_handler); self.wrapper.step(res.t, &self.collision_handler);
@ -390,10 +417,12 @@ impl<'a> World {
} }
}; };
} }
return output;
} }
}
// Public getters
impl World {
/// Get a ship physics object
pub fn get_ship(&self, ship: GameShipHandle) -> Option<&ShipWorldObject> { pub fn get_ship(&self, ship: GameShipHandle) -> Option<&ShipWorldObject> {
self.ships.get(&ship) self.ships.get(&ship)
} }
@ -418,12 +447,12 @@ impl<'a> World {
} }
/// Iterate over all ships in this physics system /// Iterate over all ships in this physics system
pub fn iter_ships(&'a self) -> impl Iterator<Item = &ShipWorldObject> + '_ { pub fn iter_ships(&self) -> impl Iterator<Item = &ShipWorldObject> + '_ {
self.ships.values() self.ships.values()
} }
/// Iterate over all ships in this physics system /// Iterate over all ships in this physics system
pub fn iter_projectiles(&'a self) -> impl Iterator<Item = &ProjectileWorldObject> + '_ { pub fn iter_projectiles(&self) -> impl Iterator<Item = &ProjectileWorldObject> + '_ {
self.projectiles.values() self.projectiles.values()
} }
} }