feat: Unfinished level loading

- Added level struct
- Added loading tests
- Partially implemented level setting up system
This commit is contained in:
Alexey 2026-03-27 18:31:46 +03:00
commit 0ab2620724
9 changed files with 414 additions and 5 deletions

View file

@ -1 +0,0 @@

73
src/layout/asset/mod.rs Normal file
View file

@ -0,0 +1,73 @@
use std::collections::HashMap;
use bevy::prelude::*;
use crate::{layout::{Level, asset::structs::{DoorData, StairsData}, door::door_bundle, lock::padlock_bundle, stairs::stairs_bundle, tilemap::tilemap_bundle}, meters, player::player_bundle};
pub mod structs;
pub fn load_level (
InRef(level_handle): InRef<Handle<structs::LevelAsset>>,
mut commands: Commands,
asset_server: Res<AssetServer>,
level_assets: Res<Assets<structs::LevelAsset>>,
) {
let Some(level) = level_assets.get(level_handle) else {
error!("Could not load level asset from {level_handle:?}");
return;
};
let Some(default_tile) = level.meta.tiles.first() else {
error!("Level meta does not contain tile ids");
return;
};
let tile_ids: HashMap<String, u16> = level.meta.tiles.iter()
.enumerate()
.map(|(v, k)| (k.to_owned(), v as u16))
.collect();
let tiles: Vec<(u16, URect)> = level.tiles.iter()
.flat_map(|(id, tiles)| {
let id = match tile_ids.get(id.as_str()) {
Some(id) => *id,
None => {
warn!("Tile ID {id} not found in level meta, using {default_tile}...");
0
}
};
tiles.clone().into_iter().map(move |t| (id, t))
}).collect();
let player_pos = vec2(meters(level.interactive.player.x + 0.5), meters(level.interactive.player.y));
commands.spawn(Level).with_children(|parent| {
parent.spawn(tilemap_bundle(&asset_server, tiles));
parent.spawn(player_bundle(&asset_server, player_pos));
for DoorData {pos, facing_left, lock} in level.interactive.doors.iter() {
let door_pos = vec2(meters(pos.x + 0.5), meters(pos.y));
let mut door = parent.spawn(door_bundle(&asset_server, door_pos, *facing_left));
if let Some(lock_facing_left) = lock {
door.with_child(padlock_bundle(&asset_server, *lock_facing_left));
}
}
for StairsData {pos, floors} in level.interactive.stairs.iter() { match floors {
&0 | &1 => continue,
_ => {
let mut pos = vec2(meters(pos.x), meters(pos.y));
let mut down = None;
let mut up = Some(vec2(meters(2.), meters(4.)));
for i in 0..*floors {
parent.spawn(stairs_bundle(&asset_server, pos, up, down));
pos.y += meters(4.);
if i == floors - 1 {
up = None;
}
down = Some(vec2(meters(-2.), meters(-4.)));
}
},
}}
});
}

View file

