Skip to content
Snippets Groups Projects
types.rs 7.73 KiB
Newer Older
use std::fmt::{Debug, Formatter};
use std::path::Path;

use bevy::math::{IVec2, UVec2};
use bevy::utils::HashMap;
use ldtk_rust::{LayerInstance, Level, TileInstance};
use num_traits::AsPrimitive;
Louis's avatar
Louis committed
use quadtree_rs::area::{Area, AreaBuilder};
use quadtree_rs::point::Point;
use quadtree_rs::Quadtree;
use serde_json::Value;

use crate::utils::{Indexer, SerdeClone};
Louis's avatar
Louis committed
use crate::{get_ldtk_tile_scale, px_to_grid, MapQuery};

#[derive(Default, Clone, Debug, Ord, PartialOrd, PartialEq, Eq)]
pub struct LevelDataUpdated(pub String);

pub struct TileRef<'a> {
	pub tile: &'a TileInstance,
}

#[repr(C)]
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default, Debug)]
pub enum TileFlip {
	#[default]
	None = 0,
	Horizontal = 1,
	Vertical = 2,
	Both = 3,
}

impl TileFlip {
	pub fn is_horizontal(&self) -> bool {
		match self {
			Self::None | Self::Vertical => false,
			Self::Horizontal | Self::Both => true,
		}
	}
	pub fn is_vertical(&self) -> bool {
		match self {
			Self::None | Self::Horizontal => false,
			Self::Vertical | Self::Both => true,
		}
	}
}

impl<'a> TileRef<'a> {
	pub fn new(tile: &'a TileInstance) -> Self {
		TileRef { tile }
	}
	pub fn gid(&self) -> usize {
		(self.tile.src[0] * self.tile.src[1]).unsigned_abs() as usize
	}
	pub fn get_flip(&self) -> TileFlip {
		match self.tile.f {
			1 => TileFlip::Horizontal,
			2 => TileFlip::Vertical,
			3 => TileFlip::Both,
			_ => TileFlip::None,
		}
	}
}

impl<'a> Debug for TileRef<'a> {
	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
		f.debug_struct("TileRef")
			.field("gid", &self.gid())
			.field("tile.t", &self.tile.t)
			.field("tile.d", &self.tile.d)
			.field("tile.px", &self.tile.px)
			.field("tile.src", &self.tile.src)
			.finish()
	}
}

#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Copy, Hash, Debug)]
pub struct SpatialIndex(i64, i64);
impl<A, B> From<(A, B)> for SpatialIndex
where
	A: AsPrimitive<i64>,
	B: AsPrimitive<i64>,
{
	fn from(value: (A, B)) -> Self {
		Self(value.0.as_(), value.1.as_())
	}
}
impl From<UVec2> for SpatialIndex {
	fn from(value: UVec2) -> Self {
		Self(value.x as i64, value.y as i64)
	}
}
impl From<IVec2> for SpatialIndex {
	fn from(value: IVec2) -> Self {
		Self(value.x as i64, value.y as i64)
	}
}

Louis's avatar
Louis committed
pub type ColliderQuadtree = Quadtree<i64, LocatedEntity>;

pub struct LdtkLevel {
	level: Level,
Louis's avatar
Louis committed
	properties: HashMap<String, Value>,
	processed_layers: Vec<LdtkLayer>,
Louis's avatar
Louis committed
	colliders: ColliderQuadtree,
Louis's avatar
Louis committed
	pub fn width(&self) -> f32 {
		self.level.px_wid as f32
	}
	pub fn width_i(&self) -> i64 {
		self.level.px_wid
	}
	pub fn height(&self) -> f32 {
		self.level.px_hei as f32
	}
	pub fn height_i(&self) -> i64 {
		self.level.px_hei
	}
	pub fn level_ref(&self) -> &Level {
		&self.level
	}
	pub fn level_ref_mut(&mut self) -> &mut Level {
		&mut self.level
	}
Louis's avatar
Louis committed
	pub fn colliders_ref(&self) -> &ColliderQuadtree {
		&self.colliders
	}
	pub fn colliders_ref_mut(&mut self) -> &mut ColliderQuadtree {
		&mut self.colliders
	}
	pub fn layers(&self) -> impl DoubleEndedIterator<Item = &LdtkLayer> {
		self.processed_layers.iter()
	}
	pub fn layers_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut LdtkLayer> {
		self.processed_layers.iter_mut()
	}
Louis's avatar
Louis committed
	pub fn properties_ref(&self) -> &HashMap<String, Value> {
		&self.properties
	}
	pub fn properties_mut(&mut self) -> &mut HashMap<String, Value> {
		&mut self.properties
	}
	pub fn property(&self, name: impl ToString) -> Option<&Value> {
		self.properties.get(&name.to_string())
	}
	pub fn property_or_null(&self, name: impl ToString) -> &Value {
		self.property(name).unwrap_or_else(|| &Value::Null)
	}
	pub fn get_indexer(&self) -> Indexer {
		Indexer::new(px_to_grid(self.level.px_wid), px_to_grid(self.level.px_hei))
	}
}

