Added basic worker components

feature/debug_draw
hheik 2025-02-21 11:16:49 +02:00
parent 4e3ac2d268
commit 3ff21d3b83
14 changed files with 431 additions and 88 deletions

Binary file not shown.

BIN
assets/sprites/missing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 B

View File

@ -1,84 +1,28 @@
use crate::{debug, game_setup};
use bevy::{prelude::*, sprite::Anchor};
use crate::{debug, game_setup, util};
use bevy::prelude::*;
use bevy_rapier2d::prelude::*;
mod item;
mod prefab;
mod systems;
mod work;
pub fn init(app: &mut App) {
let app = app.add_plugins((
game_setup::GameSetupPlugin,
RapierPhysicsPlugin::<NoUserData>::default(),
util::UtilPlugin,
debug::DebugPlugin,
));
app.add_systems(Startup, setup_2d)
.add_systems(Update, demo_2d);
}
fn setup_2d(mut commands: Commands, assets: Res<AssetServer>) {
commands.spawn((
Name::from("Demo 2D camera"),
Camera2d,
Transform::from_xyz(0.0, 0.0, 10.0),
));
commands.spawn((
Name::from("Glorb"),
Transform::from_xyz(-200.0, 0.0, 0.0),
Sprite {
image: assets.load("sprites/glorb.png"),
anchor: Anchor::BottomCenter,
..default()
},
));
commands.spawn((
Name::from("Box"),
Transform::from_xyz(-200.0, -150.0, 0.0),
Sprite {
image: assets.load("sprites/box.png"),
anchor: Anchor::Custom(Vec2 { x: 0.0, y: -0.375 }),
..default()
},
));
commands.spawn((
Name::from("Wood"),
Sprite {
image: assets.load("sprites/wood.png"),
..default()
},
));
commands.spawn((
Name::from("Tree"),
Transform::from_xyz(200.0, 0.0, 0.0),
Sprite {
image: assets.load("sprites/tree.png"),
anchor: Anchor::Custom(Vec2 { x: 0.0, y: -0.375 }),
..default()
},
));
}
fn demo_2d(
mut camera_query: Query<(&mut Transform, &mut OrthographicProjection)>,
mut mouse_events: EventReader<bevy::input::mouse::MouseMotion>,
mut scroll_events: EventReader<bevy::input::mouse::MouseWheel>,
mouse_input: Res<ButtonInput<MouseButton>>,
) {
let raw_mouse_motion: Vec2 = mouse_events.read().map(|e| e.delta).sum();
let raw_scroll_motion: f32 = scroll_events
.read()
.map(|event| match event.unit {
bevy::input::mouse::MouseScrollUnit::Line => event.y * -0.1,
bevy::input::mouse::MouseScrollUnit::Pixel => event.y * -0.05,
})
.sum();
for (mut transform, mut projection) in camera_query.iter_mut() {
projection.scale += raw_scroll_motion * projection.scale;
let mouse_motion = raw_mouse_motion * projection.scale * Vec2::new(-1.0, 1.0);
if mouse_input.pressed(MouseButton::Middle) {
transform.translation += mouse_motion.extend(0.0);
}
}
app.register_type::<item::Item>()
.register_type::<item::ItemSource>()
.register_type::<item::Inventory>()
.register_type::<work::WorkDuration>()
.register_type::<work::Worker>()
.register_type::<prefab::Glorb>()
.register_type::<prefab::Tree>()
.add_systems(Startup, systems::setup_2d)
.add_systems(Update, (systems::demo_2d, systems::work_select))
.add_systems(PostUpdate, item::update_item_sprite);
}

141
src/game/item.rs Normal file
View File

@ -0,0 +1,141 @@
use bevy::prelude::*;
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Component, Reflect)]
#[reflect(Component)]
#[require(Sprite)]
pub enum Item {
Wood,
}
#[derive(Copy, Clone, Debug, Reflect)]
pub struct ItemStack {
pub item: Item,
pub count: u32,
}
impl Default for ItemStack {
fn default() -> Self {
Self {
item: Item::Wood,
count: 1,
}
}
}
#[derive(Clone, Debug, Default, Component, Reflect)]
#[reflect(Component)]
pub struct ItemSource(pub ItemStack);
#[derive(Clone, Debug, Default, Component, Reflect)]
#[reflect(Component)]
pub struct Inventory {
pub capacity: Option<usize>,
pub items: Vec<ItemStack>,
}
pub enum InsertResult {
/// Combine to an existing stack. Contains (`index`, `new_count`)
Combine(usize, u32),
/// No existing stackable stacks, push a new one.
Push(ItemStack),
}
pub enum RemoveResult {
/// The stack will get depleted, so it can be removed. Contains the `index` of empty stack.
Empty(usize),
/// The stack will get only partially depleted. Contains (`index`, `new_count`)
Partial(usize, u32),
}
impl Inventory {
pub fn with_capacity(capacity: usize) -> Self {
Self {
capacity: Some(capacity),
..default()
}
}
pub fn is_empty(&self) -> bool {
self.items.iter().all(|stack| stack.count == 0)
}
pub fn try_insert(&self, item_stack: &ItemStack) -> Option<InsertResult> {
match self
.items
.iter()
.enumerate()
.find(|(_, stack)| stack.item == item_stack.item)
{
Some((index, stack)) => {
Some(InsertResult::Combine(index, stack.count + item_stack.count))
}
None => {
if self
.capacity
.map_or(true, |capacity| self.items.len() < capacity)
{
Some(InsertResult::Push(*item_stack))
} else {
None
}
}
}
}
pub fn try_remove(&self, item_stack: &ItemStack) -> Option<RemoveResult> {
match self
.items
.iter()
.enumerate()
.find(|(_, stack)| stack.item == item_stack.item)
{
Some((index, stack)) => match stack.count.checked_sub(item_stack.count) {
Some(new_count) => {
if new_count == 0 {
Some(RemoveResult::Empty(index))
} else {
Some(RemoveResult::Partial(index, new_count))
}
}
// Not enough items
None => None,
},
// No matching items found
None => None,
}
}
pub fn try_transfer(
&self,
to: &Self,
item_stack: &ItemStack,
) -> Option<(RemoveResult, InsertResult)> {
match (self.try_remove(item_stack), to.try_insert(item_stack)) {
(Some(remove_result), Some(insert_result)) => Some((remove_result, insert_result)),
_ => None,
}
}
pub fn sanitize(&mut self) {
if let Some(capacity) = self.capacity {
self.items.truncate(capacity);
}
self.items.retain(|stack| stack.count > 0);
}
}
#[derive(Clone, Debug, Default, Component, Reflect)]
#[reflect(Component)]
#[require(Inventory)]
pub struct Stockpile;
pub fn update_item_sprite(
mut query: Query<(&mut Sprite, &Item), Changed<Item>>,
assets: Res<AssetServer>,
) {
for (mut sprite, item) in query.iter_mut() {
sprite.image = assets.load(match item {
Item::Wood => "sprites/wood.png",
});
}
}

51
src/game/prefab.rs Normal file
View File

@ -0,0 +1,51 @@
use bevy::{prelude::*, sprite::Anchor};
use bevy_rapier2d::prelude::*;
use item::{Inventory, Item, ItemSource, ItemStack, Stockpile};
use work::{WorkDuration, Worker};
use super::*;
use crate::util::SpriteLoader;
#[derive(Clone, Debug, Default, Component, Reflect)]
#[reflect(Component)]
#[require(
Name(|| Name::from("Glorb")),
Sprite(|| Sprite {
anchor: Anchor::BottomCenter,
..default()
}),
SpriteLoader(|| SpriteLoader::from("sprites/glorb.png")),
Worker,
Inventory(|| Inventory::with_capacity(1)),
Velocity,
RigidBody(|| RigidBody::KinematicVelocityBased),
)]
pub struct Glorb;
#[derive(Clone, Debug, Default, Component, Reflect)]
#[reflect(Component)]
#[require(
Name(|| Name::from("Tree")),
Sprite(|| Sprite {
anchor: Anchor::Custom(Vec2 { x: 0.0, y: -0.375 }),
..default()
}),
SpriteLoader(|| SpriteLoader::from("sprites/tree.png")),
ItemSource(|| ItemSource(ItemStack { item: Item::Wood, count: 1 })),
WorkDuration(|| WorkDuration(5.0))
)]
pub struct Tree;
#[derive(Clone, Debug, Default, Component, Reflect)]
#[reflect(Component)]
#[require(
Name(|| Name::from("Chest")),
Sprite(|| Sprite {
anchor: Anchor::Custom(Vec2 { x: 0.0, y: -0.375 }),
..default()
}),
SpriteLoader(|| SpriteLoader::from("sprites/box.png")),
Inventory(|| Inventory::with_capacity(25)),
Stockpile,
)]
pub struct Chest;

5
src/game/systems.rs Normal file
View File

@ -0,0 +1,5 @@
mod demo;
mod worker;
pub use demo::*;
pub use worker::*;

45
src/game/systems/demo.rs Normal file
View File

@ -0,0 +1,45 @@
use bevy::prelude::*;
use crate::game::{item::Item, prefab};
pub fn setup_2d(mut commands: Commands) {
commands.spawn((
Name::from("Demo 2D camera"),
Camera2d,
Transform::from_xyz(0.0, 0.0, 10.0),
));
commands.spawn((Transform::from_xyz(-200.0, 0.0, 0.0), prefab::Glorb));
commands.spawn((Transform::from_xyz(-200.0, 100.0, 0.0), prefab::Glorb));
commands.spawn((Transform::from_xyz(-200.0, -100.0, 0.0), prefab::Glorb));
commands.spawn((Transform::from_xyz(200.0, 0.0, 0.0), prefab::Tree));
commands.spawn((Name::from("Wood"), Item::Wood));
commands.spawn((Transform::from_xyz(-200.0, -150.0, 0.0), prefab::Chest));
}
pub fn demo_2d(
mut camera_query: Query<(&mut Transform, &mut OrthographicProjection)>,
mut mouse_events: EventReader<bevy::input::mouse::MouseMotion>,
mut scroll_events: EventReader<bevy::input::mouse::MouseWheel>,
mouse_input: Res<ButtonInput<MouseButton>>,
) {
let raw_mouse_motion: Vec2 = mouse_events.read().map(|e| e.delta).sum();
let raw_scroll_motion: f32 = scroll_events
.read()
.map(|event| match event.unit {
bevy::input::mouse::MouseScrollUnit::Line => event.y * -0.1,
bevy::input::mouse::MouseScrollUnit::Pixel => event.y * -0.05,
})
.sum();
for (mut transform, mut projection) in camera_query.iter_mut() {
projection.scale += raw_scroll_motion * projection.scale;
let mouse_motion = raw_mouse_motion * projection.scale * Vec2::new(-1.0, 1.0);
if mouse_input.pressed(MouseButton::Middle) {
transform.translation += mouse_motion.extend(0.0);
}
}
}

View File

@ -0,0 +1,72 @@
use bevy::prelude::*;
use crate::game::{
item::{Inventory, ItemSource, Stockpile},
work::{Task, WorkType, Worker},
};
pub fn work_select(
mut worker_query: Query<(&mut Worker, &Inventory, &GlobalTransform)>,
store_query: Query<(Entity, &Inventory, &GlobalTransform), With<Stockpile>>,
gather_query: Query<(Entity, &ItemSource, &GlobalTransform)>,
) {
for (mut worker, worker_inventory, worker_transform) in worker_query.iter_mut() {
// Skip if worker already has a job
if worker.0.is_some() {
continue;
}
let mut task_by_distance: Option<(Task, f32)> = None;
for (stockpile_entity, stockpile_inventory, stockpile_transform) in store_query.iter() {
for worker_stack in worker_inventory.items.iter() {
if let Some((_, _)) =
worker_inventory.try_transfer(stockpile_inventory, worker_stack)
{
let stockpile_dist_squared = worker_transform
.translation()
.distance_squared(stockpile_transform.translation());
if task_by_distance.map_or(true, |(_, closest_task_dist)| {
stockpile_dist_squared < closest_task_dist
}) {
task_by_distance = Some((
Task {
target: stockpile_entity,
work_type: WorkType::Store(*worker_stack),
progress: 0.0,
},
stockpile_dist_squared,
))
}
}
}
}
if let Some((task, _)) = task_by_distance {
worker.0 = Some(task);
continue;
}
for (item_source_entity, item_source, item_source_transform) in gather_query.iter() {
if worker_inventory.try_insert(&item_source.0).is_some() {
let source_dist_squared = worker_transform
.translation()
.distance_squared(item_source_transform.translation());
if task_by_distance.map_or(true, |(_, closest_task_dist)| {
source_dist_squared < closest_task_dist
}) {
task_by_distance = Some((
Task {
target: item_source_entity,
work_type: WorkType::Gather,
progress: 0.0,
},
source_dist_squared,
))
}
}
}
worker.0 = task_by_distance.map(|(task, _)| task);
}
}

47
src/game/work.rs Normal file
View File

@ -0,0 +1,47 @@
use bevy::prelude::*;
use crate::util::Seconds;
use super::item::ItemStack;
/// Total time it takes to finish a task
#[derive(Copy, Clone, Debug, Default, Component, Reflect)]
#[reflect(Component)]
pub struct WorkDuration(pub Seconds);
impl WorkDuration {
/// Returns the `Some`(`duration_secs`) if the value is above `0.0`, and `None` otherwise
pub fn safe_duration(&self) -> Option<Seconds> {
if self.0 > 0.0 {
Some(self.0)
} else {
None
}
}
}
#[derive(Copy, Clone, Debug, Reflect)]
pub enum WorkType {
Gather,
Store(ItemStack),
}
#[derive(Copy, Clone, Debug, Reflect)]
pub struct Task {
pub progress: Seconds,
pub work_type: WorkType,
pub target: Entity,
}
impl Task {
pub fn add_progress(&mut self, work_duration: &WorkDuration, progress: Seconds) -> u32 {
match work_duration.safe_duration() {
Some(duration) => (self.progress + progress).div_euclid(duration).max(0.0) as u32,
None => 1,
}
}
}
#[derive(Copy, Clone, Debug, Default, Component, Reflect)]
#[reflect(Component)]
pub struct Worker(pub Option<Task>);

View File

@ -4,18 +4,19 @@ pub struct GameSetupPlugin;
impl Plugin for GameSetupPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(ClearColor(Color::BLACK)).add_plugins(
DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
resolution: WindowResolution::new(512.0 * 2.0, 320.0 * 2.0),
title: "Glorbs <press P to toggle debug mode>".to_string(), // NOTE: Replace this
resizable: false,
app.insert_resource(ClearColor(Color::linear_rgb(0.070, 0.094, 0.071)))
.add_plugins(
DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
resolution: WindowResolution::new(512.0 * 2.0, 320.0 * 2.0),
title: "Glorbs <press P to toggle debug mode>".to_string(), // NOTE: Replace this
resizable: false,
..default()
}),
..default()
}),
..default()
})
.set(ImagePlugin::default_nearest()),
);
})
.set(ImagePlugin::default_nearest()),
);
}
}

View File

@ -1,5 +1,4 @@
use bevy::prelude::*;
use util::create_app_graphs;
pub mod debug;
pub mod game;
@ -9,6 +8,6 @@ pub mod util;
fn main() {
let mut app = App::new();
game::init(&mut app);
create_app_graphs(&mut app);
// util::create_app_graphs(&mut app);
app.run();
}

View File

@ -4,14 +4,18 @@ use core::{fmt, ops};
use std::{fs, path::Path};
mod basis;
mod plugin;
mod transform_f64;
mod types;
mod vector2;
mod vector2_i32;
mod vector3;
mod vector3_i32;
pub use basis::*;
pub use plugin::*;
pub use transform_f64::*;
pub use types::*;
pub use vector2::*;
pub use vector2_i32::*;
pub use vector3::*;

33
src/util/plugin.rs Normal file
View File

@ -0,0 +1,33 @@
use bevy::prelude::*;
pub struct UtilPlugin;
impl Plugin for UtilPlugin {
fn build(&self, app: &mut App) {
app.register_type::<SpriteLoader>()
.add_systems(PostUpdate, load_sprite);
}
}
/// Automatically sets the `Sprite` components `image` to given asset path upon being added.
///
/// Can be used to set default sprite in required components without having access to AssetServer.
#[derive(Clone, Debug, Default, Component, Reflect)]
#[reflect(Component)]
#[require(Sprite)]
pub struct SpriteLoader(pub String);
impl SpriteLoader {
pub fn from(path: impl Into<String>) -> Self {
Self(path.into())
}
}
pub fn load_sprite(
mut sprite_query: Query<(&mut Sprite, &SpriteLoader), Added<SpriteLoader>>,
assets: Res<AssetServer>,
) {
for (mut sprite, loader) in sprite_query.iter_mut() {
sprite.image = assets.load(&loader.0);
}
}

1
src/util/types.rs Normal file
View File

@ -0,0 +1 @@
pub type Seconds = f32;