//! Spawn the main level. use bevy::{ input::mouse::AccumulatedMouseMotion, platform::collections::{HashMap, HashSet}, prelude::*, window::PrimaryWindow, }; use bevy_ecs_ldtk::{ ldtk::{NeighbourLevel, TileInstance}, prelude::*, utils::int_grid_index_to_grid_coords, }; use crate::{ asset_tracking::LoadResource, audio::music, demo::player::PlayerAssets, screens::Screen, }; use super::player::Player; pub(super) fn plugin(app: &mut App) { app.add_plugins(LdtkPlugin) .insert_resource(LevelSelection::iid("d53f9950-c640-11ed-8430-4942c04951ff")) // .insert_resource(LevelSelection::iid("34f51d20-8990-11ee-b0d1-cfeb0e9e30f6")) .insert_resource(LdtkSettings { level_spawn_behavior: LevelSpawnBehavior::UseWorldTranslation { load_level_neighbors: true, }, ..default() }) .init_resource::() .init_resource::(); app.register_type::(); app.insert_resource(PanLevel::default()); app.register_type::(); app.load_resource::(); app.register_type::(); app.register_type::(); app.register_ldtk_int_cell::(1); app.add_systems( Update, ( translate_grid_coords_entities, level_selection_follow_player, cache_wall_locations, pan_camera, ) .chain(), ); } #[derive(Resource, Default, Asset, Clone, Reflect)] #[reflect(Resource)] struct PanLevel { offset: Vec2, } #[derive(Resource, Asset, Clone, Reflect)] #[reflect(Resource)] pub struct LevelAssets { #[dependency] music: Handle, world: LdtkProjectHandle, } impl FromWorld for LevelAssets { fn from_world(world: &mut World) -> Self { let assets = world.resource::(); Self { music: assets.load("audio/music/Fluffing A Duck.ogg"), world: assets.load("levels/world.ldtk").into(), // world: assets.load("levels/tile-based-game.ldtk").into(), // world: assets.load("levels/collectathon.ldtk").into(), } } } const GRID_SIZE: i32 = 16; fn pan_camera( mut pan: ResMut, mouse_buttons: Res>, mouse_motion: Res, mut camera: Query<&mut Transform, With>, ) -> Result { let delta = mouse_motion.delta; if mouse_buttons.pressed(MouseButton::Middle) { pan.offset += delta; let mut gino = camera.single_mut()?; gino.translation += Vec3::new(-delta.x, delta.y, 0.0) / 5.0; } Ok(()) } fn translate_grid_coords_entities( mut grid_coords_entities: Query< (&mut Transform, &GridCoords), (With, Changed), >, pan: Res, ) { // TODO: what is this used for? Why it doesn't work for a moving Player? for (mut transform, grid_coords) in grid_coords_entities.iter_mut() { info!("Changed GridCoords: {grid_coords:?}"); info!("Previous traslation: {:?}", transform.translation); transform.translation = (bevy_ecs_ldtk::utils::grid_coords_to_translation( *grid_coords, IVec2::splat(GRID_SIZE), )) .extend(transform.translation.z); info!("Updated traslation: {:?}", transform.translation); } } #[derive(Default, Component)] struct Wall; #[derive(Default, Bundle, LdtkIntCell)] struct WallBundle { wall: Wall, } #[derive(Debug, Default, Eq, Hash, PartialEq, Reflect)] pub enum Direction { #[default] N, E, S, W, // we ignore the diagonals NE SE NW SW } impl TryFrom<&str> for Direction { type Error = &'static str; fn try_from(value: &str) -> Result { match value { "n" => Ok(Direction::N), "e" => Ok(Direction::E), "s" => Ok(Direction::S), "w" => Ok(Direction::W), _ => Err("Invalid direction string"), } } } pub fn convert_neighbors( neighbors: &[NeighbourLevel], ) -> Result, &'static str> { let neighbours = neighbors .iter() .filter_map(|neighbor| { Direction::try_from(neighbor.dir.as_str()) .ok() .map(|dir| (dir, LevelIid::new(neighbor.level_iid.clone()))) }) .collect(); Ok(neighbours) } #[derive(Debug, Default, Resource, Reflect)] #[reflect(Resource)] pub struct LevelWalls { wall_locations: HashSet, level_tile_pos_x: i32, level_tile_pos_y: i32, level_width: i32, level_height: i32, level_neighbours: HashMap, } impl LevelWalls { pub fn leave_level(&self, grid_coords: &GridCoords) -> bool { grid_coords.x < 0 || grid_coords.y < 0 || grid_coords.x >= self.level_width || grid_coords.y >= self.level_height } pub fn in_wall(&self, grid_coords: &GridCoords) -> bool { self.wall_locations.contains(grid_coords) } pub fn debug_collisions(&self, player_pos: &GridCoords) { debug!( "map for a level that is x: {} by y: {}", self.level_width, self.level_height ); debug!("player pos: {:?}", player_pos); debug!( "level world coordinates: x: {} y: {}", self.level_tile_pos_x, self.level_tile_pos_y ); // FIXME: kwwp aligned with MultiLevelWalls.in_wall.translated_coords let player_translated_coords = GridCoords::new( player_pos.x - self.level_tile_pos_x, player_pos.y - (-self.level_tile_pos_y - self.level_height), ); debug!("Player absolute coords: {:?}", *player_pos); info!("Player relative coords: {player_translated_coords:?}"); for y in (0..self.level_height).rev() { let mut lineoutput = String::from(""); for x in 0..self.level_width { let coords = GridCoords::new(x, y); if coords.x == 0 { // print!("[X :{:03} Y: {:03}] ", coords.x, coords.y); lineoutput = format!("{lineoutput}[X :{:03} Y: {:03}] ", coords.x, coords.y); } if coords == player_translated_coords { lineoutput = format!("{lineoutput}@"); } else if self.in_wall(&coords) { lineoutput = format!("{lineoutput}X"); } else { lineoutput = format!("{lineoutput}_"); } if coords.x == (self.level_width - 1) { // println!("[X :{:03} Y: {:03}] ", coords.x, coords.y); lineoutput = format!("{lineoutput} [X :{:03} Y: {:03}]", coords.x, coords.y); info!("{lineoutput}"); } } } } } #[derive(Debug, Default, Resource, Reflect)] #[reflect(Resource)] pub struct MultiLevelWalls { cache: HashMap, } impl MultiLevelWalls { pub fn in_wall(&self, level: &LevelIid, grid_coords: &GridCoords) -> bool { let translated_coords = GridCoords::new( // FIXME: x seems to work, y... not so much. // Level UNDER the "zero level strip" by a level_height has: // level world coordinates: x: -16 y: 0 // Player absolute coords: GridCoords { x: -8, y: -1 } y should be 15 // Player relative coords: GridCoords { x: 8, y: -1 } // Level ABOVE the "zero level strip" by a level_height has: // level world coordinates: x: 32 y: -32 // Player absolute coords: GridCoords { x: 42, y: 17 } // Player relative coords: GridCoords { x: 10, y: 17 } y should be 1 // level world coordinates: x: 32 y: -16 // Player absolute coords: GridCoords { x: 41, y: 16 } // Player relative coords: GridCoords { x: 9, y: 16 } y should be 0 grid_coords.x - self.cache[level].level_tile_pos_x, grid_coords.y - (-self.cache[level].level_tile_pos_y - self.cache[level].level_height), ); self.cache[level].in_wall(&translated_coords) } pub fn debug_collisions(&self, level: &LevelIid, player_pos: &GridCoords) { if let Some(level) = self.cache.get(level) { level.debug_collisions(player_pos); } else { warn!("No walls cached for level: {:?}", level); } } } /// A system that spawns the main level. pub fn spawn_level( mut commands: Commands, window: Single<&Window, With>, level_assets: Res, ) { // let half_size = window.size() / 2.0; commands.spawn(( Name::new("Ldtk level"), StateScoped(Screen::Gameplay), LdtkWorldBundle { ldtk_handle: level_assets.world.clone(), // transform: Transform::from_xyz(-half_size.x, half_size.y, 0.0), ..Default::default() }, )); } fn cache_wall_locations( mut levels_wall_cache: ResMut, mut level_events: EventReader, ldtk_project_entities: Query<&LdtkProjectHandle>, ldtk_project_assets: Res>, ) -> Result { let multi_level_walls = levels_wall_cache.into_inner(); let pippo: TileInstance = TileInstance::default(); for level_event in level_events.read() { if let LevelEvent::Spawned(level_iid) = level_event { let ldtk_project = ldtk_project_assets .get(ldtk_project_entities.single()?) .expect("LdtkProject should be loaded when level is spawned"); let level = ldtk_project .get_raw_level_by_iid(level_iid.get()) .expect("spawned level should exist in project"); let mut wall_locations = HashSet::::default(); let mut last_coord: GridCoords = GridCoords::default(); trace!("current level neighbours: {:?}", level.neighbours); trace!( "Level world coordinates: x [{}], y[{}]", level.world_x, level.world_y ); let level_tile_pos_x = level.world_x / GRID_SIZE; let level_tile_pos_y = level.world_y / GRID_SIZE; let level_tile_width = level.px_wid / GRID_SIZE; trace!( "Level tile coordinates: x [{}], y[{}]", level_tile_pos_x, level_tile_pos_y ); if let Some(layers) = level.layer_instances.clone() { // info!("layers: {:?}", layers); layers.iter().for_each(|field| { info!("Layer field: {:?}", field.identifier); if field.identifier == "Walls" { info!("Found walls layer: {:?}", field.int_grid_csv); for (i, value) in field.int_grid_csv.iter().enumerate() { let gc = int_grid_index_to_grid_coords( i, (level.px_wid / GRID_SIZE) as u32, (level.px_hei / GRID_SIZE) as u32, ); let Some(gc) = gc else { warn!("Invalid GridCoords for index {i}"); continue; }; last_coord = GridCoords::new(gc.x, gc.y); if last_coord.x == 0 { print!("[X :{:03} Y: {:03}] ", last_coord.x, last_coord.y); } if *value == 1 { print!("X"); wall_locations.insert(last_coord); } else { print!("_"); } if last_coord.x == (level_tile_width - 1) { println!(" [X :{:03} Y: {:03}]", last_coord.x, last_coord.y); } } } }); } multi_level_walls.cache.insert( level_iid.clone(), // You'll need to clone the key since HashMap takes ownership LevelWalls { wall_locations, level_tile_pos_x, level_tile_pos_y, level_width: level.px_wid / GRID_SIZE, level_height: level.px_hei / GRID_SIZE, level_neighbours: convert_neighbors(&level.neighbours).unwrap_or_default(), // Convert neighbours to a HashMap }, ); info!( "new level tile dimensions are x: {} y {}, neighbours: {:?}", multi_level_walls.cache[level_iid].level_width, multi_level_walls.cache[level_iid].level_height, multi_level_walls.cache[level_iid].level_neighbours, ); } } Ok(()) } fn level_selection_follow_player( players: Query<&GlobalTransform, With>, levels: Query<(&LevelIid, &GlobalTransform)>, ldtk_projects: Query<&LdtkProjectHandle>, ldtk_project_assets: Res>, mut level_selection: ResMut, ) -> Result { if let Ok(player_transform) = players.single() { let ldtk_project = ldtk_project_assets .get(ldtk_projects.single()?) .expect("ldtk project should be loaded before player is spawned"); for (level_iid, level_transform) in levels.iter() { let level = ldtk_project .get_raw_level_by_iid(level_iid.get()) .expect("level should exist in only project"); let level_bounds = Rect { min: Vec2::new( level_transform.translation().x, level_transform.translation().y, ), max: Vec2::new( level_transform.translation().x + level.px_wid as f32, level_transform.translation().y + level.px_hei as f32, ), }; if level_bounds.contains(player_transform.translation().truncate()) { if *level_selection == LevelSelection::Iid(level_iid.clone()) { // Player is already in the current level, no need to change return Ok(()); } info!("Setting current level to {level_iid}"); *level_selection = LevelSelection::Iid(level_iid.clone()); } } } Ok(()) }