Louis's avatar
Louis committed
#[derive(Debug, Clone, Default)]
pub struct LocatedEntity {
	pub position: IVec2,
	pub size: IVec2,
	pub properties: HashMap<String, Value>,
	pub collides: bool,
}

impl From<Level> for LdtkLevel {
	fn from(mut value: Level) -> Self {
		let layers = value.layer_instances.take();
Louis's avatar
Louis committed
		let mut properties = HashMap::with_capacity(value.field_instances.len());
		let fields = std::mem::take(&mut value.field_instances);
Louis's avatar
Louis committed
		for field in fields {
			properties.insert(field.identifier, field.value.unwrap_or_else(|| Value::Null));
		}

		let level_width = value.px_wid;
		let level_height = value.px_hei;

		let mut level = Self {
			colliders: Quadtree::new(0),
			processed_layers: layers
				.unwrap_or_default()
				.into_iter()
Louis's avatar
Louis committed
				.rev()
				.enumerate()
				.map(|(idx, inst)| LdtkLayer::new(idx, inst))
				.collect(),
			level: value,
Louis's avatar
Louis committed
			properties,
		};

		let mut collider_quads = Quadtree::<i64, LocatedEntity>::new(32);
		for entity in MapQuery::get_entities_of(&level) {
			if entity.tags.contains(&String::from("Collider")) {
				let mut properties = HashMap::with_capacity(entity.field_instances.len());
				for field in entity.field_instances.iter() {
					properties.insert(
						field.identifier.clone(),
						field.value.as_ref().unwrap_or_else(|| &Value::Null).clone(),
					);
				}

				let width = entity.width;
				let height = entity.height;
				let left_x = entity.px[0];
				let bottom_y = level_height - (entity.px[1] + height);

				let size = IVec2::new(entity.width as i32, entity.height as i32);
				let position = IVec2::new(level.level.px_wid as i32, level.level.px_hei as i32)
					- (IVec2::new(entity.px[0] as i32, entity.px[1] as i32) + size);

				let entity = LocatedEntity {
					position,
					size,
					properties,
					collides: true,
				};

				match AreaBuilder::default()
					.anchor(Point::from((position.x as i64, position.y as i64)))
					.dimensions((size.x as i64, size.y as i64))
					.build()
				{
					Ok(point) => {
						collider_quads.insert(point, entity);
					}
					Err(_) => {}
				}
			}
Louis's avatar
Louis committed

		level.colliders = collider_quads;
		level
Louis's avatar
Louis committed
	pub level: usize,
	layer: LayerInstance,
	position_lookup: HashMap<SpatialIndex, TileInstance>,
	indexer: Indexer,
}

impl LdtkLayer {
	pub fn new(index: usize, layer: LayerInstance) -> Self {
		let tile_list = layer.grid_tiles.iter().chain(&layer.auto_layer_tiles);
		let mut position_lookup =
			HashMap::with_capacity(layer.grid_tiles.len() + layer.auto_layer_tiles.len());

		let scale = get_ldtk_tile_scale() as i64;
		let indexer = Indexer::new(layer.c_wid, layer.c_hei);

		for tile in tile_list {
			let x = tile.px[0] / scale;
			let y = tile.px[1] / scale;

			position_lookup.insert((x, y).into(), tile.serde_clone());
		}

		Self {
			level: index,
			indexer,
			layer,
			position_lookup,
		}
	}

Louis's avatar
Louis committed
	pub fn name(&self) -> &String {
		&self.layer.identifier
	}

	pub fn indexer(&self) -> Indexer {
		self.indexer
	}

	pub fn has_tiles(&self) -> bool {
		!(self.layer.auto_layer_tiles.is_empty() && self.layer.grid_tiles.is_empty())
	}

	pub fn for_each_tile(&self, mut cb: impl FnMut(i64, i64, TileRef)) {
		self.position_lookup.iter().for_each(|(pos, tile)| {
			cb(pos.0, pos.1, TileRef::new(tile));
		});
	}

	pub fn get_z_delta(&self) -> f32 {
		(self.level as f32) / 100.0
	}

	pub fn get_tile(&self, pos: impl Into<SpatialIndex>) -> Option<TileRef> {
		self.position_lookup.get(&pos.into()).map(TileRef::new)
	}
	pub fn get_tile_at(
		&self,
		a: impl AsPrimitive<i64>,
		b: impl AsPrimitive<i64>,
	) -> Option<TileRef> {
		self.position_lookup
			.get(&SpatialIndex(a.as_(), b.as_()))
			.map(TileRef::new)
	}

	/// Returns the inferred name of the tileset used for this layer. This is assumed to be the
	/// name of the tileset file, without the preceding path segments or the file extension. Case
	/// remains unchanged
	pub fn infer_tileset_name(&self) -> Option<String> {
		self.layer.tileset_rel_path.as_ref().and_then(|path| {
			Path::new(path)
				.file_stem()
				.and_then(|stem| stem.to_str())
				.map(String::from)
		})
	}
}

impl AsRef<LayerInstance> for LdtkLayer {
	fn as_ref(&self) -> &LayerInstance {
		&self.layer
	}
}