first committ, still fighting with... a lot! 😅
Some checks are pending
CI / Format (push) Waiting to run
CI / Docs (push) Waiting to run
CI / Clippy lints (push) Waiting to run
CI / Bevy lints (push) Waiting to run
CI / Tests (push) Waiting to run
CI / Check web (push) Waiting to run

This commit is contained in:
Roberto Maurizzi 2025-07-06 22:14:17 +08:00
commit 495556500f
Signed by: robm
GPG key ID: F26E59AFAAADEA55
74 changed files with 18135 additions and 0 deletions

379
src/demo/level.rs Normal file
View file

@ -0,0 +1,379 @@
//! Spawn the main level.
use bevy::{
input::mouse::AccumulatedMouseMotion,
platform::collections::{HashMap, HashSet},
prelude::*,
window::PrimaryWindow,
};
use bevy_ecs_ldtk::{ldtk::NeighbourLevel, prelude::*};
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(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,
),
);
}
#[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/collectathon.ldtk").into(),
}
}
}
const GRID_SIZE: i32 = 16;
fn pan_camera(
mut pan: ResMut<PanLevel>,
mouse_buttons: Res<ButtonInput<MouseButton>>,
mouse_motion: Res<AccumulatedMouseMotion>,
) {
let delta = mouse_motion.delta;
if mouse_buttons.pressed(MouseButton::Middle) {
pan.offset += delta;
info!("pan offset: {}", pan.offset);
}
}
fn translate_grid_coords_entities(
mut grid_coords_entities: Query<(&mut Transform, &GridCoords), Changed<GridCoords>>,
pan: Res<PanLevel>,
) {
for (mut transform, grid_coords) in grid_coords_entities.iter_mut() {
transform.translation = (
//pan.offset +
bevy_ecs_ldtk::utils::grid_coords_to_translation(*grid_coords, IVec2::splat(GRID_SIZE))
)
.extend(transform.translation.z);
}
}
#[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,
}
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> {
info!("got neighbors: {:?}", neighbors);
let gino = neighbors
.iter()
// .map(|neighbor| {
// let direction = Direction::try_from(neighbor.dir.as_str())?;
// Ok((direction, LevelIid::from(neighbor.level_iid.clone())))
// })
.filter_map(|neighbor| {
Direction::try_from(neighbor.dir.as_str())
.ok()
.map(|dir| (dir, LevelIid::from(neighbor.level_iid.clone())))
})
.collect();
info!("converted to: {:?}", gino);
Ok(gino)
}
#[derive(Debug, Default, Resource, Reflect)]
#[reflect(Resource)]
pub struct LevelWalls {
wall_locations: HashSet<GridCoords>,
level_width: i32,
level_height: i32,
level_neighbours: HashMap<Direction, LevelIid>,
}
impl LevelWalls {
pub fn in_wall(&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
|| self.wall_locations.contains(grid_coords)
}
pub fn debug_collisions(&self, player_pos: &GridCoords) {
info!(
"map for a level that is x: {} by y: {}",
self.level_width, self.level_height
);
for y in (0..self.level_height) {
for x in 0..self.level_width {
let coords = GridCoords::new(x, y);
if coords == *player_pos {
print!("@");
} else if self.in_wall(&coords) {
print!("X");
} else {
print!("_");
}
}
println!();
}
}
}
#[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 {
self.cache[level].in_wall(grid_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"),
LdtkWorldBundle {
ldtk_handle: level_assets.world.clone(),
transform: Transform::from_xyz(-half_size.x, half_size.y, 0.0),
..Default::default()
},
));
}
fn _old_cache_wall_locations(
level_selection: Res<LevelSelection>,
mut level_walls: ResMut<LevelWalls>,
mut level_events: EventReader<LevelEvent>,
walls: Query<&GridCoords, With<Wall>>,
ldtk_project_entities: Query<&LdtkProjectHandle>,
ldtk_project_assets: Res<Assets<LdtkProject>>,
) -> Result {
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 wall_locations = walls.iter().copied().collect();
info!(
"loading level of dimension x: {} by y: {}",
level.px_wid, level.px_hei
);
let new_level_walls = LevelWalls {
wall_locations,
level_width: level.px_wid / GRID_SIZE,
level_height: level.px_hei / GRID_SIZE,
..Default::default()
};
*level_walls = new_level_walls;
info!(
"new level tile dimensions are x: {} y {}",
level_walls.level_width, level_walls.level_height
);
level_walls.debug_collisions(&GridCoords::default());
}
}
Ok(())
}
fn cache_wall_locations(
mut levels_wall_cache: ResMut<MultiLevelWalls>,
mut level_events: EventReader<LevelEvent>,
walls: Query<(&ChildOf, &GridCoords), With<Wall>>,
ldtk_project_entities: Query<&LdtkProjectHandle>,
ldtk_project_assets: Res<Assets<LdtkProject>>,
) -> Result {
let multi_level_walls = levels_wall_cache.into_inner();
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();
info!("current level neighbours: {:?}", level.neighbours);
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);
info!("Trying to format it");
// FIXME: a .rev() here? It doesn't look necessary from what gets printed
// remember to fix the supposed "map dragging" too
for y in (0..(level.px_hei / GRID_SIZE)).rev() {
for x in (0..(level.px_wid / GRID_SIZE)) {
let index = (y * level.px_wid / GRID_SIZE + x) as usize;
if let Some(value) = field.int_grid_csv.get(index) {
if *value == 1 {
print!("X");
wall_locations.insert(GridCoords::new(x, y));
} else {
print!("_");
}
}
}
println!();
}
}
});
}
// level.iter_fields().for_each(|field| {
// info!("Field: {:?}", field);
// });
// let wall_locations = walls.iter().map(|e| e.1).copied().collect();
info!(
"loading level of dimension x: {} by y: {}",
level.px_wid, level.px_hei
);
multi_level_walls.cache.insert(
level_iid.clone(), // You'll need to clone the key since HashMap takes ownership
LevelWalls {
wall_locations,
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 {}",
multi_level_walls.cache[level_iid].level_width,
multi_level_walls.cache[level_iid].level_height
);
multi_level_walls.cache[level_iid].debug_collisions(&GridCoords::default());
}
}
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()) {
*level_selection = LevelSelection::Iid(level_iid.clone());
}
}
}
Ok(())
}