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,
+		}
+	}
+}