Skip to content
Snippets Groups Projects
Verified Commit 994608ab authored by Louis's avatar Louis :fire:
Browse files

Initial version

parents
No related branches found
No related tags found
No related merge requests found
[package]
name = "micro_ldtk"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bevy = "0.9.1"
anyhow = "1.0.66"
log = "0.4.17"
serde = "1.0.147"
serde_json = "1.0.87"
ldtk_rust = "0.6.0"
num-traits = "0.2.15"
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use anyhow::Error;
use bevy::asset::{AssetEvent, AssetLoader, Assets, BoxedFuture, LoadContext, LoadedAsset};
use bevy::prelude::{EventReader, Res, ResMut, Resource};
use bevy::reflect::TypeUuid;
use ldtk_rust::{Level, Project, TilesetDefinition};
use serde_json::Value;
use crate::utils::SerdeClone;
#[derive(TypeUuid)]
#[uuid = "eb97c508-61ea-11ed-abd3-db91dd0a6e8b"]
pub struct LdtkProject(pub Project);
impl Deref for LdtkProject {
type Target = Project;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for LdtkProject {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl LdtkProject {}
#[derive(Default)]
pub struct LdtkLoader;
impl AssetLoader for LdtkLoader {
fn load<'a>(
&'a self,
bytes: &'a [u8],
load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, anyhow::Result<(), Error>> {
Box::pin(async move {
let mut ldtk: Project = serde_json::from_slice(bytes)?;
if ldtk.external_levels {
ldtk.load_external_levels(load_context.path());
}
load_context.set_default_asset(LoadedAsset::new(LdtkProject(ldtk)));
Ok(())
})
}
fn extensions(&self) -> &[&str] {
&["ldtk"]
}
}
#[derive(Resource, Default)]
pub struct LevelIndex(pub HashMap<String, Level>);
impl Deref for LevelIndex {
type Target = HashMap<String, Level>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for LevelIndex {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Default)]
pub struct TileMetadata(pub HashMap<i64, Value>);
#[derive(Resource, Default)]
pub struct TilesetIndex(pub HashMap<String, TileMetadata>);
impl Deref for TilesetIndex {
type Target = HashMap<String, TileMetadata>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for TilesetIndex {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
pub fn handle_ldtk_project_events(
mut events: EventReader<AssetEvent<LdtkProject>>,
assets: Res<Assets<LdtkProject>>,
mut level_index: ResMut<LevelIndex>,
mut tilset_index: ResMut<TilesetIndex>,
) {
for event in events.iter() {
match event {
AssetEvent::Created { handle } | AssetEvent::Modified { handle } => {
if let Some(LdtkProject(project)) = assets.get(handle) {
for level in &project.levels {
level_index.insert(level.identifier.clone(), level.serde_clone());
}
for tileset in &project.defs.tilesets {
let mut tile_meta = HashMap::new();
for custom in &tileset.custom_data {
tile_meta.insert(
custom.tile_id,
serde_json::from_str(&*custom.data).unwrap(),
);
}
tilset_index.insert(tileset.identifier.clone(), TileMetadata(tile_meta));
}
}
}
_ => {}
}
}
}
use bevy::ecs::query::ReadOnlyWorldQuery;
use bevy::ecs::system::SystemParam;
use bevy::prelude::*;
use crate::MapQuery;
pub fn lock_camera_to_level<CameraSelector: ReadOnlyWorldQuery>(
map_query: MapQuery,
mut camera_query: Query<(&mut Transform, &OrthographicProjection), CameraSelector>,
) {
let bounds = match map_query.get_camera_bounds() {
Some(bounds) => bounds,
None => return,
};
for (mut transform, proj) in &mut camera_query {
let width = proj.right - proj.left;
let height = proj.top - proj.bottom;
let val_x = bounds
.get_min_x(width)
.max(bounds.get_max_x(width).min(transform.translation.x));
let val_y = bounds
.get_min_y(height)
.max(bounds.get_max_y(height).min(transform.translation.y));
// log::info!("BEFORE {:?}", transform.translation);
transform.translation = Vec3::new(val_x, val_y, transform.translation.z);
// log::info!("AFTER {:?}", transform.translation);
}
}
#[derive(SystemParam)]
pub struct CameraBounder<'w, 's, Filter: ReadOnlyWorldQuery + 'static> {
map_query: MapQuery<'w, 's>,
query: Query<'w, 's, &'static OrthographicProjection, Filter>,
}
impl<'w, 's, Filter: ReadOnlyWorldQuery + 'static> CameraBounder<'w, 's, Filter> {
pub fn get_extremeties(&self) -> Option<Rect> {
if let Some(bounds) = self.map_query.get_camera_bounds() {
if let Ok(proj) = self.query.get_single() {
let width = proj.right - proj.left;
let height = proj.top - proj.bottom;
Some(Rect::new(
bounds.get_min_x(width),
bounds.get_min_y(height),
bounds.get_max_x(width),
bounds.get_max_y(height),
))
} else {
None
}
} else {
None
}
}
pub fn bind(&self, point: Vec2) -> Vec2 {
if let Some(bounds) = self.get_extremeties() {
bounds.min.max(point).min(bounds.max)
} else {
point
}
}
}
#[cfg(test)]
mod test {
use bevy::math::{Rect, Vec2};
#[test]
fn min_max_vec() {
let initial_point = Vec2::new(3.0, 5.0);
let expected_point = Vec2::new(3.0, 3.0);
let bounds = Rect::new(1.0, 1.0, 3.0, 3.0);
assert_eq!(
expected_point,
bounds.min.max(initial_point).min(bounds.max)
);
}
}
mod assets;
mod camera;
mod locator;
mod map_query;
pub(crate) mod utils;
pub static mut LDTK_TILE_SCALE: AtomicU32 = AtomicU32::new(32);
pub fn get_ldtk_tile_scale() -> f32 {
unsafe { *LDTK_TILE_SCALE.get_mut() as f32 }
}
pub fn set_ldtk_tile_scale_u32(scale: u32) {
unsafe {
LDTK_TILE_SCALE.store(scale, Ordering::Release);
}
}
pub fn set_ldtk_tile_scale_f32(scale: f32) {
unsafe {
LDTK_TILE_SCALE.store(scale as u32, Ordering::Release);
}
}
mod __plugin {
use std::marker::PhantomData;
use bevy::app::{App, CoreStage, Plugin};
use bevy::asset::AddAsset;
use bevy::ecs::query::ReadOnlyWorldQuery;
pub struct MicroLDTKPlugin;
impl Plugin for MicroLDTKPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<super::assets::TilesetIndex>()
.init_resource::<super::assets::LevelIndex>()
.add_asset::<super::assets::LdtkProject>()
.add_asset_loader(super::assets::LdtkLoader)
.add_system(super::assets::handle_ldtk_project_events);
}
}
impl<CameraFilter: ReadOnlyWorldQuery + Send + Sync + 'static> MicroLDTKCameraPlugin<CameraFilter> {
pub fn new() -> Self {
Self {
_p: PhantomData::default(),
}
}
}
pub struct MicroLDTKCameraPlugin<CameraFilter: ReadOnlyWorldQuery> {
_p: PhantomData<CameraFilter>,
}
impl<CameraFilter: ReadOnlyWorldQuery + Send + Sync + 'static> Plugin
for MicroLDTKCameraPlugin<CameraFilter>
{
fn build(&self, app: &mut App) {
app.add_system_to_stage(
CoreStage::PostUpdate,
super::camera::lock_camera_to_level::<CameraFilter>,
);
}
}
}
use std::sync::atomic::{AtomicU32, Ordering};
pub use __plugin::{MicroLDTKCameraPlugin, MicroLDTKPlugin};
pub use assets::{LdtkLoader, LdtkProject, LevelIndex, TileMetadata, TilesetIndex};
pub use map_query::{CameraBounds, LayerRef, MapQuery, TileRef};
pub use utils::{entity_to_worldspace, grid_to_px, px_to_grid, ActiveLevel, WorldLinked};
use bevy::prelude::{Handle, TextureAtlas};
pub trait SuppliesTileAtlas {
fn from_path(path: impl ToString) -> Option<Handle<TextureAtlas>>;
}
use std::marker::PhantomData;
use bevy::ecs::system::SystemParam;
use bevy::prelude::*;
use ldtk_rust::{EntityInstance, LayerInstance, Level, TileInstance};
// use crate::assets::level_index::LevelIndex;
use crate::assets::{LdtkProject, LevelIndex};
use crate::get_ldtk_tile_scale;
use crate::utils::ActiveLevel;
#[derive(SystemParam)]
pub struct MapQuery<'w, 's> {
assets: Res<'w, Assets<LdtkProject>>,
active: Option<Res<'w, ActiveLevel>>,
index: Res<'w, LevelIndex>,
#[system_param(ignore)]
_e: PhantomData<&'s ()>,
}
pub struct TileRef<'a> {
pub tile: &'a TileInstance,
}
impl<'a> TileRef<'a> {
pub fn new(tile: &'a TileInstance) -> Self {
TileRef { tile }
}
}
impl<'a> TileRef<'a> {
pub fn gid(&self) -> usize {
(self.tile.src[0] * self.tile.src[1]).unsigned_abs() as usize
}
}
pub struct LayerRef<'a> {
pub idx: usize,
pub layer: &'a LayerInstance,
}
impl<'a> LayerRef<'a> {
pub fn new(idx: usize, layer: &'a LayerInstance) -> Self {
LayerRef { layer, idx }
}
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.layer
.grid_tiles
.iter()
.chain(self.layer.auto_layer_tiles.iter())
.for_each(|tile: &TileInstance| {
let (x, y) = match tile.px.as_slice() {
&[x, y] => (x, y),
_ => {
return;
}
};
cb(x, y, TileRef::new(tile));
});
}
pub fn get_z_delta(&self) -> f32 {
(self.idx as f32) / 100.0
}
// pub fn get_tile_metadata_of(&self, tile_id: i64) {
// self.layer.til
// }
}
#[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<'w, 's> MapQuery<'w, 's> {
// --- 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
pub fn for_each_layer_of(level: &Level, mut cb: impl FnMut(LayerRef)) {
if let Some(layers) = level.layer_instances.as_ref() {
for layer in layers.iter().rev().enumerate() {
cb(LayerRef::new(layer.0, layer.1));
}
}
}
pub fn get_entities_of(level: &Level) -> Vec<&EntityInstance> {
level
.layer_instances
.as_ref()
.map(|layers| {
layers
.iter()
.flat_map(|layer| layer.entity_instances.iter())
.collect()
})
.unwrap_or_default()
}
pub fn get_camera_bounds_of(level: &Level) -> CameraBounds {
CameraBounds {
left: 0.0,
top: level.px_hei as f32,
bottom: 0.0,
right: level.px_wid as f32,
}
}
pub fn get_active_level(&self) -> Option<&Level> {
self.active
.as_ref()
.and_then(|index| self.index.get(&index.map))
}
pub fn get_entities(&self) -> Vec<&EntityInstance> {
self.get_active_level()
.map(|level| MapQuery::get_entities_of(level))
.unwrap_or_default()
}
pub fn get_camera_bounds(&self) -> Option<CameraBounds> {
self.get_active_level().map(MapQuery::get_camera_bounds_of)
}
pub fn for_each_layer(&self, mut cb: impl FnMut(LayerRef)) {
if let Some(level) = self.get_active_level() {
Self::for_each_layer_of(level, cb);
}
}
}
use bevy::prelude::{Component, Resource};
use ldtk_rust::EntityInstance;
use num_traits::AsPrimitive;
use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::get_ldtk_tile_scale;
pub trait SerdeClone {
fn serde_clone(&self) -> Self;
}
impl<T> SerdeClone for T
where
T: Serialize + DeserializeOwned,
{
fn serde_clone(&self) -> Self {
serde_json::from_value(serde_json::to_value(&self).unwrap()).unwrap()
}
}
pub fn px_to_grid<T: AsPrimitive<i64>>(t: T) -> i64 {
t.as_() / (get_ldtk_tile_scale() as i64)
}
pub fn grid_to_px<T: AsPrimitive<f32>>(t: T) -> f32 {
t.as_() * get_ldtk_tile_scale()
}
pub fn entity_to_worldspace(level_height: i64, entity: &EntityInstance) -> (f32, f32) {
let centre_align_pixel_x = grid_to_px(entity.grid[0]) - (get_ldtk_tile_scale() / 2.0);
let centre_align_pixel_y = grid_to_px(entity.grid[1]) - (get_ldtk_tile_scale() / 2.0);
let inverted_pixel_y = level_height as f32 - centre_align_pixel_y - get_ldtk_tile_scale();
let box_aligned_x = centre_align_pixel_x + (entity.width / 2) as f32;
let box_aligned_y = inverted_pixel_y - (entity.height / 2) as f32;
(box_aligned_x, box_aligned_y)
}
#[derive(Component)]
pub struct WorldLinked;
#[derive(Default, Resource, Clone)]
pub struct ActiveLevel {
pub map: String,
pub dirty: bool,
}
impl ActiveLevel {
pub fn new<T: ToString>(map: T) -> Self {
ActiveLevel {
map: map.to_string(),
dirty: false,
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment