diff --git a/Cargo.lock b/Cargo.lock index 80a85729cb313bcf8390acb3ebcdc4142551cf60..78ac7cedd3cdf87735f94d3f62e186e1d133a6a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1904,6 +1904,7 @@ version = "0.12.0" dependencies = [ "anyhow", "bevy", + "glam", "log", "micro_autotile", "num-traits", diff --git a/Cargo.toml b/Cargo.toml index 19c2021676812a85fe03ece414f36dccdccf0188..1270b53d2ca80f668bfe7abe886bf1c52a0130e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,4 +45,5 @@ serde_json = "1.0" num-traits = "0.2" quadtree_rs = "0.1" micro_autotile = { version = "0.2", optional = true } +glam = { version = "0.27", features = ["serde"] } diff --git a/src/lib.rs b/src/lib.rs index 65855fad18a537d4f0a3c16fe570bcdee801d950..c33749b12ba94b6831165e8901a5b6f67431be36 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,11 @@ mod assets; mod camera; #[cfg(feature = "bevy")] mod map_query; +#[cfg(not(feature = "bevy"))] +mod map_query_neutral; +#[cfg(not(feature = "bevy"))] +use map_query_neutral as map_query; + #[cfg(feature = "bevy")] mod pregen; mod system; @@ -98,7 +103,6 @@ pub use camera::CameraBounder; pub use ldtk::{LdtkProject, Level, AutoLayerRuleGroup}; #[cfg(feature = "bevy")] pub use ldtk::LdtkLoader; -#[cfg(feature = "bevy")] pub use map_query::{CameraBounds, InstanceRef, MapQuery}; #[cfg(feature = "autotile")] pub use micro_autotile as autotile; diff --git a/src/map_query_neutral.rs b/src/map_query_neutral.rs new file mode 100644 index 0000000000000000000000000000000000000000..441000a0eb3af47b8fbbf7ce1271678a8ce2a6d2 --- /dev/null +++ b/src/map_query_neutral.rs @@ -0,0 +1,238 @@ +use std::fmt::Debug; +use std::ops::Index; +use std::str::FromStr; + +use crate::ldtk::EntityInstance; +use crate::{LdtkLayer, LdtkLevel}; + +pub struct MapQuery {} + +#[derive(Clone)] +pub struct InstanceRef<'a> { + pub entity: &'a EntityInstance, +} + +impl<'a> InstanceRef<'a> { + /// Get the leftmost pixel of this entity's anchor point + pub fn x(&self) -> i64 { + self.entity.px[0] + } + /// Get the topmost pixel of this entity's anchor point + pub fn y(&self) -> i64 { + self.entity.px[1] + } + /// Get the pixel width of this entity + pub fn width(&self) -> i64 { + self.entity.width + } + /// Get the pixel width of this entity + pub fn height(&self) -> i64 { + self.entity.height + } + + /// Get the category that this instance belongs to. Exactly matches the string name + /// found in the LDTK entities list + pub fn get_type(&self) -> &'a String { + &self.entity.identifier + } + /// Try to get a type safe representation of this entity's type, as long as the target type + /// can be produced from a [str] representation + /// + /// ## Example + /// + /// ``` + /// # use std::str::FromStr; + /// # use micro_ldtk::InstanceRef; + /// # use micro_ldtk::ldtk::EntityInstance; + /// + /// #[derive(PartialEq, Debug)] + /// enum MyEntityType { + /// Player, + /// Monster, + /// } + /// + /// impl FromStr for MyEntityType { + /// # type Err = (); + /// fn from_str(s: &str) -> Result<Self, Self::Err> { + /// match s { + /// "player" => Ok(Self::Player), + /// "monster" => Ok(Self::Monster), + /// # _ => panic!("Oh no") + /// } + /// } + /// } + /// + /// let data_from_ldtk: EntityInstance = EntityInstance { + /// identifier: "player".to_string(), + /// // ...other properties + /// # smart_color: "".to_string(), + /// # grid: vec![], + /// # pivot: vec![], + /// # tags: vec![], + /// # tile: None, + /// # world_x: None, + /// # world_y: None, + /// # def_uid: 0, + /// # field_instances: vec![], + /// # height: 0, + /// # iid: "".to_string(), + /// # px: vec![], + /// # width: 0, + /// }; + /// # + /// # let process_ldtk_data = || -> InstanceRef<'_> { + /// # InstanceRef { + /// # entity: &data_from_ldtk, + /// # } + /// # }; + /// + /// let my_entity_type: InstanceRef<'_> = process_ldtk_data(); + /// assert_eq!(my_entity_type.try_get_typed_id(), Ok(MyEntityType::Player)); + /// ``` + pub fn try_get_typed_id<T: FromStr>(&self) -> Result<T, T::Err> { + T::from_str(self.get_type().as_str()) + } + + /// Retrieve an associated property from this instance. Will return [serde_json::Value::Null] + /// if there is no property with the given name + pub fn property(&self, name: impl ToString) -> serde_json::Value { + self[name].clone() + } + + /// Get a reference to the inner instance of this instance ref + pub fn instance_ref(&self) -> &EntityInstance { + self.entity + } +} + +impl<'a, T: ToString> Index<T> for InstanceRef<'a> { + type Output = serde_json::Value; + + fn index(&self, index: T) -> &Self::Output { + let name = index.to_string(); + for field in &self.entity.field_instances { + if field.identifier == name { + return field.value.as_ref().unwrap_or(&serde_json::Value::Null); + } + } + + &serde_json::Value::Null + } +} + +#[derive(Copy, Clone, Debug)] +pub struct CameraBounds { + pub left: f32, + pub top: f32, + pub bottom: f32, + pub right: f32, +} + +impl CameraBounds { + pub fn get_min_x(&self, camera_width: f32) -> f32 { + self.left + (camera_width / 2.0) // - (get_ldtk_tile_scale() / 2.0) + } + pub fn get_max_x(&self, camera_width: f32) -> f32 { + self.right - (camera_width / 2.0) // - (get_ldtk_tile_scale() / 2.0) + } + pub fn get_min_y(&self, camera_height: f32) -> f32 { + self.bottom + (camera_height / 2.0) // - (get_ldtk_tile_scale() / 2.0) + } + pub fn get_max_y(&self, camera_height: f32) -> f32 { + self.top - (camera_height / 2.0) // - (get_ldtk_tile_scale() / 2.0) + } +} + +impl MapQuery { + // --- We put our logic in static accessors because we might source a level other + // --- than the currently active one. 'active' methods are a convenience to + // --- call the static accessors on whatever the current level is + + /// Perform an action on each layer of the given LDTK level + pub fn for_each_layer_of(level: &LdtkLevel, mut cb: impl FnMut(&LdtkLayer)) { + for layer in level.layers() { + cb(layer); + } + } + + /// Retrieve an iterator over every layer in the given level, regardless of type + pub fn get_layers_of(level: &LdtkLevel) -> impl DoubleEndedIterator<Item = &LdtkLayer> { + level.layers() + } + + /// Retrieve a reference to every entity stored in the given level, regardless of which layer it is found on + pub fn get_entities_of(level: &LdtkLevel) -> Vec<&EntityInstance> { + level + .layers() + .flat_map(|layer| layer.as_ref().entity_instances.iter()) + .collect() + } + + /// Retrieve an enhanced wrapper to every entity stored in the given level, regardless of which layer it is found on + pub fn get_instance_refs_of(level: &LdtkLevel) -> Vec<InstanceRef> { + level + .layers() + .flat_map(|layer| { + layer + .as_ref() + .entity_instances + .iter() + .map(|inst| InstanceRef { entity: inst }) + }) + .collect() + } + + /// Retrieve a reference to every entity stored in the given level that matches the specified type name. + /// This must exactly match the name shown in the LDTK entity list + pub fn get_filtered_entities_of( + level: &LdtkLevel, + entity_type: impl ToString, + ) -> Vec<&EntityInstance> { + let e_type = entity_type.to_string(); + level + .layers() + .flat_map(|layer| layer.as_ref().entity_instances.iter()) + .filter(|inst| inst.identifier == e_type) + .collect() + } + + /// Retrieve an enhanced wrapper to every entity stored in the given level that matches the specified type name. + /// This must exactly match the name shown in the LDTK entity list + pub fn get_filtered_instance_refs_of( + level: &LdtkLevel, + entity_type: impl ToString, + ) -> Vec<InstanceRef> { + let e_type = entity_type.to_string(); + level + .layers() + .flat_map(|layer| { + layer + .as_ref() + .entity_instances + .iter() + .map(|inst| InstanceRef { entity: inst }) + }) + .filter(|inst| inst.entity.identifier == e_type) + .collect() + } + + /// Retrieve an owned copy of all entity data in the given level + pub fn get_owned_entities_of(level: &LdtkLevel) -> Vec<EntityInstance> { + level + .layers() + .flat_map(|layer| layer.as_ref().entity_instances.iter().cloned()) + .collect() + } + + /// Use the size of the level to create a zero-based rectangle indicating the boundaries that a camera should + ///stay within to avoid showing any out-of-level space + pub fn get_camera_bounds_of(level: &LdtkLevel) -> CameraBounds { + let level = level.level_ref(); + CameraBounds { + left: 0.0, + top: level.px_hei as f32, + bottom: 0.0, + right: level.px_wid as f32, + } + } +} diff --git a/src/system/mod.rs b/src/system/mod.rs index 45d44c8a67301d9dfb88152919bf9c2d6947e1b6..b899ec933ce6f7c8005c79e5e4436d4a77892888 100644 --- a/src/system/mod.rs +++ b/src/system/mod.rs @@ -1,11 +1,9 @@ #[cfg(feature = "bevy")] mod locator; -#[cfg(feature = "bevy")] mod types; mod utils; #[cfg(feature = "bevy")] pub use locator::*; -#[cfg(feature = "bevy")] pub use types::*; pub use utils::*; diff --git a/src/system/types.rs b/src/system/types.rs index 8debdbd5651ea660d8e1c1c52339556b67694efc..48e8587bcd272dfeef70fdbb4e0b31484a9b7f4d 100644 --- a/src/system/types.rs +++ b/src/system/types.rs @@ -4,9 +4,11 @@ use std::ops::{Deref, DerefMut, Index}; use std::path::Path; #[cfg(feature = "bevy")] -use bevy::math::{IVec2, Rect, UVec2, Vec2}; -#[cfg(feature = "bevy")] -use bevy::prelude::Event; +use bevy::math::Rect; +#[cfg(not(feature = "bevy"))] +use compat::Rect; + +use glam::{IVec2, UVec2, Vec2}; use num_traits::AsPrimitive; use quadtree_rs::area::AreaBuilder; use quadtree_rs::point::Point; @@ -17,11 +19,12 @@ use serde_json::{Map, Number, Value}; use crate::ldtk::{EntityInstance, FieldInstance, LayerInstance, Level, TileInstance}; use crate::system::Indexer; use crate::{get_ldtk_tile_scale, px_to_grid}; -#[cfg(feature = "bevy")] use crate::MapQuery; +mod compat; + #[derive(Default, Clone, Debug, Ord, PartialOrd, PartialEq, Eq)] -#[cfg_attr(feature = "bevy", derive(Event))] +#[cfg_attr(feature = "bevy", derive(bevy::prelude::Event))] pub struct LevelDataUpdated(pub String); pub struct TileRef<'a> { @@ -93,13 +96,11 @@ where Self(value.0.as_(), value.1.as_()) } } -#[cfg(feature = "bevy")] impl From<UVec2> for SpatialIndex { fn from(value: UVec2) -> Self { Self(value.x as i64, value.y as i64) } } -#[cfg(feature = "bevy")] impl From<IVec2> for SpatialIndex { fn from(value: IVec2) -> Self { Self(value.x as i64, value.y as i64) diff --git a/src/system/types/compat.rs b/src/system/types/compat.rs new file mode 100644 index 0000000000000000000000000000000000000000..c1d95c5fdbb4c4dfbaf2ef05f3be675abc350260 --- /dev/null +++ b/src/system/types/compat.rs @@ -0,0 +1,132 @@ +#![allow(dead_code)] + +use glam::Vec2; +use serde::{Deserialize, Serialize}; + +#[repr(C)] +#[derive(Default, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +pub struct Rect { + pub min: Vec2, + pub max: Vec2, +} + +impl Rect { + pub const EMPTY: Self = Self { + max: Vec2::NEG_INFINITY, + min: Vec2::INFINITY, + }; + #[inline] + pub fn new(x0: f32, y0: f32, x1: f32, y1: f32) -> Self { + Self::from_corners(Vec2::new(x0, y0), Vec2::new(x1, y1)) + } + + #[inline] + pub fn from_corners(p0: Vec2, p1: Vec2) -> Self { + Self { + min: p0.min(p1), + max: p0.max(p1), + } + } + + #[inline] + pub fn from_center_size(origin: Vec2, size: Vec2) -> Self { + assert!(size.cmpge(Vec2::ZERO).all(), "Rect size must be positive"); + let half_size = size / 2.; + Self::from_center_half_size(origin, half_size) + } + + #[inline] + pub fn from_center_half_size(origin: Vec2, half_size: Vec2) -> Self { + assert!( + half_size.cmpge(Vec2::ZERO).all(), + "Rect half_size must be positive" + ); + Self { + min: origin - half_size, + max: origin + half_size, + } + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.min.cmpge(self.max).any() + } + + #[inline] + pub fn width(&self) -> f32 { + self.max.x - self.min.x + } + + #[inline] + pub fn height(&self) -> f32 { + self.max.y - self.min.y + } + + #[inline] + pub fn size(&self) -> Vec2 { + self.max - self.min + } + + #[inline] + pub fn half_size(&self) -> Vec2 { + self.size() * 0.5 + } + + #[inline] + pub fn center(&self) -> Vec2 { + (self.min + self.max) * 0.5 + } + + #[inline] + pub fn contains(&self, point: Vec2) -> bool { + (point.cmpge(self.min) & point.cmple(self.max)).all() + } + + #[inline] + pub fn union(&self, other: Self) -> Self { + Self { + min: self.min.min(other.min), + max: self.max.max(other.max), + } + } + + #[inline] + pub fn union_point(&self, other: Vec2) -> Self { + Self { + min: self.min.min(other), + max: self.max.max(other), + } + } + + #[inline] + pub fn intersect(&self, other: Self) -> Self { + let mut r = Self { + min: self.min.max(other.min), + max: self.max.min(other.max), + }; + // Collapse min over max to enforce invariants and ensure e.g. width() or + // height() never return a negative value. + r.min = r.min.min(r.max); + r + } + + #[inline] + pub fn inflate(&self, expansion: f32) -> Self { + let mut r = Self { + min: self.min - expansion, + max: self.max + expansion, + }; + // Collapse min over max to enforce invariants and ensure e.g. width() or + // height() never return a negative value. + r.min = r.min.min(r.max); + r + } + + pub fn normalize(&self, other: Self) -> Self { + let outer_size = other.size(); + Self { + min: (self.min - other.min) / outer_size, + max: (self.max - other.min) / outer_size, + } + } +}