Compare commits

..

2 Commits

Author SHA1 Message Date
Mark ece82a8c1b
Comments 2023-12-28 17:06:33 -08:00
Mark b283fadce9
Added outfit manager 2023-12-28 17:04:41 -08:00
14 changed files with 410 additions and 164 deletions

4
content/engines.toml Normal file
View File

@ -0,0 +1,4 @@
[engine."plasma"]
thrust = 50
flare.sprite = "flare::ion"

View File

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

View File

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

46
src/content/engine.rs Normal file
View File

@ -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<Vec<Self>> {
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);
}
}

View File

@ -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<f32>,
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,
},
});
}

View File

@ -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<HashMap<String, gun::syntax::Gun>>,
pub ship: Option<HashMap<String, ship::syntax::Ship>>,
pub system: Option<HashMap<String, system::syntax::System>>,
pub engine: Option<HashMap<String, engine::syntax::Engine>>,
}
}
@ -33,11 +36,13 @@ trait Build {
Self: Sized;
}
/// Represents generic game content, not connected to any game objects.
#[derive(Debug)]
pub struct Content {
pub systems: Vec<system::System>,
pub ships: Vec<ship::Ship>,
pub guns: Vec<gun::Gun>,
pub engines: Vec<engine::Engine>,
}
macro_rules! quick_name_dup_check {
@ -70,6 +75,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 +84,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()) {

View File

@ -36,21 +36,19 @@ pub struct Ship {
pub name: String,
pub sprite: String,
pub size: f32,
pub engines: Vec<Engine>,
pub guns: Vec<ShipGun>,
pub engines: Vec<EnginePoint>,
pub guns: Vec<GunPoint>,
}
#[derive(Debug, Clone)]
pub struct Engine {
pub struct EnginePoint {
pub pos: Point2<f32>,
pub size: f32,
}
#[derive(Debug, Clone)]
pub struct ShipGun {
pub struct GunPoint {
pub pos: Point2<f32>,
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(),
});

View File

@ -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<f32>,
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,
})

View File

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

View File

@ -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<Projectile>,
}
pub struct Ship {
pub physicsbody: PhysicsBody,
pub controls: ShipControls,
sprite: SpriteTexture,
size: f32,
engines: Vec<content::Engine>,
guns: Vec<content::ShipGun>,
}
impl Ship {
pub fn new(ct: &content::Ship, pos: Point2<f32>) -> 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<Projectile> {
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,
}
}
}

29
src/game/ship/mod.rs Normal file
View File

@ -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<Projectile>,
}

129
src/game/ship/outfits.rs Normal file
View File

@ -0,0 +1,129 @@
use cgmath::{Deg, Point3};
use crate::{
content::{self, EnginePoint, GunPoint},
render::{SpriteTexture, SubSprite},
};
/// Represents a gun attached to a specific ship at a certain gunpoint.
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,
}
}
}
/// Represents a specific outfit attached to a specific ship
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<ShipOutfit>,
enginepoints: Vec<content::EnginePoint>,
gunpoints: Vec<content::GunPoint>,
// Minor performance optimization, since we
// rarely need to re-compute these.
engine_flare_sprites: Vec<SubSprite>,
}
impl<'a> ShipOutfits {
pub fn new(enginepoints: Vec<content::EnginePoint>, gunpoints: Vec<content::GunPoint>) -> 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<Item = &mut ShipGun> {
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<Item = (&mut ShipGun, &GunPoint)> {
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<Item = &content::Engine> {
self.outfits
.iter()
.map(|x| x.engine())
.filter(|x| x.is_some())
.map(|x| x.unwrap())
}
pub fn iter_enginepoints(&self) -> impl Iterator<Item = &EnginePoint> {
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<SubSprite> {
return self.engine_flare_sprites.clone();
}
}

132
src/game/ship/ship.rs Normal file
View File

@ -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<ShipOutfit>, pos: Point2<f32>) -> 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<Projectile> {
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
},
}
}
}

View File

@ -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<Vec<SubSprite>>,
}
#[derive(Debug, Clone)]
pub struct SubSprite {
/// The sprite texture to draw
pub texture: SpriteTexture,