411 lines
14 KiB
Rust
411 lines
14 KiB
Rust
//! 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::<LevelWalls>()
|
|
.init_resource::<MultiLevelWalls>();
|
|
|
|
app.register_type::<PanLevel>();
|
|
app.insert_resource(PanLevel::default());
|
|
app.register_type::<LevelAssets>();
|
|
app.load_resource::<LevelAssets>();
|
|
app.register_type::<LevelWalls>();
|
|
app.register_type::<MultiLevelWalls>();
|
|
app.register_ldtk_int_cell::<WallBundle>(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<AudioSource>,
|
|
world: LdtkProjectHandle,
|
|
}
|
|
|
|
impl FromWorld for LevelAssets {
|
|
fn from_world(world: &mut World) -> Self {
|
|
let assets = world.resource::<AssetServer>();
|
|
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<PanLevel>,
|
|
mouse_buttons: Res<ButtonInput<MouseButton>>,
|
|
mouse_motion: Res<AccumulatedMouseMotion>,
|
|
mut camera: Query<&mut Transform, With<Camera>>,
|
|
) -> 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<Player>, Changed<GridCoords>),
|
|
>,
|
|
pan: Res<PanLevel>,
|
|
) {
|
|
// 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<Self, Self::Error> {
|
|
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<HashMap<Direction, LevelIid>, &'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<GridCoords>,
|
|
level_tile_pos_x: i32,
|
|
level_tile_pos_y: i32,
|
|
level_width: i32,
|
|
level_height: i32,
|
|
level_neighbours: HashMap<Direction, LevelIid>,
|
|
}
|
|
|
|
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<LevelIid, LevelWalls>,
|
|
}
|
|
|
|
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<PrimaryWindow>>,
|
|
level_assets: Res<LevelAssets>,
|
|
) {
|
|
// 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<MultiLevelWalls>,
|
|
mut level_events: EventReader<LevelEvent>,
|
|
ldtk_project_entities: Query<&LdtkProjectHandle>,
|
|
ldtk_project_assets: Res<Assets<LdtkProject>>,
|
|
) -> 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::<GridCoords>::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<Player>>,
|
|
levels: Query<(&LevelIid, &GlobalTransform)>,
|
|
ldtk_projects: Query<&LdtkProjectHandle>,
|
|
ldtk_project_assets: Res<Assets<LdtkProject>>,
|
|
mut level_selection: ResMut<LevelSelection>,
|
|
) -> 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(())
|
|
}
|