From 3ff21d3b836617208b4399dd9fdc681892fc5a92 Mon Sep 17 00:00:00 2001 From: hheik <4469778+hheik@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:16:49 +0200 Subject: [PATCH] Added basic worker components --- assets/sprites/missing.aseprite | Bin 0 -> 529 bytes assets/sprites/missing.png | Bin 0 -> 482 bytes src/game.rs | 92 ++++----------------- src/game/item.rs | 141 ++++++++++++++++++++++++++++++++ src/game/prefab.rs | 51 ++++++++++++ src/game/systems.rs | 5 ++ src/game/systems/demo.rs | 45 ++++++++++ src/game/systems/worker.rs | 72 ++++++++++++++++ src/game/work.rs | 47 +++++++++++ src/game_setup.rs | 25 +++--- src/main.rs | 3 +- src/util.rs | 4 + src/util/plugin.rs | 33 ++++++++ src/util/types.rs | 1 + 14 files changed, 431 insertions(+), 88 deletions(-) create mode 100644 assets/sprites/missing.aseprite create mode 100644 assets/sprites/missing.png create mode 100644 src/game/item.rs create mode 100644 src/game/prefab.rs create mode 100644 src/game/systems.rs create mode 100644 src/game/systems/demo.rs create mode 100644 src/game/systems/worker.rs create mode 100644 src/game/work.rs create mode 100644 src/util/plugin.rs create mode 100644 src/util/types.rs diff --git a/assets/sprites/missing.aseprite b/assets/sprites/missing.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..132fc309855f157a0b8d838a33a595fc18c5832a GIT binary patch literal 529 zcmWe+Vqkc%l##&!2o)F@85kH+fEWSP85lu=3<5w%3osFA`mc{HU|U&$7Ki~cy8_rs zB0(q{xIwNg=#k!q`f;l>-Q(KnNI_<_8FK@$@9+C5As$q zvaz(cFEL1Hiq3nOJo|tA*(axO|M~y_|LnEfTk6^~v$7(?g36q_))*Px3{SpZy{mt^ zS699P+)_0e*4zL-4nr25aus=y^$N^zIR=LRKsGyrPhw?ik%Hk_Acs|ffkBvomEk`y zJRv}q!Hl6|&f6OYc^edXSOXp(EtEOX8*@+aOIE;x>Ho|GybkeJFy2mzm{xWB`eQe% z4I(+Q&)G`X>TZsiZhCo6PU*y*JAdS#P0W4cd9Ewd>wI^r z(mS7JDKeLJdQ+?FUoS1zdAB&IFZyo%ng5+~w$mi#uJgQ)K5lE9^ sWZDdw>!(idyms$)>b}iiZ|(X1@~`dQ$7R!I36y56vHHSrQfFfn0M6)|82|tP literal 0 HcmV?d00001 diff --git a/assets/sprites/missing.png b/assets/sprites/missing.png new file mode 100644 index 0000000000000000000000000000000000000000..55b9d1154df869b996afe9b4e46fa5a51b9407ea GIT binary patch literal 482 zcmV<80UiE{P)Px$oJmAMRCt{2noVxPKnzArRH?_whFvx}4(f?=95(FOaFX7DbWypDqR`F|+uw)A zPeMW{0{g||`3WLPk|Ygti2lL88|IShZud(_Y+(X4@$>0gbp1FTMZ{l3h=9Hu=F-I1 zc#CisuqXK?<8*Y9;3~i>dBsD5n*i(N6>AAD0=$w}EG2XnU$VqId28N~Kdv2KDHV4< zU1gkx*`f-}hd@aMJet~`Su5<7U^)a!lfT_BhpoW%^L1r-V%`HRh?GDnKqO8At$-*} zfaRA(H!us#E8s5)HPcqqg34tGY)SCi&F#f)CHO7j_w=VG`N-uo-5n`S-m0RgW&y-T zilzc;lDF(96arAmix3O}^!Wj_0&0_gsWZX% z%%30cKd;h%k9q(-5z_a~+<8W_0uVIa1K4H(suQ8y4g+XjK=VBSLesN=()2nYNs@$x YFXCem0c^oeuK)l507*qoM6N<$g5cfDr2qf` literal 0 HcmV?d00001 diff --git a/src/game.rs b/src/game.rs index a36d8ef..c9fec69 100644 --- a/src/game.rs +++ b/src/game.rs @@ -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::::default(), + util::UtilPlugin, debug::DebugPlugin, )); - app.add_systems(Startup, setup_2d) - .add_systems(Update, demo_2d); -} - -fn setup_2d(mut commands: Commands, assets: Res) { - 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, - mut scroll_events: EventReader, - mouse_input: Res>, -) { - 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::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .add_systems(Startup, systems::setup_2d) + .add_systems(Update, (systems::demo_2d, systems::work_select)) + .add_systems(PostUpdate, item::update_item_sprite); } diff --git a/src/game/item.rs b/src/game/item.rs new file mode 100644 index 0000000..96ac0a4 --- /dev/null +++ b/src/game/item.rs @@ -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, + pub items: Vec, +} + +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 { + 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 { + 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>, + assets: Res, +) { + for (mut sprite, item) in query.iter_mut() { + sprite.image = assets.load(match item { + Item::Wood => "sprites/wood.png", + }); + } +} diff --git a/src/game/prefab.rs b/src/game/prefab.rs new file mode 100644 index 0000000..9561abe --- /dev/null +++ b/src/game/prefab.rs @@ -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; diff --git a/src/game/systems.rs b/src/game/systems.rs new file mode 100644 index 0000000..116a60f --- /dev/null +++ b/src/game/systems.rs @@ -0,0 +1,5 @@ +mod demo; +mod worker; + +pub use demo::*; +pub use worker::*; diff --git a/src/game/systems/demo.rs b/src/game/systems/demo.rs new file mode 100644 index 0000000..a288725 --- /dev/null +++ b/src/game/systems/demo.rs @@ -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, + mut scroll_events: EventReader, + mouse_input: Res>, +) { + 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); + } + } +} diff --git a/src/game/systems/worker.rs b/src/game/systems/worker.rs new file mode 100644 index 0000000..abf267e --- /dev/null +++ b/src/game/systems/worker.rs @@ -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>, + 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); + } +} diff --git a/src/game/work.rs b/src/game/work.rs new file mode 100644 index 0000000..473a483 --- /dev/null +++ b/src/game/work.rs @@ -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 { + 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); diff --git a/src/game_setup.rs b/src/game_setup.rs index 78456b6..a756319 100644 --- a/src/game_setup.rs +++ b/src/game_setup.rs @@ -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 ".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 ".to_string(), // NOTE: Replace this + resizable: false, + ..default() + }), ..default() - }), - ..default() - }) - .set(ImagePlugin::default_nearest()), - ); + }) + .set(ImagePlugin::default_nearest()), + ); } } diff --git a/src/main.rs b/src/main.rs index 5b08ab9..4c159db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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(); } diff --git a/src/util.rs b/src/util.rs index 8c0bc35..a4b872a 100644 --- a/src/util.rs +++ b/src/util.rs @@ -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::*; diff --git a/src/util/plugin.rs b/src/util/plugin.rs new file mode 100644 index 0000000..8a6520e --- /dev/null +++ b/src/util/plugin.rs @@ -0,0 +1,33 @@ +use bevy::prelude::*; + +pub struct UtilPlugin; + +impl Plugin for UtilPlugin { + fn build(&self, app: &mut App) { + app.register_type::() + .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) -> Self { + Self(path.into()) + } +} + +pub fn load_sprite( + mut sprite_query: Query<(&mut Sprite, &SpriteLoader), Added>, + assets: Res, +) { + for (mut sprite, loader) in sprite_query.iter_mut() { + sprite.image = assets.load(&loader.0); + } +} diff --git a/src/util/types.rs b/src/util/types.rs new file mode 100644 index 0000000..9b673c4 --- /dev/null +++ b/src/util/types.rs @@ -0,0 +1 @@ +pub type Seconds = f32;