generated from 2ndbeam/bevy-template
refactor!: Split everything into submodules
- Bump version to 0.2.0 - Split every module to systems, observers, plugins, etc - Renamed Crate to Container - Removed Wall component - Removed try_insert_item system - Removed inventory::ui module
This commit is contained in:
parent
3094a8af13
commit
7c386d4128
20 changed files with 830 additions and 772 deletions
|
|
@ -2,7 +2,7 @@ cargo-features = ["codegen-backend"]
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "expedition_demo"
|
name = "expedition_demo"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
||||||
41
src/input/mod.rs
Normal file
41
src/input/mod.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use leafwing_input_manager::prelude::*;
|
||||||
|
use serde::{
|
||||||
|
Serialize,
|
||||||
|
Deserialize
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod plugin;
|
||||||
|
|
||||||
|
#[derive(Actionlike, PartialEq, Eq, Hash, Debug, Clone, Reflect, Serialize, Deserialize)]
|
||||||
|
pub enum InputAction {
|
||||||
|
#[actionlike(Axis)]
|
||||||
|
Move,
|
||||||
|
ToggleInventory,
|
||||||
|
Interact,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputAction {
|
||||||
|
pub fn default_input_map() -> InputMap<Self> {
|
||||||
|
InputMap::default()
|
||||||
|
.with_axis(Self::Move, VirtualAxis::ad())
|
||||||
|
.with_axis(Self::Move, GamepadAxis::LeftStickX)
|
||||||
|
.with(Self::ToggleInventory, KeyCode::KeyI)
|
||||||
|
.with(Self::ToggleInventory, GamepadButton::Select)
|
||||||
|
.with(Self::Interact, KeyCode::KeyE)
|
||||||
|
.with(Self::Interact, GamepadButton::East)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Actionlike, PartialEq, Eq, Hash, Debug, Clone, Reflect, Serialize, Deserialize)]
|
||||||
|
pub enum UiAction {
|
||||||
|
Rotate,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiAction {
|
||||||
|
pub fn default_input_map() -> InputMap<Self> {
|
||||||
|
InputMap::default()
|
||||||
|
.with(Self::Rotate, KeyCode::KeyR)
|
||||||
|
.with(Self::Rotate, GamepadButton::West)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
pub mod item;
|
pub mod item;
|
||||||
pub mod ui;
|
|
||||||
|
|
||||||
#[derive(Component, Reflect, Default)]
|
#[derive(Component, Reflect, Default)]
|
||||||
pub struct Inventory {
|
pub struct Inventory {
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
pub use crate::ui::inventory::*;
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_rapier2d::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
GameState,
|
||||||
|
PIXELS_PER_METER,
|
||||||
|
inventory::{
|
||||||
|
ActiveInventory,
|
||||||
|
Inventory,
|
||||||
|
item::Item,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const CRATE_CLOSED_ASSET: &'static str = "sprites/interactive/crate_closed.png";
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
#[require(Sprite, InteractiveObject, Inventory)]
|
||||||
|
pub struct Container;
|
||||||
|
|
||||||
|
fn on_container_interact(
|
||||||
|
event: On<InteractionEvent>,
|
||||||
|
mut commands: Commands,
|
||||||
|
locked_query: Query<(), With<Locked>>,
|
||||||
|
crate_query: Query<(), With<Container>>,
|
||||||
|
mut next_state: ResMut<NextState<GameState>>,
|
||||||
|
) {
|
||||||
|
if locked_query.get(event.entity).is_ok() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if crate_query.get(event.entity).is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
commands.entity(event.entity).insert(ActiveInventory);
|
||||||
|
next_state.set(GameState::Inventory);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn container_bundle(
|
||||||
|
asset_server: &Res<AssetServer>,
|
||||||
|
position: Vec2,
|
||||||
|
inventory_size: UVec2,
|
||||||
|
items: Vec<Item>
|
||||||
|
) -> impl Bundle {
|
||||||
|
let image = asset_server.load(CRATE_CLOSED_ASSET);
|
||||||
|
(
|
||||||
|
Container,
|
||||||
|
Transform::from_xyz(position.x, position.y - PIXELS_PER_METER * 0.5, 0.),
|
||||||
|
Sprite::from_image(image),
|
||||||
|
Inventory::new(inventory_size),
|
||||||
|
Observer::new(on_container_interact),
|
||||||
|
Children::spawn((
|
||||||
|
SpawnIter(items.into_iter()),
|
||||||
|
Spawn((
|
||||||
|
Collider::cuboid(PIXELS_PER_METER, PIXELS_PER_METER),
|
||||||
|
Sensor,
|
||||||
|
Transform::from_xyz(0., PIXELS_PER_METER * 0.5, 0.),
|
||||||
|
)),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_rapier2d::prelude::*;
|
||||||
|
|
||||||
|
use crate::PIXELS_PER_METER;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const DOOR_OPENED_ASSET: &'static str = "sprites/interactive/door_opened.png";
|
||||||
|
const DOOR_CLOSED_ASSET: &'static str = "sprites/interactive/door_closed.png";
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
#[require(Sprite, InteractiveObject)]
|
||||||
|
pub struct Door(pub i8);
|
||||||
|
|
||||||
|
fn on_door_interact(
|
||||||
|
event: On<InteractionEvent>,
|
||||||
|
mut commands: Commands,
|
||||||
|
locked_query: Query<(), With<Locked>>,
|
||||||
|
collider_query: Query<(), (With<Door>, With<Collider>)>,
|
||||||
|
no_collider_query: Query<(), (With<Door>, Without<Collider>)>,
|
||||||
|
door_query: Query<(&Door, &Children)>,
|
||||||
|
mut sprite_query: Query<(&mut Sprite, &mut Transform)>,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
) {
|
||||||
|
if locked_query.get(event.entity).is_ok() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let was_opened = if collider_query.get(event.entity).is_ok() { false }
|
||||||
|
else if no_collider_query.get(event.entity).is_ok() { true }
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok((door, children)) = door_query.get(event.entity) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for child in children {
|
||||||
|
if let Ok((mut sprite, mut transform)) = sprite_query.get_mut(*child) {
|
||||||
|
let (image, translation) = if was_opened { (DOOR_CLOSED_ASSET, 0.) }
|
||||||
|
else { (DOOR_OPENED_ASSET, door.0 as f32 * PIXELS_PER_METER * 0.5) };
|
||||||
|
sprite.image = asset_server.load(image);
|
||||||
|
transform.translation.x = translation;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if was_opened {
|
||||||
|
commands.entity(event.entity).insert(door_collider());
|
||||||
|
} else {
|
||||||
|
commands.entity(event.entity).remove::<Collider>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn door_collider() -> Collider {
|
||||||
|
Collider::cuboid(PIXELS_PER_METER * 0.06125, PIXELS_PER_METER)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn door_bundle(asset_server: &Res<AssetServer>, position: Vec2, facing_left: bool) -> impl Bundle {
|
||||||
|
let direction = if facing_left { -1 } else { 1 };
|
||||||
|
let image = asset_server.load(DOOR_CLOSED_ASSET);
|
||||||
|
(
|
||||||
|
Door(direction),
|
||||||
|
Transform::from_xyz(position.x, position.y, 0.),
|
||||||
|
Name::new(format!("Door ({}, {})", position.x, position.y)),
|
||||||
|
door_collider(),
|
||||||
|
Observer::new(on_door_interact),
|
||||||
|
children![
|
||||||
|
(
|
||||||
|
Collider::cuboid(PIXELS_PER_METER * 0.5, PIXELS_PER_METER),
|
||||||
|
Sensor,
|
||||||
|
Transform::from_xyz(0., 0., 0.),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Sprite {
|
||||||
|
image,
|
||||||
|
flip_x: facing_left,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform::from_xyz(0., 0., 0.),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,33 +1,15 @@
|
||||||
use bevy::{ecs::query::QueryFilter, prelude::*};
|
use bevy::prelude::*;
|
||||||
use bevy_rapier2d::prelude::*;
|
|
||||||
|
|
||||||
use crate::{GameState, PIXELS_PER_METER, inventory::{ActiveInventory, Inventory, item::Item}, player::Player};
|
|
||||||
|
|
||||||
pub mod container;
|
pub mod container;
|
||||||
pub mod door;
|
pub mod door;
|
||||||
pub mod systems;
|
pub mod systems;
|
||||||
|
|
||||||
const DOOR_OPENED_ASSET: &'static str = "sprites/interactive/door_opened.png";
|
|
||||||
const DOOR_CLOSED_ASSET: &'static str = "sprites/interactive/door_closed.png";
|
|
||||||
const CRATE_CLOSED_ASSET: &'static str = "sprites/interactive/crate_closed.png";
|
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct MayInteract;
|
pub struct MayInteract;
|
||||||
|
|
||||||
#[derive(Component, Default)]
|
#[derive(Component, Default)]
|
||||||
pub struct InteractiveObject;
|
pub struct InteractiveObject;
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
pub struct Wall;
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
#[require(Sprite, InteractiveObject)]
|
|
||||||
pub struct Door(pub i8);
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
#[require(Sprite, InteractiveObject, Inventory)]
|
|
||||||
pub struct Crate;
|
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct Locked;
|
pub struct Locked;
|
||||||
|
|
||||||
|
|
@ -35,187 +17,3 @@ pub struct Locked;
|
||||||
pub struct InteractionEvent {
|
pub struct InteractionEvent {
|
||||||
pub entity: Entity,
|
pub entity: Entity,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_interactive_id<F: QueryFilter>(
|
|
||||||
tested_id: Entity,
|
|
||||||
interactive_query: Query<(), F>,
|
|
||||||
parent_query: Query<&ChildOf, With<Collider>>,
|
|
||||||
) -> Option<Entity> {
|
|
||||||
if interactive_query.get(tested_id).is_ok() {
|
|
||||||
return Some(tested_id);
|
|
||||||
}
|
|
||||||
let Ok(parent_id) = parent_query.get(tested_id) else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
match interactive_query.get(parent_id.0) {
|
|
||||||
Ok(_) => Some(parent_id.0),
|
|
||||||
Err(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn detect_interact_collisions(
|
|
||||||
mut commands: Commands,
|
|
||||||
mut collision_events: MessageReader<CollisionEvent>,
|
|
||||||
player_query: Query<(), With<Player>>,
|
|
||||||
interactive_query1: Query<(), (With<InteractiveObject>, Without<MayInteract>)>,
|
|
||||||
interactive_query2: Query<(), (With<InteractiveObject>, With<MayInteract>)>,
|
|
||||||
parent_query: Query<&ChildOf, With<Collider>>,
|
|
||||||
) {
|
|
||||||
for collision_event in collision_events.read() {
|
|
||||||
match collision_event {
|
|
||||||
CollisionEvent::Started(first, second, _) => {
|
|
||||||
let Some(interactive_id) = get_interactive_id(*first, interactive_query1, parent_query) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if player_query.get(*second).is_err() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
commands.entity(interactive_id).insert(MayInteract);
|
|
||||||
},
|
|
||||||
CollisionEvent::Stopped(first, second, _) => {
|
|
||||||
let Some(interactive_id) = get_interactive_id(*first, interactive_query2, parent_query) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if player_query.get(*second).is_err() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
commands.entity(interactive_id).remove::<MayInteract>();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_crate_interact(
|
|
||||||
event: On<InteractionEvent>,
|
|
||||||
mut commands: Commands,
|
|
||||||
locked_query: Query<(), With<Locked>>,
|
|
||||||
crate_query: Query<(), With<Crate>>,
|
|
||||||
mut next_state: ResMut<NextState<GameState>>,
|
|
||||||
) {
|
|
||||||
if locked_query.get(event.entity).is_ok() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if crate_query.get(event.entity).is_err() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
commands.entity(event.entity).insert(ActiveInventory);
|
|
||||||
next_state.set(GameState::Inventory);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn crate_bundle(image: Handle<Image>, position: Vec2, inventory_size: UVec2, items: Vec<Item>) -> impl Bundle {
|
|
||||||
(
|
|
||||||
Crate,
|
|
||||||
Transform::from_xyz(position.x, position.y - PIXELS_PER_METER * 0.5, 0.),
|
|
||||||
Sprite::from_image(image),
|
|
||||||
Inventory::new(inventory_size),
|
|
||||||
Observer::new(on_crate_interact),
|
|
||||||
Children::spawn((
|
|
||||||
SpawnIter(items.into_iter()),
|
|
||||||
Spawn((
|
|
||||||
Collider::cuboid(PIXELS_PER_METER, PIXELS_PER_METER),
|
|
||||||
Sensor,
|
|
||||||
Transform::from_xyz(0., PIXELS_PER_METER * 0.5, 0.),
|
|
||||||
)),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_door_interact(
|
|
||||||
event: On<InteractionEvent>,
|
|
||||||
mut commands: Commands,
|
|
||||||
locked_query: Query<(), With<Locked>>,
|
|
||||||
collider_query: Query<(), (With<Door>, With<Collider>)>,
|
|
||||||
no_collider_query: Query<(), (With<Door>, Without<Collider>)>,
|
|
||||||
door_query: Query<(&Door, &Children)>,
|
|
||||||
mut sprite_query: Query<(&mut Sprite, &mut Transform)>,
|
|
||||||
asset_server: Res<AssetServer>,
|
|
||||||
) {
|
|
||||||
if locked_query.get(event.entity).is_ok() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let was_opened = if collider_query.get(event.entity).is_ok() { false }
|
|
||||||
else if no_collider_query.get(event.entity).is_ok() { true }
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Ok((door, children)) = door_query.get(event.entity) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
for child in children {
|
|
||||||
if let Ok((mut sprite, mut transform)) = sprite_query.get_mut(*child) {
|
|
||||||
let (image, translation) = if was_opened { (DOOR_CLOSED_ASSET, 0.) }
|
|
||||||
else { (DOOR_OPENED_ASSET, door.0 as f32 * PIXELS_PER_METER * 0.5) };
|
|
||||||
sprite.image = asset_server.load(image);
|
|
||||||
transform.translation.x = translation;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if was_opened {
|
|
||||||
commands.entity(event.entity).insert(door_collider());
|
|
||||||
} else {
|
|
||||||
commands.entity(event.entity).remove::<Collider>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn door_collider() -> Collider {
|
|
||||||
Collider::cuboid(PIXELS_PER_METER * 0.06125, PIXELS_PER_METER)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn door_bundle(image: Handle<Image>, position: Vec2, facing_left: bool) -> impl Bundle {
|
|
||||||
let direction = if facing_left { -1 } else { 1 };
|
|
||||||
(
|
|
||||||
Door(direction),
|
|
||||||
Transform::from_xyz(position.x, position.y, 0.),
|
|
||||||
Name::new(format!("Door ({}, {})", position.x, position.y)),
|
|
||||||
door_collider(),
|
|
||||||
Observer::new(on_door_interact),
|
|
||||||
children![
|
|
||||||
(
|
|
||||||
Collider::cuboid(PIXELS_PER_METER * 0.5, PIXELS_PER_METER),
|
|
||||||
Sensor,
|
|
||||||
Transform::from_xyz(0., 0., 0.),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Sprite {
|
|
||||||
image,
|
|
||||||
flip_x: facing_left,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
Transform::from_xyz(0., 0., 0.),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wall_bundle(position: Vec2) -> impl Bundle {
|
|
||||||
(
|
|
||||||
Wall,
|
|
||||||
Transform::from_xyz(position.x, position.y, 0.),
|
|
||||||
Collider::cuboid(PIXELS_PER_METER * 0.5, PIXELS_PER_METER),
|
|
||||||
Name::new(format!("Wall ({}, {})", position.x, position.y)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setup_world(
|
|
||||||
mut commands: Commands,
|
|
||||||
asset_server: Res<AssetServer>,
|
|
||||||
) {
|
|
||||||
let items = vec![
|
|
||||||
Item::new_positioned(uvec2(1, 1), uvec2(0, 0)),
|
|
||||||
Item::new_positioned(uvec2(2, 1), uvec2(1, 0)),
|
|
||||||
Item::new_positioned(uvec2(3, 1), uvec2(3, 0)),
|
|
||||||
Item::new_positioned(uvec2(2, 2), uvec2(6, 0)),
|
|
||||||
Item::new_positioned(uvec2(6, 2), uvec2(0, 1)),
|
|
||||||
Item::new_positioned(uvec2(2, 3), uvec2(6, 2)),
|
|
||||||
Item::new_positioned(uvec2(4, 4), uvec2(0, 4)),
|
|
||||||
];
|
|
||||||
let door_image = asset_server.load(DOOR_CLOSED_ASSET);
|
|
||||||
let crate_image = asset_server.load(CRATE_CLOSED_ASSET);
|
|
||||||
commands.spawn(door_bundle(door_image.clone(), vec2(16., 0.), true));
|
|
||||||
commands.spawn(door_bundle(door_image.clone(), vec2(48., 0.), false));
|
|
||||||
commands.spawn(door_bundle(door_image.clone(), vec2(80., 0.), false)).insert(Locked);
|
|
||||||
commands.spawn(wall_bundle(vec2(-48., 0.)));
|
|
||||||
commands.spawn(crate_bundle(crate_image.clone(), vec2(-32., 0.), uvec2(8, 8), items));
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
use bevy::{
|
||||||
|
ecs::query::QueryFilter,
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
use bevy_rapier2d::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
inventory::item::Item,
|
||||||
|
player::Player,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn get_interactive_id<F: QueryFilter>(
|
||||||
|
tested_id: Entity,
|
||||||
|
interactive_query: Query<(), F>,
|
||||||
|
parent_query: Query<&ChildOf, With<Collider>>,
|
||||||
|
) -> Option<Entity> {
|
||||||
|
if interactive_query.get(tested_id).is_ok() {
|
||||||
|
return Some(tested_id);
|
||||||
|
}
|
||||||
|
let Ok(parent_id) = parent_query.get(tested_id) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
match interactive_query.get(parent_id.0) {
|
||||||
|
Ok(_) => Some(parent_id.0),
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect_interact_collisions(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut collision_events: MessageReader<CollisionEvent>,
|
||||||
|
player_query: Query<(), With<Player>>,
|
||||||
|
interactive_query1: Query<(), (With<InteractiveObject>, Without<MayInteract>)>,
|
||||||
|
interactive_query2: Query<(), (With<InteractiveObject>, With<MayInteract>)>,
|
||||||
|
parent_query: Query<&ChildOf, With<Collider>>,
|
||||||
|
) {
|
||||||
|
for collision_event in collision_events.read() {
|
||||||
|
match collision_event {
|
||||||
|
CollisionEvent::Started(first, second, _) => {
|
||||||
|
let Some(interactive_id) = get_interactive_id(*first, interactive_query1, parent_query) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if player_query.get(*second).is_err() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
commands.entity(interactive_id).insert(MayInteract);
|
||||||
|
},
|
||||||
|
CollisionEvent::Stopped(first, second, _) => {
|
||||||
|
let Some(interactive_id) = get_interactive_id(*first, interactive_query2, parent_query) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if player_query.get(*second).is_err() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
commands.entity(interactive_id).remove::<MayInteract>();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_world(
|
||||||
|
mut commands: Commands,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
) {
|
||||||
|
let items = vec![
|
||||||
|
Item::new_positioned(uvec2(1, 1), uvec2(0, 0)),
|
||||||
|
Item::new_positioned(uvec2(2, 1), uvec2(1, 0)),
|
||||||
|
Item::new_positioned(uvec2(3, 1), uvec2(3, 0)),
|
||||||
|
Item::new_positioned(uvec2(2, 2), uvec2(6, 0)),
|
||||||
|
Item::new_positioned(uvec2(6, 2), uvec2(0, 1)),
|
||||||
|
Item::new_positioned(uvec2(2, 3), uvec2(6, 2)),
|
||||||
|
Item::new_positioned(uvec2(4, 4), uvec2(0, 4)),
|
||||||
|
];
|
||||||
|
commands.spawn(door::door_bundle(&asset_server, vec2(16., 0.), true));
|
||||||
|
commands.spawn(door::door_bundle(&asset_server, vec2(48., 0.), false));
|
||||||
|
commands.spawn(door::door_bundle(&asset_server, vec2(80., 0.), false)).insert(Locked);
|
||||||
|
commands.spawn(container::container_bundle(&asset_server, vec2(-32., 0.), uvec2(8, 8), items));
|
||||||
|
}
|
||||||
69
src/lib.rs
69
src/lib.rs
|
|
@ -1,3 +1,12 @@
|
||||||
|
use bevy::{
|
||||||
|
prelude::*,
|
||||||
|
ui_widgets::ScrollbarPlugin
|
||||||
|
};
|
||||||
|
use bevy_rapier2d::{
|
||||||
|
prelude::*,
|
||||||
|
rapier::prelude::IntegrationParameters
|
||||||
|
};
|
||||||
|
|
||||||
pub mod player;
|
pub mod player;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
|
|
@ -6,28 +15,10 @@ pub mod ui;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use bevy::{prelude::*, ui_widgets::ScrollbarPlugin};
|
|
||||||
use bevy_rapier2d::{prelude::*, rapier::prelude::IntegrationParameters};
|
|
||||||
use leafwing_input_manager::prelude::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
pub const PIXELS_PER_METER: f32 = 16.0;
|
pub const PIXELS_PER_METER: f32 = 16.0;
|
||||||
|
|
||||||
pub struct ExpeditionPlugin;
|
pub struct ExpeditionPlugin;
|
||||||
|
|
||||||
#[derive(Actionlike, PartialEq, Eq, Hash, Debug, Clone, Reflect, Serialize, Deserialize)]
|
|
||||||
pub enum InputAction {
|
|
||||||
#[actionlike(Axis)]
|
|
||||||
Move,
|
|
||||||
ToggleInventory,
|
|
||||||
Interact,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Actionlike, PartialEq, Eq, Hash, Debug, Clone, Reflect, Serialize, Deserialize)]
|
|
||||||
pub enum UiAction {
|
|
||||||
Rotate,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)]
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)]
|
||||||
pub enum GameState {
|
pub enum GameState {
|
||||||
#[default]
|
#[default]
|
||||||
|
|
@ -35,26 +26,6 @@ pub enum GameState {
|
||||||
Inventory,
|
Inventory,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputAction {
|
|
||||||
pub fn default_input_map() -> InputMap<Self> {
|
|
||||||
InputMap::default()
|
|
||||||
.with_axis(Self::Move, VirtualAxis::ad())
|
|
||||||
.with_axis(Self::Move, GamepadAxis::LeftStickX)
|
|
||||||
.with(Self::ToggleInventory, KeyCode::KeyI)
|
|
||||||
.with(Self::ToggleInventory, GamepadButton::Select)
|
|
||||||
.with(Self::Interact, KeyCode::KeyE)
|
|
||||||
.with(Self::Interact, GamepadButton::East)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UiAction {
|
|
||||||
pub fn default_input_map() -> InputMap<Self> {
|
|
||||||
InputMap::default()
|
|
||||||
.with(Self::Rotate, KeyCode::KeyR)
|
|
||||||
.with(Self::Rotate, GamepadButton::West)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert_entity_name(names: Query<(Entity, &mut Name), Added<Name>>) {
|
pub fn insert_entity_name(names: Query<(Entity, &mut Name), Added<Name>>) {
|
||||||
for (entity, mut name) in names {
|
for (entity, mut name) in names {
|
||||||
name.mutate(|name| name.insert_str(0, format!("{entity}: ").as_str()));
|
name.mutate(|name| name.insert_str(0, format!("{entity}: ").as_str()));
|
||||||
|
|
@ -87,8 +58,8 @@ fn setup_global(mut commands: Commands) {
|
||||||
impl Plugin for ExpeditionPlugin {
|
impl Plugin for ExpeditionPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_plugins((
|
app.add_plugins((
|
||||||
input::InputAssetPlugin::<InputAction>::default(),
|
input::plugin::InputAssetPlugin::<input::InputAction>::default(),
|
||||||
input::InputAssetPlugin::<UiAction>::default(),
|
input::plugin::InputAssetPlugin::<input::UiAction>::default(),
|
||||||
ScrollbarPlugin,
|
ScrollbarPlugin,
|
||||||
RapierPhysicsPlugin::<()>::default()
|
RapierPhysicsPlugin::<()>::default()
|
||||||
.with_custom_initialization(RapierContextInitialization::InitializeDefaultRapierContext {
|
.with_custom_initialization(RapierContextInitialization::InitializeDefaultRapierContext {
|
||||||
|
|
@ -107,22 +78,16 @@ impl Plugin for ExpeditionPlugin {
|
||||||
))
|
))
|
||||||
.init_state::<GameState>()
|
.init_state::<GameState>()
|
||||||
.insert_resource(ui::WindowSize::default())
|
.insert_resource(ui::WindowSize::default())
|
||||||
.add_systems(Startup, (player::setup_player, setup_global, layout::setup_world))
|
.add_systems(Startup, (player::systems::setup_player, setup_global, layout::systems::setup_world))
|
||||||
.add_systems(Update, (
|
.add_systems(Update, (
|
||||||
player::handle_input,
|
player::systems::handle_input,
|
||||||
ui::update_window_size,
|
ui::update_window_size,
|
||||||
ui::handle_input,
|
ui::handle_input,
|
||||||
insert_entity_name,
|
insert_entity_name,
|
||||||
layout::detect_interact_collisions,
|
layout::systems::detect_interact_collisions,
|
||||||
))
|
))
|
||||||
.add_systems(OnEnter(GameState::Inventory), inventory::ui::setup_ui_inventory)
|
.add_systems(OnEnter(GameState::Inventory), ui::inventory::systems::setup_ui_inventory)
|
||||||
.add_systems(OnExit(GameState::Inventory), inventory::ui::clear_ui_inventory)
|
.add_systems(OnExit(GameState::Inventory), ui::inventory::systems::clear_ui_inventory)
|
||||||
.register_type::<inventory::Inventory>()
|
.add_observer(ui::inventory::observers::on_ui_rotate);
|
||||||
.register_type::<inventory::ActiveInventory>()
|
|
||||||
.register_type::<inventory::item::Item>()
|
|
||||||
.register_type::<inventory::ui::UiItem>()
|
|
||||||
.register_type::<inventory::ui::UiInventorySlot>()
|
|
||||||
.register_type::<inventory::ui::UiInventory>()
|
|
||||||
.add_observer(inventory::ui::on_ui_rotate);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
use bevy::{prelude::*, remote::{RemotePlugin, http::RemoteHttpPlugin}};
|
use bevy::{
|
||||||
|
prelude::*,
|
||||||
|
remote::{
|
||||||
|
RemotePlugin,
|
||||||
|
http::RemoteHttpPlugin,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
use expedition_demo::ExpeditionPlugin;
|
use expedition_demo::ExpeditionPlugin;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_rapier2d::prelude::*;
|
use bevy_rapier2d::prelude::*;
|
||||||
use leafwing_input_manager::prelude::*;
|
|
||||||
|
|
||||||
use crate::{GameState, InputAction as Action, PIXELS_PER_METER, inventory::{Inventory, item::Item}, layout::{InteractionEvent, MayInteract}};
|
use crate::{
|
||||||
|
input::InputAction as Action,
|
||||||
|
PIXELS_PER_METER,
|
||||||
|
inventory::Inventory,
|
||||||
|
};
|
||||||
|
|
||||||
pub mod systems;
|
pub mod systems;
|
||||||
|
|
||||||
|
|
@ -12,7 +15,7 @@ pub struct Player {
|
||||||
speed: f32,
|
speed: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn player_bundle(asset_server: &Res<AssetServer>) -> impl Bundle {
|
pub fn player_bundle(asset_server: &Res<AssetServer>) -> impl Bundle {
|
||||||
let image = asset_server.load("sprites/player/player.png");
|
let image = asset_server.load("sprites/player/player.png");
|
||||||
(
|
(
|
||||||
Player {
|
Player {
|
||||||
|
|
@ -35,83 +38,3 @@ fn player_bundle(asset_server: &Res<AssetServer>) -> impl Bundle {
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setup_player(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|
||||||
commands.spawn(player_bundle(&asset_server));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn try_insert_item(
|
|
||||||
mut commands: Commands,
|
|
||||||
item_query: Query<&Item>,
|
|
||||||
inventory_query: Query<(Entity, &Inventory, Option<&Children>)>,
|
|
||||||
) {
|
|
||||||
let mut item = Item::new(UVec2::new(1, 1));
|
|
||||||
let name = Name::new(format!("Item {}x{}", item.size.x, item.size.y));
|
|
||||||
for (entity, inventory, children) in inventory_query.iter().sort::<Entity>() {
|
|
||||||
let children = match children {
|
|
||||||
Some(children) => &children[..],
|
|
||||||
None => &[],
|
|
||||||
};
|
|
||||||
match inventory.find_free_space(item_query, children, item.size) {
|
|
||||||
Some((position, should_rotate)) => {
|
|
||||||
if should_rotate {
|
|
||||||
item.rotate();
|
|
||||||
}
|
|
||||||
item.position = Some(position);
|
|
||||||
info!("Spawning item {item:?}");
|
|
||||||
commands.entity(entity).with_child((item, name));
|
|
||||||
break;
|
|
||||||
},
|
|
||||||
None => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_input(
|
|
||||||
mut commands: Commands,
|
|
||||||
time: Res<Time>,
|
|
||||||
state: Res<State<GameState>>,
|
|
||||||
mut next_state: ResMut<NextState<GameState>>,
|
|
||||||
interactables: Query<Entity, With<MayInteract>>,
|
|
||||||
mut player: Query<(&Player, &mut ActionState<Action>, &mut KinematicCharacterController, &mut Sprite)>,
|
|
||||||
) {
|
|
||||||
let player = player.single_mut().expect("Player should be single");
|
|
||||||
match state.get() {
|
|
||||||
GameState::Running => {
|
|
||||||
let (Player {speed}, mut action_state, mut controller, mut sprite) = player;
|
|
||||||
|
|
||||||
if action_state.just_released(&Action::ToggleInventory) {
|
|
||||||
next_state.set(GameState::Inventory);
|
|
||||||
action_state.reset(&Action::ToggleInventory);
|
|
||||||
}
|
|
||||||
|
|
||||||
let direction = action_state.clamped_value(&Action::Move);
|
|
||||||
|
|
||||||
controller.translation = Some(vec2(direction * speed * time.delta_secs(), 0.));
|
|
||||||
if direction != 0f32 {
|
|
||||||
sprite.flip_x = direction < 0f32;
|
|
||||||
}
|
|
||||||
|
|
||||||
if action_state.just_released(&Action::Interact) {
|
|
||||||
let mut action_happened = false;
|
|
||||||
for interactable_id in interactables {
|
|
||||||
commands.trigger(InteractionEvent { entity: interactable_id });
|
|
||||||
action_happened = true;
|
|
||||||
}
|
|
||||||
if !action_happened {
|
|
||||||
commands.run_system_cached(try_insert_item);
|
|
||||||
}
|
|
||||||
action_state.reset(&Action::Interact);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
GameState::Inventory => {
|
|
||||||
let (_, mut action_state, _, _) = player;
|
|
||||||
if action_state.just_released(&Action::ToggleInventory)
|
|
||||||
|| action_state.just_released(&Action::Interact) {
|
|
||||||
next_state.set(GameState::Running);
|
|
||||||
action_state.reset(&Action::ToggleInventory);
|
|
||||||
action_state.reset(&Action::Interact);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use leafwing_input_manager::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
GameState,
|
||||||
|
layout::{
|
||||||
|
InteractionEvent,
|
||||||
|
MayInteract,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn setup_player(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||||
|
commands.spawn(player_bundle(&asset_server));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_input(
|
||||||
|
mut commands: Commands,
|
||||||
|
time: Res<Time>,
|
||||||
|
state: Res<State<GameState>>,
|
||||||
|
mut next_state: ResMut<NextState<GameState>>,
|
||||||
|
interactables: Query<Entity, With<MayInteract>>,
|
||||||
|
mut player: Query<(&Player, &mut ActionState<Action>, &mut KinematicCharacterController, &mut Sprite)>,
|
||||||
|
) {
|
||||||
|
let player = player.single_mut().expect("Player should be single");
|
||||||
|
match state.get() {
|
||||||
|
GameState::Running => {
|
||||||
|
let (Player {speed}, mut action_state, mut controller, mut sprite) = player;
|
||||||
|
|
||||||
|
if action_state.just_released(&Action::ToggleInventory) {
|
||||||
|
next_state.set(GameState::Inventory);
|
||||||
|
action_state.reset(&Action::ToggleInventory);
|
||||||
|
}
|
||||||
|
|
||||||
|
let direction = action_state.clamped_value(&Action::Move);
|
||||||
|
|
||||||
|
controller.translation = Some(vec2(direction * speed * time.delta_secs(), 0.));
|
||||||
|
if direction != 0f32 {
|
||||||
|
sprite.flip_x = direction < 0f32;
|
||||||
|
}
|
||||||
|
|
||||||
|
if action_state.just_released(&Action::Interact) {
|
||||||
|
for interactable_id in interactables {
|
||||||
|
commands.trigger(InteractionEvent { entity: interactable_id });
|
||||||
|
}
|
||||||
|
action_state.reset(&Action::Interact);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GameState::Inventory => {
|
||||||
|
let (_, mut action_state, _, _) = player;
|
||||||
|
if action_state.just_released(&Action::ToggleInventory)
|
||||||
|
|| action_state.just_released(&Action::Interact) {
|
||||||
|
next_state.set(GameState::Running);
|
||||||
|
action_state.reset(&Action::ToggleInventory);
|
||||||
|
action_state.reset(&Action::Interact);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use super::super::*;
|
use super::super::*;
|
||||||
|
use crate::input::plugin::*;
|
||||||
use leafwing_input_manager::prelude::*;
|
use leafwing_input_manager::prelude::*;
|
||||||
#[derive(Actionlike, Reflect, Clone, Debug, PartialEq, Eq, Hash, Default)]
|
#[derive(Actionlike, Reflect, Clone, Debug, PartialEq, Eq, Hash, Default)]
|
||||||
enum Action {
|
enum Action {
|
||||||
|
|
@ -22,39 +23,39 @@ fn asset_from_map() {
|
||||||
input_map.insert_dual_axis(Action::DualAxis, GamepadStick::RIGHT);
|
input_map.insert_dual_axis(Action::DualAxis, GamepadStick::RIGHT);
|
||||||
input_map.insert_dual_axis(Action::DualAxis, VirtualDPad::wasd());
|
input_map.insert_dual_axis(Action::DualAxis, VirtualDPad::wasd());
|
||||||
|
|
||||||
let mut expected_input_asset = input::InputAsset::default();
|
let mut expected_input_asset = InputAsset::default();
|
||||||
expected_input_asset.insert(Action::Button, input::MultiInput {
|
expected_input_asset.insert(Action::Button, MultiInput {
|
||||||
keyboard: Some(vec![KeyCode::KeyE]),
|
keyboard: Some(vec![KeyCode::KeyE]),
|
||||||
gamepad: Some(vec![GamepadButton::East]),
|
gamepad: Some(vec![GamepadButton::East]),
|
||||||
mouse: None,
|
mouse: None,
|
||||||
}.into());
|
}.into());
|
||||||
expected_input_asset.insert(Action::SingleAxis, input::InputKind::Axis(vec![
|
expected_input_asset.insert(Action::SingleAxis, InputKind::Axis(vec![
|
||||||
Box::new(GamepadAxis::LeftStickX),
|
Box::new(GamepadAxis::LeftStickX),
|
||||||
Box::new(VirtualAxis::ad()),
|
Box::new(VirtualAxis::ad()),
|
||||||
]));
|
]));
|
||||||
expected_input_asset.insert(Action::DualAxis, input::InputKind::DualAxis(vec![
|
expected_input_asset.insert(Action::DualAxis, InputKind::DualAxis(vec![
|
||||||
Box::new(GamepadStick::RIGHT),
|
Box::new(GamepadStick::RIGHT),
|
||||||
Box::new(VirtualDPad::wasd()),
|
Box::new(VirtualDPad::wasd()),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
let input_asset = input::InputAsset::from(input_map);
|
let input_asset = InputAsset::from(input_map);
|
||||||
|
|
||||||
assert_eq!(input_asset, expected_input_asset);
|
assert_eq!(input_asset, expected_input_asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn map_from_asset() {
|
fn map_from_asset() {
|
||||||
let mut input_asset = input::InputAsset::default();
|
let mut input_asset = InputAsset::default();
|
||||||
input_asset.insert(Action::Button, input::MultiInput {
|
input_asset.insert(Action::Button, MultiInput {
|
||||||
keyboard: Some(vec![KeyCode::KeyE]),
|
keyboard: Some(vec![KeyCode::KeyE]),
|
||||||
gamepad: Some(vec![GamepadButton::East]),
|
gamepad: Some(vec![GamepadButton::East]),
|
||||||
mouse: None,
|
mouse: None,
|
||||||
}.into());
|
}.into());
|
||||||
input_asset.insert(Action::SingleAxis, input::InputKind::Axis(vec![
|
input_asset.insert(Action::SingleAxis, InputKind::Axis(vec![
|
||||||
Box::new(GamepadAxis::LeftStickX),
|
Box::new(GamepadAxis::LeftStickX),
|
||||||
Box::new(VirtualAxis::ad()),
|
Box::new(VirtualAxis::ad()),
|
||||||
]));
|
]));
|
||||||
input_asset.insert(Action::DualAxis, input::InputKind::DualAxis(vec![
|
input_asset.insert(Action::DualAxis, InputKind::DualAxis(vec![
|
||||||
Box::new(GamepadStick::RIGHT),
|
Box::new(GamepadStick::RIGHT),
|
||||||
Box::new(VirtualDPad::wasd()),
|
Box::new(VirtualDPad::wasd()),
|
||||||
]));
|
]));
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use crate::inventory::{Inventory, item::Item, ui::DraggedItem};
|
use crate::{
|
||||||
|
inventory::{
|
||||||
|
Inventory,
|
||||||
|
item::Item,
|
||||||
|
},
|
||||||
|
ui::inventory::DraggedItem,
|
||||||
|
};
|
||||||
|
|
||||||
fn inventory() -> Inventory {
|
fn inventory() -> Inventory {
|
||||||
Inventory::new(UVec2::splat(4))
|
Inventory::new(UVec2::splat(4))
|
||||||
|
|
|
||||||
148
src/ui/inventory/bundles.rs
Normal file
148
src/ui/inventory/bundles.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
use bevy::{
|
||||||
|
ecs::relationship::RelatedSpawner,
|
||||||
|
prelude::*,
|
||||||
|
ui_widgets::{
|
||||||
|
ControlOrientation,
|
||||||
|
CoreScrollbarThumb,
|
||||||
|
Scrollbar,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn ui_manager_bundle(children: Vec<Entity>, aligned_left: bool) -> impl Bundle {
|
||||||
|
let left = if aligned_left { Val::ZERO } else { percent(50.) };
|
||||||
|
(
|
||||||
|
UiInventoryManager,
|
||||||
|
Node {
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
left,
|
||||||
|
width: percent(50.),
|
||||||
|
height: percent(100.),
|
||||||
|
scrollbar_width: 8.,
|
||||||
|
display: Display::Grid,
|
||||||
|
grid_template_columns: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)],
|
||||||
|
grid_template_rows: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)],
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Pickable::IGNORE,
|
||||||
|
GlobalZIndex::default(),
|
||||||
|
Children::spawn(SpawnWith(move |parent: &mut RelatedSpawner<ChildOf>| {
|
||||||
|
let scroll_area_id = parent.spawn((
|
||||||
|
Node {
|
||||||
|
width: percent(100.),
|
||||||
|
height: percent(100.),
|
||||||
|
display: Display::Flex,
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
justify_content: JustifyContent::SpaceAround,
|
||||||
|
overflow: Overflow::scroll_y(),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
ScrollPosition(Vec2::ZERO),
|
||||||
|
)).add_children(children.as_slice()).id();
|
||||||
|
parent.spawn((
|
||||||
|
Node {
|
||||||
|
min_width: px(8),
|
||||||
|
grid_row: GridPlacement::start(1),
|
||||||
|
grid_column: GridPlacement::start(2),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Scrollbar {
|
||||||
|
orientation: ControlOrientation::Vertical,
|
||||||
|
target: scroll_area_id,
|
||||||
|
min_thumb_length: 8.0,
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::hsl(0., 0., 0.5)),
|
||||||
|
children![(
|
||||||
|
Node {
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
border_radius: BorderRadius::all(px(4.)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::hsl(0., 0., 0.3)),
|
||||||
|
CoreScrollbarThumb,
|
||||||
|
)],
|
||||||
|
));
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui_inventory_bundle(
|
||||||
|
inventory: &Inventory,
|
||||||
|
inventory_entity: Entity,
|
||||||
|
) -> impl Bundle {
|
||||||
|
(
|
||||||
|
UiInventory(inventory_entity),
|
||||||
|
Node {
|
||||||
|
align_self: AlignSelf::Stretch,
|
||||||
|
align_content: AlignContent::Center,
|
||||||
|
display: Display::Grid,
|
||||||
|
width: percent(100.),
|
||||||
|
aspect_ratio: Some(inventory.size.x as f32 / inventory.size.y as f32),
|
||||||
|
grid_auto_columns: vec![GridTrack::percent(100. / inventory.size.x as f32)],
|
||||||
|
grid_auto_rows: vec![GridTrack::percent(100. / inventory.size.y as f32)],
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Pickable::IGNORE,
|
||||||
|
GlobalZIndex::default(),
|
||||||
|
Name::new(format!("UiInventory ({}x{})", inventory.size.x, inventory.size.y)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inventory_slot_bundle(x: u32, y: u32, image: Handle<Image>) -> impl Bundle {
|
||||||
|
(
|
||||||
|
UiInventorySlot(UVec2::new(x, y)),
|
||||||
|
ImageNode {
|
||||||
|
color: Color::WHITE,
|
||||||
|
image,
|
||||||
|
image_mode: NodeImageMode::Stretch,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Node {
|
||||||
|
width: percent(100.),
|
||||||
|
height: percent(100.),
|
||||||
|
grid_column: GridPlacement::start(x as i16 + 1),
|
||||||
|
grid_row: GridPlacement::start(y as i16 + 1),
|
||||||
|
aspect_ratio: Some(1.),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Pickable {
|
||||||
|
should_block_lower: true,
|
||||||
|
is_hoverable: true,
|
||||||
|
},
|
||||||
|
GlobalZIndex(1),
|
||||||
|
BackgroundColor::DEFAULT,
|
||||||
|
Name::new(format!("UiInventorySlot({x},{y})")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui_item_bundle(item: &Item, item_entity: Entity, image: Handle<Image>) -> impl Bundle {
|
||||||
|
let (left, top, min_width, min_height, ui_transform) = ui_item_node_data(item);
|
||||||
|
(
|
||||||
|
UiItem(item_entity),
|
||||||
|
ImageNode {
|
||||||
|
image,
|
||||||
|
image_mode: NodeImageMode::Stretch,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Node {
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
min_width,
|
||||||
|
min_height,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::hsla(0., 0., 0., 0.5)),
|
||||||
|
ui_transform,
|
||||||
|
GlobalZIndex(2),
|
||||||
|
Pickable {
|
||||||
|
should_block_lower: false,
|
||||||
|
is_hoverable: true,
|
||||||
|
},
|
||||||
|
Name::new(format!("UiItem ({},{})",
|
||||||
|
item.position.unwrap_or_default().x,
|
||||||
|
item.position.unwrap_or_default().y,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
use std::f32::consts::FRAC_PI_2;
|
use std::f32::consts::FRAC_PI_2;
|
||||||
|
|
||||||
use bevy::{ecs::relationship::RelatedSpawner, prelude::*, ui_widgets::{ControlOrientation, CoreScrollbarThumb, Scrollbar}};
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::{inventory::{ActiveInventory, Inventory, item::Item}, player::Player, ui::{UiRoot, UiRotateEvent}};
|
use crate::{
|
||||||
|
inventory::{
|
||||||
|
Inventory,
|
||||||
|
item::Item,
|
||||||
|
},
|
||||||
|
ui::UiRotateEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod bundles;
|
||||||
|
pub mod observers;
|
||||||
pub mod systems;
|
pub mod systems;
|
||||||
|
|
||||||
const UI_SLOT_ASSET_PATH: &'static str = "sprites/ui/inventory_slot.png";
|
|
||||||
const TEMP_ITEM_PATH: &'static str = "sprites/items/choco_bar.png";
|
|
||||||
|
|
||||||
#[derive(Component, Reflect)]
|
#[derive(Component, Reflect)]
|
||||||
#[require(Node)]
|
#[require(Node)]
|
||||||
pub struct UiInventoryManager;
|
pub struct UiInventoryManager;
|
||||||
|
|
@ -68,414 +73,3 @@ fn reset_slots_colors(query: Query<&mut ImageNode, With<UiInventorySlot>>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_slot_over(
|
|
||||||
e: On<Pointer<Over>>,
|
|
||||||
mut commands: Commands,
|
|
||||||
mut image_query: Query<&mut ImageNode, With<UiInventorySlot>>,
|
|
||||||
hovered_slots: Query<Entity, With<HoveredSlot>>,
|
|
||||||
has_dragged_item: Option<Single<(), With<DraggedItem>>>,
|
|
||||||
) {
|
|
||||||
if let Ok(mut image) = image_query.get_mut(e.event_target()) {
|
|
||||||
image.color = Color::WHITE.darker(0.3);
|
|
||||||
};
|
|
||||||
for slot_id in hovered_slots {
|
|
||||||
commands.entity(slot_id).remove::<HoveredSlot>();
|
|
||||||
}
|
|
||||||
if has_dragged_item.is_none() {
|
|
||||||
commands.entity(e.event_target()).insert(HoveredSlot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_slot_out(e: On<Pointer<Out>>, mut query: Query<&mut ImageNode, With<UiInventorySlot>>) {
|
|
||||||
if let Ok(mut image) = query.get_mut(e.event_target()) {
|
|
||||||
image.color = Color::WHITE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_item_over(
|
|
||||||
e: On<Pointer<Over>>,
|
|
||||||
mut commands: Commands,
|
|
||||||
query: Query<(), With<UiItem>>,
|
|
||||||
has_hovered_item: Option<Single<(), With<HoveredItem>>>,
|
|
||||||
) {
|
|
||||||
if has_hovered_item.is_some() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Ok(_) = query.get(e.event_target()) {
|
|
||||||
commands.entity(e.event_target()).insert(HoveredItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_item_out(
|
|
||||||
e: On<Pointer<Out>>,
|
|
||||||
mut commands: Commands,
|
|
||||||
query: Query<(), (With<UiItem>, With<HoveredItem>)>,
|
|
||||||
) {
|
|
||||||
if let Ok(_) = query.get(e.event_target()) {
|
|
||||||
commands.entity(e.event_target()).remove::<HoveredItem>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_item_drag_start(
|
|
||||||
e: On<Pointer<DragStart>>,
|
|
||||||
mut commands: Commands,
|
|
||||||
ui_query: Query<&UiItem, With<HoveredItem>>,
|
|
||||||
item_query: Query<&Item>,
|
|
||||||
hovered_slot: Single<&UiInventorySlot, With<HoveredSlot>>
|
|
||||||
) {
|
|
||||||
if let Ok(ui_item) = ui_query.get(e.event_target()) {
|
|
||||||
let Ok(item) = item_query.get(ui_item.0) else {
|
|
||||||
error!("UiItem {} is pointing to non-existing Item", e.event_target());
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(item_position) = item.position else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let slot_position = hovered_slot.0;
|
|
||||||
let diff = slot_position - item_position;
|
|
||||||
commands.entity(e.event_target()).insert(DraggedItem(item.clone(), diff));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_item_drag(e: On<Pointer<Drag>>, mut query: Query<&mut UiTransform, With<UiItem>>) {
|
|
||||||
if let Ok(mut transform) = query.get_mut(e.event_target()) {
|
|
||||||
transform.translation = Val2::px(e.distance.x, e.distance.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_item_drag_end(
|
|
||||||
e: On<Pointer<DragEnd>>,
|
|
||||||
mut commands: Commands,
|
|
||||||
mut query: Query<&mut UiTransform, With<UiItem>>,
|
|
||||||
) {
|
|
||||||
if let Ok(mut transform) = query.get_mut(e.event_target()) {
|
|
||||||
transform.translation = Val2::ZERO;
|
|
||||||
commands.entity(e.event_target()).remove::<DraggedItem>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_item_drag_drop(
|
|
||||||
event: On<Pointer<DragDrop>>,
|
|
||||||
mut commands: Commands,
|
|
||||||
mut ui_item_query: Query<(Entity, &UiItem, &mut DraggedItem, &mut Node, &mut UiTransform)>,
|
|
||||||
ui_inventory_query: Query<&UiInventory>,
|
|
||||||
mut item_query: Query<&mut Item>,
|
|
||||||
slot_query: Query<(&ChildOf, &UiInventorySlot, Option<&Children>)>,
|
|
||||||
slot_id_query: Query<(Entity, &UiInventorySlot, &ChildOf), With<UiInventorySlot>>,
|
|
||||||
inventory_query: Query<(&Inventory, Option<&Children>)>,
|
|
||||||
) {
|
|
||||||
let Ok((ui_item_entity, UiItem(item_entity), mut dragged_item, node, ui_transform)) = ui_item_query.get_mut(event.dropped) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Ok((slot_parent, UiInventorySlot(new_position), slot_children)) = slot_query.get(event.event_target()) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if slot_children.is_some() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Ok(UiInventory(inventory_id)) = ui_inventory_query.get(slot_parent.0) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Ok((inventory, inventory_children)) = inventory_query.get(*inventory_id) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let items = match inventory_children {
|
|
||||||
Some(children) => &children[..],
|
|
||||||
None => &[],
|
|
||||||
};
|
|
||||||
|
|
||||||
let actual_position = new_position.as_ivec2() - dragged_item.1.as_ivec2();
|
|
||||||
info!("{actual_position:?}");
|
|
||||||
let temp_item = &mut dragged_item.0;
|
|
||||||
if actual_position.is_negative_bitmask() == 0 {
|
|
||||||
temp_item.position = Some(actual_position.as_uvec2());
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some((slot_id, _, _)) = slot_id_query.iter().find(|(_, slot_pos, slot_parent_id)| slot_pos.0 == temp_item.position.unwrap() && slot_parent_id == &slot_parent) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if inventory.can_replace(item_query.as_readonly(), items, *item_entity, temp_item) {
|
|
||||||
let mut item = item_query.get_mut(*item_entity).unwrap();
|
|
||||||
item.position = temp_item.position;
|
|
||||||
item.size = temp_item.size;
|
|
||||||
item.rotated = temp_item.rotated;
|
|
||||||
commands.entity(ui_item_entity).insert(ChildOf(slot_id));
|
|
||||||
commands.entity(*item_entity).insert(ChildOf(*inventory_id));
|
|
||||||
commands.entity(slot_id).insert(HoveredSlot);
|
|
||||||
update_ui_node(item.as_ref(), node, ui_transform);
|
|
||||||
} else {
|
|
||||||
if let Ok(item) = item_query.get(*item_entity) {
|
|
||||||
update_ui_node(item, node, ui_transform);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
commands.run_system_cached(reset_slots_colors);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_ui_rotate(
|
|
||||||
_: On<UiRotateEvent>,
|
|
||||||
mut item_query: Query<&mut Item>,
|
|
||||||
parent_query: Query<&ChildOf>,
|
|
||||||
inventory_query: Query<(&Inventory, Option<&Children>)>,
|
|
||||||
ui_item_query: Query<(&UiItem, &mut UiTransform, &mut Node, Option<&mut DraggedItem>), With<HoveredItem>>,
|
|
||||||
) {
|
|
||||||
for (ui_item, ui_transform, node, maybe_dragged) in ui_item_query {
|
|
||||||
let Ok(item_parent) = parent_query.get(ui_item.0) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Ok((inventory, children)) = inventory_query.get(item_parent.0) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let children = match children {
|
|
||||||
Some(children) => &children[..],
|
|
||||||
None => &[],
|
|
||||||
};
|
|
||||||
let (was_rotated, item) = match maybe_dragged {
|
|
||||||
Some(mut temp_item) => {
|
|
||||||
temp_item.0.rotate();
|
|
||||||
temp_item.1 = uvec2(temp_item.1.y, temp_item.1.x);
|
|
||||||
(true, Some(temp_item.0.clone()))
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
let result = inventory.can_rotate(item_query.as_readonly(), children, ui_item.0);
|
|
||||||
let mut out_item = None;
|
|
||||||
if result {
|
|
||||||
if let Ok(mut item) = item_query.get_mut(ui_item.0) {
|
|
||||||
item.rotate();
|
|
||||||
out_item = Some(item.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(result, out_item)
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if was_rotated {
|
|
||||||
update_ui_node(&item.unwrap(), node, ui_transform);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ui_manager_bundle(children: Vec<Entity>, aligned_left: bool) -> impl Bundle {
|
|
||||||
let left = if aligned_left { Val::ZERO } else { percent(50.) };
|
|
||||||
(
|
|
||||||
UiInventoryManager,
|
|
||||||
Node {
|
|
||||||
position_type: PositionType::Absolute,
|
|
||||||
left,
|
|
||||||
width: percent(50.),
|
|
||||||
height: percent(100.),
|
|
||||||
scrollbar_width: 8.,
|
|
||||||
display: Display::Grid,
|
|
||||||
grid_template_columns: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)],
|
|
||||||
grid_template_rows: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)],
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
Pickable::IGNORE,
|
|
||||||
GlobalZIndex::default(),
|
|
||||||
Children::spawn(SpawnWith(move |parent: &mut RelatedSpawner<ChildOf>| {
|
|
||||||
let scroll_area_id = parent.spawn((
|
|
||||||
Node {
|
|
||||||
width: percent(100.),
|
|
||||||
height: percent(100.),
|
|
||||||
display: Display::Flex,
|
|
||||||
flex_direction: FlexDirection::Column,
|
|
||||||
align_items: AlignItems::Center,
|
|
||||||
justify_content: JustifyContent::SpaceAround,
|
|
||||||
overflow: Overflow::scroll_y(),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
ScrollPosition(Vec2::ZERO),
|
|
||||||
)).add_children(children.as_slice()).id();
|
|
||||||
parent.spawn((
|
|
||||||
Node {
|
|
||||||
min_width: px(8),
|
|
||||||
grid_row: GridPlacement::start(1),
|
|
||||||
grid_column: GridPlacement::start(2),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
Scrollbar {
|
|
||||||
orientation: ControlOrientation::Vertical,
|
|
||||||
target: scroll_area_id,
|
|
||||||
min_thumb_length: 8.0,
|
|
||||||
},
|
|
||||||
BackgroundColor(Color::hsl(0., 0., 0.5)),
|
|
||||||
children![(
|
|
||||||
Node {
|
|
||||||
position_type: PositionType::Absolute,
|
|
||||||
border_radius: BorderRadius::all(px(4.)),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
BackgroundColor(Color::hsl(0., 0., 0.3)),
|
|
||||||
CoreScrollbarThumb,
|
|
||||||
)],
|
|
||||||
));
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ui_inventory_bundle(
|
|
||||||
inventory: &Inventory,
|
|
||||||
inventory_entity: Entity,
|
|
||||||
) -> impl Bundle {
|
|
||||||
(
|
|
||||||
UiInventory(inventory_entity),
|
|
||||||
Node {
|
|
||||||
align_self: AlignSelf::Stretch,
|
|
||||||
align_content: AlignContent::Center,
|
|
||||||
display: Display::Grid,
|
|
||||||
width: percent(100.),
|
|
||||||
aspect_ratio: Some(inventory.size.x as f32 / inventory.size.y as f32),
|
|
||||||
grid_auto_columns: vec![GridTrack::percent(100. / inventory.size.x as f32)],
|
|
||||||
grid_auto_rows: vec![GridTrack::percent(100. / inventory.size.y as f32)],
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
Pickable::IGNORE,
|
|
||||||
GlobalZIndex::default(),
|
|
||||||
Name::new(format!("UiInventory ({}x{})", inventory.size.x, inventory.size.y)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inventory_slot_bundle(x: u32, y: u32, image: Handle<Image>) -> impl Bundle {
|
|
||||||
(
|
|
||||||
UiInventorySlot(UVec2::new(x, y)),
|
|
||||||
ImageNode {
|
|
||||||
color: Color::WHITE,
|
|
||||||
image,
|
|
||||||
image_mode: NodeImageMode::Stretch,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
Node {
|
|
||||||
width: percent(100.),
|
|
||||||
height: percent(100.),
|
|
||||||
grid_column: GridPlacement::start(x as i16 + 1),
|
|
||||||
grid_row: GridPlacement::start(y as i16 + 1),
|
|
||||||
aspect_ratio: Some(1.),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
Pickable {
|
|
||||||
should_block_lower: true,
|
|
||||||
is_hoverable: true,
|
|
||||||
},
|
|
||||||
GlobalZIndex(1),
|
|
||||||
BackgroundColor::DEFAULT,
|
|
||||||
Name::new(format!("UiInventorySlot({x},{y})")),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ui_item_bundle(item: &Item, item_entity: Entity, image: Handle<Image>) -> impl Bundle {
|
|
||||||
let (left, top, min_width, min_height, ui_transform) = ui_item_node_data(item);
|
|
||||||
(
|
|
||||||
UiItem(item_entity),
|
|
||||||
ImageNode {
|
|
||||||
image,
|
|
||||||
image_mode: NodeImageMode::Stretch,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
Node {
|
|
||||||
left,
|
|
||||||
top,
|
|
||||||
min_width,
|
|
||||||
min_height,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
BackgroundColor(Color::hsla(0., 0., 0., 0.5)),
|
|
||||||
ui_transform,
|
|
||||||
GlobalZIndex(2),
|
|
||||||
Pickable {
|
|
||||||
should_block_lower: false,
|
|
||||||
is_hoverable: true,
|
|
||||||
},
|
|
||||||
Name::new(format!("UiItem ({},{})",
|
|
||||||
item.position.unwrap_or_default().x,
|
|
||||||
item.position.unwrap_or_default().y,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setup_ui_inventory(
|
|
||||||
mut commands: Commands,
|
|
||||||
asset_server: Res<AssetServer>,
|
|
||||||
inventory_query: Query<(Option<&ChildOf>, Entity, &Inventory, Option<&Children>)>,
|
|
||||||
player_query: Query<(), With<Player>>,
|
|
||||||
active_inventory_query: Query<Entity, With<ActiveInventory>>,
|
|
||||||
item_query: Query<&Item>,
|
|
||||||
root_query: Query<Entity, With<UiRoot>>,
|
|
||||||
) {
|
|
||||||
let Ok(root) = root_query.single() else {
|
|
||||||
error!("Query contains more than one UiRoot");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let ui_slot_image: Handle<Image> = asset_server.load(UI_SLOT_ASSET_PATH);
|
|
||||||
let temp_item_image: Handle<Image> = asset_server.load(TEMP_ITEM_PATH);
|
|
||||||
let (mut player_inventory_ids, mut active_inventory_ids) = (Vec::new(), Vec::new());
|
|
||||||
for (inventory_parent, inventory_entity, inventory, children) in inventory_query.iter().sort::<Entity>() {
|
|
||||||
let is_player = match inventory_parent {
|
|
||||||
Some(parent) => {
|
|
||||||
if player_query.get(parent.0)
|
|
||||||
.is_ok() { true }
|
|
||||||
else {
|
|
||||||
if active_inventory_query.get(inventory_entity)
|
|
||||||
.is_ok() { false }
|
|
||||||
else { continue; }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
if active_inventory_query.get(inventory_entity)
|
|
||||||
.is_ok() { false }
|
|
||||||
else { continue; }
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let items = match children {
|
|
||||||
Some(children) => {
|
|
||||||
children.iter().filter_map(|item_entity| {
|
|
||||||
match item_query.get(item_entity) {
|
|
||||||
Ok(item) => Some((item, item_entity)),
|
|
||||||
Err(_) => None,
|
|
||||||
}
|
|
||||||
}).collect::<Vec<(&Item, Entity)>>()
|
|
||||||
}
|
|
||||||
None => Vec::new(),
|
|
||||||
};
|
|
||||||
let inventory_id = commands.spawn(ui_inventory_bundle(inventory, inventory_entity))
|
|
||||||
.with_children(|commands| {
|
|
||||||
for x in 0..inventory.size.x { for y in 0..inventory.size.y {
|
|
||||||
let mut slot_commands = commands.spawn(inventory_slot_bundle(x, y, ui_slot_image.clone()));
|
|
||||||
slot_commands.observe(on_slot_over)
|
|
||||||
.observe(on_slot_out)
|
|
||||||
.observe(on_item_drag_drop);
|
|
||||||
if let Some((item, entity)) = items.iter()
|
|
||||||
.find(|(i, _)| i.position.unwrap_or_default() == UVec2::new(x, y)) {
|
|
||||||
slot_commands.with_children(|commands| {
|
|
||||||
commands.spawn(ui_item_bundle(item, *entity, temp_item_image.clone()))
|
|
||||||
.observe(on_item_over)
|
|
||||||
.observe(on_item_out)
|
|
||||||
.observe(on_item_drag_start)
|
|
||||||
.observe(on_item_drag)
|
|
||||||
.observe(on_item_drag_end);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} }
|
|
||||||
}).id();
|
|
||||||
if is_player {
|
|
||||||
player_inventory_ids.push(inventory_id);
|
|
||||||
} else {
|
|
||||||
active_inventory_ids.push(inventory_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let player_inventory_manager = commands.spawn(ui_manager_bundle(player_inventory_ids, true)).id();
|
|
||||||
let active_inventory_manager = commands.spawn(ui_manager_bundle(active_inventory_ids, false)).id();
|
|
||||||
for entity in active_inventory_query {
|
|
||||||
commands.entity(entity).remove::<ActiveInventory>();
|
|
||||||
}
|
|
||||||
commands.entity(root).add_children(&[player_inventory_manager, active_inventory_manager]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_ui_inventory(
|
|
||||||
mut commands: Commands,
|
|
||||||
inventory_query: Query<Entity, With<UiInventoryManager>>,
|
|
||||||
) {
|
|
||||||
for entity in inventory_query {
|
|
||||||
commands.entity(entity).despawn();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
192
src/ui/inventory/observers.rs
Normal file
192
src/ui/inventory/observers.rs
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn on_slot_over(
|
||||||
|
e: On<Pointer<Over>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
mut image_query: Query<&mut ImageNode, With<UiInventorySlot>>,
|
||||||
|
hovered_slots: Query<Entity, With<HoveredSlot>>,
|
||||||
|
has_dragged_item: Option<Single<(), With<DraggedItem>>>,
|
||||||
|
) {
|
||||||
|
if let Ok(mut image) = image_query.get_mut(e.event_target()) {
|
||||||
|
image.color = Color::WHITE.darker(0.3);
|
||||||
|
};
|
||||||
|
for slot_id in hovered_slots {
|
||||||
|
commands.entity(slot_id).remove::<HoveredSlot>();
|
||||||
|
}
|
||||||
|
if has_dragged_item.is_none() {
|
||||||
|
commands.entity(e.event_target()).insert(HoveredSlot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_slot_out(e: On<Pointer<Out>>, mut query: Query<&mut ImageNode, With<UiInventorySlot>>) {
|
||||||
|
if let Ok(mut image) = query.get_mut(e.event_target()) {
|
||||||
|
image.color = Color::WHITE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_item_over(
|
||||||
|
e: On<Pointer<Over>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
query: Query<(), With<UiItem>>,
|
||||||
|
has_hovered_item: Option<Single<(), With<HoveredItem>>>,
|
||||||
|
) {
|
||||||
|
if has_hovered_item.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Ok(_) = query.get(e.event_target()) {
|
||||||
|
commands.entity(e.event_target()).insert(HoveredItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_item_out(
|
||||||
|
e: On<Pointer<Out>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
query: Query<(), (With<UiItem>, With<HoveredItem>)>,
|
||||||
|
) {
|
||||||
|
if let Ok(_) = query.get(e.event_target()) {
|
||||||
|
commands.entity(e.event_target()).remove::<HoveredItem>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_item_drag_start(
|
||||||
|
e: On<Pointer<DragStart>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
ui_query: Query<&UiItem, With<HoveredItem>>,
|
||||||
|
item_query: Query<&Item>,
|
||||||
|
hovered_slot: Single<&UiInventorySlot, With<HoveredSlot>>
|
||||||
|
) {
|
||||||
|
if let Ok(ui_item) = ui_query.get(e.event_target()) {
|
||||||
|
let Ok(item) = item_query.get(ui_item.0) else {
|
||||||
|
error!("UiItem {} is pointing to non-existing Item", e.event_target());
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(item_position) = item.position else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let slot_position = hovered_slot.0;
|
||||||
|
let diff = slot_position - item_position;
|
||||||
|
commands.entity(e.event_target()).insert(DraggedItem(item.clone(), diff));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_item_drag(e: On<Pointer<Drag>>, mut query: Query<&mut UiTransform, With<UiItem>>) {
|
||||||
|
if let Ok(mut transform) = query.get_mut(e.event_target()) {
|
||||||
|
transform.translation = Val2::px(e.distance.x, e.distance.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_item_drag_end(
|
||||||
|
e: On<Pointer<DragEnd>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
mut query: Query<&mut UiTransform, With<UiItem>>,
|
||||||
|
) {
|
||||||
|
if let Ok(mut transform) = query.get_mut(e.event_target()) {
|
||||||
|
transform.translation = Val2::ZERO;
|
||||||
|
commands.entity(e.event_target()).remove::<DraggedItem>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_item_drag_drop(
|
||||||
|
event: On<Pointer<DragDrop>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
mut ui_item_query: Query<(Entity, &UiItem, &mut DraggedItem, &mut Node, &mut UiTransform)>,
|
||||||
|
ui_inventory_query: Query<&UiInventory>,
|
||||||
|
mut item_query: Query<&mut Item>,
|
||||||
|
slot_query: Query<(&ChildOf, &UiInventorySlot, Option<&Children>)>,
|
||||||
|
slot_id_query: Query<(Entity, &UiInventorySlot, &ChildOf), With<UiInventorySlot>>,
|
||||||
|
inventory_query: Query<(&Inventory, Option<&Children>)>,
|
||||||
|
) {
|
||||||
|
let Ok((ui_item_entity, UiItem(item_entity), mut dragged_item, node, ui_transform)) = ui_item_query.get_mut(event.dropped) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok((slot_parent, UiInventorySlot(new_position), slot_children)) = slot_query.get(event.event_target()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if slot_children.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Ok(UiInventory(inventory_id)) = ui_inventory_query.get(slot_parent.0) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok((inventory, inventory_children)) = inventory_query.get(*inventory_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let items = match inventory_children {
|
||||||
|
Some(children) => &children[..],
|
||||||
|
None => &[],
|
||||||
|
};
|
||||||
|
|
||||||
|
let actual_position = new_position.as_ivec2() - dragged_item.1.as_ivec2();
|
||||||
|
info!("{actual_position:?}");
|
||||||
|
let temp_item = &mut dragged_item.0;
|
||||||
|
if actual_position.is_negative_bitmask() == 0 {
|
||||||
|
temp_item.position = Some(actual_position.as_uvec2());
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some((slot_id, _, _)) = slot_id_query.iter().find(|(_, slot_pos, slot_parent_id)| slot_pos.0 == temp_item.position.unwrap() && slot_parent_id == &slot_parent) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if inventory.can_replace(item_query.as_readonly(), items, *item_entity, temp_item) {
|
||||||
|
let mut item = item_query.get_mut(*item_entity).unwrap();
|
||||||
|
item.position = temp_item.position;
|
||||||
|
item.size = temp_item.size;
|
||||||
|
item.rotated = temp_item.rotated;
|
||||||
|
commands.entity(ui_item_entity).insert(ChildOf(slot_id));
|
||||||
|
commands.entity(*item_entity).insert(ChildOf(*inventory_id));
|
||||||
|
commands.entity(slot_id).insert(HoveredSlot);
|
||||||
|
update_ui_node(item.as_ref(), node, ui_transform);
|
||||||
|
} else {
|
||||||
|
if let Ok(item) = item_query.get(*item_entity) {
|
||||||
|
update_ui_node(item, node, ui_transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commands.run_system_cached(reset_slots_colors);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_ui_rotate(
|
||||||
|
_: On<UiRotateEvent>,
|
||||||
|
mut item_query: Query<&mut Item>,
|
||||||
|
parent_query: Query<&ChildOf>,
|
||||||
|
inventory_query: Query<(&Inventory, Option<&Children>)>,
|
||||||
|
ui_item_query: Query<(&UiItem, &mut UiTransform, &mut Node, Option<&mut DraggedItem>), With<HoveredItem>>,
|
||||||
|
) {
|
||||||
|
for (ui_item, ui_transform, node, maybe_dragged) in ui_item_query {
|
||||||
|
let Ok(item_parent) = parent_query.get(ui_item.0) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Ok((inventory, children)) = inventory_query.get(item_parent.0) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let children = match children {
|
||||||
|
Some(children) => &children[..],
|
||||||
|
None => &[],
|
||||||
|
};
|
||||||
|
let (was_rotated, item) = match maybe_dragged {
|
||||||
|
Some(mut temp_item) => {
|
||||||
|
temp_item.0.rotate();
|
||||||
|
temp_item.1 = uvec2(temp_item.1.y, temp_item.1.x);
|
||||||
|
(true, Some(temp_item.0.clone()))
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let result = inventory.can_rotate(item_query.as_readonly(), children, ui_item.0);
|
||||||
|
let mut out_item = None;
|
||||||
|
if result {
|
||||||
|
if let Ok(mut item) = item_query.get_mut(ui_item.0) {
|
||||||
|
item.rotate();
|
||||||
|
out_item = Some(item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(result, out_item)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if was_rotated {
|
||||||
|
update_ui_node(&item.unwrap(), node, ui_transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
inventory::ActiveInventory,
|
||||||
|
player::Player,
|
||||||
|
ui::UiRoot,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{*, bundles::*, observers::*};
|
||||||
|
|
||||||
|
const UI_SLOT_ASSET_PATH: &'static str = "sprites/ui/inventory_slot.png";
|
||||||
|
const TEMP_ITEM_PATH: &'static str = "sprites/items/choco_bar.png";
|
||||||
|
|
||||||
|
pub fn setup_ui_inventory(
|
||||||
|
mut commands: Commands,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
inventory_query: Query<(Option<&ChildOf>, Entity, &Inventory, Option<&Children>)>,
|
||||||
|
player_query: Query<(), With<Player>>,
|
||||||
|
active_inventory_query: Query<Entity, With<ActiveInventory>>,
|
||||||
|
item_query: Query<&Item>,
|
||||||
|
root_query: Query<Entity, With<UiRoot>>,
|
||||||
|
) {
|
||||||
|
let Ok(root) = root_query.single() else {
|
||||||
|
error!("Query contains more than one UiRoot");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let ui_slot_image: Handle<Image> = asset_server.load(UI_SLOT_ASSET_PATH);
|
||||||
|
let temp_item_image: Handle<Image> = asset_server.load(TEMP_ITEM_PATH);
|
||||||
|
let (mut player_inventory_ids, mut active_inventory_ids) = (Vec::new(), Vec::new());
|
||||||
|
for (inventory_parent, inventory_entity, inventory, children) in inventory_query.iter().sort::<Entity>() {
|
||||||
|
let is_player = match inventory_parent {
|
||||||
|
Some(parent) => {
|
||||||
|
if player_query.get(parent.0)
|
||||||
|
.is_ok() { true }
|
||||||
|
else {
|
||||||
|
if active_inventory_query.get(inventory_entity)
|
||||||
|
.is_ok() { false }
|
||||||
|
else { continue; }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
if active_inventory_query.get(inventory_entity)
|
||||||
|
.is_ok() { false }
|
||||||
|
else { continue; }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let items = match children {
|
||||||
|
Some(children) => {
|
||||||
|
children.iter().filter_map(|item_entity| {
|
||||||
|
match item_query.get(item_entity) {
|
||||||
|
Ok(item) => Some((item, item_entity)),
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
}).collect::<Vec<(&Item, Entity)>>()
|
||||||
|
}
|
||||||
|
None => Vec::new(),
|
||||||
|
};
|
||||||
|
let inventory_id = commands.spawn(ui_inventory_bundle(inventory, inventory_entity))
|
||||||
|
.with_children(|commands| {
|
||||||
|
for x in 0..inventory.size.x { for y in 0..inventory.size.y {
|
||||||
|
let mut slot_commands = commands.spawn(inventory_slot_bundle(x, y, ui_slot_image.clone()));
|
||||||
|
slot_commands.observe(on_slot_over)
|
||||||
|
.observe(on_slot_out)
|
||||||
|
.observe(on_item_drag_drop);
|
||||||
|
if let Some((item, entity)) = items.iter()
|
||||||
|
.find(|(i, _)| i.position.unwrap_or_default() == UVec2::new(x, y)) {
|
||||||
|
slot_commands.with_children(|commands| {
|
||||||
|
commands.spawn(ui_item_bundle(item, *entity, temp_item_image.clone()))
|
||||||
|
.observe(on_item_over)
|
||||||
|
.observe(on_item_out)
|
||||||
|
.observe(on_item_drag_start)
|
||||||
|
.observe(on_item_drag)
|
||||||
|
.observe(on_item_drag_end);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} }
|
||||||
|
}).id();
|
||||||
|
if is_player {
|
||||||
|
player_inventory_ids.push(inventory_id);
|
||||||
|
} else {
|
||||||
|
active_inventory_ids.push(inventory_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let player_inventory_manager = commands.spawn(ui_manager_bundle(player_inventory_ids, true)).id();
|
||||||
|
let active_inventory_manager = commands.spawn(ui_manager_bundle(active_inventory_ids, false)).id();
|
||||||
|
for entity in active_inventory_query {
|
||||||
|
commands.entity(entity).remove::<ActiveInventory>();
|
||||||
|
}
|
||||||
|
commands.entity(root).add_children(&[player_inventory_manager, active_inventory_manager]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_ui_inventory(
|
||||||
|
mut commands: Commands,
|
||||||
|
inventory_query: Query<Entity, With<UiInventoryManager>>,
|
||||||
|
) {
|
||||||
|
for entity in inventory_query {
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
use bevy::{prelude::*, window::WindowResized};
|
use bevy::{
|
||||||
|
prelude::*,
|
||||||
|
window::WindowResized,
|
||||||
|
};
|
||||||
use leafwing_input_manager::prelude::*;
|
use leafwing_input_manager::prelude::*;
|
||||||
|
|
||||||
use crate::UiAction;
|
use crate::input::UiAction;
|
||||||
|
|
||||||
pub mod inventory;
|
pub mod inventory;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue