generated from 2ndbeam/bevy-template
feat: Level loading from TOML asset
- Fixed detect_interactions system - Added test for deserializing asset with plugin
This commit is contained in:
parent
0ab2620724
commit
79fe190b6b
9 changed files with 124 additions and 97 deletions
|
|
@ -2,17 +2,42 @@ use std::collections::HashMap;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
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};
|
use crate::{
|
||||||
|
LoadingState,
|
||||||
|
meters,
|
||||||
|
item::lockpick::lockpick_bundle,
|
||||||
|
layout::{
|
||||||
|
Level,
|
||||||
|
LevelAssetHandle,
|
||||||
|
asset::structs::*,
|
||||||
|
container::container_bundle,
|
||||||
|
door::door_bundle,
|
||||||
|
lock::padlock_bundle,
|
||||||
|
stairs::stairs_bundle,
|
||||||
|
tilemap::tilemap_bundle
|
||||||
|
},
|
||||||
|
player::player_bundle,
|
||||||
|
};
|
||||||
|
|
||||||
pub mod structs;
|
pub mod structs;
|
||||||
|
|
||||||
|
pub fn check_loading(
|
||||||
|
level_handle: Res<LevelAssetHandle>,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
mut next_state: ResMut<NextState<LoadingState>>,
|
||||||
|
) {
|
||||||
|
if asset_server.is_loaded(level_handle.id()) {
|
||||||
|
next_state.set(LoadingState::Ready);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load_level (
|
pub fn load_level (
|
||||||
InRef(level_handle): InRef<Handle<structs::LevelAsset>>,
|
level_handle: Res<LevelAssetHandle>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
asset_server: Res<AssetServer>,
|
asset_server: Res<AssetServer>,
|
||||||
level_assets: Res<Assets<structs::LevelAsset>>,
|
level_assets: Res<Assets<structs::LevelAsset>>,
|
||||||
) {
|
) {
|
||||||
let Some(level) = level_assets.get(level_handle) else {
|
let Some(level) = level_assets.get(&level_handle.0) else {
|
||||||
error!("Could not load level asset from {level_handle:?}");
|
error!("Could not load level asset from {level_handle:?}");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
@ -53,21 +78,34 @@ pub fn load_level (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for StairsData {pos, floors} in level.interactive.stairs.iter() { match floors {
|
for StairsData { pos, floors } in level.interactive.stairs.iter() { match *floors {
|
||||||
&0 | &1 => continue,
|
0 | 1 => continue,
|
||||||
_ => {
|
_ => {
|
||||||
let mut pos = vec2(meters(pos.x), meters(pos.y));
|
let mut pos = vec2(meters(pos.x), meters(pos.y));
|
||||||
let mut down = None;
|
let mut down = None;
|
||||||
let mut up = Some(vec2(meters(2.), meters(4.)));
|
let mut up = Some(vec2(meters(2.), meters(4.)));
|
||||||
for i in 0..*floors {
|
for i in 0..*floors {
|
||||||
parent.spawn(stairs_bundle(&asset_server, pos, up, down));
|
if i == *floors - 1 {
|
||||||
pos.y += meters(4.);
|
|
||||||
if i == floors - 1 {
|
|
||||||
up = None;
|
up = None;
|
||||||
}
|
}
|
||||||
|
parent.spawn(stairs_bundle(&asset_server, pos, up, down));
|
||||||
|
pos.y += meters(4.);
|
||||||
down = Some(vec2(meters(-2.), meters(-4.)));
|
down = Some(vec2(meters(-2.), meters(-4.)));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
for ContainerData { pos, size, items } in level.interactive.containers.iter() {
|
||||||
|
let pos = vec2(meters(pos.x), meters(pos.y));
|
||||||
|
|
||||||
|
let mut container = parent.spawn(container_bundle(&asset_server, pos, *size));
|
||||||
|
|
||||||
|
for item in items {
|
||||||
|
// TODO: replace with proper item-by-id system
|
||||||
|
if item.id == "lockpick" {
|
||||||
|
container.with_child(lockpick_bundle(&asset_server, item.pos, item.rotated));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@ impl From<ContainerDataInner> for ContainerData {
|
||||||
Self {
|
Self {
|
||||||
pos: pos.into(),
|
pos: pos.into(),
|
||||||
size: size.unwrap_or_default().into(),
|
size: size.unwrap_or_default().into(),
|
||||||
items,
|
items: items.unwrap_or_default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ pub struct ContainerData {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub size: UVec2,
|
pub size: UVec2,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub items: Option<Vec<ItemData>>,
|
pub items: Vec<ItemData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone, Reflect)]
|
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone, Reflect)]
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,13 @@ pub struct Locked;
|
||||||
|
|
||||||
#[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)]
|
#[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)]
|
||||||
#[reflect(Component, Debug, PartialEq, Default, Clone)]
|
#[reflect(Component, Debug, PartialEq, Default, Clone)]
|
||||||
|
#[require(Transform, InheritedVisibility)]
|
||||||
pub struct Level;
|
pub struct Level;
|
||||||
|
|
||||||
|
#[derive(Resource, Debug, PartialEq, Eq, Default, Clone, Reflect, Deref, DerefMut)]
|
||||||
|
#[reflect(Resource, Debug, PartialEq, Default, Clone)]
|
||||||
|
pub struct LevelAssetHandle(Handle<asset::structs::LevelAsset>);
|
||||||
|
|
||||||
#[derive(EntityEvent, Reflect, Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(EntityEvent, Reflect, Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
#[reflect(Event, Debug, PartialEq, Clone)]
|
#[reflect(Event, Debug, PartialEq, Clone)]
|
||||||
pub struct InteractionEvent {
|
pub struct InteractionEvent {
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ pub fn on_stairs_interact(
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Ok(mut player_transform) = player_query.single_mut() else {
|
let Ok(mut player_transform) = player_query.single_mut() else {
|
||||||
error!("Player is not single");
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,7 @@ use bevy::{
|
||||||
use bevy_rapier2d::prelude::*;
|
use bevy_rapier2d::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
meters,
|
player::Player,
|
||||||
item::lockpick::lockpick_bundle,
|
|
||||||
player::{
|
|
||||||
Player,
|
|
||||||
player_bundle,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -33,6 +28,22 @@ fn get_interactive_id<F: QueryFilter>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn interact_collisions_inner<F: QueryFilter>(
|
||||||
|
first: Entity,
|
||||||
|
second: Entity,
|
||||||
|
interactive_query: Query<(), F>,
|
||||||
|
player_query: Query<(), With<Player>>,
|
||||||
|
parent_query: Query<&ChildOf, With<Collider>>,
|
||||||
|
) -> Option<Entity> {
|
||||||
|
let Some(interactive_id) = get_interactive_id(first, interactive_query, parent_query) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
if player_query.get(second).is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(interactive_id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn detect_interact_collisions(
|
pub fn detect_interact_collisions(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut collision_events: MessageReader<CollisionEvent>,
|
mut collision_events: MessageReader<CollisionEvent>,
|
||||||
|
|
@ -44,22 +55,20 @@ pub fn detect_interact_collisions(
|
||||||
for collision_event in collision_events.read() {
|
for collision_event in collision_events.read() {
|
||||||
match collision_event {
|
match collision_event {
|
||||||
CollisionEvent::Started(first, second, _) => {
|
CollisionEvent::Started(first, second, _) => {
|
||||||
let Some(interactive_id) = get_interactive_id(*first, interactive_query1, parent_query) else {
|
if let Some(interactive_id) = interact_collisions_inner(*first, *second, interactive_query1, player_query, parent_query) {
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if player_query.get(*second).is_err() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
commands.entity(interactive_id).insert(MayInteract);
|
commands.entity(interactive_id).insert(MayInteract);
|
||||||
|
}
|
||||||
|
if let Some(interactive_id) = interact_collisions_inner(*second, *first, interactive_query1, player_query, parent_query) {
|
||||||
|
commands.entity(interactive_id).insert(MayInteract);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
CollisionEvent::Stopped(first, second, _) => {
|
CollisionEvent::Stopped(first, second, _) => {
|
||||||
let Some(interactive_id) = get_interactive_id(*first, interactive_query2, parent_query) else {
|
if let Some(interactive_id) = interact_collisions_inner(*first, *second, interactive_query2, player_query, parent_query) {
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if player_query.get(*second).is_err() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
commands.entity(interactive_id).remove::<MayInteract>();
|
commands.entity(interactive_id).remove::<MayInteract>();
|
||||||
|
}
|
||||||
|
if let Some(interactive_id) = interact_collisions_inner(*second, *first, interactive_query2, player_query, parent_query) {
|
||||||
|
commands.entity(interactive_id).remove::<MayInteract>();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -78,66 +87,6 @@ pub fn setup_world(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
asset_server: Res<AssetServer>,
|
asset_server: Res<AssetServer>,
|
||||||
) {
|
) {
|
||||||
let tiles = [
|
let level_handle = asset_server.load("levels/level.toml");
|
||||||
( 0, ( 0, 0, 16, 1 )), // 1F floor
|
commands.insert_resource(LevelAssetHandle(level_handle));
|
||||||
( 0, ( 0, 4, 16, 1 )), // 1F ceiling / 2F floor
|
|
||||||
( 0, ( 0, 8, 16, 1 )), // 2F ceiling
|
|
||||||
|
|
||||||
( 1, ( 0, 1, 1, 3 )), // 1F left wall
|
|
||||||
( 1, ( 0, 5, 1, 3 )), // 2F left wall
|
|
||||||
( 1, ( 15, 1, 1, 3 )), // 1F right wall
|
|
||||||
( 1, ( 15, 5, 1, 3 )), // 2F right wall
|
|
||||||
|
|
||||||
( 2, ( 4, 3, 1, 1 )), // 1F left door connector
|
|
||||||
( 2, ( 4, 7, 1, 1 )), // 2F left door connector
|
|
||||||
( 2, ( 11, 3, 1, 1 )), // 1F right door connector
|
|
||||||
( 2, ( 11, 7, 1, 1 )), // 2F right door connector
|
|
||||||
].into_iter().map(|(id, (x, y, w, h))| {
|
|
||||||
(id, URect::from_corners(uvec2(x, y), uvec2(x + w - 1, y + h - 1)))
|
|
||||||
}).collect::<Vec<(u16, URect)>>();
|
|
||||||
|
|
||||||
commands.spawn(tilemap::tilemap_bundle(&asset_server, tiles));
|
|
||||||
|
|
||||||
commands.spawn(player_bundle(&asset_server, vec2(meters(1.5), meters(1.))));
|
|
||||||
|
|
||||||
commands.spawn(door::door_bundle(&asset_server, vec2(meters(4.5), meters(1.)), false));
|
|
||||||
commands.spawn(door::door_bundle(&asset_server, vec2(meters(4.5), meters(5.)), false))
|
|
||||||
.with_child(lock::padlock_bundle(&asset_server, false));
|
|
||||||
commands.spawn(door::door_bundle(&asset_server, vec2(meters(11.5), meters(1.)), true))
|
|
||||||
.with_child(lock::padlock_bundle(&asset_server, true));
|
|
||||||
commands.spawn(door::door_bundle(&asset_server, vec2(meters(11.5), meters(5.)), true));
|
|
||||||
|
|
||||||
commands.spawn(stairs::stairs_bundle(
|
|
||||||
&asset_server,
|
|
||||||
vec2(meters(8.), meters(1.)),
|
|
||||||
Some(vec2(meters(2.), meters(4.))),
|
|
||||||
None,
|
|
||||||
));
|
|
||||||
commands.spawn(stairs::stairs_bundle(
|
|
||||||
&asset_server,
|
|
||||||
vec2(meters(8.), meters(5.)),
|
|
||||||
None,
|
|
||||||
Some(vec2(meters(-2.), meters(-4.))),
|
|
||||||
));
|
|
||||||
|
|
||||||
commands.spawn(container::container_bundle(
|
|
||||||
&asset_server,
|
|
||||||
vec2(meters(2.), meters(1.)),
|
|
||||||
uvec2(1, 1),
|
|
||||||
));
|
|
||||||
commands.spawn(container::container_bundle(
|
|
||||||
&asset_server,
|
|
||||||
vec2(meters(14.), meters(1.)),
|
|
||||||
uvec2(1, 1),
|
|
||||||
));
|
|
||||||
commands.spawn(container::container_bundle(
|
|
||||||
&asset_server,
|
|
||||||
vec2(meters(2.), meters(5.)),
|
|
||||||
uvec2(4, 4),
|
|
||||||
)).with_child(lockpick_bundle(&asset_server, uvec2(2, 2), false));
|
|
||||||
commands.spawn(container::container_bundle(
|
|
||||||
&asset_server,
|
|
||||||
vec2(meters(14.), meters(5.)),
|
|
||||||
uvec2(2, 2),
|
|
||||||
)).with_child(lockpick_bundle(&asset_server, UVec2::ZERO, true));
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
src/lib.rs
14
src/lib.rs
|
|
@ -2,6 +2,7 @@ use bevy::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
ui_widgets::ScrollbarPlugin,
|
ui_widgets::ScrollbarPlugin,
|
||||||
};
|
};
|
||||||
|
use bevy_common_assets::toml::TomlAssetPlugin;
|
||||||
use bevy_rapier2d::{
|
use bevy_rapier2d::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
rapier::prelude::IntegrationParameters,
|
rapier::prelude::IntegrationParameters,
|
||||||
|
|
@ -28,6 +29,14 @@ pub enum GameState {
|
||||||
Inventory,
|
Inventory,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States, Reflect)]
|
||||||
|
#[reflect(Clone, PartialEq, Debug, Default, Hash, State)]
|
||||||
|
pub enum LoadingState {
|
||||||
|
#[default]
|
||||||
|
Loading,
|
||||||
|
Ready,
|
||||||
|
}
|
||||||
|
|
||||||
fn camera_bundle() -> impl Bundle {
|
fn camera_bundle() -> impl Bundle {
|
||||||
(
|
(
|
||||||
Camera2d,
|
Camera2d,
|
||||||
|
|
@ -81,9 +90,11 @@ impl Plugin for ExpeditionPlugin {
|
||||||
RapierPhysicsPlugin::<()>::default()
|
RapierPhysicsPlugin::<()>::default()
|
||||||
.with_custom_initialization(rapier_init),
|
.with_custom_initialization(rapier_init),
|
||||||
ScrollbarPlugin,
|
ScrollbarPlugin,
|
||||||
|
TomlAssetPlugin::<layout::asset::structs::LevelAsset>::new(&["toml"]),
|
||||||
input::plugin::InputAssetPlugin::<input::InputAction>::default(),
|
input::plugin::InputAssetPlugin::<input::InputAction>::default(),
|
||||||
input::plugin::InputAssetPlugin::<input::UiAction>::default(),
|
input::plugin::InputAssetPlugin::<input::UiAction>::default(),
|
||||||
))
|
))
|
||||||
|
.init_state::<LoadingState>()
|
||||||
.init_state::<GameState>()
|
.init_state::<GameState>()
|
||||||
.insert_resource(ui::WindowSize::default())
|
.insert_resource(ui::WindowSize::default())
|
||||||
.add_systems(Startup, (
|
.add_systems(Startup, (
|
||||||
|
|
@ -92,12 +103,15 @@ impl Plugin for ExpeditionPlugin {
|
||||||
))
|
))
|
||||||
.add_systems(Update, (
|
.add_systems(Update, (
|
||||||
insert_entity_name,
|
insert_entity_name,
|
||||||
|
layout::asset::check_loading
|
||||||
|
.run_if(|state: Res<State<LoadingState>>| *state == LoadingState::Loading),
|
||||||
layout::systems::detect_interact_collisions,
|
layout::systems::detect_interact_collisions,
|
||||||
layout::systems::lock_door,
|
layout::systems::lock_door,
|
||||||
player::systems::handle_input,
|
player::systems::handle_input,
|
||||||
ui::update_window_size,
|
ui::update_window_size,
|
||||||
ui::handle_input,
|
ui::handle_input,
|
||||||
))
|
))
|
||||||
|
.add_systems(OnEnter(LoadingState::Ready), layout::asset::load_level)
|
||||||
.add_systems(OnEnter(GameState::Inventory), ui::inventory::systems::setup_ui_inventory)
|
.add_systems(OnEnter(GameState::Inventory), ui::inventory::systems::setup_ui_inventory)
|
||||||
.add_systems(OnExit(GameState::Inventory), ui::inventory::systems::clear_ui_inventory)
|
.add_systems(OnExit(GameState::Inventory), ui::inventory::systems::clear_ui_inventory)
|
||||||
.add_observer(ui::inventory::observers::on_ui_rotate)
|
.add_observer(ui::inventory::observers::on_ui_rotate)
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ pub fn handle_input(
|
||||||
interactables: Query<Entity, With<MayInteract>>,
|
interactables: Query<Entity, With<MayInteract>>,
|
||||||
mut player: Query<(&Player, &mut ActionState<Action>, &mut KinematicCharacterController, &mut Sprite)>,
|
mut player: Query<(&Player, &mut ActionState<Action>, &mut KinematicCharacterController, &mut Sprite)>,
|
||||||
) {
|
) {
|
||||||
let player = player.single_mut().expect("Player should be single");
|
let Ok(player) = player.single_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
match state.get() {
|
match state.get() {
|
||||||
GameState::Running => {
|
GameState::Running => {
|
||||||
let (Player {speed}, mut action_state, mut controller, mut sprite) = player;
|
let (Player {speed}, mut action_state, mut controller, mut sprite) = player;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
use super::super::*;
|
use bevy::prelude::*;
|
||||||
|
use bevy_common_assets::toml::TomlAssetPlugin;
|
||||||
|
|
||||||
|
use crate::layout::asset::structs::LevelAsset;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deserialize_levels() {
|
fn deserialize_from_str() {
|
||||||
let level_str = include_str!("../../assets/levels/level.toml");
|
let level_str = include_str!("../../assets/levels/level.toml");
|
||||||
let level_alt_str = include_str!("../../assets/levels/level_alt.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 = toml::de::from_str::<LevelAsset>(level_str).unwrap();
|
||||||
let level_alt = toml::de::from_str::<layout::asset::structs::LevelAsset>(level_alt_str).unwrap();
|
let level_alt = toml::de::from_str::<LevelAsset>(level_alt_str).unwrap();
|
||||||
|
|
||||||
assert_eq!(level.meta, level_alt.meta);
|
assert_eq!(level.meta, level_alt.meta);
|
||||||
assert_eq!(level.interactive, level_alt.interactive);
|
assert_eq!(level.interactive, level_alt.interactive);
|
||||||
|
|
@ -14,3 +17,20 @@ fn deserialize_levels() {
|
||||||
assert_eq!(&tiles, other_tiles);
|
assert_eq!(&tiles, other_tiles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_from_app_asset() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins((
|
||||||
|
MinimalPlugins,
|
||||||
|
AssetPlugin {
|
||||||
|
file_path: "../../assets".to_string(),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TomlAssetPlugin::<LevelAsset>::new(&["toml"]),
|
||||||
|
));
|
||||||
|
|
||||||
|
if let Err(error) = app.world_mut().run_system_cached(|asset_server: Res<AssetServer>| {
|
||||||
|
let _ = asset_server.load::<LevelAsset>("levels/level.toml");
|
||||||
|
}) { panic!("{error}") };
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue