From 07ec685c139eee96894d421710404b261cee0a44 Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 9 Jan 2024 17:23:54 -0800 Subject: [PATCH] Reworked guns and projectile spawning --- crates/galactica/src/game.rs | 42 ++--- crates/gameobject/src/gamedata.rs | 19 +- crates/gameobject/src/ship/outfitset.rs | 34 +++- crates/gameobject/src/ship/ship.rs | 54 +++++- crates/world/src/objects/ship.rs | 4 - crates/world/src/stepresources.rs | 8 - crates/world/src/world.rs | 231 +++++++++++++----------- 7 files changed, 237 insertions(+), 155 deletions(-) diff --git a/crates/galactica/src/game.rs b/crates/galactica/src/game.rs index 46eb552..3f06625 100644 --- a/crates/galactica/src/game.rs +++ b/crates/galactica/src/game.rs @@ -1,7 +1,4 @@ -use object::{ - ship::{OutfitSet, ShipPersonality}, - GameData, GameShipHandle, -}; +use object::{ship::ShipPersonality, GameData, GameShipHandle}; use std::time::Instant; use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode}; @@ -12,7 +9,7 @@ use galactica_constants; use galactica_content as content; use galactica_gameobject as object; use galactica_render::RenderState; -use galactica_world::{objects::ShipControls, ParticleBuilder, StepResources, World}; +use galactica_world::{objects::ShipControls, util, ParticleBuilder, StepResources, World}; pub struct Game { input: InputStatus, @@ -36,45 +33,35 @@ impl Game { pub fn new(ct: content::Content) -> Self { 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( &ct, content::ShipHandle { index: 0 }, content::FactionHandle { index: 0 }, ShipPersonality::Player, - o1.clone(), &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( &ct, content::ShipHandle { index: 0 }, content::FactionHandle { index: 1 }, ShipPersonality::Dummy, - OutfitSet::new(ss.space, &[]), &content::SystemHandle { index: 0 }, ); - let mut o1 = OutfitSet::new(ss.space, &[]); - o1.add(&ct.get_outfit(content::OutfitHandle { index: 0 })); - //o1.add_gun(&ct, content::GunHandle { index: 0 }); - - gamedata.create_ship( + let a = gamedata.create_ship( &ct, content::ShipHandle { index: 0 }, content::FactionHandle { index: 1 }, ShipPersonality::Point, - o1, &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 }); @@ -127,7 +114,9 @@ impl Game { pub fn update(&mut self) { 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_controls: ShipControls { left: self.input.key_left, @@ -147,7 +136,12 @@ impl Game { 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(); } diff --git a/crates/gameobject/src/gamedata.rs b/crates/gameobject/src/gamedata.rs index fd47b06..acc595b 100644 --- a/crates/gameobject/src/gamedata.rs +++ b/crates/gameobject/src/gamedata.rs @@ -1,10 +1,6 @@ use std::collections::HashMap; -use crate::{ - handles::GameShipHandle, - ship::Ship, - ship::{OutfitSet, ShipPersonality}, -}; +use crate::{handles::GameShipHandle, ship::Ship, ship::ShipPersonality}; use galactica_content as content; /// Keeps track of all objects in the galaxy. @@ -44,7 +40,6 @@ impl GameData { ship: content::ShipHandle, faction: content::FactionHandle, personality: ShipPersonality, - outfits: OutfitSet, system: &content::SystemHandle, ) -> GameShipHandle { let handle = GameShipHandle { @@ -53,15 +48,19 @@ impl GameData { }; self.index += 1; - self.ships.insert( - handle, - Ship::new(ct, handle, ship, faction, personality, outfits), - ); + self.ships + .insert(handle, Ship::new(ct, handle, ship, faction, personality)); self.system_ship_table.get_mut(system).unwrap().push(handle); self.ship_system_table.insert(handle, *system); return handle; } + + pub fn step(&mut self, t: f32) { + for (_, s) in &mut self.ships { + s.step(t); + } + } } // Public getters diff --git a/crates/gameobject/src/ship/outfitset.rs b/crates/gameobject/src/ship/outfitset.rs index a888d1a..d7df554 100644 --- a/crates/gameobject/src/ship/outfitset.rs +++ b/crates/gameobject/src/ship/outfitset.rs @@ -14,6 +14,10 @@ pub enum OutfitAddResult { /// outfits may need outfit AND weapon space. In these cases, this result /// should name the "most specific" kind of space we lack. 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 @@ -37,6 +41,9 @@ pub(crate) struct ShieldGenerator { } /// 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)] pub struct OutfitSet { /// What outfits does this statsum contain? @@ -68,7 +75,7 @@ pub struct 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 { outfits: HashMap::new(), 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) { 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.engine_thrust += o.engine_thrust; @@ -107,7 +129,7 @@ impl OutfitSet { 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) { return OutfitRemoveResult::NotExist; } else { @@ -177,6 +199,12 @@ impl OutfitSet { 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 { + self.gun_points.get(point).unwrap().clone() + } + /// Total available outfit space pub fn get_total_space(&self) -> &OutfitSpace { &self.total_space diff --git a/crates/gameobject/src/ship/ship.rs b/crates/gameobject/src/ship/ship.rs index d0fb0d1..8d532d7 100644 --- a/crates/gameobject/src/ship/ship.rs +++ b/crates/gameobject/src/ship/ship.rs @@ -1,9 +1,11 @@ -use std::time::Instant; +use std::{collections::HashMap, time::Instant}; use crate::GameShipHandle; use super::{OutfitSet, ShipPersonality}; +use content::GunPoint; use galactica_content as content; +use rand::{rngs::ThreadRng, Rng}; #[derive(Debug)] pub struct Ship { @@ -19,6 +21,8 @@ pub struct Ship { // TODO: unified ship stats struct, like outfit space hull: f32, shields: f32, + gun_cooldowns: HashMap, + rng: ThreadRng, // Utility values /// The last time this ship was damaged @@ -32,29 +36,61 @@ impl Ship { ct_handle: content::ShipHandle, faction: content::FactionHandle, personality: ShipPersonality, - outfits: OutfitSet, ) -> Self { let s = ct.get_ship(ct_handle); - let shields = outfits.get_shield_strength(); Ship { handle, ct_handle, faction, - outfits, + outfits: OutfitSet::new(s.space, &s.guns), personality, last_hit: Instant::now(), + rng: rand::thread_rng(), // Initial stats 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. pub fn is_dead(&self) -> bool { 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 pub fn apply_damage(&mut self, mut d: f32) { if self.is_dead() { @@ -72,6 +108,14 @@ impl Ship { /// Update this ship's state by `t` seconds 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(); if self.shields != self.outfits.get_shield_strength() { for g in self.outfits.iter_shield_generators() { diff --git a/crates/world/src/objects/ship.rs b/crates/world/src/objects/ship.rs index 38561c0..c7a5438 100644 --- a/crates/world/src/objects/ship.rs +++ b/crates/world/src/objects/ship.rs @@ -298,10 +298,6 @@ impl ShipWorldObject { rigid_body .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; - //} } } diff --git a/crates/world/src/stepresources.rs b/crates/world/src/stepresources.rs index 8e86be8..8b2b82e 100644 --- a/crates/world/src/stepresources.rs +++ b/crates/world/src/stepresources.rs @@ -1,5 +1,4 @@ use crate::{objects::ShipControls, ParticleBuilder}; -use cgmath::Point2; use galactica_content::Content; use galactica_gameobject::{GameData, GameShipHandle}; @@ -24,10 +23,3 @@ pub struct StepResources<'a> { /// The ship that the player controls 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, -} diff --git a/crates/world/src/world.rs b/crates/world/src/world.rs index 59b7340..c58a467 100644 --- a/crates/world/src/world.rs +++ b/crates/world/src/world.rs @@ -1,4 +1,5 @@ use cgmath::{Deg, EuclideanSpace, InnerSpace, Matrix2, Point2, Rad, Vector2, Zero}; +use content::{GunPoint, OutfitHandle}; use crossbeam::channel::Receiver; use nalgebra::{point, vector}; use object::{ship::Ship, GameData, GameShipHandle}; @@ -15,7 +16,7 @@ use crate::{ objects::{ProjectileWorldObject, ShipWorldObject}, util, wrapper::Wrapper, - ParticleBuilder, StepOutput, StepResources, + ParticleBuilder, StepResources, }; use galactica_content as content; use galactica_gameobject as object; @@ -23,7 +24,7 @@ use galactica_gameobject as object; /// Manages the physics state of one system pub struct World { /// The system this world is attached to - system: content::SystemHandle, + _system: content::SystemHandle, wrapper: Wrapper, projectiles: HashMap, @@ -73,67 +74,6 @@ impl<'a> World { 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 = 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( &mut self, res: &mut StepResources, @@ -152,10 +92,17 @@ impl<'a> World { let ship_d = res.dt.get_ship_mut(ship.data_handle).unwrap(); - // TODO: check faction - ship_d.apply_damage(projectile.content.damage); + let f = res.ct.get_faction(projectile.faction); + 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 .wrapper .rigid_body_set @@ -210,14 +157,14 @@ impl<'a> World { } // Public methods -impl<'a> World { +impl World { /// Create a new physics system pub fn new(ct: &content::Content, dt: &GameData, system: content::SystemHandle) -> Self { let (collision_send, collision_queue) = crossbeam::channel::unbounded(); let (contact_force_send, _) = crossbeam::channel::unbounded(); let mut w = Self { - system, + _system: system, wrapper: Wrapper::new(), projectiles: HashMap::new(), ships: HashMap::new(), @@ -283,45 +230,125 @@ impl<'a> World { ); } - /// Step this physics system by `t` seconds - pub fn step(&mut self, mut res: StepResources) -> StepOutput { - let mut output = StepOutput { - player_position: Point2 { x: 0.0, y: 0.0 }, - }; - - // Run ship updates - // TODO: maybe reorganize projectile creation? - //let mut projectiles = Vec::new(); + /// Run ship updates: + /// - updates ship controls (runs behaviors) + /// - applies ship controls + /// - creates projectiles + fn step_ships(&mut self, res: &mut StepResources) { + let mut projectiles = Vec::new(); let mut to_remove = Vec::new(); - for (_, s) 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())); - //} - + for (_, ship_object) in &mut self.ships { //if s.remove_from_world() { // to_remove.push(s.physics_handle); // 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)> = 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 { 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 = + 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 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> { self.ships.get(&ship) } @@ -418,12 +447,12 @@ impl<'a> World { } /// Iterate over all ships in this physics system - pub fn iter_ships(&'a self) -> impl Iterator + '_ { + pub fn iter_ships(&self) -> impl Iterator + '_ { self.ships.values() } /// Iterate over all ships in this physics system - pub fn iter_projectiles(&'a self) -> impl Iterator + '_ { + pub fn iter_projectiles(&self) -> impl Iterator + '_ { self.projectiles.values() } }