diff --git a/src/components.rs b/src/components.rs index ab47681a..07e47bfd 100644 --- a/src/components.rs +++ b/src/components.rs @@ -121,6 +121,12 @@ impl From for GridCoords { } } +impl From for TilePos { + fn from(grid_coords: GridCoords) -> Self { + TilePos(grid_coords.x as u32, grid_coords.y as u32) + } +} + impl Add for GridCoords { type Output = GridCoords; fn add(self, rhs: GridCoords) -> Self::Output { @@ -173,6 +179,10 @@ impl MulAssign for GridCoords { } impl GridCoords { + pub fn new(x: i32, y: i32) -> GridCoords { + GridCoords { x, y } + } + /// Creates a [GridCoords] from the entity information available to the /// [LdtkEntity::bundle_entity] method. /// @@ -186,6 +196,25 @@ impl GridCoords { } } +/// [Component] for storing user-defined custom data for a paticular tile in an LDtk tileset +/// definition. +/// +/// Automatically inserted on any tiles with metadata. +#[derive(Clone, Eq, PartialEq, Debug, Default, Hash, Component)] +pub struct TileMetadata { + pub data: String, +} + +/// [Component] for storing user-defined, enum-based tags for a particular tile in an LDtk tileset +/// definition. +/// +/// Automatically inserted on any tiles with enum tags. +#[derive(Clone, Eq, PartialEq, Debug, Default, Hash, Component)] +pub struct TileEnumTags { + pub tags: Vec, + pub source_enum_uid: Option, +} + #[derive(Clone, Default, Bundle)] pub(crate) struct TileGridBundle { #[bundle] diff --git a/src/level.rs b/src/level.rs new file mode 100644 index 00000000..fc16e309 --- /dev/null +++ b/src/level.rs @@ -0,0 +1,672 @@ +//! Functions related to spawning levels. + +use crate::{ + app::{ + LdtkEntity, LdtkEntityMap, LdtkIntCellMap, PhantomLdtkEntity, PhantomLdtkEntityTrait, + PhantomLdtkIntCell, PhantomLdtkIntCellTrait, + }, + assets::{LdtkLevel, TilesetMap}, + components::*, + ldtk::{ + EntityDefinition, EnumTagValue, LayerDefinition, LevelBackgroundPosition, + TileCustomMetadata, TileInstance, TilesetDefinition, Type, + }, + resources::{IntGridRendering, LdtkSettings, LevelBackground}, + tile_makers::*, + utils::*, +}; + +use bevy::{prelude::*, render::render_resource::*, sprite}; +use bevy_ecs_tilemap::prelude::*; +use std::collections::{HashMap, HashSet}; + +use thiserror::Error; + +const CHUNK_SIZE: ChunkSize = ChunkSize(32, 32); + +#[derive(Error, Debug)] +enum BackgroundImageError { + #[error("background image handle not loaded into the image assets store")] + ImageNotLoaded, +} + +fn background_image_sprite_sheet_bundle( + images: &Assets, + texture_atlases: &mut Assets, + background_image_handle: &Handle, + background_position: &LevelBackgroundPosition, + level_height: i32, + transform_z: f32, +) -> Result { + if let Some(background_image) = images.get(background_image_handle) { + // We need to use a texture atlas to apply the correct crop to the image + let tile_size = Vec2::new( + background_image.texture_descriptor.size.width as f32, + background_image.texture_descriptor.size.height as f32, + ); + let mut texture_atlas = TextureAtlas::new_empty(background_image_handle.clone(), tile_size); + + let min = Vec2::new( + background_position.crop_rect[0], + background_position.crop_rect[1], + ); + + let size = Vec2::new( + background_position.crop_rect[2], + background_position.crop_rect[3], + ); + + let max = min + size; + + let crop_rect = sprite::Rect { min, max }; + + texture_atlas.textures.push(crop_rect); + + let texture_atlas_handle = texture_atlases.add(texture_atlas); + + let scale = background_position.scale; + + let scaled_size = size * scale; + + let top_left_translation = + ldtk_pixel_coords_to_translation(background_position.top_left_px, level_height); + + let center_translation = + top_left_translation + (Vec2::new(scaled_size.x, -scaled_size.y) / 2.); + + Ok(SpriteSheetBundle { + texture_atlas: texture_atlas_handle, + transform: Transform::from_translation(center_translation.extend(transform_z)) + .with_scale(scale.extend(1.)), + ..Default::default() + }) + } else { + Err(BackgroundImageError::ImageNotLoaded) + } +} + +pub(crate) fn tile_to_grid_coords( + tile_instance: &TileInstance, + layer_height_in_tiles: i32, + layer_grid_size: i32, +) -> GridCoords { + ldtk_pixel_coords_to_grid_coords( + IVec2::new(tile_instance.px[0], tile_instance.px[1]), + layer_height_in_tiles, + IVec2::splat(layer_grid_size), + ) +} + +fn insert_metadata_to_tile( + commands: &mut Commands, + tile_instance: &TileInstance, + tile_entity: Entity, + metadata_map: &HashMap, + enum_tags_map: &HashMap, +) -> bool { + let mut entity_commands = commands.entity(tile_entity); + + let mut metadata_inserted = false; + + if let Some(tile_metadata) = metadata_map.get(&tile_instance.t) { + entity_commands.insert(tile_metadata.clone()); + metadata_inserted = true; + } + + if let Some(enum_tags) = enum_tags_map.get(&tile_instance.t) { + entity_commands.insert(enum_tags.clone()); + metadata_inserted = true; + } + + metadata_inserted +} + +fn transform_bundle_for_tiles( + grid_coords: GridCoords, + grid_size: i32, + layer_scale: Vec3, + parent: Entity, +) -> (Transform, GlobalTransform, Parent) { + let mut translation = + grid_coords_to_translation_centered(grid_coords, IVec2::splat(grid_size)).extend(0.); + + translation /= layer_scale; + + ( + Transform::from_translation(translation), + GlobalTransform::default(), + Parent(parent), + ) +} + +#[allow(clippy::too_many_arguments)] +fn insert_metadata_for_layer( + commands: &mut Commands, + layer_builder: &mut LayerBuilder, + grid_tiles: &[TileInstance], + layer_instance: &LayerInstance, + metadata_map: &HashMap, + enum_tags_map: &HashMap, + layer_scale: Vec3, + layer_entity: Entity, +) { + for tile in grid_tiles { + let grid_coords = tile_to_grid_coords(tile, layer_instance.c_hei, layer_instance.grid_size); + + let tile_entity = layer_builder + .get_tile_entity(commands, grid_coords.into()) + .unwrap(); + + if insert_metadata_to_tile(commands, tile, tile_entity, metadata_map, enum_tags_map) { + commands + .entity(tile_entity) + .insert_bundle(transform_bundle_for_tiles( + grid_coords, + layer_instance.grid_size, + layer_scale, + layer_entity, + )); + } + } +} + +fn layer_grid_tiles(grid_tiles: Vec) -> Vec> { + let mut layer = Vec::new(); + let mut overflow = Vec::new(); + for tile in grid_tiles { + if layer.iter().any(|t: &TileInstance| t.px == tile.px) { + overflow.push(tile); + } else { + layer.push(tile); + } + } + + let mut layered_grid_tiles = vec![layer]; + if !overflow.is_empty() { + layered_grid_tiles.extend(layer_grid_tiles(overflow)); + } + + layered_grid_tiles +} + +#[allow(clippy::too_many_arguments)] +pub fn spawn_level( + ldtk_level: &LdtkLevel, + commands: &mut Commands, + asset_server: &AssetServer, + images: &mut Assets, + texture_atlases: &mut Assets, + meshes: &mut ResMut>, + ldtk_entity_map: &LdtkEntityMap, + ldtk_int_cell_map: &LdtkIntCellMap, + entity_definition_map: &HashMap, + layer_definition_map: &HashMap, + tileset_map: &TilesetMap, + tileset_definition_map: &HashMap, + worldly_set: HashSet, + ldtk_entity: Entity, + ldtk_settings: &LdtkSettings, +) { + let level = &ldtk_level.level; + + let mut map = Map::new(level.uid as u16, ldtk_entity); + + if let Some(layer_instances) = &level.layer_instances { + let mut layer_id = 0; + + // creating an image to use for the background color, and for intgrid colors + let mut white_image = Image::new_fill( + Extent3d { + width: level.px_wid as u32, + height: level.px_hei as u32, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + &[255, 255, 255, 255], + TextureFormat::Rgba8UnormSrgb, + ); + white_image.texture_descriptor.usage = + TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_SRC | TextureUsages::COPY_DST; + + let white_image_handle = images.add(white_image); + + if ldtk_settings.level_background == LevelBackground::Rendered { + let settings = LayerSettings::new( + MapSize(1, 1), + ChunkSize(1, 1), + TileSize(level.px_wid as f32, level.px_hei as f32), + TextureSize(level.px_wid as f32, level.px_hei as f32), + ); + + let (mut layer_builder, layer_entity) = + LayerBuilder::::new(commands, settings, map.id, layer_id); + + match layer_builder.set_tile( + TilePos(0, 0), + TileBundle { + tile: Tile { + color: level.bg_color, + ..Default::default() + }, + ..Default::default() + }, + ) { + Ok(()) => (), + Err(_) => warn!("Encountered error when setting background tile"), + } + + let layer_bundle = layer_builder.build(commands, meshes, white_image_handle.clone()); + commands.entity(layer_entity).insert_bundle(layer_bundle); + map.add_layer(commands, layer_id, layer_entity); + layer_id += 1; + + // Spawn background image + if let (Some(background_image_handle), Some(background_position)) = + (&ldtk_level.background_image, &level.bg_pos) + { + match background_image_sprite_sheet_bundle( + images, + texture_atlases, + background_image_handle, + background_position, + level.px_hei, + layer_id as f32, + ) { + Ok(sprite_sheet_bundle) => { + commands + .spawn_bundle(sprite_sheet_bundle) + .insert(Parent(ldtk_entity)); + + layer_id += 1; + } + Err(e) => warn!("{}", e), + } + } + } + + for layer_instance in layer_instances.iter().rev() { + match layer_instance.layer_instance_type { + Type::Entities => { + commands.entity(ldtk_entity).with_children(|commands| { + for entity_instance in &layer_instance.entity_instances { + let transform = calculate_transform_from_entity_instance( + entity_instance, + entity_definition_map, + level.px_hei, + layer_id as f32, + ); + // Note: entities do not seem to be affected visually by layer offsets in + // the editor, so no layer offset is added to the transform here. + + let mut entity_commands = commands.spawn(); + + let (tileset, tileset_definition) = match &entity_instance.tile { + Some(t) => ( + tileset_map.get(&t.tileset_uid), + tileset_definition_map.get(&t.tileset_uid).copied(), + ), + None => (None, None), + }; + + let predicted_worldly = Worldly::bundle_entity( + entity_instance, + layer_instance, + tileset, + tileset_definition, + asset_server, + texture_atlases, + ); + + if !worldly_set.contains(&predicted_worldly) { + let default_ldtk_entity: Box = + Box::new(PhantomLdtkEntity::::new()); + + ldtk_map_get_or_default( + layer_instance.identifier.clone(), + entity_instance.identifier.clone(), + &default_ldtk_entity, + ldtk_entity_map, + ) + .evaluate( + &mut entity_commands, + entity_instance, + layer_instance, + tileset, + tileset_definition, + asset_server, + texture_atlases, + ); + + entity_commands + .insert(transform) + .insert(GlobalTransform::default()); + } + } + }); + layer_id += 1; + } + _ => { + // The remaining layers have a lot of shared code. + // This is because: + // 1. There is virtually no difference between AutoTile and Tile layers + // 2. IntGrid layers can sometimes have AutoTile functionality + + let map_size = MapSize( + (layer_instance.c_wid as f32 / CHUNK_SIZE.0 as f32).ceil() as u32, + (layer_instance.c_hei as f32 / CHUNK_SIZE.1 as f32).ceil() as u32, + ); + + let tileset_definition = layer_instance + .tileset_def_uid + .map(|u| tileset_definition_map.get(&u).unwrap()); + + let tile_size = match tileset_definition { + Some(tileset_definition) => TileSize( + tileset_definition.tile_grid_size as f32, + tileset_definition.tile_grid_size as f32, + ), + None => TileSize( + layer_instance.grid_size as f32, + layer_instance.grid_size as f32, + ), + }; + + let texture_size = match tileset_definition { + Some(tileset_definition) => TextureSize( + tileset_definition.px_wid as f32, + tileset_definition.px_hei as f32, + ), + None => TextureSize( + layer_instance.grid_size as f32, + layer_instance.grid_size as f32, + ), + }; + + let mut settings = + LayerSettings::new(map_size, CHUNK_SIZE, tile_size, texture_size); + + if let Some(tileset_definition) = tileset_definition { + settings.grid_size = Vec2::splat(layer_instance.grid_size as f32); + if tileset_definition.spacing != 0 { + // TODO: Check that this is still an issue with upcoming + // bevy_ecs_tilemap releases + #[cfg(not(feature = "atlas"))] + { + warn!( + "Tile spacing on Tile and AutoTile layers requires the \"atlas\" feature" + ); + } + + #[cfg(feature = "atlas")] + { + settings.tile_spacing = + Vec2::splat(tileset_definition.spacing as f32); + } + } + } + + // The change to the settings.grid_size above is supposed to help handle cases + // where the tileset's tile size and the layer's tile size are different. + // However, changing the grid_size doesn't have any affect with the current + // bevy_ecs_tilemap, so the workaround is to scale up the entire layer. + let layer_scale = (settings.grid_size + / Vec2::new(settings.tile_size.0 as f32, settings.tile_size.1 as f32)) + .extend(1.); + + let image_handle = match tileset_definition { + Some(tileset_definition) => { + tileset_map.get(&tileset_definition.uid).unwrap().clone() + } + None => white_image_handle.clone(), + }; + + let metadata_map: HashMap = tileset_definition + .map(|tileset_definition| { + tileset_definition + .custom_data + .iter() + .map(|TileCustomMetadata { data, tile_id }| { + (*tile_id, TileMetadata { data: data.clone() }) + }) + .collect() + }) + .unwrap_or_default(); + + let mut enum_tags_map: HashMap = HashMap::new(); + + if let Some(tileset_definition) = tileset_definition { + for EnumTagValue { + enum_value_id, + tile_ids, + } in tileset_definition.enum_tags.iter() + { + for tile_id in tile_ids { + enum_tags_map + .entry(*tile_id) + .or_insert_with(|| TileEnumTags { + tags: Vec::new(), + source_enum_uid: tileset_definition.tags_source_enum_uid, + }) + .tags + .push(enum_value_id.clone()); + } + } + } + + let mut grid_tiles = layer_instance.grid_tiles.clone(); + grid_tiles.extend(layer_instance.auto_layer_tiles.clone()); + + for (i, grid_tiles) in layer_grid_tiles(grid_tiles).into_iter().enumerate() { + let layer_entity = if layer_instance.layer_instance_type == Type::IntGrid { + // The current spawning of IntGrid layers doesn't allow using + // LayerBuilder::new_batch(). + // So, the actual LayerBuilder usage diverges greatly here + + let (mut layer_builder, layer_entity) = + LayerBuilder::::new( + commands, + settings, + map.id, + layer_id as u16, + ); + + match tileset_definition { + Some(_) => { + set_all_tiles_with_func( + &mut layer_builder, + tile_pos_to_tile_bundle_maker( + tile_pos_to_transparent_tile_maker( + tile_pos_to_int_grid_with_grid_tiles_tile_maker( + &grid_tiles, + &layer_instance.int_grid_csv, + layer_instance.c_wid, + layer_instance.c_hei, + layer_instance.grid_size, + ), + layer_instance.opacity, + ), + ), + ); + } + None => { + let int_grid_value_defs = &layer_definition_map + .get(&layer_instance.layer_def_uid) + .expect("Encountered layer without definition") + .int_grid_values; + + match ldtk_settings.int_grid_rendering { + IntGridRendering::Colorful => { + set_all_tiles_with_func( + &mut layer_builder, + tile_pos_to_tile_bundle_maker( + tile_pos_to_transparent_tile_maker( + tile_pos_to_int_grid_colored_tile_maker( + &layer_instance.int_grid_csv, + int_grid_value_defs, + layer_instance.c_wid, + layer_instance.c_hei, + ), + layer_instance.opacity, + ), + ), + ); + } + IntGridRendering::Invisible => { + set_all_tiles_with_func( + &mut layer_builder, + tile_pos_to_tile_bundle_maker( + tile_pos_to_transparent_tile_maker( + tile_pos_to_tile_if_int_grid_nonzero_maker( + tile_pos_to_invisible_tile, + &layer_instance.int_grid_csv, + layer_instance.c_wid, + layer_instance.c_hei, + ), + layer_instance.opacity, + ), + ), + ); + } + } + } + } + + if i == 0 { + for (i, value) in layer_instance + .int_grid_csv + .iter() + .enumerate() + .filter(|(_, v)| **v != 0) + { + let grid_coords = int_grid_index_to_grid_coords( + i, + layer_instance.c_wid as u32, + layer_instance.c_hei as u32, + ).expect("int_grid_csv indices should be within the bounds of 0..(layer_width * layer_height)"); + + let tile_entity = layer_builder + .get_tile_entity(commands, grid_coords.into()) + .unwrap(); + + let mut entity_commands = commands.entity(tile_entity); + + let default_ldtk_int_cell: Box = + Box::new(PhantomLdtkIntCell::::new()); + + ldtk_map_get_or_default( + layer_instance.identifier.clone(), + *value, + &default_ldtk_int_cell, + ldtk_int_cell_map, + ) + .evaluate( + &mut entity_commands, + IntGridCell { value: *value }, + layer_instance, + ); + + let transform_bundle = transform_bundle_for_tiles( + grid_coords, + layer_instance.grid_size, + layer_scale, + layer_entity, + ); + + entity_commands.insert_bundle(transform_bundle); + } + } + + if !(metadata_map.is_empty() && enum_tags_map.is_empty()) { + insert_metadata_for_layer( + commands, + &mut layer_builder, + &grid_tiles, + layer_instance, + &metadata_map, + &enum_tags_map, + layer_scale, + layer_entity, + ); + } + + let layer_bundle = + layer_builder.build(commands, meshes, image_handle.clone()); + + commands.entity(layer_entity).insert_bundle(layer_bundle); + + layer_entity + } else { + let tile_bundle_maker = + tile_pos_to_tile_bundle_maker(tile_pos_to_transparent_tile_maker( + tile_pos_to_tile_maker( + &grid_tiles, + layer_instance.c_hei, + layer_instance.grid_size, + ), + layer_instance.opacity, + )); + + if !(metadata_map.is_empty() && enum_tags_map.is_empty()) { + // When we add metadata to tiles, we need to add additional + // components to them. + // This can't be accomplished using LayerBuilder::new_batch, + // so the logic for building layers with metadata is slower. + let (mut layer_builder, layer_entity) = + LayerBuilder::::new( + commands, + settings, + map.id, + layer_id as u16, + ); + + set_all_tiles_with_func(&mut layer_builder, tile_bundle_maker); + + insert_metadata_for_layer( + commands, + &mut layer_builder, + &grid_tiles, + layer_instance, + &metadata_map, + &enum_tags_map, + layer_scale, + layer_entity, + ); + + let layer_bundle = + layer_builder.build(commands, meshes, image_handle.clone()); + + commands.entity(layer_entity).insert_bundle(layer_bundle); + + layer_entity + } else { + LayerBuilder::::new_batch( + commands, + settings, + meshes, + image_handle.clone(), + map.id, + layer_id as u16, + tile_bundle_maker, + ) + } + }; + + let layer_offset = Vec3::new( + layer_instance.px_total_offset_x as f32, + -layer_instance.px_total_offset_y as f32, + layer_id as f32, + ); + + commands.entity(layer_entity).insert( + Transform::from_translation(layer_offset).with_scale(layer_scale), + ); + + map.add_layer(commands, layer_id as u16, layer_entity); + layer_id += 1; + } + } + } + } + } + commands.entity(ldtk_entity).insert(map); +} diff --git a/src/lib.rs b/src/lib.rs index 6582f69e..f3b3210d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -104,6 +104,7 @@ pub mod app; mod assets; mod components; pub mod ldtk; +mod level; mod resources; pub mod systems; mod tile_makers; @@ -192,7 +193,10 @@ pub mod prelude { pub use crate::{ app::{LdtkEntity, LdtkIntCell, RegisterLdtkObjects}, assets::{LdtkAsset, LdtkLevel}, - components::{EntityInstance, GridCoords, IntGridCell, LdtkWorldBundle, LevelSet, Worldly}, + components::{ + EntityInstance, GridCoords, IntGridCell, LdtkWorldBundle, LevelSet, TileEnumTags, + TileMetadata, Worldly, + }, ldtk::{self, FieldValue, LayerInstance, TilesetDefinition}, plugin::LdtkPlugin, resources::{ diff --git a/src/systems.rs b/src/systems.rs index 4b3d1d4c..4629f5f9 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -1,18 +1,12 @@ //! System functions used by the plugin for processing ldtk files. use crate::{ - app::{ - LdtkEntity, LdtkEntityMap, LdtkIntCellMap, PhantomLdtkEntity, PhantomLdtkEntityTrait, - PhantomLdtkIntCell, PhantomLdtkIntCellTrait, - }, - assets::{LdtkAsset, LdtkLevel, TilesetMap}, + app::{LdtkEntityMap, LdtkIntCellMap}, + assets::{LdtkAsset, LdtkLevel}, components::*, - ldtk::{EntityDefinition, LayerDefinition, TileInstance, TilesetDefinition, Type}, - resources::{ - IntGridRendering, LdtkSettings, LevelBackground, LevelEvent, LevelSelection, - LevelSpawnBehavior, SetClearColor, - }, - tile_makers::*, + ldtk::TilesetDefinition, + level::spawn_level, + resources::{LdtkSettings, LevelEvent, LevelSelection, LevelSpawnBehavior, SetClearColor}, utils::*, }; @@ -20,8 +14,6 @@ use bevy::{prelude::*, render::render_resource::*}; use bevy_ecs_tilemap::prelude::*; use std::collections::{HashMap, HashSet}; -const CHUNK_SIZE: ChunkSize = ChunkSize(32, 32); - pub fn choose_levels( level_selection: Option>, ldtk_settings: Res, @@ -297,9 +289,6 @@ pub fn process_ldtk_levels( mut level_events: EventWriter, ldtk_settings: Res, ) { - // This function uses code from the bevy_ecs_tilemap ldtk example - // https://github.com/StarArawn/bevy_ecs_tilemap/blob/main/examples/ldtk/ldtk.rs - for (ldtk_entity, level_handle, parent) in level_query.iter() { if let Ok(ldtk_handle) = ldtk_query.get(parent.0) { if let Some(ldtk_asset) = ldtk_assets.get(ldtk_handle) { @@ -344,429 +333,6 @@ pub fn process_ldtk_levels( } } -#[allow(clippy::too_many_arguments)] -fn spawn_level( - ldtk_level: &LdtkLevel, - commands: &mut Commands, - asset_server: &AssetServer, - images: &mut Assets, - texture_atlases: &mut Assets, - meshes: &mut ResMut>, - ldtk_entity_map: &LdtkEntityMap, - ldtk_int_cell_map: &LdtkIntCellMap, - entity_definition_map: &HashMap, - layer_definition_map: &HashMap, - tileset_map: &TilesetMap, - tileset_definition_map: &HashMap, - worldly_set: HashSet, - ldtk_entity: Entity, - ldtk_settings: &LdtkSettings, -) { - let level = &ldtk_level.level; - - let mut map = Map::new(level.uid as u16, ldtk_entity); - - if let Some(layer_instances) = &level.layer_instances { - let mut layer_id = 0; - - // creating an image to use for the background color, and for intgrid colors - let mut white_image = Image::new_fill( - Extent3d { - width: level.px_wid as u32, - height: level.px_hei as u32, - depth_or_array_layers: 1, - }, - TextureDimension::D2, - &[255, 255, 255, 255], - TextureFormat::Rgba8UnormSrgb, - ); - white_image.texture_descriptor.usage = - TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_SRC | TextureUsages::COPY_DST; - - let white_image_handle = images.add(white_image); - - if ldtk_settings.level_background == LevelBackground::Rendered { - let settings = LayerSettings::new( - MapSize(1, 1), - ChunkSize(1, 1), - TileSize(level.px_wid as f32, level.px_hei as f32), - TextureSize(level.px_wid as f32, level.px_hei as f32), - ); - - let (mut layer_builder, layer_entity) = - LayerBuilder::::new(commands, settings, map.id, layer_id); - - match layer_builder.set_tile( - TilePos(0, 0), - TileBundle { - tile: Tile { - color: level.bg_color, - ..Default::default() - }, - ..Default::default() - }, - ) { - Ok(()) => (), - Err(_) => warn!("Encountered error when setting background tile"), - } - - let layer_bundle = layer_builder.build(commands, meshes, white_image_handle.clone()); - commands.entity(layer_entity).insert_bundle(layer_bundle); - map.add_layer(commands, layer_id, layer_entity); - layer_id += 1; - - // Spawn background image - if let (Some(background_image_handle), Some(background_position)) = - (&ldtk_level.background_image, &level.bg_pos) - { - match background_image_sprite_sheet_bundle( - images, - texture_atlases, - background_image_handle, - background_position, - level.px_hei, - layer_id as f32, - ) { - Ok(sprite_sheet_bundle) => { - commands - .spawn_bundle(sprite_sheet_bundle) - .insert(Parent(ldtk_entity)); - - layer_id += 1; - } - Err(e) => warn!("{}", e), - } - } - } - - for layer_instance in layer_instances.iter().rev() { - match layer_instance.layer_instance_type { - Type::Entities => { - commands.entity(ldtk_entity).with_children(|commands| { - for entity_instance in &layer_instance.entity_instances { - let transform = calculate_transform_from_entity_instance( - entity_instance, - entity_definition_map, - level.px_hei, - layer_id as f32, - ); - // Note: entities do not seem to be affected visually by layer offsets in - // the editor, so no layer offset is added to the transform here. - - let mut entity_commands = commands.spawn(); - - let (tileset, tileset_definition) = match &entity_instance.tile { - Some(t) => ( - tileset_map.get(&t.tileset_uid), - tileset_definition_map.get(&t.tileset_uid).copied(), - ), - None => (None, None), - }; - - let predicted_worldly = Worldly::bundle_entity( - entity_instance, - layer_instance, - tileset, - tileset_definition, - asset_server, - texture_atlases, - ); - - if !worldly_set.contains(&predicted_worldly) { - let default_ldtk_entity: Box = - Box::new(PhantomLdtkEntity::::new()); - - ldtk_map_get_or_default( - layer_instance.identifier.clone(), - entity_instance.identifier.clone(), - &default_ldtk_entity, - ldtk_entity_map, - ) - .evaluate( - &mut entity_commands, - entity_instance, - layer_instance, - tileset, - tileset_definition, - asset_server, - texture_atlases, - ); - - entity_commands - .insert(transform) - .insert(GlobalTransform::default()); - } - } - }); - layer_id += 1; - } - _ => { - // The remaining layers have a lot of shared code. - // This is because: - // 1. There is virtually no difference between AutoTile and Tile layers - // 2. IntGrid layers can sometimes have AutoTile functionality - - let map_size = MapSize( - (layer_instance.c_wid as f32 / CHUNK_SIZE.0 as f32).ceil() as u32, - (layer_instance.c_hei as f32 / CHUNK_SIZE.1 as f32).ceil() as u32, - ); - - let tileset_definition = layer_instance - .tileset_def_uid - .map(|u| tileset_definition_map.get(&u).unwrap()); - - let tile_size = match tileset_definition { - Some(tileset_definition) => TileSize( - tileset_definition.tile_grid_size as f32, - tileset_definition.tile_grid_size as f32, - ), - None => TileSize( - layer_instance.grid_size as f32, - layer_instance.grid_size as f32, - ), - }; - - let texture_size = match tileset_definition { - Some(tileset_definition) => TextureSize( - tileset_definition.px_wid as f32, - tileset_definition.px_hei as f32, - ), - None => TextureSize( - layer_instance.grid_size as f32, - layer_instance.grid_size as f32, - ), - }; - - let mut settings = - LayerSettings::new(map_size, CHUNK_SIZE, tile_size, texture_size); - - if let Some(tileset_definition) = tileset_definition { - settings.grid_size = Vec2::splat(layer_instance.grid_size as f32); - if tileset_definition.spacing != 0 { - // TODO: Check that this is still an issue with upcoming - // bevy_ecs_tilemap releases - #[cfg(not(feature = "atlas"))] - { - warn!( - "Tile spacing on Tile and AutoTile layers requires the \"atlas\" feature" - ); - } - - #[cfg(feature = "atlas")] - { - settings.tile_spacing = - Vec2::splat(tileset_definition.spacing as f32); - } - } - } - - // The change to the settings.grid_size above is supposed to help handle cases - // where the tileset's tile size and the layer's tile size are different. - // However, changing the grid_size doesn't have any affect with the current - // bevy_ecs_tilemap, so the workaround is to scale up the entire layer. - let layer_scale = (settings.grid_size - / Vec2::new(settings.tile_size.0 as f32, settings.tile_size.1 as f32)) - .extend(1.); - - let image_handle = match tileset_definition { - Some(tileset_definition) => { - tileset_map.get(&tileset_definition.uid).unwrap().clone() - } - None => white_image_handle.clone(), - }; - - let mut grid_tiles = layer_instance.grid_tiles.clone(); - grid_tiles.extend(layer_instance.auto_layer_tiles.clone()); - - for (i, grid_tiles) in layer_grid_tiles(grid_tiles).into_iter().enumerate() { - let layer_entity = if layer_instance.layer_instance_type == Type::IntGrid { - // The current spawning of IntGrid layers doesn't allow using - // LayerBuilder::new_batch(). - // So, the actual LayerBuilder usage diverges greatly here - - let (mut layer_builder, layer_entity) = - LayerBuilder::::new( - commands, - settings, - map.id, - layer_id as u16, - ); - - match tileset_definition { - Some(_) => { - set_all_tiles_with_func( - &mut layer_builder, - tile_pos_to_tile_bundle_maker( - tile_pos_to_transparent_tile_maker( - tile_pos_to_int_grid_with_grid_tiles_tile_maker( - &grid_tiles, - &layer_instance.int_grid_csv, - layer_instance.c_wid, - layer_instance.c_hei, - layer_instance.grid_size, - ), - layer_instance.opacity, - ), - ), - ); - } - None => { - let int_grid_value_defs = &layer_definition_map - .get(&layer_instance.layer_def_uid) - .expect("Encountered layer without definition") - .int_grid_values; - - match ldtk_settings.int_grid_rendering { - IntGridRendering::Colorful => { - set_all_tiles_with_func( - &mut layer_builder, - tile_pos_to_tile_bundle_maker( - tile_pos_to_transparent_tile_maker( - tile_pos_to_int_grid_colored_tile_maker( - &layer_instance.int_grid_csv, - int_grid_value_defs, - layer_instance.c_wid, - layer_instance.c_hei, - ), - layer_instance.opacity, - ), - ), - ); - } - IntGridRendering::Invisible => { - set_all_tiles_with_func( - &mut layer_builder, - tile_pos_to_tile_bundle_maker( - tile_pos_to_transparent_tile_maker( - tile_pos_to_tile_if_int_grid_nonzero_maker( - tile_pos_to_invisible_tile, - &layer_instance.int_grid_csv, - layer_instance.c_wid, - layer_instance.c_hei, - ), - layer_instance.opacity, - ), - ), - ); - } - } - } - } - - if i == 0 { - for (i, value) in layer_instance - .int_grid_csv - .iter() - .enumerate() - .filter(|(_, v)| **v != 0) - { - let tile_pos = int_grid_index_to_tile_pos( - i, - layer_instance.c_wid as u32, - layer_instance.c_hei as u32, - ).expect("int_grid_csv indices should be within the bounds of 0..(layer_widthd * layer_height)"); - - let tile_entity = - layer_builder.get_tile_entity(commands, tile_pos).unwrap(); - - let mut translation = tile_pos_to_translation_centered( - tile_pos, - IVec2::splat(layer_instance.grid_size), - ) - .extend(layer_id as f32); - - translation /= layer_scale; - - let mut entity_commands = commands.entity(tile_entity); - - let default_ldtk_int_cell: Box = - Box::new(PhantomLdtkIntCell::::new()); - - ldtk_map_get_or_default( - layer_instance.identifier.clone(), - *value, - &default_ldtk_int_cell, - ldtk_int_cell_map, - ) - .evaluate( - &mut entity_commands, - IntGridCell { value: *value }, - layer_instance, - ); - - entity_commands - .insert(Transform::from_translation(translation)) - .insert(GlobalTransform::default()) - .insert(Parent(layer_entity)); - } - } - - let layer_bundle = - layer_builder.build(commands, meshes, image_handle.clone()); - - commands.entity(layer_entity).insert_bundle(layer_bundle); - - layer_entity - } else { - let tile_maker = tile_pos_to_transparent_tile_maker( - tile_pos_to_tile_maker( - &grid_tiles, - layer_instance.c_hei, - layer_instance.grid_size, - ), - layer_instance.opacity, - ); - - LayerBuilder::::new_batch( - commands, - settings, - meshes, - image_handle.clone(), - map.id, - layer_id as u16, - tile_pos_to_tile_bundle_maker(tile_maker), - ) - }; - - let layer_offset = Vec3::new( - layer_instance.px_total_offset_x as f32, - -layer_instance.px_total_offset_y as f32, - layer_id as f32, - ); - - commands.entity(layer_entity).insert( - Transform::from_translation(layer_offset).with_scale(layer_scale), - ); - - map.add_layer(commands, layer_id as u16, layer_entity); - layer_id += 1; - } - } - } - } - } - commands.entity(ldtk_entity).insert(map); -} - -fn layer_grid_tiles(grid_tiles: Vec) -> Vec> { - let mut layer = Vec::new(); - let mut overflow = Vec::new(); - for tile in grid_tiles { - if layer.iter().any(|t: &TileInstance| t.px == tile.px) { - overflow.push(tile); - } else { - layer.push(tile); - } - } - - let mut layered_grid_tiles = vec![layer]; - if !overflow.is_empty() { - layered_grid_tiles.extend(layer_grid_tiles(overflow)); - } - - layered_grid_tiles -} - pub fn worldly_adoption( mut worldly_query: Query<(&mut Transform, &mut Parent), Added>, transform_query: Query<(&Transform, &Parent), Without>, diff --git a/src/tile_makers.rs b/src/tile_makers.rs index 63eb3392..7fe33f4e 100644 --- a/src/tile_makers.rs +++ b/src/tile_makers.rs @@ -16,6 +16,7 @@ use crate::{ components::TileGridBundle, ldtk::{IntGridValueDefinition, TileInstance}, + level::tile_to_grid_coords, utils::*, }; use bevy::prelude::*; @@ -42,7 +43,7 @@ pub(crate) fn tile_pos_to_int_grid_map( ) -> HashMap { int_grid_csv.iter().enumerate().filter(|(_, v)| **v != 0).map(|(i, v)| { ( - int_grid_index_to_tile_pos(i, layer_width_in_tiles as u32, layer_height_in_tiles as u32).expect("int_grid_csv indices should be within the bounds of 0..(layer_width * layer_height)",), + int_grid_index_to_grid_coords(i, layer_width_in_tiles as u32, layer_height_in_tiles as u32).expect("int_grid_csv indices should be within the bounds of 0..(layer_width * layer_height)",).into(), *v, ) }).collect() @@ -60,10 +61,7 @@ pub(crate) fn tile_pos_to_tile_maker( .iter() .map(|t| { ( - TilePos( - (t.px[0] / layer_grid_size) as u32, - layer_height_in_tiles as u32 - (t.px[1] / layer_grid_size) as u32 - 1, - ), + tile_to_grid_coords(t, layer_height_in_tiles, layer_grid_size).into(), t.clone(), ) }) diff --git a/src/utils.rs b/src/utils.rs index 3f98fcee..195ccd2a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -7,22 +7,21 @@ use crate::{ }; use crate::ldtk::*; -use bevy::{prelude::*, sprite}; +use bevy::prelude::*; use bevy_ecs_tilemap::prelude::*; -use thiserror::Error; use std::{collections::HashMap, hash::Hash}; /// The `int_grid_csv` field of a [LayerInstance] is a 1-dimensional [Vec]. -/// This function can map the indices of this [Vec] to a corresponding [TilePos]. +/// This function can map the indices of this [Vec] to a corresponding [GridCoords]. /// -/// Will return [None] if the resulting [TilePos] is out of the bounds implied by the width and +/// Will return [None] if the resulting [GridCoords] is out of the bounds implied by the width and /// height. -pub fn int_grid_index_to_tile_pos( +pub fn int_grid_index_to_grid_coords( index: usize, layer_width_in_tiles: u32, layer_height_in_tiles: u32, -) -> Option { +) -> Option { if layer_width_in_tiles * layer_height_in_tiles == 0 { // Checking for potential n mod 0 and n / 0 issues // Also it just doesn't make sense for either of these to be 0. @@ -39,7 +38,7 @@ pub fn int_grid_index_to_tile_pos( // is a natural number. // This means tile_x == index where index < n, and tile_x < index where index >= n. - Some(ldtk_grid_coords_to_tile_pos( + Some(ldtk_grid_coords_to_grid_coords( IVec2::new(tile_x as i32, inverted_y as i32), layer_height_in_tiles as i32, )) @@ -111,18 +110,6 @@ pub fn translation_to_ldtk_pixel_coords(translation: Vec2, ldtk_pixel_height: i3 ldtk_coord_conversion(translation.as_ivec2(), ldtk_pixel_height) } -/// Performs LDtk grid coordinate to [TilePos] conversion. -/// -/// This conversion is performed so that both the LDtk grid coords and the resulting [TilePos] -/// refer to the same tile. -/// This is different from them referring to the same position in space, because the tile is -/// referenced by its top-left corner in LDtk, and by its bottom-left corner with [TilePos]. -pub fn ldtk_grid_coords_to_tile_pos(ldtk_coords: IVec2, ldtk_grid_height: i32) -> TilePos { - let tile_coords = - ldtk_coord_conversion_origin_adjusted(ldtk_coords, ldtk_grid_height).as_uvec2(); - TilePos(tile_coords.x, tile_coords.y) -} - /// Performs LDtk grid coordinate to [GridCoords] conversion. /// /// This conversion is performed so that both the LDtk grid coords and the resulting [GridCoords] @@ -133,15 +120,46 @@ pub fn ldtk_grid_coords_to_grid_coords(ldtk_coords: IVec2, ldtk_grid_height: i32 ldtk_coord_conversion_origin_adjusted(ldtk_coords, ldtk_grid_height).into() } -/// Performs [TilePos] to LDtk grid coordinate conversion. +/// Performs [GridCoords] to LDtk grid coordinate conversion. /// -/// This conversion is performed so that both the [TilePos] and the resulting LDtk grid coords +/// This conversion is performed so that both the [GridCoords] and the resulting LDtk grid coords /// refer to the same tile. /// This is different from them referring to the same position in space, because the tile is -/// referenced by its top-left corner in LDtk, and by its bottom-left corner with [TilePos]. -pub fn tile_pos_to_ldtk_grid_coords(tile_pos: TilePos, ldtk_grid_height: i32) -> IVec2 { - let tile_coords: UVec2 = tile_pos.into(); - ldtk_coord_conversion_origin_adjusted(tile_coords.as_ivec2(), ldtk_grid_height) +/// referenced by its top-left corner in LDtk, and by its bottom-left corner with [GridCoords]. +pub fn grid_coords_to_ldtk_grid_coords(grid_coords: GridCoords, ldtk_grid_height: i32) -> IVec2 { + ldtk_coord_conversion_origin_adjusted(grid_coords.into(), ldtk_grid_height) +} + +/// Performs translation to [GridCoords] conversion. +/// +/// This is inherently lossy since `GridCoords` space is less detailed than translation space. +/// +/// Assumes that the origin of the grid is at [Vec2::ZERO]. +pub fn translation_to_grid_coords(translation: Vec2, grid_size: IVec2) -> GridCoords { + (translation / grid_size.as_vec2()).as_ivec2().into() +} + +/// Performs [GridCoords] to translation conversion, so that the resulting translation is in the +/// the center of the tile. +/// +/// Assumes that the origin the grid is at [Vec2::ZERO]. +/// +/// Internally, this transform is used to place [IntGridCell]s as children of the level. +pub fn grid_coords_to_translation_centered(grid_coords: GridCoords, tile_size: IVec2) -> Vec2 { + let tile_coords: IVec2 = grid_coords.into(); + let tile_size = tile_size.as_vec2(); + (tile_size * tile_coords.as_vec2()) + (tile_size / Vec2::splat(2.)) +} + +/// Performs LDtk pixel coordinate to [GridCoords] conversion. +/// +/// This is inherently lossy since `GridCoords` space is less detailed than ldtk pixel coord space. +pub fn ldtk_pixel_coords_to_grid_coords( + ldtk_coords: IVec2, + ldtk_grid_height: i32, + grid_size: IVec2, +) -> GridCoords { + ldtk_grid_coords_to_grid_coords(ldtk_coords / grid_size, ldtk_grid_height) } /// Performs LDtk grid coordinate to translation conversion, so that the resulting translation is @@ -155,18 +173,6 @@ pub fn ldtk_grid_coords_to_translation_centered( + Vec2::new(grid_size.x as f32 / 2., -grid_size.y as f32 / 2.) } -/// Performs [TilePos] to translation conversion, so that the resulting translation is in the in -/// the center of the tile. -/// -/// Assumes that the bottom-left corner of the origin tile is at [Vec2::ZERO]. -/// -/// Internally, this transform is used to place [IntGridCell]s as children of the level. -pub fn tile_pos_to_translation_centered(tile_pos: TilePos, tile_size: IVec2) -> Vec2 { - let tile_coords: UVec2 = tile_pos.into(); - let tile_size = tile_size.as_vec2(); - (tile_size * tile_coords.as_vec2()) + (tile_size / Vec2::splat(2.)) -} - /// Performs LDtk pixel coordinate to translation conversion, with "pivot" support. /// /// In LDtk, the "pivot" of an entity indicates the percentage that an entity's visual is adjusted @@ -312,91 +318,45 @@ pub fn sprite_bundle_from_entity_info(tileset: Option<&Handle>) -> Sprite } } -#[derive(Error, Debug)] -pub(crate) enum BackgroundImageError { - #[error("background image handle not loaded into the image assets store")] - ImageNotLoaded, -} - -pub(crate) fn background_image_sprite_sheet_bundle( - images: &Assets, - texture_atlases: &mut Assets, - background_image_handle: &Handle, - background_position: &LevelBackgroundPosition, - level_height: i32, - transform_z: f32, -) -> Result { - if let Some(background_image) = images.get(background_image_handle) { - // We need to use a texture atlas to apply the correct crop to the image - let tile_size = Vec2::new( - background_image.texture_descriptor.size.width as f32, - background_image.texture_descriptor.size.height as f32, - ); - let mut texture_atlas = TextureAtlas::new_empty(background_image_handle.clone(), tile_size); - - let min = Vec2::new( - background_position.crop_rect[0], - background_position.crop_rect[1], - ); - - let size = Vec2::new( - background_position.crop_rect[2], - background_position.crop_rect[3], - ); - - let max = min + size; - - let crop_rect = sprite::Rect { min, max }; - - texture_atlas.textures.push(crop_rect); - - let texture_atlas_handle = texture_atlases.add(texture_atlas); - - let scale = background_position.scale; - - let scaled_size = size * scale; - - let top_left_translation = - ldtk_pixel_coords_to_translation(background_position.top_left_px, level_height); - - let center_translation = - top_left_translation + (Vec2::new(scaled_size.x, -scaled_size.y) / 2.); - - Ok(SpriteSheetBundle { - texture_atlas: texture_atlas_handle, - transform: Transform::from_translation(center_translation.extend(transform_z)) - .with_scale(scale.extend(1.)), - ..Default::default() - }) - } else { - Err(BackgroundImageError::ImageNotLoaded) - } -} - #[cfg(test)] mod tests { use super::*; #[test] fn test_int_grid_index_to_tile_pos() { - assert_eq!(int_grid_index_to_tile_pos(3, 4, 5), Some(TilePos(3, 4))); + assert_eq!( + int_grid_index_to_grid_coords(3, 4, 5), + Some(GridCoords::new(3, 4)) + ); - assert_eq!(int_grid_index_to_tile_pos(10, 5, 5), Some(TilePos(0, 2))); + assert_eq!( + int_grid_index_to_grid_coords(10, 5, 5), + Some(GridCoords::new(0, 2)) + ); - assert_eq!(int_grid_index_to_tile_pos(49, 10, 5), Some(TilePos(9, 0))); + assert_eq!( + int_grid_index_to_grid_coords(49, 10, 5), + Some(GridCoords::new(9, 0)) + ); - assert_eq!(int_grid_index_to_tile_pos(64, 100, 1), Some(TilePos(64, 0))); + assert_eq!( + int_grid_index_to_grid_coords(64, 100, 1), + Some(GridCoords::new(64, 0)) + ); - assert_eq!(int_grid_index_to_tile_pos(35, 1, 100), Some(TilePos(0, 64))); + assert_eq!( + int_grid_index_to_grid_coords(35, 1, 100), + Some(GridCoords::new(0, 64)) + ); } #[test] fn test_int_grid_index_out_of_range() { - assert_eq!(int_grid_index_to_tile_pos(3, 0, 5), None); + assert_eq!(int_grid_index_to_grid_coords(3, 0, 5), None); - assert_eq!(int_grid_index_to_tile_pos(3, 5, 0), None); + assert_eq!(int_grid_index_to_grid_coords(3, 5, 0), None); - assert_eq!(int_grid_index_to_tile_pos(25, 5, 5), None); + assert_eq!(int_grid_index_to_grid_coords(25, 5, 5), None); } #[test] @@ -540,17 +500,17 @@ mod tests { #[test] fn test_tile_pos_to_translation_centered() { assert_eq!( - tile_pos_to_translation_centered(TilePos(1, 2), IVec2::splat(32)), + grid_coords_to_translation_centered(GridCoords::new(1, 2), IVec2::splat(32)), Vec2::new(48., 80.) ); assert_eq!( - tile_pos_to_translation_centered(TilePos(1, 0), IVec2::splat(100)), + grid_coords_to_translation_centered(GridCoords::new(1, 0), IVec2::splat(100)), Vec2::new(150., 50.) ); assert_eq!( - tile_pos_to_translation_centered(TilePos(0, 5), IVec2::splat(1)), + grid_coords_to_translation_centered(GridCoords::new(0, 5), IVec2::splat(1)), Vec2::new(0.5, 5.5) ); }