generated from hheik/bevy-template
Working gather-pickup-store work system
parent
bf50c19ba0
commit
d9319bfd5c
|
|
@ -2424,6 +2424,7 @@ dependencies = [
|
||||||
"bevy_mod_debugdump",
|
"bevy_mod_debugdump",
|
||||||
"bevy_prototype_lyon",
|
"bevy_prototype_lyon",
|
||||||
"bevy_rapier2d",
|
"bevy_rapier2d",
|
||||||
|
"fastrand",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ bevy-inspector-egui = "0.28.1"
|
||||||
bevy_mod_debugdump = "0.12.1"
|
bevy_mod_debugdump = "0.12.1"
|
||||||
bevy_prototype_lyon = "0.13.0"
|
bevy_prototype_lyon = "0.13.0"
|
||||||
bevy_rapier2d = "0.28.0"
|
bevy_rapier2d = "0.28.0"
|
||||||
|
fastrand = "2.3.0"
|
||||||
num-traits = "0.2.19"
|
num-traits = "0.2.19"
|
||||||
|
|
||||||
# Enable a small amount of optimization in debug mode
|
# Enable a small amount of optimization in debug mode
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ impl Plugin for DebugPlugin {
|
||||||
|
|
||||||
app.add_plugins((
|
app.add_plugins((
|
||||||
bevy_inspector_egui::quick::WorldInspectorPlugin::new().run_if(is_debug_enabled),
|
bevy_inspector_egui::quick::WorldInspectorPlugin::new().run_if(is_debug_enabled),
|
||||||
bevy_rapier2d::prelude::RapierDebugRenderPlugin::default(),
|
// bevy_rapier2d::prelude::RapierDebugRenderPlugin::default(),
|
||||||
));
|
));
|
||||||
|
|
||||||
app.register_type::<DebugCanvas>()
|
app.register_type::<DebugCanvas>()
|
||||||
|
|
|
||||||
16
src/game.rs
16
src/game.rs
|
|
@ -26,14 +26,22 @@ pub fn init(app: &mut App) {
|
||||||
.register_type::<creature::Mover>()
|
.register_type::<creature::Mover>()
|
||||||
.register_type::<prefab::Glorb>()
|
.register_type::<prefab::Glorb>()
|
||||||
.register_type::<prefab::Tree>()
|
.register_type::<prefab::Tree>()
|
||||||
|
.add_event::<work::OnTaskFinish>()
|
||||||
|
.add_event::<item::SpawnItem>()
|
||||||
.add_systems(Startup, systems::setup_2d)
|
.add_systems(Startup, systems::setup_2d)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
Update,
|
Update,
|
||||||
(
|
(
|
||||||
systems::demo_2d,
|
(
|
||||||
systems::work_select,
|
systems::demo_2d,
|
||||||
systems::worker_movement,
|
systems::work_select,
|
||||||
),
|
systems::worker_movement,
|
||||||
|
systems::spawn_items,
|
||||||
|
),
|
||||||
|
(systems::do_work,),
|
||||||
|
(systems::apply_task_result,),
|
||||||
|
)
|
||||||
|
.chain(),
|
||||||
)
|
)
|
||||||
.add_systems(PostUpdate, item::update_item_sprite)
|
.add_systems(PostUpdate, item::update_item_sprite)
|
||||||
.add_systems(Update, (systems::draw_job_targets).in_set(debug::DebugSet));
|
.add_systems(Update, (systems::draw_job_targets).in_set(debug::DebugSet));
|
||||||
|
|
|
||||||
149
src/game/item.rs
149
src/game/item.rs
|
|
@ -1,5 +1,7 @@
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::util::Kilograms;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Component, Reflect)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Component, Reflect)]
|
||||||
#[reflect(Component)]
|
#[reflect(Component)]
|
||||||
#[require(Sprite)]
|
#[require(Sprite)]
|
||||||
|
|
@ -7,44 +9,78 @@ pub enum Item {
|
||||||
Wood,
|
Wood,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Item {
|
||||||
|
// TODO: implement
|
||||||
|
fn stack_max_size(&self) -> Option<u32> {
|
||||||
|
match self {
|
||||||
|
_ => Some(100),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement
|
||||||
|
fn weight(&self) -> Kilograms {
|
||||||
|
match self {
|
||||||
|
Self::Wood => 1.0, // Paper birch, height 30cm, diameter 18cm ~= 6 kg, chopped in 6 pieces ~= 1kg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Reflect)]
|
#[derive(Copy, Clone, Debug, Reflect)]
|
||||||
pub struct ItemStack {
|
pub struct ItemStack {
|
||||||
pub item: Item,
|
pub item: Item,
|
||||||
pub count: u32,
|
pub count: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ItemStack {
|
impl From<Item> for ItemStack {
|
||||||
fn default() -> Self {
|
fn from(item: Item) -> Self {
|
||||||
Self {
|
Self { item, count: 1 }
|
||||||
item: Item::Wood,
|
|
||||||
count: 1,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Component, Reflect)]
|
#[derive(Clone, Debug, Default, Component, Reflect)]
|
||||||
#[reflect(Component)]
|
#[reflect(Component)]
|
||||||
pub struct ItemSource(pub ItemStack);
|
pub struct ItemSource {
|
||||||
|
/// What items are spawned when gathered
|
||||||
|
pub drops: Vec<ItemStack>,
|
||||||
|
/// How many times this entity can be gathered before it despawns
|
||||||
|
pub gather_limit: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Component, Reflect)]
|
#[derive(Clone, Debug, Default, Component, Reflect)]
|
||||||
#[reflect(Component)]
|
#[reflect(Component)]
|
||||||
pub struct Inventory {
|
pub struct Inventory {
|
||||||
pub capacity: Option<usize>,
|
pub capacity: Option<usize>,
|
||||||
|
// TODO: Create a "Reserved" item stack type, that indicates that someone has reserved a slot
|
||||||
|
// for some new item type in the inventory
|
||||||
pub items: Vec<ItemStack>,
|
pub items: Vec<ItemStack>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum InsertResult {
|
#[derive(Clone, Debug)]
|
||||||
/// Combine to an existing stack. Contains (`index`, `new_count`)
|
pub enum InventoryError {
|
||||||
Combine(usize, u32),
|
IndexNotFound {
|
||||||
/// No existing stackable stacks, push a new one.
|
index: usize,
|
||||||
Push(ItemStack),
|
inventory: Inventory,
|
||||||
|
info: Option<String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Result for trying to insert an item to inventory. Should be used and discarded immediately, as
|
||||||
|
/// it might not be valid after mutating inventory. In future, It could contain a hash value for
|
||||||
|
/// the valid inventory state that it aplies to!
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum InsertResult {
|
||||||
|
/// Combine to an existing stack. Contains (`index`, `new_count`)
|
||||||
|
Combine { index: usize, new_count: u32 },
|
||||||
|
/// No existing stackable stacks, push a new one.
|
||||||
|
Push { stack: ItemStack },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum RemoveResult {
|
pub enum RemoveResult {
|
||||||
/// The stack will get depleted, so it can be removed. Contains the `index` of empty stack.
|
/// The stack will get depleted, so it can be removed. Contains the `index` of empty stack.
|
||||||
Empty(usize),
|
Empty { index: usize },
|
||||||
/// The stack will get only partially depleted. Contains (`index`, `new_count`)
|
/// The stack will get only partially depleted. Contains (`index`, `new_count`)
|
||||||
Partial(usize, u32),
|
Partial { index: usize, new_count: u32 },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Inventory {
|
impl Inventory {
|
||||||
|
|
@ -55,26 +91,30 @@ impl Inventory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn weight(&self) -> Kilograms {
|
||||||
self.items.iter().all(|stack| stack.count == 0)
|
self.items
|
||||||
|
.iter()
|
||||||
|
.map(|stack| stack.item.weight() * stack.count as f32)
|
||||||
|
.sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn try_insert(&self, item_stack: &ItemStack) -> Option<InsertResult> {
|
pub fn try_insert(&self, item_stack: ItemStack) -> Option<InsertResult> {
|
||||||
match self
|
match self
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.find(|(_, stack)| stack.item == item_stack.item)
|
.find(|(_, stack)| stack.item == item_stack.item)
|
||||||
{
|
{
|
||||||
Some((index, stack)) => {
|
Some((index, stack)) => Some(InsertResult::Combine {
|
||||||
Some(InsertResult::Combine(index, stack.count + item_stack.count))
|
index,
|
||||||
}
|
new_count: stack.count + item_stack.count,
|
||||||
|
}),
|
||||||
None => {
|
None => {
|
||||||
if self
|
if self
|
||||||
.capacity
|
.capacity
|
||||||
.map_or(true, |capacity| self.items.len() < capacity)
|
.map_or(true, |capacity| self.items.len() < capacity)
|
||||||
{
|
{
|
||||||
Some(InsertResult::Push(*item_stack))
|
Some(InsertResult::Push { stack: item_stack })
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -82,7 +122,7 @@ impl Inventory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn try_remove(&self, item_stack: &ItemStack) -> Option<RemoveResult> {
|
pub fn try_remove(&self, item_stack: ItemStack) -> Option<RemoveResult> {
|
||||||
match self
|
match self
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -92,9 +132,9 @@ impl Inventory {
|
||||||
Some((index, stack)) => match stack.count.checked_sub(item_stack.count) {
|
Some((index, stack)) => match stack.count.checked_sub(item_stack.count) {
|
||||||
Some(new_count) => {
|
Some(new_count) => {
|
||||||
if new_count == 0 {
|
if new_count == 0 {
|
||||||
Some(RemoveResult::Empty(index))
|
Some(RemoveResult::Empty { index })
|
||||||
} else {
|
} else {
|
||||||
Some(RemoveResult::Partial(index, new_count))
|
Some(RemoveResult::Partial { index, new_count })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Not enough items
|
// Not enough items
|
||||||
|
|
@ -108,7 +148,7 @@ impl Inventory {
|
||||||
pub fn try_transfer(
|
pub fn try_transfer(
|
||||||
&self,
|
&self,
|
||||||
to: &Self,
|
to: &Self,
|
||||||
item_stack: &ItemStack,
|
item_stack: ItemStack,
|
||||||
) -> Option<(RemoveResult, InsertResult)> {
|
) -> Option<(RemoveResult, InsertResult)> {
|
||||||
match (self.try_remove(item_stack), to.try_insert(item_stack)) {
|
match (self.try_remove(item_stack), to.try_insert(item_stack)) {
|
||||||
(Some(remove_result), Some(insert_result)) => Some((remove_result, insert_result)),
|
(Some(remove_result), Some(insert_result)) => Some((remove_result, insert_result)),
|
||||||
|
|
@ -116,6 +156,58 @@ impl Inventory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn apply_remove(&mut self, remove_result: RemoveResult) -> Result<(), InventoryError> {
|
||||||
|
match remove_result {
|
||||||
|
RemoveResult::Empty { index } => {
|
||||||
|
if index >= self.items.len() {
|
||||||
|
Err(InventoryError::IndexNotFound {
|
||||||
|
index,
|
||||||
|
inventory: self.clone(),
|
||||||
|
info: Some("RemoveResult::Empty".to_string()),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
self.items.remove(index);
|
||||||
|
self.sanitize();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RemoveResult::Partial { index, new_count } => match self.items.get_mut(index) {
|
||||||
|
Some(stack) => {
|
||||||
|
stack.count = new_count;
|
||||||
|
self.sanitize();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
None => Err(InventoryError::IndexNotFound {
|
||||||
|
index,
|
||||||
|
inventory: self.clone(),
|
||||||
|
info: Some("RemoveResult::Partial".to_string()),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_insert(&mut self, insert_result: InsertResult) -> Result<(), InventoryError> {
|
||||||
|
match insert_result {
|
||||||
|
InsertResult::Push { stack } => {
|
||||||
|
self.items.push(stack);
|
||||||
|
self.sanitize();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
InsertResult::Combine { index, new_count } => match self.items.get_mut(index) {
|
||||||
|
Some(stack) => {
|
||||||
|
stack.count = new_count;
|
||||||
|
self.sanitize();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
None => Err(InventoryError::IndexNotFound {
|
||||||
|
index,
|
||||||
|
inventory: self.clone(),
|
||||||
|
info: Some("InsertResult::Combine".to_string()),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn sanitize(&mut self) {
|
pub fn sanitize(&mut self) {
|
||||||
if let Some(capacity) = self.capacity {
|
if let Some(capacity) = self.capacity {
|
||||||
self.items.truncate(capacity);
|
self.items.truncate(capacity);
|
||||||
|
|
@ -139,3 +231,10 @@ pub fn update_item_sprite(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Event, Clone, Debug)]
|
||||||
|
pub struct SpawnItem {
|
||||||
|
pub item: Item,
|
||||||
|
pub to: Vec2,
|
||||||
|
pub velocity: Vec2,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ pub struct Glorb;
|
||||||
..default()
|
..default()
|
||||||
}),
|
}),
|
||||||
SpriteLoader(|| SpriteLoader::from("sprites/tree.png")),
|
SpriteLoader(|| SpriteLoader::from("sprites/tree.png")),
|
||||||
ItemSource(|| ItemSource(ItemStack { item: Item::Wood, count: 1 })),
|
ItemSource(|| ItemSource { drops: vec![ItemStack { item: Item::Wood, count: 1 }], gather_limit: None }),
|
||||||
WorkDuration(|| WorkDuration(5.0))
|
WorkDuration(|| WorkDuration(5.0))
|
||||||
)]
|
)]
|
||||||
pub struct Tree;
|
pub struct Tree;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
mod creature;
|
mod creature;
|
||||||
mod demo;
|
mod demo;
|
||||||
|
mod item;
|
||||||
mod worker;
|
mod worker;
|
||||||
|
|
||||||
pub use creature::*;
|
pub use creature::*;
|
||||||
pub use demo::*;
|
pub use demo::*;
|
||||||
|
pub use item::*;
|
||||||
pub use worker::*;
|
pub use worker::*;
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,35 @@
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::game::{creature::Mover, work::Worker};
|
use crate::{
|
||||||
|
game::{creature::Mover, item::Inventory, work::Worker},
|
||||||
|
util::{inverse_lerp, lerp},
|
||||||
|
};
|
||||||
|
|
||||||
pub fn worker_movement(
|
pub fn worker_movement(
|
||||||
mut worker_query: Query<(Entity, &Mover, &Worker, &mut Transform)>,
|
mut worker_query: Query<(Entity, &Mover, &Worker, &mut Transform, Option<&Inventory>)>,
|
||||||
global_query: Query<&GlobalTransform>,
|
global_query: Query<&GlobalTransform>,
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
) {
|
) {
|
||||||
for (entity, mover, worker, mut transform) in worker_query.iter_mut() {
|
for (entity, mover, worker, mut transform, inventory) in worker_query.iter_mut() {
|
||||||
let Some(task) = worker.0 else { continue };
|
let Some(task) = worker.task.as_ref() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let (Ok(from), Ok(to)) = (global_query.get(entity), global_query.get(task.target)) else {
|
let (Ok(from), Ok(to)) = (global_query.get(entity), global_query.get(task.target)) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let diff = (to.translation() - from.translation()).xy();
|
let diff = (to.translation() - from.translation()).xy();
|
||||||
let dist = diff.length().max(0.);
|
let dist = diff.length().max(0.);
|
||||||
let dir = diff.normalize_or_zero();
|
let dir = diff.normalize_or_zero();
|
||||||
let movement = dir.extend(0.) * mover.speed * time.delta_secs();
|
let movement = dir.extend(0.)
|
||||||
|
* mover.speed
|
||||||
|
* inventory.map_or(1.0, |inventory| {
|
||||||
|
lerp(
|
||||||
|
1.0,
|
||||||
|
0.2,
|
||||||
|
inverse_lerp(0.0, 10.0, inventory.weight()).min(1.0),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
* time.delta_secs();
|
||||||
transform.translation += movement.clamp_length_max(dist);
|
transform.translation += movement.clamp_length_max(dist);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_rapier2d::prelude::{Damping, RigidBody, Velocity};
|
||||||
|
|
||||||
|
use crate::game::{item::SpawnItem, work::WorkDuration};
|
||||||
|
|
||||||
|
pub fn spawn_items(mut spawn_item_events: EventReader<SpawnItem>, mut commands: Commands) {
|
||||||
|
for event in spawn_item_events.read() {
|
||||||
|
commands.spawn((
|
||||||
|
Name::new(format!("{:?}", event.item)),
|
||||||
|
event.item,
|
||||||
|
WorkDuration(0.1),
|
||||||
|
Transform {
|
||||||
|
translation: event.to.extend(1.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
RigidBody::Dynamic,
|
||||||
|
Damping {
|
||||||
|
linear_damping: 4.0,
|
||||||
|
angular_damping: 1.0,
|
||||||
|
},
|
||||||
|
Velocity {
|
||||||
|
linvel: event.velocity,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,44 +1,104 @@
|
||||||
use bevy::{color::palettes::css, prelude::*};
|
use bevy::{color::palettes::css, prelude::*, utils::HashMap};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
debug::{ColoredShape, DebugDraw, Shape},
|
debug::{ColoredShape, DebugDraw, Shape},
|
||||||
game::{
|
game::{
|
||||||
item::{Inventory, ItemSource, Stockpile},
|
item::{Inventory, Item, ItemSource, ItemStack, SpawnItem, Stockpile},
|
||||||
work::{Task, WorkType, Worker},
|
work::{OnTaskFinish, Task, WorkDuration, WorkType, Worker},
|
||||||
},
|
},
|
||||||
|
util::random_vec2,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn work_select(
|
pub fn work_select(
|
||||||
mut worker_query: Query<(&mut Worker, &Inventory, &GlobalTransform)>,
|
workers: Query<(Entity, &Inventory, &GlobalTransform), With<Worker>>,
|
||||||
|
mut worker_query: Query<&mut Worker>,
|
||||||
|
pickup_query: Query<(Entity, &Item, &GlobalTransform)>,
|
||||||
store_query: Query<(Entity, &Inventory, &GlobalTransform), With<Stockpile>>,
|
store_query: Query<(Entity, &Inventory, &GlobalTransform), With<Stockpile>>,
|
||||||
gather_query: Query<(Entity, &ItemSource, &GlobalTransform)>,
|
gather_query: Query<(Entity, &GlobalTransform), With<ItemSource>>,
|
||||||
) {
|
) {
|
||||||
for (mut worker, worker_inventory, worker_transform) in worker_query.iter_mut() {
|
// What tasks are already targeting given entity
|
||||||
|
let mut target_tasks_map: HashMap<Entity, Vec<Task>> = HashMap::new();
|
||||||
|
|
||||||
|
fn add_to_task_map(map: &mut HashMap<Entity, Vec<Task>>, task: Task) {
|
||||||
|
match map.get_mut(&task.target) {
|
||||||
|
Some(tasks) => {
|
||||||
|
tasks.push(task);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
map.insert(task.target, vec![task]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for task in worker_query.iter().filter_map(|worker| worker.task.clone()) {
|
||||||
|
add_to_task_map(&mut target_tasks_map, task);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (worker_entity, worker_inventory, worker_transform) in workers.iter() {
|
||||||
|
let mut worker = worker_query.get_mut(worker_entity).unwrap();
|
||||||
|
|
||||||
// Skip if worker already has a job
|
// Skip if worker already has a job
|
||||||
if worker.0.is_some() {
|
if worker.task.is_some() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut task_by_distance: Option<(Task, f32)> = None;
|
let mut task_by_distance: Option<(Task, f32)> = None;
|
||||||
|
|
||||||
|
for (task_entity, item, item_transform) in pickup_query.iter() {
|
||||||
|
// Skip items that are already targeted by any task
|
||||||
|
if target_tasks_map.contains_key(&task_entity) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Check if the item fits into workers inventory
|
||||||
|
if worker_inventory
|
||||||
|
.try_insert(ItemStack::from(*item))
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
let dist_squared = worker_transform
|
||||||
|
.translation()
|
||||||
|
.distance_squared(item_transform.translation());
|
||||||
|
if task_by_distance
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|(_, closest_task_dist)| dist_squared < *closest_task_dist)
|
||||||
|
{
|
||||||
|
task_by_distance = Some((
|
||||||
|
Task {
|
||||||
|
target: task_entity,
|
||||||
|
work_type: WorkType::Pickup,
|
||||||
|
progress: 0.0,
|
||||||
|
},
|
||||||
|
dist_squared,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((task, _)) = task_by_distance {
|
||||||
|
worker.task = Some(task.clone());
|
||||||
|
add_to_task_map(&mut target_tasks_map, task);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
for (stockpile_entity, stockpile_inventory, stockpile_transform) in store_query.iter() {
|
for (stockpile_entity, stockpile_inventory, stockpile_transform) in store_query.iter() {
|
||||||
for worker_stack in worker_inventory.items.iter() {
|
for worker_stack in worker_inventory.items.iter() {
|
||||||
if let Some((_, _)) =
|
if worker_inventory
|
||||||
worker_inventory.try_transfer(stockpile_inventory, worker_stack)
|
.try_transfer(stockpile_inventory, *worker_stack)
|
||||||
|
.is_some()
|
||||||
{
|
{
|
||||||
let stockpile_dist_squared = worker_transform
|
let dist_squared = worker_transform
|
||||||
.translation()
|
.translation()
|
||||||
.distance_squared(stockpile_transform.translation());
|
.distance_squared(stockpile_transform.translation());
|
||||||
if task_by_distance.is_none_or(|(_, closest_task_dist)| {
|
if task_by_distance
|
||||||
stockpile_dist_squared < closest_task_dist
|
.as_ref()
|
||||||
}) {
|
.is_none_or(|(_, closest_task_dist)| dist_squared < *closest_task_dist)
|
||||||
|
{
|
||||||
task_by_distance = Some((
|
task_by_distance = Some((
|
||||||
Task {
|
Task {
|
||||||
target: stockpile_entity,
|
target: stockpile_entity,
|
||||||
work_type: WorkType::Store(*worker_stack),
|
work_type: WorkType::Store(*worker_stack),
|
||||||
progress: 0.0,
|
progress: 0.0,
|
||||||
},
|
},
|
||||||
stockpile_dist_squared,
|
dist_squared,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -46,31 +106,174 @@ pub fn work_select(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((task, _)) = task_by_distance {
|
if let Some((task, _)) = task_by_distance {
|
||||||
worker.0 = Some(task);
|
worker.task = Some(task.clone());
|
||||||
|
add_to_task_map(&mut target_tasks_map, task);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (item_source_entity, item_source, item_source_transform) in gather_query.iter() {
|
for (item_source_entity, item_source_transform) in gather_query.iter() {
|
||||||
if worker_inventory.try_insert(&item_source.0).is_some() {
|
let dist_squared = worker_transform
|
||||||
let source_dist_squared = worker_transform
|
.translation()
|
||||||
.translation()
|
.distance_squared(item_source_transform.translation());
|
||||||
.distance_squared(item_source_transform.translation());
|
if task_by_distance
|
||||||
if task_by_distance
|
.as_ref()
|
||||||
.is_none_or(|(_, closest_task_dist)| source_dist_squared < closest_task_dist)
|
.is_none_or(|(_, closest_task_dist)| dist_squared < *closest_task_dist)
|
||||||
{
|
{
|
||||||
task_by_distance = Some((
|
task_by_distance = Some((
|
||||||
Task {
|
Task {
|
||||||
target: item_source_entity,
|
target: item_source_entity,
|
||||||
work_type: WorkType::Gather,
|
work_type: WorkType::Gather,
|
||||||
progress: 0.0,
|
progress: 0.0,
|
||||||
},
|
},
|
||||||
source_dist_squared,
|
dist_squared,
|
||||||
))
|
))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
worker.0 = task_by_distance.map(|(task, _)| task);
|
// worker.task = task_by_distance.map(|(task, _)| task.clone());
|
||||||
|
if let Some((task, _)) = task_by_distance {
|
||||||
|
worker.task = Some(task.clone());
|
||||||
|
add_to_task_map(&mut target_tasks_map, task);
|
||||||
|
} else {
|
||||||
|
worker.task = None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn do_work(
|
||||||
|
mut worker_query: Query<(Entity, &mut Worker)>,
|
||||||
|
mut on_task_finish: EventWriter<OnTaskFinish>,
|
||||||
|
global_query: Query<&GlobalTransform>,
|
||||||
|
duration_query: Query<&WorkDuration>,
|
||||||
|
time: Res<Time>,
|
||||||
|
) {
|
||||||
|
for (entity, mut worker) in worker_query.iter_mut() {
|
||||||
|
// Skip if worker already has a job
|
||||||
|
let task = match worker.task.as_mut() {
|
||||||
|
Some(task) => task,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DISTANCE_THRESHOLD_SQUARED: f32 = 32.0 * 32.0;
|
||||||
|
match (global_query.get(entity), global_query.get(task.target)) {
|
||||||
|
(Ok(a), Ok(b)) => {
|
||||||
|
if a.translation().xy().distance_squared(b.translation().xy())
|
||||||
|
> DISTANCE_THRESHOLD_SQUARED
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(worker_global, target_global) => {
|
||||||
|
warn!("Either worker or target doesn't have a GlobalTransform component! Stopping the task. worker: {worker_global:?} target: {target_global:?}");
|
||||||
|
worker.task = None;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let finished_tasks = match duration_query.get(task.target) {
|
||||||
|
Ok(duration) => task.add_progress(duration, time.delta_secs()),
|
||||||
|
Err(_) => 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
if finished_tasks == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let task = worker.task.take().unwrap();
|
||||||
|
for _ in 0..finished_tasks {
|
||||||
|
debug!("{entity} task done: {task:?}");
|
||||||
|
on_task_finish.send(OnTaskFinish {
|
||||||
|
task: task.clone(),
|
||||||
|
worker: entity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_task_result(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut spawn_item: EventWriter<SpawnItem>,
|
||||||
|
mut on_task_finish: EventReader<OnTaskFinish>,
|
||||||
|
mut inventory_query: Query<&mut Inventory>,
|
||||||
|
item_query: Query<(Entity, &Item)>,
|
||||||
|
gather_query: Query<(&ItemSource, &GlobalTransform)>,
|
||||||
|
) {
|
||||||
|
for OnTaskFinish { worker, task } in on_task_finish.read() {
|
||||||
|
let target = task.target;
|
||||||
|
match task.work_type {
|
||||||
|
WorkType::Pickup => {
|
||||||
|
let mut inventory = match inventory_query.get_mut(*worker) {
|
||||||
|
Ok(inventory) => inventory,
|
||||||
|
Err(err) => {
|
||||||
|
error!("Pickup: can't get Inventory component from {worker}: {err:?}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let (item_entity, item) = match item_query.get(target) {
|
||||||
|
Ok(item) => item,
|
||||||
|
Err(err) => {
|
||||||
|
error!("Pickup: can't get Item component from {target}: {err:?}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match inventory.try_insert(ItemStack::from(*item)) {
|
||||||
|
Some(insert) => {
|
||||||
|
inventory
|
||||||
|
.apply_insert(insert)
|
||||||
|
.expect("Worker picks up an item");
|
||||||
|
commands
|
||||||
|
.get_entity(item_entity)
|
||||||
|
.expect("Getting item entity to despawn it")
|
||||||
|
.despawn_recursive();
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Not fatal, something could have filled the inventory on the way.
|
||||||
|
warn!("Worker {worker} could not pickup {item:?} possibly due to full inventory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WorkType::Gather => {
|
||||||
|
let (item_source, source_transform) = match gather_query.get(target) {
|
||||||
|
Ok(item_source) => item_source,
|
||||||
|
Err(err) => {
|
||||||
|
error!("Gather: can't get ItemSource component from {target}: {err:?}",);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for stack in item_source.drops.iter() {
|
||||||
|
for _ in 0..stack.count {
|
||||||
|
spawn_item.send(SpawnItem {
|
||||||
|
item: stack.item,
|
||||||
|
to: source_transform.translation().xy(),
|
||||||
|
velocity: random_vec2(250.0, 250.0),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WorkType::Store(stack) => {
|
||||||
|
let [mut worker_inventory, mut stockpile_inventory] =
|
||||||
|
match inventory_query.get_many_mut([*worker, target]) {
|
||||||
|
Ok(inventory) => inventory,
|
||||||
|
Err(err) => {
|
||||||
|
error!("Store: can't get Inventory components: {err:?}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match worker_inventory.try_transfer(&stockpile_inventory, stack) {
|
||||||
|
Some((remove, insert)) => {
|
||||||
|
worker_inventory
|
||||||
|
.apply_remove(remove)
|
||||||
|
.expect("Worker stores an item");
|
||||||
|
stockpile_inventory
|
||||||
|
.apply_insert(insert)
|
||||||
|
.expect("Stockpile takes an item");
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
warn!("Worker {worker} could not store {stack:?} into {target}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,44 +284,39 @@ pub fn draw_job_targets(
|
||||||
global_query: Query<&GlobalTransform>,
|
global_query: Query<&GlobalTransform>,
|
||||||
) {
|
) {
|
||||||
for (worker_entity, worker) in worker_query.iter() {
|
for (worker_entity, worker) in worker_query.iter() {
|
||||||
let worker_global = global_query.get(worker_entity).unwrap();
|
let worker_global = match global_query.get(worker_entity) {
|
||||||
let colored_shapes = match worker.0 {
|
Ok(global) => global,
|
||||||
Some(task) => match task.work_type {
|
Err(_) => continue,
|
||||||
WorkType::Gather => vec![
|
};
|
||||||
|
let colored_shapes = match worker.task.as_ref() {
|
||||||
|
Some(task) => {
|
||||||
|
let target_global = match global_query.get(task.target) {
|
||||||
|
Ok(global) => global,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
let color = match task.work_type {
|
||||||
|
WorkType::Pickup => css::AQUA,
|
||||||
|
WorkType::Gather => css::GREEN,
|
||||||
|
WorkType::Store(_) => css::YELLOW,
|
||||||
|
};
|
||||||
|
vec![
|
||||||
ColoredShape {
|
ColoredShape {
|
||||||
shape: Shape::Polygon {
|
shape: Shape::Polygon {
|
||||||
center: worker_global.translation().xy(),
|
center: worker_global.translation().xy(),
|
||||||
sides: 3,
|
sides: 3,
|
||||||
radius: 16.,
|
radius: 16.,
|
||||||
},
|
},
|
||||||
color: css::GREEN,
|
color,
|
||||||
},
|
},
|
||||||
ColoredShape {
|
ColoredShape {
|
||||||
shape: Shape::Line {
|
shape: Shape::Line {
|
||||||
from: worker_global.translation().xy(),
|
from: worker_global.translation().xy(),
|
||||||
to: global_query.get(task.target).unwrap().translation().xy(),
|
to: target_global.translation().xy(),
|
||||||
},
|
},
|
||||||
color: css::GREEN,
|
color,
|
||||||
},
|
},
|
||||||
],
|
]
|
||||||
WorkType::Store(_) => vec![
|
}
|
||||||
ColoredShape {
|
|
||||||
shape: Shape::Polygon {
|
|
||||||
center: worker_global.translation().xy(),
|
|
||||||
sides: 3,
|
|
||||||
radius: 16.,
|
|
||||||
},
|
|
||||||
color: css::YELLOW,
|
|
||||||
},
|
|
||||||
ColoredShape {
|
|
||||||
shape: Shape::Line {
|
|
||||||
from: worker_global.translation().xy(),
|
|
||||||
to: global_query.get(task.target).unwrap().translation().xy(),
|
|
||||||
},
|
|
||||||
color: css::YELLOW,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
None => vec![ColoredShape {
|
None => vec![ColoredShape {
|
||||||
shape: Shape::Polygon {
|
shape: Shape::Polygon {
|
||||||
center: worker_global.translation().xy(),
|
center: worker_global.translation().xy(),
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use crate::util::Seconds;
|
||||||
use super::item::ItemStack;
|
use super::item::ItemStack;
|
||||||
|
|
||||||
/// Total time it takes to finish a task
|
/// Total time it takes to finish a task
|
||||||
#[derive(Copy, Clone, Debug, Default, Component, Reflect)]
|
#[derive(Clone, Debug, Default, Component, Reflect)]
|
||||||
#[reflect(Component)]
|
#[reflect(Component)]
|
||||||
pub struct WorkDuration(pub Seconds);
|
pub struct WorkDuration(pub Seconds);
|
||||||
|
|
||||||
|
|
@ -20,13 +20,14 @@ impl WorkDuration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Reflect)]
|
#[derive(Clone, Debug, Reflect)]
|
||||||
pub enum WorkType {
|
pub enum WorkType {
|
||||||
|
Pickup,
|
||||||
Gather,
|
Gather,
|
||||||
Store(ItemStack),
|
Store(ItemStack),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Reflect)]
|
#[derive(Clone, Debug, Reflect)]
|
||||||
pub struct Task {
|
pub struct Task {
|
||||||
pub progress: Seconds,
|
pub progress: Seconds,
|
||||||
pub work_type: WorkType,
|
pub work_type: WorkType,
|
||||||
|
|
@ -36,12 +37,25 @@ pub struct Task {
|
||||||
impl Task {
|
impl Task {
|
||||||
pub fn add_progress(&mut self, work_duration: &WorkDuration, progress: Seconds) -> u32 {
|
pub fn add_progress(&mut self, work_duration: &WorkDuration, progress: Seconds) -> u32 {
|
||||||
match work_duration.safe_duration() {
|
match work_duration.safe_duration() {
|
||||||
Some(duration) => (self.progress + progress).div_euclid(duration).max(0.0) as u32,
|
Some(duration) => {
|
||||||
|
self.progress += progress;
|
||||||
|
let times_completed = self.progress.div_euclid(duration).max(0.0) as u32;
|
||||||
|
self.progress = self.progress.rem_euclid(duration);
|
||||||
|
times_completed
|
||||||
|
}
|
||||||
None => 1,
|
None => 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, Component, Reflect)]
|
#[derive(Event, Clone, Debug)]
|
||||||
|
pub struct OnTaskFinish {
|
||||||
|
pub worker: Entity,
|
||||||
|
pub task: Task,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Component, Reflect)]
|
||||||
#[reflect(Component)]
|
#[reflect(Component)]
|
||||||
pub struct Worker(pub Option<Task>);
|
pub struct Worker {
|
||||||
|
pub task: Option<Task>,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_mod_debugdump::{render_graph, render_graph_dot};
|
use bevy_mod_debugdump::{render_graph, render_graph_dot};
|
||||||
use core::{fmt, ops};
|
use core::{fmt, ops};
|
||||||
use std::{fs, path::Path};
|
use std::{f32::consts::PI, fs, path::Path};
|
||||||
|
|
||||||
mod basis;
|
mod basis;
|
||||||
mod plugin;
|
mod plugin;
|
||||||
|
|
@ -153,6 +153,10 @@ pub fn loop_value(from: f32, to: f32, value: f32) -> f32 {
|
||||||
value - inverse_lerp(from, to, value).floor() * range
|
value - inverse_lerp(from, to, value).floor() * range
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn random_vec2(min_length: f32, max_length: f32) -> Vec2 {
|
||||||
|
Vec2::from_angle(fastrand::f32() * PI * 2.0) * lerp(min_length, max_length, fastrand::f32())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn create_app_graphs(app: &mut App) {
|
pub fn create_app_graphs(app: &mut App) {
|
||||||
// TODO: Figure out how to list schedules under the new interned ScheduleLabel system
|
// TODO: Figure out how to list schedules under the new interned ScheduleLabel system
|
||||||
println!("Writing render graph");
|
println!("Writing render graph");
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
pub type Seconds = f32;
|
pub type Seconds = f32;
|
||||||
|
pub type Kilograms = f32;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue