Compare commits

...

2 Commits

Author SHA1 Message Date
Mark 3b7ac5bc9a
Added basic landing 2024-01-12 14:34:31 -08:00
Mark c920ebefbc
Comments 2024-01-12 14:34:22 -08:00
19 changed files with 393 additions and 100 deletions

1
Cargo.lock generated
View File

@ -702,6 +702,7 @@ dependencies = [
"cgmath",
"galactica-content",
"galactica-packer",
"galactica-playeragent",
"galactica-system",
"galactica-util",
"glyphon",

View File

@ -1,3 +1,5 @@
# TODO: big objects in one config
[system."12 Autumn Above"]
object.star.sprite = "star::star"

View File

@ -37,6 +37,15 @@ impl PartialEq for SpriteHandle {
}
}
/// A lightweight representation of system body
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SystemObjectHandle {
/// TODO: pub in crate
pub system_handle: SystemHandle,
/// The index of this object in system.objects
pub body_index: usize,
}
/// A lightweight representation of an outfit
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OutfitHandle {

View File

@ -18,9 +18,7 @@ use std::{
use toml;
use walkdir::WalkDir;
pub use handle::{
EffectHandle, FactionHandle, GunHandle, OutfitHandle, ShipHandle, SpriteHandle, SystemHandle,
};
pub use handle::*;
pub use part::*;
mod syntax {
@ -333,6 +331,11 @@ impl Content {
return &self.systems[h.index];
}
/// Get a system object from a handle
pub fn get_system_object(&self, h: SystemObjectHandle) -> &SystemObject {
return &self.get_system(h.system_handle).objects[h.body_index];
}
/// Get a faction from a handle
pub fn get_faction(&self, h: FactionHandle) -> &Faction {
return &self.factions[h.index];

View File

@ -19,4 +19,4 @@ pub use ship::{
ShipCollapse,
};
pub use sprite::{RepeatMode, Sprite};
pub use system::{Object, System};
pub use system::{System, SystemObject};

View File

@ -2,7 +2,10 @@ use anyhow::{bail, Context, Result};
use cgmath::{Deg, Point3, Rad};
use std::collections::{HashMap, HashSet};
use crate::{handle::SpriteHandle, util::Polar, Content, ContentBuildContext};
use crate::{
handle::SpriteHandle, util::Polar, Content, ContentBuildContext, SystemHandle,
SystemObjectHandle,
};
pub(crate) mod syntax {
use serde::Deserialize;
@ -83,8 +86,11 @@ pub struct System {
/// This star system's name
pub name: String,
/// This star system's handle
pub handle: SystemHandle,
/// Objects in this system
pub objects: Vec<Object>,
pub objects: Vec<SystemObject>,
}
/// Represents an orbiting body in a star system
@ -92,10 +98,13 @@ pub struct System {
/// These may be landable and may be decorative.
/// System objects to not interact with the physics engine.
#[derive(Debug, Clone)]
pub struct Object {
pub struct SystemObject {
/// This object's sprite
pub sprite: SpriteHandle,
/// This object's handle
pub handle: SystemObjectHandle,
/// This object's size.
/// Measured as height in game units.
/// This value is scaled for distance
@ -185,11 +194,15 @@ impl crate::Build for System {
for (system_name, system) in system {
let mut objects = Vec::new();
let system_handle = SystemHandle {
index: content.systems.len(),
};
for (label, obj) in &system.object {
let mut cycle_detector = HashSet::new();
cycle_detector.insert(label.clone());
let handle = match content.sprite_index.get(&obj.sprite) {
let sprite_handle = match content.sprite_index.get(&obj.sprite) {
None => bail!(
"In system `{}`: sprite `{}` doesn't exist",
system_name,
@ -198,17 +211,33 @@ impl crate::Build for System {
Some(t) => *t,
};
objects.push(Object {
sprite: handle,
objects.push(SystemObject {
sprite: sprite_handle,
pos: resolve_position(&system.object, &obj, cycle_detector)
.with_context(|| format!("In object {:#?}", label))?,
size: obj.size,
angle: Deg(obj.angle.unwrap_or(0.0)).into(),
handle: SystemObjectHandle {
system_handle,
body_index: 0,
},
});
}
// Sort by z-distance. This is important, since these are
// rendered in this order. We need far objects to be behind
// near objects!
objects.sort_by(|a, b| b.pos.z.total_cmp(&a.pos.z));
// Update object handles
let mut i = 0;
for o in &mut objects {
o.handle.body_index = i;
i += 1;
}
content.systems.push(Self {
handle: system_handle,
name: system_name,
objects,
});

View File

@ -104,8 +104,10 @@ impl<'a> Game {
}
}
pub fn update_player_controls(&mut self, player: &PlayerAgent) {
self.state.systemsim.update_player_controls(player)
pub fn update_player_controls(&mut self, player: &mut PlayerAgent) {
self.state
.systemsim
.update_player_controls(&self.ct, player)
}
pub fn get_state(&self) -> &GameState {

View File

@ -1,10 +1,14 @@
mod game;
use anyhow::{bail, Result};
use cgmath::Point2;
use galactica_content::{Content, SystemHandle};
use galactica_playeragent::{PlayerAgent, PlayerStatus};
use galactica_render::RenderInput;
use galactica_system::phys::{util, PhysSimShipHandle};
use galactica_system::{
data::ShipState,
phys::{util, PhysSimShipHandle},
};
use galactica_util::constants::ASSET_CACHE;
use std::{
fs,
@ -56,7 +60,7 @@ fn main() -> Result<()> {
ct: &content,
systemsim: &game.get_state().systemsim,
particles: game.get_particles(),
player_ship: PhysSimShipHandle(player.ship.unwrap()),
player: &player,
current_system: SystemHandle { index: 0 },
timing: game.get_state().timing.clone(),
};
@ -71,9 +75,10 @@ fn main() -> Result<()> {
}
Event::MainEventsCleared => {
game.update_player_controls(&player);
game.update_player_controls(&mut player);
game.update();
// TODO: clean up
let player_status = {
let pos = {
let o = &game
@ -81,11 +86,23 @@ fn main() -> Result<()> {
.systemsim
.get_ship(&PhysSimShipHandle(player.ship.unwrap()));
if let Some(o) = o {
let r = &game.get_state().systemsim.get_rigid_body(o.rigid_body);
if let Some(r) = r {
Some(util::rigidbody_position(r))
} else {
None
match o.data.get_state() {
ShipState::Collapsing { .. } | ShipState::Flying => {
let r =
&game.get_state().systemsim.get_rigid_body(o.rigid_body);
if let Some(r) = r {
Some(util::rigidbody_position(r))
} else {
None
}
}
ShipState::Landed { target } => {
let b = content.get_system_object(*target);
Some(Point2 {
x: b.pos.x,
y: b.pos.y,
})
}
}
} else {
None
@ -98,6 +115,7 @@ fn main() -> Result<()> {
// This must be updated BEFORE rendering!
player.step(&content, player_status);
player.input.clear_inputs();
gpu.window().request_redraw();
}

View File

@ -1,12 +1,18 @@
use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode};
pub struct InputStatus {
// Parameters
scroll_speed: f32,
pub key_left: bool,
pub key_right: bool,
pub key_thrust: bool,
pub key_guns: bool,
pub v_scroll: f32,
// Continuous keys
key_left: bool,
key_right: bool,
key_thrust: bool,
key_guns: bool,
// One-shot keys (audomatically released at the end of each frame)
key_land: bool,
v_scroll: f32,
}
impl InputStatus {
@ -16,6 +22,7 @@ impl InputStatus {
key_right: false,
key_thrust: false,
key_guns: false,
key_land: false,
v_scroll: 0.0,
scroll_speed: 10.0,
}
@ -26,15 +33,34 @@ impl InputStatus {
self.key_right = false;
self.key_thrust = false;
self.key_guns = false;
self.key_land = false;
}
/// Called at the end of every frame,
/// resets one-shot keys.
pub fn clear_inputs(&mut self) {
self.key_land = false;
self.v_scroll = 0.0;
}
pub fn process_key(&mut self, state: &ElementState, key: &VirtualKeyCode) {
let down = state == &ElementState::Pressed;
match key {
VirtualKeyCode::Left => self.key_left = down,
VirtualKeyCode::Right => self.key_right = down,
VirtualKeyCode::Left => {
self.key_left = down;
if down {
self.key_right = false;
}
}
VirtualKeyCode::Right => {
self.key_right = down;
if down {
self.key_left = false;
}
}
VirtualKeyCode::Up => self.key_thrust = down,
VirtualKeyCode::Space => self.key_guns = down,
VirtualKeyCode::L => self.key_land = down,
_ => {}
}
}
@ -58,3 +84,41 @@ impl InputStatus {
}
}
}
// Public get-state methods
impl InputStatus {
/// Has the player applied vertical scroll?
/// This is measured in lines, scaled by scroll_speed
///
/// A positive value means scroll up, a negative value means scroll down.
/// This is reset to zero at the end of each frame.
pub fn get_v_scroll(&self) -> f32 {
self.v_scroll
}
/// Is the player pressing the "turn left" key?
pub fn pressed_left(&self) -> bool {
self.key_left
}
/// Is the player pressing the "turn right" key?
pub fn pressed_right(&self) -> bool {
self.key_right
}
/// Is the player pressing the "fowards" key?
pub fn pressed_thrust(&self) -> bool {
self.key_thrust
}
/// Is the player pressing the "fire guns" key?
pub fn pressed_guns(&self) -> bool {
self.key_guns
}
/// Has the player pressed the "land" key?
/// (One-shot, reset to false at the start of each frame)
pub fn pressed_land(&self) -> bool {
self.key_land
}
}

View File

@ -1,21 +1,43 @@
use galactica_content::Content;
use galactica_content::{Content, SystemHandle, SystemObjectHandle};
use rapier2d::geometry::ColliderHandle;
use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode};
use crate::{camera::Camera, inputstatus::InputStatus, PlayerStatus};
/// What the player has selected
#[derive(Debug, Clone, Copy)]
pub enum PlayerSelection {
/// We have nothing selected
None,
/// We have a system body selected
OrbitingBody(SystemObjectHandle),
/// We have a ship selected
Ship(ColliderHandle),
}
impl PlayerSelection {
pub fn get_planet(&self) -> Option<SystemObjectHandle> {
match self {
Self::OrbitingBody(h) => Some(*h),
_ => None,
}
}
}
pub struct PlayerAgent {
/// Which ship this player is controlling
pub ship: Option<ColliderHandle>,
/// What the player has selected
pub selection: PlayerSelection,
/// This player's camera
pub camera: Camera,
/// What buttons this player is pressing
pub input: InputStatus,
/// What the player currently has selected
pub selection: Option<()>,
}
impl PlayerAgent {
@ -24,7 +46,10 @@ impl PlayerAgent {
input: InputStatus::new(),
camera: Camera::new(),
ship: Some(ship),
selection: None,
selection: PlayerSelection::OrbitingBody(SystemObjectHandle {
system_handle: SystemHandle { index: 0 },
body_index: 1,
}),
}
}
@ -45,10 +70,9 @@ impl PlayerAgent {
}
pub fn step(&mut self, ct: &Content, status: PlayerStatus) {
if self.input.v_scroll != 0.0 {
self.camera.zoom = (self.camera.zoom + self.input.v_scroll)
if self.input.get_v_scroll() != 0.0 {
self.camera.zoom = (self.camera.zoom + self.input.get_v_scroll())
.clamp(ct.get_config().zoom_min, ct.get_config().zoom_max);
self.input.v_scroll = 0.0;
}
if status.pos.is_some() {

View File

@ -21,6 +21,7 @@ galactica-content = { workspace = true }
galactica-util = { workspace = true }
galactica-packer = { workspace = true }
galactica-system = { workspace = true }
galactica-playeragent = { workspace = true }
anyhow = { workspace = true }
cgmath = { workspace = true }

View File

@ -38,6 +38,7 @@ fn vertex_main(
instance: InstanceInput,
) -> VertexOutput {
var out: VertexOutput;
// TODO: adjust position
out.position = vec4(vertex.position, 1.0);
out.diameter = instance.diameter;
out.stroke = instance.stroke;

View File

@ -1,6 +1,7 @@
use cgmath::Point2;
use galactica_content::{Content, SystemHandle};
use galactica_system::phys::{ParticleBuilder, PhysSim, PhysSimShipHandle};
use galactica_playeragent::PlayerAgent;
use galactica_system::phys::{ParticleBuilder, PhysSim};
use galactica_util::timing::Timing;
use glyphon::{FontSystem, SwashCache, TextAtlas, TextRenderer};
use std::rc::Rc;
@ -15,7 +16,7 @@ pub struct RenderInput<'a> {
pub camera_pos: Point2<f32>,
/// Player ship data
pub player_ship: PhysSimShipHandle,
pub player: &'a PlayerAgent,
/// The system we're currently in
pub current_system: SystemHandle,

View File

@ -2,7 +2,7 @@
use bytemuck;
use cgmath::{EuclideanSpace, InnerSpace, Point2, Vector2};
use galactica_system::phys::util;
use galactica_system::{data::ShipState, phys::util};
use galactica_util::constants::OBJECT_SPRITE_INSTANCE_LIMIT;
use crate::{
@ -19,6 +19,11 @@ impl GPUState {
screen_clip: (Point2<f32>, Point2<f32>),
) {
for ship in state.systemsim.iter_ships() {
match ship.data.get_state() {
ShipState::Collapsing { .. } | ShipState::Flying => {}
ShipState::Landed { .. } => continue,
}
let r = state.systemsim.get_rigid_body(ship.rigid_body).unwrap();
let ship_pos = util::rigidbody_position(&r);
let ship_rot = util::rigidbody_rotation(r);

View File

@ -31,9 +31,21 @@ impl Radar {
// TODO: maybe a cleaner solution for last posititon?
// This is necessary because the player may be dead or landed
let player_ship = input.systemsim.get_ship(&input.player_ship).unwrap();
let player_ship = input
.systemsim
.get_ship(&galactica_system::phys::PhysSimShipHandle(
input.player.ship.unwrap(),
))
.unwrap();
match player_ship.data.get_state() {
ShipState::Landed { target } => {
let landed_body = input.ct.get_system_object(*target);
self.last_player_position = Point2 {
x: landed_body.pos.x,
y: landed_body.pos.y,
};
}
ShipState::Flying | ShipState::Collapsing { .. } => {
let player_body = input
.systemsim
@ -44,6 +56,7 @@ impl Radar {
}
};
// TODO: don't hard-code these, add config options
let planet_sprite = input.ct.get_sprite_handle("ui::planetblip");
let ship_sprite = input.ct.get_sprite_handle("ui::shipblip");
let arrow_sprite = input.ct.get_sprite_handle("ui::centerarrow");
@ -120,6 +133,9 @@ impl Radar {
// This will be None if this ship is dead.
// Stays around while the physics system runs a collapse sequence
let color = match s.data.get_state() {
ShipState::Landed { .. } => {
continue;
}
ShipState::Collapsing { .. } => {
// TODO: configurable
[0.2, 0.2, 0.2, 1.0]

View File

@ -1,7 +1,6 @@
use std::f32::consts::TAU;
use galactica_system::data::ShipState;
use galactica_util::constants::{RADIALBAR_SPRITE_INSTANCE_LIMIT, UI_SPRITE_INSTANCE_LIMIT};
use std::f32::consts::TAU;
use crate::{
datastructs::RenderState,
@ -30,10 +29,15 @@ impl Status {
let current_shields;
let current_hull;
let max_hull;
let player_ship = input.systemsim.get_ship(&input.player_ship).unwrap();
let player_ship = input
.systemsim
.get_ship(&galactica_system::phys::PhysSimShipHandle(
input.player.ship.unwrap(),
))
.unwrap();
match player_ship.data.get_state() {
ShipState::Collapsing { .. } | ShipState::Flying => {
ShipState::Landed { .. } | ShipState::Collapsing { .. } | ShipState::Flying => {
max_shields = player_ship.data.get_outfits().get_shield_strength();
current_shields = player_ship.data.get_shields();
current_hull = player_ship.data.get_hull();

View File

@ -1,7 +1,7 @@
use std::{collections::HashMap, time::Instant};
use super::{OutfitSet, ShipPersonality};
use galactica_content::{Content, FactionHandle, GunPoint, Outfit, ShipHandle};
use galactica_content::{Content, FactionHandle, GunPoint, Outfit, ShipHandle, SystemObjectHandle};
use rand::{rngs::ThreadRng, Rng};
/// Ship state machine.
@ -21,9 +21,38 @@ pub enum ShipState {
/// 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,
},
}
impl ShipState {
/// Is this ship playing its collapse sequence?
pub fn is_collapsing(&self) -> bool {
match self {
Self::Collapsing { .. } => true,
_ => false,
}
}
/// Is this ship flying in open space?
pub fn is_flying(&self) -> bool {
match self {
Self::Flying => true,
_ => false,
}
}
/// Is this ship landed on a planet?
pub fn is_landed(&self) -> bool {
match self {
Self::Landed { .. } => true,
_ => false,
}
}
/// True if this ship has been destroyed and has finished it's collapse sequence.
/// Ships are deleted once this is true.
pub fn should_be_removed(&self) -> bool {
@ -33,11 +62,11 @@ impl ShipState {
}
}
/// Is this ship playing its collapse sequence?
pub fn is_collapsing(&self) -> bool {
/// What planet is this ship landed on?
pub fn landed_on(&self) -> Option<SystemObjectHandle> {
match self {
Self::Collapsing { .. } => true,
_ => false,
Self::Landed { target } => Some(*target),
_ => None,
}
}
@ -52,14 +81,6 @@ impl ShipState {
_ => None,
}
}
/// Is this ship flying in open space?
pub fn is_flying(&self) -> bool {
match self {
Self::Flying => true,
_ => false,
}
}
}
/// Represents all attributes of a single ship
@ -115,6 +136,28 @@ impl ShipData {
}
}
/// Land this ship on `target`
pub fn land_on(&mut self, target: SystemObjectHandle) -> bool {
if self.state.is_flying() {
self.state = ShipState::Landed { target };
} else {
return false;
}
return true;
}
/// Take off from `target`
pub fn unland(&mut self) -> bool {
if self.state.is_landed() {
self.state = ShipState::Flying;
} else {
return false;
}
return true;
}
/// Add an outfit to this ship
pub fn add_outfit(&mut self, o: &Outfit) -> super::OutfitAddResult {
let r = self.outfits.add(o);
@ -181,6 +224,20 @@ impl ShipData {
*elapsed += t;
}
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 {

View File

@ -99,6 +99,7 @@ impl PhysSimShip {
ShipState::Flying => {
return self.step_live(res, rigid_body, collider);
}
ShipState::Landed { .. } => {}
}
}

View File

@ -1,7 +1,7 @@
use cgmath::{Deg, EuclideanSpace, InnerSpace, Matrix2, Point2, Rad, Vector2, Zero};
use galactica_content::{
Content, FactionHandle, GunPoint, OutfitHandle, ProjectileCollider, Relationship, ShipHandle,
SystemHandle,
SystemHandle, SystemObjectHandle,
};
use galactica_playeragent::PlayerAgent;
use nalgebra::{point, vector};
@ -13,7 +13,7 @@ use rapier2d::{
};
use std::{collections::HashMap, f32::consts::PI};
use crate::data::ShipPersonality;
use crate::data::{ShipPersonality, ShipState};
use super::{
controller::ShipController,
@ -67,6 +67,40 @@ impl<'a> PhysSim {
return Some((r, p));
}
fn land_ship(&mut self, collider: ColliderHandle, target: SystemObjectHandle) {
let ship = self.ships.get_mut(&collider).unwrap();
self.rigid_body_set
.get_mut(ship.rigid_body)
.unwrap()
.set_enabled(false);
self.collider_set
.get_mut(ship.collider)
.unwrap()
.set_enabled(false);
ship.data.land_on(target);
}
fn unland_ship(&mut self, ct: &Content, collider: ColliderHandle) {
let ship = self.ships.get_mut(&collider).unwrap();
let obj = ship.data.get_state().landed_on().unwrap();
let obj = ct.get_system_object(obj);
ship.data.unland();
self.rigid_body_set
.get_mut(ship.rigid_body)
.unwrap()
.set_position(point![obj.pos.x, obj.pos.y].into(), true);
self.rigid_body_set
.get_mut(ship.rigid_body)
.unwrap()
.set_enabled(true);
self.collider_set
.get_mut(ship.collider)
.unwrap()
.set_enabled(true);
}
fn remove_ship(&mut self, res: &mut PhysStepResources, colliderhandle: ColliderHandle) {
let ship = match self.ships.get(&colliderhandle) {
None => return,
@ -213,16 +247,31 @@ impl PhysSim {
}
/// Update a player ship's controls
pub fn update_player_controls(&mut self, player: &PlayerAgent) {
pub fn update_player_controls(&mut self, ct: &Content, player: &PlayerAgent) {
if player.ship.is_none() {
return;
}
let ship_object = self.ships.get_mut(&player.ship.unwrap());
if let Some(ship_object) = ship_object {
ship_object.controls.guns = player.input.key_guns;
ship_object.controls.left = player.input.key_left;
ship_object.controls.right = player.input.key_right;
ship_object.controls.thrust = player.input.key_thrust;
ship_object.controls.guns = player.input.pressed_guns();
ship_object.controls.left = player.input.pressed_left();
ship_object.controls.right = player.input.pressed_right();
ship_object.controls.thrust = player.input.pressed_thrust();
if player.input.pressed_land() {
match ship_object.data.get_state() {
ShipState::Flying => {
self.land_ship(
player.ship.unwrap(),
player.selection.get_planet().unwrap(),
);
}
ShipState::Landed { .. } => {
self.unland_ship(ct, player.ship.unwrap());
}
_ => {}
}
}
}
}
@ -247,48 +296,54 @@ impl PhysSim {
continue;
}
// This ship is playing a collapse sequence
// and needs no additional logic
if ship.data.get_state().is_collapsing() {
let ship = self.ships.get_mut(&collider).unwrap();
ship.step(
res,
&mut self.rigid_body_set[ship.rigid_body],
&mut self.collider_set[ship.collider],
);
continue;
}
match ship.data.get_state() {
ShipState::Landed { .. } => {}
// Compute new controls
let controls;
let b = self.ship_behaviors.get_mut(&collider).unwrap();
controls = b.update_controls(&res, &self.rigid_body_set, &self.ships, ship.collider);
ShipState::Collapsing { .. } => {
let ship = self.ships.get_mut(&collider).unwrap();
ship.step(
res,
&mut self.rigid_body_set[ship.rigid_body],
&mut self.collider_set[ship.collider],
);
}
let ship = self.ships.get_mut(&collider).unwrap();
ShipState::Flying => {
// Compute new controls
// This is why we borrow immutably first
let controls;
let b = self.ship_behaviors.get_mut(&collider).unwrap();
controls =
b.update_controls(&res, &self.rigid_body_set, &self.ships, ship.collider);
if let Some(controls) = controls {
ship.controls = controls;
}
ship.step(
res,
&mut self.rigid_body_set[ship.rigid_body],
&mut self.collider_set[ship.collider],
);
// Re-borrow mutably to apply changes
let ship = self.ships.get_mut(&collider).unwrap();
// If we're firing, try to fire each gun
if ship.controls.guns {
// 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();
if let Some(controls) = controls {
ship.controls = controls;
}
ship.step(
res,
&mut self.rigid_body_set[ship.rigid_body],
&mut self.collider_set[ship.collider],
);
for (gun, outfit) in pairs {
if ship.data.fire_gun(res.ct, &gun) {
projectiles.push((ship.collider, gun.clone(), outfit.unwrap()));
// If we're firing, try to fire each gun
if ship.controls.guns {
// 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.collider, gun.clone(), outfit.unwrap()));
}
}
}
}
}