diff --git a/content/engines.toml b/content/engines.toml new file mode 100644 index 0000000..ee2d2aa --- /dev/null +++ b/content/engines.toml @@ -0,0 +1,4 @@ +[engine."plasma"] + +thrust = 50 +flare.sprite = "flare::ion" diff --git a/content/guns.toml b/content/guns.toml index e2f2f38..2cb4db5 100644 --- a/content/guns.toml +++ b/content/guns.toml @@ -1,5 +1,22 @@ [gun."blaster"] + +# Angle of fire cone +# Smaller angle = more accurate +spread = 2 + +# Average delay between shots +rate = 0.2 +# Random rate variation (+- this in both directions) +rate_rng = 0.1 + + projectile.sprite = "projectile::blaster" -projectile.size = 100 +# Height of projectile in game units +projectile.size = 10 +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 diff --git a/content/ship.toml b/content/ship.toml index 81d00f5..1b15216 100644 --- a/content/ship.toml +++ b/content/ship.toml @@ -3,4 +3,4 @@ sprite = "ship::gypsum" size = 100 engines = [{ x = 0.0, y = -105, size = 50.0 }] -guns = [{ x = 0.0, y = 100 }] +guns = [{ x = 0.0, y = 100 }, { x = 10.0, y = 80 }, { x = -10.0, y = 80 }] diff --git a/src/content/engine.rs b/src/content/engine.rs new file mode 100644 index 0000000..099e798 --- /dev/null +++ b/src/content/engine.rs @@ -0,0 +1,46 @@ +use anyhow::Result; + +pub(super) mod syntax { + use serde::Deserialize; + // Raw serde syntax structs. + // These are never seen by code outside this crate. + + #[derive(Debug, Deserialize)] + pub struct Engine { + pub thrust: f32, + pub flare: Flare, + } + + #[derive(Debug, Deserialize)] + pub struct Flare { + pub sprite: String, + } +} + +#[derive(Debug, Clone)] +pub struct Engine { + pub name: String, + pub thrust: f32, + pub flare_sprite: String, +} + +impl super::Build for Engine { + fn build(root: &super::syntax::Root) -> Result> { + let engine = if let Some(engine) = &root.engine { + engine + } else { + return Ok(vec![]); + }; + + let mut out = Vec::new(); + for (engine_name, engine) in engine { + out.push(Self { + name: engine_name.to_owned(), + thrust: engine.thrust, + flare_sprite: engine.flare.sprite.clone(), + }); + } + + return Ok(out); + } +} diff --git a/src/content/gun.rs b/src/content/gun.rs index 8a58d6c..52231aa 100644 --- a/src/content/gun.rs +++ b/src/content/gun.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use cgmath::Deg; pub(super) mod syntax { use serde::Deserialize; @@ -8,14 +9,20 @@ pub(super) mod syntax { #[derive(Debug, Deserialize)] pub struct Gun { pub projectile: Projectile, + pub spread: f32, + pub rate: f32, + pub rate_rng: f32, } #[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, } } @@ -23,14 +30,20 @@ pub(super) mod syntax { pub struct Gun { pub name: String, pub projectile: Projectile, + pub spread: Deg, + pub rate: f32, + pub rate_rng: f32, } #[derive(Debug, Clone)] 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, } impl super::Build for Gun { @@ -45,11 +58,17 @@ impl super::Build for Gun { for (gun_name, gun) in gun { out.push(Self { name: gun_name.to_owned(), + spread: Deg(gun.spread), + rate: gun.rate, + rate_rng: gun.rate_rng, projectile: Projectile { sprite: gun.projectile.sprite.to_owned(), 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, }, }); } diff --git a/src/content/mod.rs b/src/content/mod.rs index 95d62e0..910d927 100644 --- a/src/content/mod.rs +++ b/src/content/mod.rs @@ -1,10 +1,12 @@ #![allow(dead_code)] +mod engine; mod gun; mod ship; mod system; +pub use engine::Engine; pub use gun::{Gun, Projectile}; -pub use ship::{Engine, Ship, ShipGun}; +pub use ship::{EnginePoint, GunPoint, Ship}; pub use system::{Object, System}; use anyhow::{bail, Context, Result}; @@ -15,7 +17,7 @@ use walkdir::WalkDir; mod syntax { use super::HashMap; - use super::{gun, ship, system}; + use super::{engine, gun, ship, system}; use serde::Deserialize; #[derive(Debug, Deserialize)] @@ -23,6 +25,7 @@ mod syntax { pub gun: Option>, pub ship: Option>, pub system: Option>, + pub engine: Option>, } } @@ -38,6 +41,7 @@ pub struct Content { pub systems: Vec, pub ships: Vec, pub guns: Vec, + pub engines: Vec, } macro_rules! quick_name_dup_check { @@ -70,6 +74,7 @@ impl Content { quick_name_dup_check!(self.systems, root, system::System::build); quick_name_dup_check!(self.guns, root, gun::Gun::build); quick_name_dup_check!(self.ships, root, ship::Ship::build); + quick_name_dup_check!(self.engines, root, engine::Engine::build); return Ok(()); } @@ -78,6 +83,7 @@ impl Content { systems: Vec::new(), ships: Vec::new(), guns: Vec::new(), + engines: Vec::new(), }; for e in WalkDir::new(path).into_iter().filter_map(|e| e.ok()) { diff --git a/src/content/ship.rs b/src/content/ship.rs index 1ca6482..64ee67e 100644 --- a/src/content/ship.rs +++ b/src/content/ship.rs @@ -36,21 +36,19 @@ pub struct Ship { pub name: String, pub sprite: String, pub size: f32, - pub engines: Vec, - pub guns: Vec, + pub engines: Vec, + pub guns: Vec, } #[derive(Debug, Clone)] -pub struct Engine { +pub struct EnginePoint { pub pos: Point2, pub size: f32, } #[derive(Debug, Clone)] -pub struct ShipGun { +pub struct GunPoint { pub pos: Point2, - pub cooldown: f32, - pub active_cooldown: f32, } impl super::Build for Ship { @@ -70,7 +68,7 @@ impl super::Build for Ship { engines: ship .engines .iter() - .map(|e| Engine { + .map(|e| EnginePoint { pos: Point2 { x: e.x, y: e.y }, size: e.size, }) @@ -78,10 +76,8 @@ impl super::Build for Ship { guns: ship .guns .iter() - .map(|e| ShipGun { + .map(|e| GunPoint { pos: Point2 { x: e.x, y: e.y }, - cooldown: 0.2, - active_cooldown: 0.0, }) .collect(), }); diff --git a/src/game/game.rs b/src/game/game.rs index 14be1b4..9edd315 100644 --- a/src/game/game.rs +++ b/src/game/game.rs @@ -2,7 +2,7 @@ use cgmath::{Deg, Point2, Point3, Vector2}; use std::time::Instant; use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode}; -use super::{Camera, InputStatus, Ship, System}; +use super::{ship, Camera, InputStatus, System}; use crate::{ consts, content::Content, @@ -16,6 +16,7 @@ pub struct Projectile { pub sprite: SpriteTexture, pub angle: Deg, pub lifetime: f32, + pub size: f32, } impl Projectile { @@ -32,8 +33,8 @@ impl Projectile { pub struct Game { pub input: InputStatus, pub last_update: Instant, - pub player: Ship, - pub test: Ship, + pub player: ship::Ship, + pub test: ship::Ship, pub system: System, pub camera: Camera, paused: bool, @@ -47,7 +48,15 @@ impl Game { last_update: Instant::now(), input: InputStatus::new(), projectiles: Vec::new(), - player: Ship::new(&ct.ships[0], (0.0, 0.0).into()), + player: ship::Ship::new( + &ct.ships[0], + vec![ + ship::ShipOutfit::Gun(ship::ShipGun::new(ct.guns[0].clone(), 1)), + ship::ShipOutfit::Gun(ship::ShipGun::new(ct.guns[0].clone(), 2)), + ship::ShipOutfit::Engine(ct.engines[0].clone()), + ], + (0.0, 0.0).into(), + ), camera: Camera { pos: (0.0, 0.0).into(), zoom: 500.0, @@ -56,7 +65,7 @@ impl Game { paused: false, time_scale: 1.0, - test: Ship::new(&ct.ships[0], (100.0, 100.0).into()), + test: ship::Ship::new(&ct.ships[0], vec![], (100.0, 100.0).into()), } } @@ -130,7 +139,7 @@ impl Game { y: p.position.y, z: 1.0, }, - size: 10.0, + size: p.size, angle: p.angle, children: None, }) diff --git a/src/game/mod.rs b/src/game/mod.rs index 993fc86..7b24d87 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -8,6 +8,5 @@ mod systemobject; pub use camera::Camera; pub use game::Game; pub use inputstatus::InputStatus; -pub use ship::Ship; pub use system::System; pub use systemobject::SystemObject; diff --git a/src/game/ship.rs b/src/game/ship.rs deleted file mode 100644 index 391d680..0000000 --- a/src/game/ship.rs +++ /dev/null @@ -1,143 +0,0 @@ -use cgmath::{Deg, EuclideanSpace, Matrix2, Point2, Point3, Vector2}; - -use crate::{ - content, - physics::PhysicsBody, - render::{Sprite, SpriteTexture, Spriteable, SubSprite}, -}; - -use super::{game::Projectile, InputStatus}; - -pub struct ShipControls { - pub left: bool, - pub right: bool, - pub thrust: bool, - pub guns: bool, -} - -impl ShipControls { - pub fn new() -> Self { - ShipControls { - left: false, - right: false, - thrust: false, - guns: false, - } - } -} - -pub struct ShipTickResult { - pub projectiles: Vec, -} - -pub struct Ship { - pub physicsbody: PhysicsBody, - pub controls: ShipControls, - - sprite: SpriteTexture, - size: f32, - engines: Vec, - guns: Vec, -} - -impl Ship { - pub fn new(ct: &content::Ship, pos: Point2) -> Self { - Ship { - physicsbody: PhysicsBody::new(pos), - sprite: SpriteTexture(ct.sprite.clone()), - size: ct.size, - engines: ct.engines.clone(), - guns: ct.guns.clone(), - controls: ShipControls::new(), - } - } - - pub fn update_controls(&mut self, input: &InputStatus) { - self.controls.thrust = input.key_thrust; - self.controls.right = input.key_right; - self.controls.left = input.key_left; - self.controls.guns = input.key_guns; - } - - pub fn fire_guns(&mut self) -> Vec { - let mut out = Vec::new(); - for i in &mut self.guns { - if i.active_cooldown > 0.0 { - continue; - } - i.active_cooldown = i.cooldown; - - let p = self.physicsbody.pos - + (Matrix2::from_angle(self.physicsbody.angle) * i.pos.to_vec()); - - out.push(Projectile { - position: p, - velocity: self.physicsbody.vel - + (Matrix2::from_angle(self.physicsbody.angle) * Vector2 { x: 0.0, y: 400.0 }), - angle: self.physicsbody.angle, - sprite: SpriteTexture("projectile::blaster".into()), - lifetime: 5.0, - }) - } - return out; - } - - pub fn tick(&mut self, t: f32) -> ShipTickResult { - if self.controls.thrust { - self.physicsbody.thrust(50.0 * t); - } - - if self.controls.right { - self.physicsbody.rot(Deg(35.0) * t); - } - - if self.controls.left { - self.physicsbody.rot(Deg(-35.0) * t); - } - - let p = if self.controls.guns { - self.fire_guns() - } else { - Vec::new() - }; - - self.physicsbody.tick(t); - for i in &mut self.guns { - i.active_cooldown -= t; - } - - return ShipTickResult { projectiles: p }; - } -} - -impl Spriteable for Ship { - fn get_sprite(&self) -> Sprite { - let engines = if self.controls.thrust { - Some( - self.engines - .iter() - .map(|e| SubSprite { - pos: Point3 { - x: e.pos.x, - y: e.pos.y, - z: 1.0, - }, - texture: SpriteTexture("flare::ion".to_owned()), - angle: Deg(0.0), - size: e.size, - }) - .collect(), - ) - } else { - None - }; - - Sprite { - pos: (self.physicsbody.pos.x, self.physicsbody.pos.y, 1.0).into(), - texture: self.sprite.clone(), // TODO: sprite texture should be easy to clone - angle: self.physicsbody.angle, - size: self.size, - children: engines, - } - } -} diff --git a/src/game/ship/mod.rs b/src/game/ship/mod.rs new file mode 100644 index 0000000..23f0740 --- /dev/null +++ b/src/game/ship/mod.rs @@ -0,0 +1,29 @@ +mod outfits; +mod ship; + +pub use outfits::{ShipGun, ShipOutfit, ShipOutfits}; +pub use ship::Ship; + +use super::{game::Projectile, InputStatus}; + +pub struct ShipControls { + pub left: bool, + pub right: bool, + pub thrust: bool, + pub guns: bool, +} + +impl ShipControls { + pub fn new() -> Self { + ShipControls { + left: false, + right: false, + thrust: false, + guns: false, + } + } +} + +pub struct ShipTickResult { + pub projectiles: Vec, +} diff --git a/src/game/ship/outfits.rs b/src/game/ship/outfits.rs new file mode 100644 index 0000000..9c9e661 --- /dev/null +++ b/src/game/ship/outfits.rs @@ -0,0 +1,127 @@ +use cgmath::{Deg, Point3}; + +use crate::{ + content::{self, EnginePoint, GunPoint}, + render::{SpriteTexture, SubSprite}, +}; + +pub struct ShipGun { + pub kind: content::Gun, + pub cooldown: f32, + pub point: usize, +} + +impl ShipGun { + pub fn new(kind: content::Gun, point: usize) -> Self { + Self { + kind, + point, + cooldown: 0.0, + } + } +} + +pub enum ShipOutfit { + Gun(ShipGun), + Engine(content::Engine), +} + +impl ShipOutfit { + pub fn gun(&mut self) -> Option<&mut ShipGun> { + match self { + Self::Gun(g) => Some(g), + _ => None, + } + } + + pub fn engine(&self) -> Option<&content::Engine> { + match self { + Self::Engine(e) => Some(e), + _ => None, + } + } +} + +pub struct ShipOutfits { + outfits: Vec, + enginepoints: Vec, + gunpoints: Vec, + + // Minor performance optimization, since we + // rarely need to re-compute these. + engine_flare_sprites: Vec, +} + +impl<'a> ShipOutfits { + pub fn new(enginepoints: Vec, gunpoints: Vec) -> Self { + Self { + outfits: Vec::new(), + enginepoints, + gunpoints, + engine_flare_sprites: vec![], + } + } + + pub fn add(&mut self, o: ShipOutfit) { + self.outfits.push(o); + self.update_engine_flares(); + } + + pub fn iter_guns(&mut self) -> impl Iterator { + self.outfits + .iter_mut() + .map(|x| x.gun()) + .filter(|x| x.is_some()) + .map(|x| x.unwrap()) + } + + pub fn iter_guns_points(&mut self) -> impl Iterator { + self.outfits + .iter_mut() + .map(|x| x.gun()) + .filter(|x| x.is_some()) + .map(|x| x.unwrap()) + .map(|x| (&self.gunpoints[x.point], x)) + .map(|(a, b)| (b, a)) + } + + pub fn iter_engines(&self) -> impl Iterator { + self.outfits + .iter() + .map(|x| x.engine()) + .filter(|x| x.is_some()) + .map(|x| x.unwrap()) + } + + pub fn iter_enginepoints(&self) -> impl Iterator { + self.enginepoints.iter() + } + + pub fn update_engine_flares(&mut self) { + // TODO: better way to pick flare texture + self.engine_flare_sprites.clear(); + let t = if let Some(e) = self.iter_engines().next() { + SpriteTexture(e.flare_sprite.clone()) + } else { + return; + }; + + self.engine_flare_sprites = self + .iter_enginepoints() + .map(|p| SubSprite { + pos: Point3 { + x: p.pos.x, + y: p.pos.y, + z: 1.0, + }, + texture: t.clone(), + angle: Deg(0.0), + size: p.size, + }) + .collect(); + } + + pub fn get_engine_flares(&self) -> Vec { + return self.engine_flare_sprites.clone(); + } +} diff --git a/src/game/ship/ship.rs b/src/game/ship/ship.rs new file mode 100644 index 0000000..df85617 --- /dev/null +++ b/src/game/ship/ship.rs @@ -0,0 +1,132 @@ +use cgmath::{Deg, EuclideanSpace, Matrix2, Point2, Vector2}; +use rand::Rng; + +use super::super::game::Projectile; +use super::ShipOutfit; +use super::{outfits::ShipOutfits, InputStatus, ShipControls, ShipTickResult}; +use crate::{ + content, + physics::PhysicsBody, + render::{Sprite, SpriteTexture, Spriteable}, +}; + +pub struct Ship { + pub physicsbody: PhysicsBody, + pub controls: ShipControls, + outfits: ShipOutfits, + + sprite: SpriteTexture, + size: f32, +} + +impl Ship { + pub fn new(ct: &content::Ship, outfits: Vec, pos: Point2) -> Self { + let mut o = ShipOutfits::new(ct.engines.clone(), ct.guns.clone()); + for x in outfits.into_iter() { + o.add(x) + } + + Ship { + physicsbody: PhysicsBody::new(pos), + controls: ShipControls::new(), + + outfits: o, + sprite: SpriteTexture(ct.sprite.clone()), + size: ct.size, + } + } + + pub fn update_controls(&mut self, input: &InputStatus) { + self.controls.thrust = input.key_thrust; + self.controls.right = input.key_right; + self.controls.left = input.key_left; + self.controls.guns = input.key_guns; + } + + pub fn fire_guns(&mut self) -> Vec { + let mut rng = rand::thread_rng(); + + let mut out = Vec::new(); + + for (g, p) in self.outfits.iter_guns_points() { + if g.cooldown > 0.0 { + continue; + } + + g.cooldown = g.kind.rate + rng.gen_range(-g.kind.rate_rng..=g.kind.rate_rng); + + let pos = self.physicsbody.pos + + (Matrix2::from_angle(self.physicsbody.angle) * p.pos.to_vec()); + + let vel = self.physicsbody.vel + + (Matrix2::from_angle( + self.physicsbody.angle + + Deg(rng.gen_range(-(g.kind.spread.0 / 2.0)..=g.kind.spread.0 / 2.0)), + ) * Vector2 { + x: 0.0, + y: g.kind.projectile.speed + + rng.gen_range(-g.kind.projectile.speed_rng..=g.kind.projectile.speed_rng), + }); + + out.push(Projectile { + position: pos, + velocity: vel, + angle: self.physicsbody.angle, + sprite: SpriteTexture(g.kind.projectile.sprite.clone()), + lifetime: g.kind.projectile.lifetime + + rng.gen_range( + -g.kind.projectile.lifetime_rng..=g.kind.projectile.lifetime_rng, + ), + size: g.kind.projectile.size + + rng.gen_range(-g.kind.projectile.size_rng..=g.kind.projectile.size_rng), + }) + } + return out; + } + + pub fn tick(&mut self, t: f32) -> ShipTickResult { + if self.controls.thrust { + for e in self.outfits.iter_engines() { + self.physicsbody.thrust(e.thrust * t); + } + } + + if self.controls.right { + self.physicsbody.rot(Deg(35.0) * t); + } + + if self.controls.left { + self.physicsbody.rot(Deg(-35.0) * t); + } + + let p = if self.controls.guns { + self.fire_guns() + } else { + Vec::new() + }; + + self.physicsbody.tick(t); + for i in self.outfits.iter_guns() { + i.cooldown -= t; + } + + return ShipTickResult { projectiles: p }; + } +} + +impl Spriteable for Ship { + fn get_sprite(&self) -> Sprite { + Sprite { + pos: (self.physicsbody.pos.x, self.physicsbody.pos.y, 1.0).into(), + texture: self.sprite.clone(), // TODO: sprite texture should be easy to clone + angle: self.physicsbody.angle, + size: self.size, + + children: if self.controls.thrust { + Some(self.outfits.get_engine_flares()) + } else { + None + }, + } + } +} diff --git a/src/render/sprite.rs b/src/render/sprite.rs index 6323aca..aae3256 100644 --- a/src/render/sprite.rs +++ b/src/render/sprite.rs @@ -2,6 +2,7 @@ use cgmath::{Deg, Point3}; use super::SpriteTexture; +#[derive(Debug, Clone)] pub struct Sprite { /// The sprite texture to draw pub texture: SpriteTexture, @@ -21,6 +22,7 @@ pub struct Sprite { pub children: Option>, } +#[derive(Debug, Clone)] pub struct SubSprite { /// The sprite texture to draw pub texture: SpriteTexture,