@ -0,0 +1,212 @@
use super::*;
pub(super) fn default_floors() -> u8 { 2 }
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone, Copy, Reflect)]
#[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)]
#[serde(default)]
pub(super) struct Pos {
pub x: f32,
pub y: f32,
}
impl From<Pos> for Vec2 {
fn from(Pos { x, y }: Pos) -> Self {
Self { x, y }
}
}
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Reflect)]
#[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)]
#[serde(default)]
pub(super) struct UPos {
pub x: u32,
pub y: u32,
}
impl From<UPos> for UVec2 {
fn from(UPos { x, y }: UPos) -> Self {
Self { x, y }
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone, Copy, Reflect)]
#[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)]
#[serde(default)]
pub(super) struct USize {
pub w: u32,
pub h: u32,
}
impl Default for USize {
fn default() -> Self {
Self { w: 1, h: 1 }
}
}
impl From<USize> for UVec2 {
fn from(USize { w, h }: USize) -> Self {
Self { x: w, y: h }
}
}
#[derive(Debug, Deserialize, Serialize, Default, PartialEq, Eq, Clone, Copy, Reflect)]
#[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)]
#[serde(default)]
pub(super) struct USizeRect {
#[serde(flatten)]
pub pos: UPos,
#[serde(flatten)]
pub size: USize,
}
impl From<USizeRect> for URect {
fn from(USizeRect { pos, size: USize { w, h } }: USizeRect) -> Self {
URect::from_corners(pos.into(), uvec2(pos.x + w - 1, pos.y + h - 1))
}
}
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Reflect)]
#[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)]
#[serde(rename_all = "lowercase")]
pub(super) enum Facing {
#[default]
Left,
Right,
}
impl From<Facing> for bool {
fn from(value: Facing) -> Self {
match value {
Facing::Left => true,
Facing::Right => false,
}
}
}
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone, Copy, Reflect)]
#[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)]
pub(super) struct DoorDataInner {
#[serde(flatten)]
pub pos: Pos,
#[serde(default)]
pub facing: Option<Facing>,
#[serde(default)]
pub lock: Option<Facing>,
}
impl From<DoorDataInner> for DoorData {
fn from(DoorDataInner { pos, facing, lock }: DoorDataInner) -> Self {
let lock =
if let Some(lock) = lock { Some(lock.into()) }
else { None };
Self {
pos: pos.into(),
facing_left: facing.unwrap_or_default().into(),
lock,
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy, Reflect)]
#[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)]
pub(super) struct StairsInnerData {
#[serde(flatten)]
pub pos: Pos,
#[serde(default = "default_floors")]
pub floors: u8,
}
impl From<StairsInnerData> for StairsData {
fn from(StairsInnerData { pos, floors }: StairsInnerData) -> Self {
Self {
pos: pos.into(),
floors,
}
}
}
impl Default for StairsInnerData {
fn default() -> Self {
Self {
pos: Pos::default(),
floors: default_floors(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone, Reflect)]
#[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)]
pub(super) struct ItemDataInner {
pub id: String,
#[serde(default, flatten)]
pub pos: UPos,
#[serde(default)]
pub rotated: bool,
}
impl From<ItemDataInner> for ItemData {
fn from(ItemDataInner { id, pos, rotated }: ItemDataInner) -> Self {
Self {
id,
pos: pos.into(),
rotated,
}
}
}
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone, Reflect)]
#[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)]
pub(super) struct ContainerDataInner {
#[serde(flatten)]
pub pos: Pos,
#[serde(flatten, default)]
pub size: Option<USize>,
#[serde(default)]
pub items: Option<Vec<ItemData>>,
}
impl From<ContainerDataInner> for ContainerData {
fn from(ContainerDataInner { pos, size, items }: ContainerDataInner) -> Self {
Self {
pos: pos.into(),
size: size.unwrap_or_default().into(),
items,
}
}
}
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone, Reflect)]
#[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)]
pub(super) struct InteractiveInner {
pub player: Pos,
#[serde(default)]
pub doors: Option<Vec<DoorData>>,
#[serde(default)]
pub stairs: Option<Vec<StairsData>>,
#[serde(default)]
pub containers: Option<Vec<ContainerData>>,
}
impl From<InteractiveInner> for Interactive {
fn from(InteractiveInner { player, doors, stairs, containers }: InteractiveInner) -> Self {
Self {
player: player.into(),
doors: doors.unwrap_or_default(),
stairs: stairs.unwrap_or_default(),
containers: containers.unwrap_or_default(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone, Reflect)]
#[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)]
#[serde(transparent)]
pub(super) struct TilesInner(HashMap<String, Vec<USizeRect>>);
impl From<TilesInner> for Tiles {
fn from(TilesInner(tiles): TilesInner) -> Self {
// This is probably the funniest one-liner I've ever written
Tiles { tiles: tiles.into_iter().map(|(k, v)| (k, v.into_iter().map(|v| v.into()).collect())).collect() }
}
}

View file

@ -0,0 +1,98 @@
use std::collections::HashMap;
use bevy::prelude::*;
use serde::{
Deserialize,
Serialize,
};
mod inner;
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone, Reflect)]
#[reflect(Debug, Deserialize, Serialize, PartialEq, Clone)]
pub struct Meta {
pub tiles: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone, Copy, Reflect)]
#[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)]
#[serde(from = "inner::DoorDataInner")]
pub struct DoorData {
pub pos: Vec2,
#[serde(default)]
pub facing_left: bool,
#[serde(default)]
pub lock: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy, Reflect)]
#[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)]
#[serde(from = "inner::StairsInnerData")]
pub struct StairsData {
pub pos: Vec2,
#[serde(default = "default_floors")]
pub floors: u8,
}
impl Default for StairsData {
fn default() -> Self {
Self {
pos: Vec2::default(),
floors: inner::default_floors(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone, Reflect)]
#[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)]
#[serde(from = "inner::ItemDataInner")]
pub struct ItemData {
pub id: String,
#[serde(default)]
pub pos: UVec2,
#[serde(default)]
pub rotated: bool,
}
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone, Reflect)]
#[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)]
#[serde(from = "inner::ContainerDataInner")]
pub struct ContainerData {
pub pos: Vec2,
#[serde(default)]
pub size: UVec2,
#[serde(default)]
pub items: Option<Vec<ItemData>>,
}
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone, Reflect)]
#[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)]
#[serde(from = "inner::InteractiveInner")]
pub struct Interactive {
pub player: Vec2,
#[serde(default)]
pub doors: Vec<DoorData>,
#[serde(default)]
pub stairs: Vec<StairsData>,
#[serde(default)]
pub containers: Vec<ContainerData>,
}
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone, Deref, DerefMut, Reflect)]
#[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)]
#[serde(from = "inner::TilesInner")]
pub struct Tiles {
#[serde(flatten)]
pub tiles: HashMap<String, Vec<URect>>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Reflect, Asset)]
#[reflect(Debug, Deserialize, Serialize, PartialEq, Clone)]
pub struct LevelAsset {
pub meta: Meta,
pub tiles: Tiles,
#[serde(default)]
pub interactive: Interactive,
}

View file

@ -20,6 +20,10 @@ pub struct InteractiveObject;
#[reflect(Component, Debug, PartialEq, Default, Clone)]
pub struct Locked;
#[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Default, Clone)]
pub struct Level;
#[derive(EntityEvent, Reflect, Clone, Copy, PartialEq, Eq, Debug)]
#[reflect(Event, Debug, PartialEq, Clone)]
pub struct InteractionEvent {

16
src/tests/level.rs Normal file
View file

@ -0,0 +1,16 @@
use super::super::*;
#[test]
fn deserialize_levels() {
let level_str = include_str!("../../assets/levels/level.toml");
let level_alt_str = include_str!("../../assets/levels/level_alt.toml");
let level = toml::de::from_str::<layout::asset::structs::LevelAsset>(level_str).unwrap();
let level_alt = toml::de::from_str::<layout::asset::structs::LevelAsset>(level_alt_str).unwrap();
assert_eq!(level.meta, level_alt.meta);
assert_eq!(level.interactive, level_alt.interactive);
for (tiles_id, tiles) in level.tiles.tiles {
let (_, other_tiles) = level_alt.tiles.iter().find(|(k, _)| k == &&tiles_id).unwrap();
assert_eq!(&tiles, other_tiles);
}
}

View file

@ -1,2 +1,3 @@
mod input;
mod inventory;
mod level;