From 0ab26207248c85c1a01b6711ad0957e0dab417ca Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Fri, 27 Mar 2026 18:31:46 +0300 Subject: [PATCH] feat: Unfinished level loading - Added level struct - Added loading tests - Partially implemented level setting up system --- assets/levels/level.toml | 9 +- assets/levels/level_alt.toml | 5 +- src/layout/asset.rs | 1 - src/layout/asset/mod.rs | 73 ++++++++++ src/layout/asset/structs/inner.rs | 212 ++++++++++++++++++++++++++++++ src/layout/asset/structs/mod.rs | 98 ++++++++++++++ src/layout/mod.rs | 4 + src/tests/level.rs | 16 +++ src/tests/mod.rs | 1 + 9 files changed, 414 insertions(+), 5 deletions(-) delete mode 100644 src/layout/asset.rs create mode 100644 src/layout/asset/mod.rs create mode 100644 src/layout/asset/structs/inner.rs create mode 100644 src/layout/asset/structs/mod.rs create mode 100644 src/tests/level.rs diff --git a/assets/levels/level.toml b/assets/levels/level.toml index 39eaf4a..b432e2b 100644 --- a/assets/levels/level.toml +++ b/assets/levels/level.toml @@ -1,3 +1,6 @@ +[meta] +tiles = [ "floors", "walls", "wall_connectors" ] + [[tiles.floors]] x = 0 y = 0 @@ -72,7 +75,7 @@ lock = "left" [[interactive.doors]] x = 11 -y = 1 +y = 5 facing = "left" [[interactive.stairs]] @@ -94,7 +97,7 @@ y = 5 w = 4 h = 4 -[interactive.containers.items] +[[interactive.containers.items]] id = "lockpick" x = 2 y = 2 @@ -105,6 +108,6 @@ y = 5 w = 2 h = 2 -[interactive.containers.items] +[[interactive.containers.items]] id = "lockpick" rotated = true diff --git a/assets/levels/level_alt.toml b/assets/levels/level_alt.toml index 04d4343..b1aa68a 100644 --- a/assets/levels/level_alt.toml +++ b/assets/levels/level_alt.toml @@ -1,3 +1,6 @@ +[meta] +tiles = [ "floors", "walls", "wall_connectors" ] + [tiles] floors = [ { x = 0, y = 0, w = 16 }, @@ -26,7 +29,7 @@ doors = [ { x = 11, y = 5, facing = "left" }, ] stairs = [ - { x = 8, y = 1, floors = 2 }, + { x = 8, y = 1 }, ] containers = [ { x = 2, y = 1 }, diff --git a/src/layout/asset.rs b/src/layout/asset.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/layout/asset.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/layout/asset/mod.rs b/src/layout/asset/mod.rs new file mode 100644 index 0000000..c22b4df --- /dev/null +++ b/src/layout/asset/mod.rs @@ -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>, + mut commands: Commands, + asset_server: Res, + level_assets: Res>, +) { + 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 = 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.))); + } + }, + }} + }); +} diff --git a/src/layout/asset/structs/inner.rs b/src/layout/asset/structs/inner.rs new file mode 100644 index 0000000..0fe3149 --- /dev/null +++ b/src/layout/asset/structs/inner.rs @@ -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 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 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 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 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 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, + #[serde(default)] + pub lock: Option, +} + +impl From 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 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 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, + #[serde(default)] + pub items: Option>, +} + +impl From 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>, + #[serde(default)] + pub stairs: Option>, + #[serde(default)] + pub containers: Option>, +} + +impl From 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>); + +impl From 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() } + } +} diff --git a/src/layout/asset/structs/mod.rs b/src/layout/asset/structs/mod.rs new file mode 100644 index 0000000..8b127bb --- /dev/null +++ b/src/layout/asset/structs/mod.rs @@ -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, +} + +#[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, +} + + +#[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>, +} + +#[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, + #[serde(default)] + pub stairs: Vec, + #[serde(default)] + pub containers: Vec, +} + +#[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>, +} + +#[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, +} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 32f6e72..680d4d4 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -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 { diff --git a/src/tests/level.rs b/src/tests/level.rs new file mode 100644 index 0000000..1bff547 --- /dev/null +++ b/src/tests/level.rs @@ -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::(level_str).unwrap(); + let level_alt = toml::de::from_str::(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); + } +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 416574d..19f64cf 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,2 +1,3 @@ mod input; mod inventory; +mod level;