diff --git a/Cargo.lock b/Cargo.lock index ceee21c..1f13806 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -579,6 +579,8 @@ version = "0.0.0" dependencies = [ "anyhow", "cgmath", + "image", + "nalgebra", "serde", "toml", "walkdir", @@ -598,8 +600,6 @@ dependencies = [ "pollster", "rand", "rapier2d", - "serde", - "toml", "walkdir", "wgpu", "winit", diff --git a/Cargo.toml b/Cargo.toml index 52a2cc1..92620c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,8 +37,6 @@ galactica-content = { path = "crates/content" } # Files image = { version = "0.24", features = ["png"] } -toml = "0.8.8" -serde = { version = "1.0.193", features = ["derive"] } # Graphics winit = "0.28" wgpu = "0.18" diff --git a/assets b/assets index 3360f44..711e4fd 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 3360f44d6ac26a103fddc794d46fe945b49ce27c +Subproject commit 711e4fd58100ee31e41c3c27273f7caa706d8d91 diff --git a/content/ship.toml b/content/ship.toml index e0cdd65..ae4c0eb 100644 --- a/content/ship.toml +++ b/content/ship.toml @@ -1,7 +1,59 @@ [ship."Gypsum"] sprite = "ship::gypsum" size = 100 +aspect = 0.47 hull = 200 engines = [{ x = 0.0, y = -1.05, size = 50.0 }] guns = [{ x = 0.0, y = 1 }, { x = 0.1, y = 0.80 }, { x = -0.1, y = 0.80 }] + + +collision.points = [ + #[rustfmt:skip], + [0.53921, 1.0000], + [0.53921, 0.29343], + [0.63725, 0.29343], + [0.63725, -0.03088], + [0.90019, -0.03088], + [0.90019, -0.61776], + [0.66666, -0.61776], + [0.66666, -0.86486], + [0.24509, -0.86486], + [0.24509, -1.00000], + + [-0.24509, -1.00000], + [-0.24509, -0.86486], + [-0.66666, -0.86486], + [-0.66666, -0.61776], + [-0.90019, -0.61776], + [-0.90019, -0.03088], + [-0.63725, -0.03088], + [-0.63725, 0.29343], + [-0.53921, 0.29343], + [-0.53921, 1.0000], +] + +# TODO: generate this automatically +collision.indices = [ + #[rustfmt:skip], + [0, 1], + [1, 2], + [2, 3], + [3, 4], + [4, 5], + [5, 6], + [6, 7], + [7, 8], + [8, 9], + [9, 10], + [10, 11], + [11, 12], + [12, 13], + [13, 14], + [14, 15], + [15, 16], + [16, 17], + [17, 18], + [18, 19], + [19, 0], +] diff --git a/content/textures.toml b/content/textures.toml new file mode 100644 index 0000000..f7f2408 --- /dev/null +++ b/content/textures.toml @@ -0,0 +1,20 @@ +[texture."starfield"] +path = "starfield.png" + +[texture."star::star"] +path = "star/B-09.png" + +[texture."flare::ion"] +path = "flare/1.png" + +[texture."planet::earth"] +path = "planet/earth.png" + +[texture."planet::luna"] +path = "planet/luna.png" + +[texture."projectile::blaster"] +path = "projectile/blaster.png" + +[texture."ship::gypsum"] +path = "ship/gypsum.png" diff --git a/crates/content/Cargo.toml b/crates/content/Cargo.toml index 28c2d79..14e05d3 100644 --- a/crates/content/Cargo.toml +++ b/crates/content/Cargo.toml @@ -9,3 +9,5 @@ serde = { version = "1.0.193", features = ["derive"] } anyhow = "1.0" cgmath = "0.18.0" walkdir = "2.4.0" +nalgebra = "0.32.3" +image = { version = "0.24", features = ["png"] } diff --git a/crates/content/src/engine.rs b/crates/content/src/engine.rs index 0f15d0c..ed4207d 100644 --- a/crates/content/src/engine.rs +++ b/crates/content/src/engine.rs @@ -1,4 +1,8 @@ -use anyhow::Result; +use std::collections::HashMap; + +use anyhow::{bail, Result}; + +use crate::{Content, TextureHandle}; pub(super) mod syntax { use serde::Deserialize; @@ -29,26 +33,30 @@ pub struct Engine { /// The flare sprite this engine creates. /// Its location and size is determined by a ship's /// engine points. - pub flare_sprite: String, + pub flare_sprite: TextureHandle, } 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![]); - }; + type InputSyntax = HashMap; - let mut out = Vec::new(); + fn build(engine: Self::InputSyntax, ct: &mut Content) -> Result<()> { for (engine_name, engine) in engine { - out.push(Self { - name: engine_name.to_owned(), + let th = match ct.texture_index.get(&engine.flare.sprite) { + None => bail!( + "In engine `{}`: texture `{}` doesn't exist", + engine_name, + engine.flare.sprite + ), + Some(t) => *t, + }; + + ct.engines.push(Self { + name: engine_name, thrust: engine.thrust, - flare_sprite: engine.flare.sprite.clone(), + flare_sprite: th, }); } - return Ok(out); + return Ok(()); } } diff --git a/crates/content/src/gun.rs b/crates/content/src/gun.rs index 25c348e..74e8b33 100644 --- a/crates/content/src/gun.rs +++ b/crates/content/src/gun.rs @@ -1,6 +1,10 @@ -use anyhow::Result; +use std::collections::HashMap; + +use anyhow::{bail, Result}; use cgmath::Deg; +use crate::{Content, TextureHandle}; + pub(super) mod syntax { use serde::Deserialize; // Raw serde syntax structs. @@ -55,7 +59,7 @@ pub struct Gun { #[derive(Debug, Clone)] pub struct Projectile { /// The projectile sprite - pub sprite: String, + pub sprite: TextureHandle, /// The average size of this projectile /// (height in game units) @@ -79,22 +83,26 @@ pub struct Projectile { } impl super::Build for Gun { - fn build(root: &super::syntax::Root) -> Result> { - let gun = if let Some(gun) = &root.gun { - gun - } else { - return Ok(vec![]); - }; + type InputSyntax = HashMap; - let mut out = Vec::new(); + fn build(gun: Self::InputSyntax, ct: &mut Content) -> Result<()> { for (gun_name, gun) in gun { - out.push(Self { - name: gun_name.to_owned(), + let th = match ct.texture_index.get(&gun.projectile.sprite) { + None => bail!( + "In gun `{}`: texture `{}` doesn't exist", + gun_name, + gun.projectile.sprite + ), + Some(t) => *t, + }; + + ct.guns.push(Self { + name: gun_name, spread: Deg(gun.spread), rate: gun.rate, rate_rng: gun.rate_rng, projectile: Projectile { - sprite: gun.projectile.sprite.to_owned(), + sprite: th, size: gun.projectile.size, size_rng: gun.projectile.size_rng, speed: gun.projectile.speed, @@ -106,6 +114,6 @@ impl super::Build for Gun { }); } - return Ok(out); + return Ok(()); } } diff --git a/crates/content/src/lib.rs b/crates/content/src/lib.rs index 808c181..07b730a 100644 --- a/crates/content/src/lib.rs +++ b/crates/content/src/lib.rs @@ -7,22 +7,30 @@ mod engine; mod gun; mod ship; mod system; +mod texture; mod util; pub use engine::Engine; pub use gun::{Gun, Projectile}; pub use ship::{EnginePoint, GunPoint, Ship}; pub use system::{Object, System}; +pub use texture::Texture; use anyhow::{bail, Context, Result}; -use std::collections::HashMap; -use std::{fs::File, io::Read, path::Path}; +use std::{ + cmp::Eq, + collections::HashMap, + fs::File, + hash::Hash, + io::Read, + path::{Path, PathBuf}, +}; use toml; use walkdir::WalkDir; mod syntax { - use super::HashMap; - use super::{engine, gun, ship, system}; + use super::{bail, HashMap, Result}; + use super::{engine, gun, ship, system, texture}; use serde::Deserialize; #[derive(Debug, Deserialize)] @@ -31,16 +39,135 @@ mod syntax { pub ship: Option>, pub system: Option>, pub engine: Option>, + pub texture: Option>, + } + + impl Root { + pub fn new() -> Self { + Self { + gun: None, + ship: None, + system: None, + engine: None, + texture: None, + } + } + + pub fn merge(&mut self, other: Root) -> Result<()> { + // Insert if not exists + // TODO: replace with a macro and try_insert once that is stable + if let Some(a) = other.gun { + if self.gun.is_none() { + self.gun = Some(a); + } else { + let sg = self.gun.as_mut().unwrap(); + for (k, v) in a { + if sg.contains_key(&k) { + bail!("Repeated gun name {k}"); + } else { + sg.insert(k, v); + } + } + } + } + + if let Some(a) = other.ship { + if self.ship.is_none() { + self.ship = Some(a); + } else { + let sg = self.ship.as_mut().unwrap(); + for (k, v) in a { + if sg.contains_key(&k) { + bail!("Repeated ship name {k}"); + } else { + sg.insert(k, v); + } + } + } + } + + if let Some(a) = other.system { + if self.system.is_none() { + self.system = Some(a); + } else { + let sg = self.system.as_mut().unwrap(); + for (k, v) in a { + if sg.contains_key(&k) { + bail!("Repeated system name {k}"); + } else { + sg.insert(k, v); + } + } + } + } + + if let Some(a) = other.engine { + if self.engine.is_none() { + self.engine = Some(a); + } else { + let sg = self.engine.as_mut().unwrap(); + for (k, v) in a { + if sg.contains_key(&k) { + bail!("Repeated engine name {k}"); + } else { + sg.insert(k, v); + } + } + } + } + + if let Some(a) = other.texture { + if self.texture.is_none() { + self.texture = Some(a); + } else { + let sg = self.texture.as_mut().unwrap(); + for (k, v) in a { + if sg.contains_key(&k) { + bail!("Repeated texture name {k}"); + } else { + sg.insert(k, v); + } + } + } + } + + return Ok(()); + } } } trait Build { + type InputSyntax; + /// Build a processed System struct from raw serde data - fn build(root: &syntax::Root) -> Result> + fn build(root: Self::InputSyntax, ct: &mut Content) -> Result<()> where Self: Sized; } +/// Represents a specific texture defined in the content dir. +#[derive(Debug, Clone, Copy)] +pub struct TextureHandle { + /// The index of this texture in content.textures + pub index: usize, + + /// The aspect ratio of this texture (width / height) + pub aspect: f32, +} + +impl Hash for TextureHandle { + fn hash(&self, state: &mut H) { + self.index.hash(state) + } +} + +impl Eq for TextureHandle {} +impl PartialEq for TextureHandle { + fn eq(&self, other: &Self) -> bool { + self.index.eq(&other.index) + } +} + /// Represents generic game content, not connected to any game objects. #[derive(Debug)] pub struct Content { @@ -55,24 +182,22 @@ pub struct Content { /// Engine outfits pub engines: Vec, -} -macro_rules! quick_name_dup_check { - ($array:expr, $root:ident, $build:expr) => {{ - let mut p = $build(&$root)?; - for s in &$array { - for o in &p { - if s.name == o.name { - bail!( - "Error parsing content: duplicate ship names `{}` and `{}`", - s.name, - o.name - ) - } - } - } - $array.append(&mut p); - }}; + /// Textures + pub textures: Vec, + + /// Map strings to texture handles + /// This is never used outside this crate. + texture_index: HashMap, + + /// The texture to use for starfield stars + starfield_handle: Option, + + /// Root directory for textures + texture_root: PathBuf, + + /// Name of starfield texture + starfield_texture_name: String, } impl Content { @@ -83,22 +208,28 @@ impl Content { return Ok(toml::from_str(&file_string)?); } - fn add_root(&mut self, root: syntax::Root) -> Result<()> { - 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(()); + /// Get the texture handle for the starfield texture + pub fn get_starfield_handle(&self) -> TextureHandle { + match self.starfield_handle { + Some(h) => h, + None => unreachable!("Starfield texture hasn't been loaded yet!"), + } + } + + /// Get a texture from a handle + pub fn get_texture(&self, h: TextureHandle) -> &texture::Texture { + // In theory, this could fail if h has a bad index, but that shouldn't ever happen. + // The only TextureHandles that exist should be created by this crate. + return &self.textures[h.index]; } /// Load content from a directory. - pub fn load_dir(path: &str) -> Result { - let mut content = Self { - systems: Vec::new(), - ships: Vec::new(), - guns: Vec::new(), - engines: Vec::new(), - }; + pub fn load_dir( + path: PathBuf, + texture_root: PathBuf, + starfield_texture_name: String, + ) -> Result { + let mut root = syntax::Root::new(); for e in WalkDir::new(path).into_iter().filter_map(|e| e.ok()) { if e.metadata().unwrap().is_file() { @@ -117,14 +248,43 @@ impl Content { } let path = e.path(); - let root = Self::try_parse(path) - .with_context(|| format!("Could not load {:#?}", e.path()))?; - content - .add_root(root) + let this_root = Self::try_parse(path) + .with_context(|| format!("Could not read {}", path.display()))?; + + root.merge(this_root) .with_context(|| format!("Could not parse {}", path.display()))?; } } + let mut content = Self { + systems: Vec::new(), + ships: Vec::new(), + guns: Vec::new(), + engines: Vec::new(), + textures: Vec::new(), + texture_index: HashMap::new(), + starfield_handle: None, + texture_root, + starfield_texture_name, + }; + + // Order here matters, usually + if root.texture.is_some() { + texture::Texture::build(root.texture.take().unwrap(), &mut content)?; + } + if root.ship.is_some() { + ship::Ship::build(root.ship.take().unwrap(), &mut content)?; + } + if root.gun.is_some() { + gun::Gun::build(root.gun.take().unwrap(), &mut content)?; + } + if root.engine.is_some() { + engine::Engine::build(root.engine.take().unwrap(), &mut content)?; + } + if root.system.is_some() { + system::System::build(root.system.take().unwrap(), &mut content)?; + } + return Ok(content); } } diff --git a/crates/content/src/ship.rs b/crates/content/src/ship.rs index 8b1114e..4be2ce1 100644 --- a/crates/content/src/ship.rs +++ b/crates/content/src/ship.rs @@ -1,5 +1,10 @@ -use anyhow::Result; +use std::collections::HashMap; + +use anyhow::{bail, Result}; use cgmath::Point2; +use nalgebra::{point, Point}; + +use crate::{Content, TextureHandle}; pub(super) mod syntax { use serde::Deserialize; @@ -13,6 +18,14 @@ pub(super) mod syntax { pub engines: Vec, pub guns: Vec, pub hull: f32, + pub collision: Collision, + pub aspect: f32, + } + + #[derive(Debug, Deserialize)] + pub struct Collision { + pub points: Vec<[f32; 2]>, + pub indices: Vec<[u32; 2]>, } #[derive(Debug, Deserialize)] @@ -39,7 +52,7 @@ pub struct Ship { pub name: String, /// This ship's sprite - pub sprite: String, + pub sprite: TextureHandle, /// The size of this ship. /// Measured as unrotated height, @@ -56,6 +69,19 @@ pub struct Ship { /// This ship's hull strength pub hull: f32, + + /// Collision shape for this ship + pub collision: Collision, + + /// Remove later + pub aspect: f32, +} + +/// Collision shape for this ship +#[derive(Debug, Clone)] +pub struct Collision { + pub points: Vec>, + pub indices: Vec<[u32; 2]>, } /// An engine point on a ship. @@ -80,20 +106,26 @@ pub struct GunPoint { } impl super::Build for Ship { - fn build(root: &super::syntax::Root) -> Result> { - let ship = if let Some(ship) = &root.ship { - ship - } else { - return Ok(vec![]); - }; + type InputSyntax = HashMap; - let mut out = Vec::new(); + fn build(ship: Self::InputSyntax, ct: &mut Content) -> Result<()> { for (ship_name, ship) in ship { let size = ship.size; + let aspect = ship.aspect; - out.push(Self { + let th = match ct.texture_index.get(&ship.sprite) { + None => bail!( + "In ship `{}`: texture `{}` doesn't exist", + ship_name, + ship.sprite + ), + Some(t) => *t, + }; + + ct.ships.push(Self { + aspect, name: ship_name.to_owned(), - sprite: ship.sprite.to_owned(), + sprite: th, size, hull: ship.hull, engines: ship @@ -101,8 +133,8 @@ impl super::Build for Ship { .iter() .map(|e| EnginePoint { pos: Point2 { - x: e.x * size, - y: e.y * size, + x: e.x * size * aspect / 2.0, + y: e.y * size / 2.0, }, size: e.size, }) @@ -112,14 +144,23 @@ impl super::Build for Ship { .iter() .map(|e| GunPoint { pos: Point2 { - x: e.x * size, - y: e.y * size, + x: e.x * size * aspect / 2.0, + y: e.y * size / 2.0, }, }) .collect(), + collision: Collision { + indices: ship.collision.indices.clone(), + points: ship + .collision + .points + .iter() + .map(|x| point![x[0] * (size / 2.0) * aspect, x[1] * size / 2.0]) + .collect(), + }, }); } - return Ok(out); + return Ok(()); } } diff --git a/crates/content/src/system.rs b/crates/content/src/system.rs index c74be55..78273b2 100644 --- a/crates/content/src/system.rs +++ b/crates/content/src/system.rs @@ -2,7 +2,7 @@ use anyhow::{bail, Context, Result}; use cgmath::{Deg, Point3}; use std::collections::{HashMap, HashSet}; -use crate::util::Polar; +use crate::{util::Polar, Content, TextureHandle}; pub(super) mod syntax { use super::HashMap; @@ -94,7 +94,7 @@ pub struct System { #[derive(Debug)] pub struct Object { /// This object's sprite - pub sprite: String, + pub sprite: TextureHandle, /// This object's size. /// Measured as height in game units. @@ -175,23 +175,27 @@ fn resolve_position( } impl super::Build for System { - fn build(root: &super::syntax::Root) -> Result> { - let system = if let Some(system) = &root.system { - system - } else { - return Ok(vec![]); - }; + type InputSyntax = HashMap; - let mut out = Vec::new(); + fn build(system: Self::InputSyntax, ct: &mut Content) -> Result<()> { for (system_name, system) in system { let mut objects = Vec::new(); for (label, obj) in &system.object { let mut cycle_detector = HashSet::new(); - cycle_detector.insert(label.to_owned()); + cycle_detector.insert(label.clone()); + + let th = match ct.texture_index.get(&obj.sprite) { + None => bail!( + "In system `{}`: texture `{}` doesn't exist", + system_name, + obj.sprite + ), + Some(t) => *t, + }; objects.push(Object { - sprite: obj.sprite.clone(), + sprite: th, position: resolve_position(&system.object, &obj, cycle_detector) .with_context(|| format!("In object {:#?}", label))?, size: obj.size, @@ -199,12 +203,12 @@ impl super::Build for System { }); } - out.push(Self { - name: system_name.to_owned(), + ct.systems.push(Self { + name: system_name, objects, }); } - return Ok(out); + return Ok(()); } } diff --git a/crates/content/src/texture.rs b/crates/content/src/texture.rs new file mode 100644 index 0000000..5897ca4 --- /dev/null +++ b/crates/content/src/texture.rs @@ -0,0 +1,86 @@ +use std::{collections::HashMap, path::PathBuf}; + +use anyhow::{bail, Context, Result}; +use image::io::Reader; + +use crate::{Content, TextureHandle}; + +pub(super) mod syntax { + use std::path::PathBuf; + + use serde::Deserialize; + // Raw serde syntax structs. + // These are never seen by code outside this crate. + + #[derive(Debug, Deserialize)] + pub struct Texture { + pub path: PathBuf, + } +} + +/// Represents a texture that may be used in the game. +#[derive(Debug, Clone)] +pub struct Texture { + /// The name of this texture + pub name: String, + + /// The handle for this texture + pub handle: TextureHandle, + + /// The path to this texture's image file + pub path: PathBuf, +} + +impl super::Build for Texture { + type InputSyntax = HashMap; + + fn build(texture: Self::InputSyntax, ct: &mut Content) -> Result<()> { + for (texture_name, t) in texture { + let path = ct.texture_root.join(t.path); + let reader = Reader::open(&path).with_context(|| { + format!( + "Failed to read texture `{}` from file `{}`", + texture_name, + path.display() + ) + })?; + let dim = reader.into_dimensions().with_context(|| { + format!( + "Failed to dimensions of texture `{}` from file `{}`", + texture_name, + path.display() + ) + })?; + + let h = TextureHandle { + index: ct.textures.len(), + aspect: dim.0 as f32 / dim.1 as f32, + }; + ct.texture_index.insert(texture_name.clone(), h); + + if texture_name == ct.starfield_texture_name { + if ct.starfield_handle.is_none() { + ct.starfield_handle = Some(h) + } else { + // This can't happen, since this is a hashmap. + unreachable!("Found two starfield textures! Something is very wrong.") + } + } + + ct.textures.push(Self { + name: texture_name, + path: path, + handle: h, + }); + } + + if ct.starfield_handle.is_none() { + bail!( + "Could not find a starfield texture (name: `{}`)", + ct.starfield_texture_name + ) + } + + return Ok(()); + } +} diff --git a/src/consts.rs b/src/consts.rs index 4c3c9cd..893fffd 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -25,5 +25,11 @@ pub const STARFIELD_DENSITY: f64 = 0.01; /// Must fit inside an i32 pub const STARFIELD_COUNT: u64 = (STARFIELD_SIZE as f64 * STARFIELD_DENSITY) as u64; +/// Name of starfield texture +pub const STARFIELD_TEXTURE_NAME: &'static str = "starfield"; + /// Root directory of game content pub const CONTENT_ROOT: &'static str = "./content"; + +/// Root directory of game textures +pub const TEXTURE_ROOT: &'static str = "./assets"; diff --git a/src/game/game.rs b/src/game/game.rs index ef17ebf..b9a6ae1 100644 --- a/src/game/game.rs +++ b/src/game/game.rs @@ -40,7 +40,7 @@ impl Game { ); let h2 = physics.add_ship(&ct.ships[0], vec![], Point2 { x: 300.0, y: 300.0 }); - let h3 = physics.add_ship( + let _h3 = physics.add_ship( &ct.ships[0], vec![outfits::ShipOutfit::Gun(outfits::ShipGun::new( ct.guns[0].clone(), @@ -54,7 +54,7 @@ impl Game { let mut shipbehaviors: Vec> = Vec::new(); shipbehaviors.push(shipbehavior::Player::new(h1)); - shipbehaviors.push(shipbehavior::Point::new(h3)); + //shipbehaviors.push(shipbehavior::Point::new(h3)); shipbehaviors.push(shipbehavior::Dummy::new(h2)); Game { diff --git a/src/game/outfits.rs b/src/game/outfits.rs index ed75576..4123cb4 100644 --- a/src/game/outfits.rs +++ b/src/game/outfits.rs @@ -2,7 +2,7 @@ use cgmath::{Deg, Point3}; use crate::{ content::{self, EnginePoint, GunPoint}, - render::{SpriteTexture, SubSprite}, + render::SubSprite, }; /// Represents a gun attached to a specific ship at a certain gunpoint. @@ -103,7 +103,7 @@ impl<'a> ShipOutfits { // 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()) + e.flare_sprite } else { return; }; @@ -116,7 +116,7 @@ impl<'a> ShipOutfits { y: p.pos.y, z: 1.0, }, - texture: t.clone(), + texture: t, angle: Deg(0.0), size: p.size, }) diff --git a/src/game/system.rs b/src/game/system.rs index 126167a..6277c58 100644 --- a/src/game/system.rs +++ b/src/game/system.rs @@ -2,7 +2,7 @@ use cgmath::{Point3, Vector2}; use rand::{self, Rng}; use super::SystemObject; -use crate::{consts, content, render::Sprite, render::SpriteTexture}; +use crate::{consts, content, render::Sprite}; pub struct StarfieldStar { /// Star coordinates, in world space. @@ -50,7 +50,7 @@ impl System { for o in &ct.objects { s.bodies.push(SystemObject { pos: o.position, - sprite: SpriteTexture(o.sprite.to_owned()), + sprite: o.sprite, size: o.size, angle: o.angle, }); diff --git a/src/game/systemobject.rs b/src/game/systemobject.rs index 5b73365..c14fa61 100644 --- a/src/game/systemobject.rs +++ b/src/game/systemobject.rs @@ -1,9 +1,10 @@ use cgmath::{Deg, Point3}; +use galactica_content::TextureHandle; -use crate::{render::Sprite, render::SpriteTexture}; +use crate::render::Sprite; pub struct SystemObject { - pub sprite: SpriteTexture, + pub sprite: TextureHandle, pub pos: Point3, pub size: f32, pub angle: Deg, diff --git a/src/main.rs b/src/main.rs index 8ca5a91..7717fb5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,8 @@ mod physics; mod render; mod shipbehavior; +use std::path::PathBuf; + use anyhow::Result; use galactica_content as content; use winit::{ @@ -16,18 +18,22 @@ use winit::{ fn main() -> Result<()> { // TODO: error if missing - let content = content::Content::load_dir(consts::CONTENT_ROOT)?; - let game = game::Game::new(content); + let content = content::Content::load_dir( + PathBuf::from(consts::CONTENT_ROOT), + PathBuf::from(consts::TEXTURE_ROOT), + consts::STARFIELD_TEXTURE_NAME.to_owned(), + )?; - pollster::block_on(run(game))?; + pollster::block_on(run(content))?; return Ok(()); } -async fn run(mut game: game::Game) -> Result<()> { +async fn run(content: content::Content) -> Result<()> { let event_loop = EventLoop::new(); let window = WindowBuilder::new().build(&event_loop).unwrap(); - let mut gpu = render::GPUState::new(window).await?; + let mut gpu = render::GPUState::new(window, &content).await?; + let mut game = game::Game::new(content); gpu.update_starfield_buffer(&game); diff --git a/src/objects/projectile.rs b/src/objects/projectile.rs index 12cc6bd..c0b9b0d 100644 --- a/src/objects/projectile.rs +++ b/src/objects/projectile.rs @@ -1,18 +1,16 @@ use cgmath::Point3; +use galactica_content::TextureHandle; use rapier2d::{ dynamics::{RigidBody, RigidBodyBuilder, RigidBodyHandle}, geometry::{ColliderBuilder, ColliderHandle}, }; -use crate::{ - physics::util, - render::{Sprite, SpriteTexture}, -}; +use crate::{physics::util, render::Sprite}; pub struct ProjectileBuilder { pub rigid_body: RigidBodyBuilder, pub collider: ColliderBuilder, - pub sprite: SpriteTexture, + pub sprite: TextureHandle, pub lifetime: f32, pub size: f32, pub damage: f32, @@ -35,7 +33,7 @@ impl ProjectileBuilder { pub struct Projectile { pub rigid_body: RigidBodyHandle, pub collider: ColliderHandle, - pub sprite: SpriteTexture, + pub sprite: TextureHandle, pub lifetime: f32, pub size: f32, pub damage: f32, diff --git a/src/objects/ship.rs b/src/objects/ship.rs index a8952a9..42c5fb6 100644 --- a/src/objects/ship.rs +++ b/src/objects/ship.rs @@ -1,4 +1,5 @@ use cgmath::{Deg, EuclideanSpace, Matrix2, Rad, Vector2}; +use content::TextureHandle; use nalgebra::vector; use rand::Rng; use rapier2d::{ @@ -12,7 +13,7 @@ use crate::{ content, game::outfits, physics::{util, ShipHandle}, - render::{Sprite, SpriteTexture}, + render::Sprite, }; pub struct ShipTickResult { @@ -41,7 +42,7 @@ pub struct Ship { pub physics_handle: ShipHandle, outfits: outfits::ShipOutfits, - sprite: SpriteTexture, + sprite: TextureHandle, size: f32, pub hull: f32, pub controls: ShipControls, @@ -61,7 +62,7 @@ impl Ship { Ship { physics_handle, outfits: o, - sprite: SpriteTexture(c.sprite.clone()), + sprite: c.sprite, size: c.size, hull: c.hull, controls: ShipControls::new(), @@ -107,7 +108,7 @@ impl Ship { out.push(ProjectileBuilder { rigid_body: p_r, collider: p_c, - sprite: SpriteTexture(g.kind.projectile.sprite.clone()), + sprite: g.kind.projectile.sprite, lifetime: g.kind.projectile.lifetime + rng.gen_range( -g.kind.projectile.lifetime_rng..=g.kind.projectile.lifetime_rng, @@ -132,11 +133,11 @@ impl Ship { } if self.controls.right { - r.apply_torque_impulse(500.0 * t, true); + r.apply_torque_impulse(-500.0 * t, true); } if self.controls.left { - r.apply_torque_impulse(-500.0 * t, true); + r.apply_torque_impulse(500.0 * t, true); } let p = if self.controls.guns { diff --git a/src/physics/physics.rs b/src/physics/physics.rs index 6b9cd22..ade3667 100644 --- a/src/physics/physics.rs +++ b/src/physics/physics.rs @@ -84,10 +84,15 @@ impl Physics { outfits: Vec, position: Point2, ) -> ShipHandle { + let cl = ColliderBuilder::convex_decomposition( + &ct.collision.points[..], + &ct.collision.indices[..], + ) + .mass(1.0); + let rb = RigidBodyBuilder::dynamic() .translation(vector![position.x, position.y]) .can_sleep(false); - let cl = ColliderBuilder::ball(50.0).restitution(0.7).mass(1.0); let r = self.wrapper.rigid_body_set.insert(rb.build()); let c = self.wrapper.collider_set.insert_with_parent( diff --git a/src/physics/util.rs b/src/physics/util.rs index a1c21a2..637d6b1 100644 --- a/src/physics/util.rs +++ b/src/physics/util.rs @@ -11,10 +11,10 @@ pub fn rigidbody_position(r: &RigidBody) -> cgmath::Point2 { pub fn rigidbody_angle(r: &RigidBody) -> Deg { Vector2 { - x: r.rotation().re, - y: r.rotation().im, + x: r.rotation().im, + y: r.rotation().re, } - .angle(Vector2 { x: 1.0, y: 0.0 }) + .angle(Vector2 { x: 0.0, y: 1.0 }) .into() } diff --git a/src/render/consts.rs b/src/render/consts.rs index b605e39..9ae302f 100644 --- a/src/render/consts.rs +++ b/src/render/consts.rs @@ -8,12 +8,6 @@ pub const SPRITE_INSTANCE_LIMIT: u64 = 500; // Must be small enough to fit in an i32 pub const STARFIELD_INSTANCE_LIMIT: u64 = consts::STARFIELD_COUNT * 24; -/// Texture index file name -pub const TEXTURE_INDEX_NAME: &'static str = "_index.toml"; - -/// Texture index dir -pub const TEXTURE_INDEX_PATH: &'static str = "./assets"; - /// Shader entry points pub const SHADER_MAIN_VERTEX: &'static str = "vertex_main"; pub const SHADER_MAIN_FRAGMENT: &'static str = "fragment_main"; diff --git a/src/render/gpustate.rs b/src/render/gpustate.rs index f61c796..e886bc5 100644 --- a/src/render/gpustate.rs +++ b/src/render/gpustate.rs @@ -1,6 +1,7 @@ use anyhow::Result; use bytemuck; use cgmath::{Deg, EuclideanSpace, Matrix4, Point2, Vector2, Vector3}; +use galactica_content::Content; use std::{iter, rc::Rc}; use wgpu; use winit::{self, dpi::PhysicalSize, window::Window}; @@ -45,7 +46,7 @@ struct VertexBuffers { } impl GPUState { - pub async fn new(window: Window) -> Result { + pub async fn new(window: Window, ct: &Content) -> Result { let window_size = window.inner_size(); let window_aspect = window_size.width as f32 / window_size.height as f32; @@ -128,7 +129,7 @@ impl GPUState { // Load uniforms let global_data = GlobalData::new(&device); - let texture_array = TextureArray::new(&device, &queue)?; + let texture_array = TextureArray::new(&device, &queue, ct)?; // Make sure these match the indices in each shader let bind_group_layouts = &[ @@ -215,7 +216,7 @@ impl GPUState { } - game.camera.pos.to_vec()) / s.pos.z }; - let texture = self.texture_array.get_sprite_texture(s.texture); + let texture = self.texture_array.get_texture(s.texture); // Game dimensions of this sprite post-scale. // Don't divide by 2, we use this later. @@ -298,7 +299,7 @@ impl GPUState { parent_pos: Point2, parent_angle: Deg, ) { - let texture = self.texture_array.get_sprite_texture(s.texture); + let texture = self.texture_array.get_texture(s.texture); let scale = s.size / (s.pos.z * game.camera.zoom); let sprite_aspect_and_scale = Matrix4::from_nonuniform_scale(texture.aspect * scale, scale, 1.0); diff --git a/src/render/mod.rs b/src/render/mod.rs index 79a5c48..857441d 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -8,9 +8,3 @@ mod vertexbuffer; pub use gpustate::GPUState; pub use sprite::{Sprite, SubSprite}; - -/// A handle to a sprite texture -/// TODO: This should be easy to copy, -/// but string references create unnecessary complexity -#[derive(Debug, Clone)] -pub struct SpriteTexture(pub String); diff --git a/src/render/sprite.rs b/src/render/sprite.rs index 853e8c7..4497970 100644 --- a/src/render/sprite.rs +++ b/src/render/sprite.rs @@ -1,11 +1,10 @@ use cgmath::{Deg, Point3}; - -use super::SpriteTexture; +use galactica_content::TextureHandle; #[derive(Debug, Clone)] pub struct Sprite { /// The sprite texture to draw - pub texture: SpriteTexture, + pub texture: TextureHandle, /// This object's position, in world coordinates. pub pos: Point3, @@ -25,7 +24,7 @@ pub struct Sprite { #[derive(Debug, Clone)] pub struct SubSprite { /// The sprite texture to draw - pub texture: SpriteTexture, + pub texture: TextureHandle, /// This object's position, in world coordinates. /// This is relative to this sprite's parent. diff --git a/src/render/texturearray/array.rs b/src/render/texturearray/array.rs index 4fc2c62..af5c34f 100644 --- a/src/render/texturearray/array.rs +++ b/src/render/texturearray/array.rs @@ -1,38 +1,48 @@ use anyhow::Result; -use std::{collections::HashMap, num::NonZeroU32, path::PathBuf}; +use galactica_content::{Content, TextureHandle}; +use std::{collections::HashMap, fs::File, io::Read, num::NonZeroU32}; use wgpu::BindGroupLayout; -use super::{ - super::consts::TEXTURE_INDEX_PATH, loader::TextureLoader, Texture, TextureArrayConfig, -}; -use crate::render::SpriteTexture; +use super::{rawtexture::RawTexture, Texture}; pub struct TextureArray { pub bind_group: wgpu::BindGroup, pub bind_group_layout: BindGroupLayout, - textures: HashMap, - config: TextureArrayConfig, + starfield_handle: TextureHandle, + textures: HashMap, } impl TextureArray { pub fn get_starfield_texture(&self) -> Texture { - return self.get_texture(&self.config.starfield); + *self.textures.get(&self.starfield_handle).unwrap() } - pub fn get_sprite_texture(&self, sprite: SpriteTexture) -> Texture { - return self.get_texture(&sprite.0); - } - - fn get_texture(&self, name: &str) -> Texture { - match self.textures.get(name) { + pub fn get_texture(&self, handle: TextureHandle) -> Texture { + match self.textures.get(&handle) { Some(x) => *x, - None => self.get_texture(&self.config.error), + None => unreachable!("Tried to get a texture that doesn't exist"), } } - pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Result { + pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, ct: &Content) -> Result { // Load all textures - let loader = TextureLoader::load(device, queue, &PathBuf::from(TEXTURE_INDEX_PATH))?; + let mut texture_data = Vec::new(); + let mut textures = HashMap::new(); + + for t in &ct.textures { + let mut f = File::open(&t.path)?; + let mut bytes = Vec::new(); + f.read_to_end(&mut bytes)?; + textures.insert( + t.handle, + Texture { + index: texture_data.len() as u32, + aspect: t.handle.aspect, + }, + ); + + texture_data.push(RawTexture::from_bytes(&device, &queue, &bytes, &t.name)?); + } let sampler = device.create_sampler(&wgpu::SamplerDescriptor { address_mode_u: wgpu::AddressMode::ClampToEdge, @@ -56,7 +66,7 @@ impl TextureArray { view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, }, - count: NonZeroU32::new(loader.texture_data.len() as u32), + count: NonZeroU32::new(texture_data.len() as u32), }, // Texture sampler wgpu::BindGroupLayoutEntry { @@ -68,7 +78,7 @@ impl TextureArray { ], }); - let views: Vec<&wgpu::TextureView> = loader.texture_data.iter().map(|x| &x.view).collect(); + let views: Vec<&wgpu::TextureView> = texture_data.iter().map(|x| &x.view).collect(); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("TextureArray bind group"), layout: &bind_group_layout, @@ -88,8 +98,8 @@ impl TextureArray { return Ok(Self { bind_group, bind_group_layout, - textures: loader.textures, - config: loader.config, + textures: textures, + starfield_handle: ct.get_starfield_handle(), }); } } diff --git a/src/render/texturearray/loader.rs b/src/render/texturearray/loader.rs deleted file mode 100644 index 6b4895d..0000000 --- a/src/render/texturearray/loader.rs +++ /dev/null @@ -1,193 +0,0 @@ -use anyhow::Result; -use std::{collections::HashMap, fs::File, io::Read, path::Path, path::PathBuf}; - -use super::{ - super::consts::TEXTURE_INDEX_NAME, rawtexture::RawTexture, Texture, TextureArrayConfig, -}; - -mod fields { - - use super::{File, HashMap, Path, PathBuf, RawTexture, Read, Result, TextureArrayConfig}; - use serde::Deserialize; - - // Config is used outside of this file, - // so it is defined in mod.rs. - - #[derive(Deserialize)] - pub struct Index { - pub config: TextureArrayConfig, - pub include: Option>, - pub texture: Vec, - } - - impl Index { - pub fn to_sub(self) -> (TextureArrayConfig, SubIndex) { - ( - self.config, - SubIndex { - include: self.include, - texture: self.texture, - }, - ) - } - } - - #[derive(Deserialize)] - pub struct SubIndex { - pub include: Option>, - pub texture: Vec, - } - - #[derive(Deserialize)] - pub struct Texture { - pub name: String, - pub path: String, - } - - impl Texture { - pub fn read( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - root: &Path, - ) -> Result { - let mut f = File::open(root.join(&self.path))?; - let mut bytes = Vec::new(); - f.read_to_end(&mut bytes)?; - RawTexture::from_bytes(&device, &queue, &bytes, &self.name) - } - } -} - -pub struct TextureLoader { - pub textures: HashMap, - pub texture_data: Vec, - pub config: TextureArrayConfig, -} - -impl TextureLoader { - pub fn load(device: &wgpu::Device, queue: &wgpu::Queue, index_path: &Path) -> Result { - let index: fields::Index = { - let mut texture_index_string = String::new(); - let _ = File::open(index_path.join(TEXTURE_INDEX_NAME))? - .read_to_string(&mut texture_index_string); - toml::from_str(&texture_index_string)? - }; - - let (config, index) = index.to_sub(); - let texture_raw = - Self::inner_load_textures(device, queue, index, index_path, "".to_string())?; - - // TODO: validate configuration - // TODO: handle error states - - let mut textures = HashMap::new(); - let mut texture_data = Vec::new(); - let mut i = 0; - for (name, data) in texture_raw { - textures.insert( - name, - Texture { - index: i, - aspect: data.dimensions.0 as f32 / data.dimensions.1 as f32, - }, - ); - texture_data.push(data); - i += 1; - } - - return Ok(Self { - textures, - texture_data, - config, - }); - } - - fn inner_load_textures( - device: &wgpu::Device, - queue: &wgpu::Queue, - index: fields::SubIndex, - texture_root: &Path, - prefix: String, - ) -> Result> { - // Load all textures - let mut texture_raw = HashMap::new(); - for t in index.texture { - let data = match t.read(device, queue, &texture_root) { - Ok(r) => r, - Err(_) => { - println!( - "[WARNING] Failed to load texture `{}` from `{}`: file doesn't exist.", - if prefix.is_empty() { - t.name - } else { - format!("{prefix}::{}", t.name) - }, - texture_root.display(), - ); - continue; - } - }; - - if texture_raw.contains_key(&t.name) { - println!( - "[WARNING] Subindex in `{}` has a duplicate texture `{}`, skipping.", - texture_root.display(), - if prefix.is_empty() { - t.name - } else { - format!("{prefix}::{}", t.name) - }, - ); - continue; - } - - texture_raw.insert( - if prefix.is_empty() { - t.name - } else { - format!("{prefix}::{}", t.name) - }, - data, - ); - } - - // Load included files - if let Some(include) = index.include { - for (path, sub_prefix) in include { - let sub_root = texture_root.join(&path); - let index: fields::SubIndex = { - let mut texture_index_string = String::new(); - let _ = File::open(sub_root.join(TEXTURE_INDEX_NAME))? - .read_to_string(&mut texture_index_string); - toml::from_str(&texture_index_string)? - }; - - let sub_textures = Self::inner_load_textures( - device, - queue, - index, - &sub_root, - if prefix.is_empty() { - sub_prefix - } else { - format!("{prefix}::{sub_prefix}") - }, - )?; - for (s, data) in sub_textures { - if texture_raw.contains_key(&s) { - println!( - "[WARNING] Subindex in `{}` has a duplicate texture `{}`, skipping.", - sub_root.display(), - &s, - ); - continue; - } - texture_raw.insert(s, data); - } - } - } - - return Ok(texture_raw); - } -} diff --git a/src/render/texturearray/mod.rs b/src/render/texturearray/mod.rs index 9da07c3..3b08995 100644 --- a/src/render/texturearray/mod.rs +++ b/src/render/texturearray/mod.rs @@ -1,18 +1,10 @@ mod array; -mod loader; mod rawtexture; pub use array::TextureArray; -use serde::Deserialize; #[derive(Debug, Clone, Copy)] pub struct Texture { pub index: u32, // Index in texture array pub aspect: f32, // width / height } - -#[derive(Deserialize)] -pub struct TextureArrayConfig { - pub error: String, - pub starfield: String, -} diff --git a/src/render/texturearray/rawtexture.rs b/src/render/texturearray/rawtexture.rs index c9ebbba..1a7e82c 100644 --- a/src/render/texturearray/rawtexture.rs +++ b/src/render/texturearray/rawtexture.rs @@ -3,7 +3,6 @@ use image::GenericImageView; pub(super) struct RawTexture { pub(super) view: wgpu::TextureView, - pub(super) dimensions: (u32, u32), } impl RawTexture { @@ -61,6 +60,6 @@ impl RawTexture { size, ); - Ok(Self { view, dimensions }) + Ok(Self { view }) } }