Compare commits

..

No commits in common. "master" and "feature/debug_draw" have entirely different histories.

18 changed files with 97 additions and 820 deletions

8
Cargo.lock generated
View File

@ -230,12 +230,6 @@ dependencies = [
"libloading", "libloading",
] ]
[[package]]
name = "assert_float_eq"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10d2119f741b79fe9907f5396d19bffcb46568cfcc315e78677d731972ac7085"
[[package]] [[package]]
name = "assert_type_match" name = "assert_type_match"
version = "0.1.1" version = "0.1.1"
@ -2425,13 +2419,11 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
name = "glorbs" name = "glorbs"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"assert_float_eq",
"bevy", "bevy",
"bevy-inspector-egui", "bevy-inspector-egui",
"bevy_mod_debugdump", "bevy_mod_debugdump",
"bevy_prototype_lyon", "bevy_prototype_lyon",
"bevy_rapier2d", "bevy_rapier2d",
"fastrand",
"num-traits", "num-traits",
] ]

View File

@ -11,12 +11,8 @@ 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"
[dev-dependencies]
assert_float_eq = "1.1.4"
# Enable a small amount of optimization in debug mode # Enable a small amount of optimization in debug mode
[profile.dev] [profile.dev]
opt-level = 1 opt-level = 1

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

View File

@ -8,18 +8,15 @@ pub struct DebugSet;
impl Plugin for DebugPlugin { impl Plugin for DebugPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.configure_sets(First, DebugSet.run_if(is_debug_enabled)) app.configure_sets(Last, DebugSet.run_if(is_debug_enabled))
.configure_sets(PreUpdate, DebugSet.run_if(is_debug_enabled)) .configure_sets(PostUpdate, DebugSet.run_if(is_debug_enabled));
.configure_sets(Update, DebugSet.run_if(is_debug_enabled))
.configure_sets(PostUpdate, DebugSet.run_if(is_debug_enabled))
.configure_sets(Last, DebugSet.run_if(is_debug_enabled));
app.insert_resource(DebugMode::off()) app.insert_resource(DebugMode::off())
.insert_resource(DebugDraw::default()); .insert_resource(DebugDraw::default());
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>()

View File

@ -3,7 +3,6 @@ use bevy::prelude::*;
use bevy_prototype_lyon::prelude::*; use bevy_prototype_lyon::prelude::*;
use bevy_rapier2d::prelude::*; use bevy_rapier2d::prelude::*;
mod creature;
mod item; mod item;
mod prefab; mod prefab;
mod systems; mod systems;
@ -23,26 +22,10 @@ pub fn init(app: &mut App) {
.register_type::<item::Inventory>() .register_type::<item::Inventory>()
.register_type::<work::WorkDuration>() .register_type::<work::WorkDuration>()
.register_type::<work::Worker>() .register_type::<work::Worker>()
.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, (systems::demo_2d, systems::work_select))
Update,
(
(
systems::demo_2d,
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));
} }

View File

@ -1,62 +0,0 @@
use bevy::prelude::*;
use crate::util::Kilograms;
use super::item::Inventory;
#[derive(Clone, Debug, Component, Reflect)]
#[reflect(Component)]
#[require(Transform)]
pub struct Mover {
pub speed: f32,
}
impl Default for Mover {
fn default() -> Self {
Self { speed: 100.0 }
}
}
#[derive(Clone, Debug, Component, Reflect)]
#[reflect(Component)]
#[require(Inventory)]
pub struct WeightSlowdown {
/// At what weight should speed be halved.
///
/// Should NOT be zero!
pub halfpoint: Kilograms,
}
impl Default for WeightSlowdown {
fn default() -> Self {
Self { halfpoint: 50.0 }
}
}
impl WeightSlowdown {
pub fn multiplier(&self, weight: Kilograms) -> f32 {
1.0 / (weight.max(0.0) / self.halfpoint + 1.0)
}
}
#[cfg(test)]
mod tests {
use super::WeightSlowdown;
use assert_float_eq::assert_f32_near;
#[test]
fn slowdown_calculates_correctly() {
let slowdown = WeightSlowdown { halfpoint: 10.0 };
assert_f32_near!(slowdown.multiplier(0.0), 1.0);
assert_f32_near!(slowdown.multiplier(5.0), 2.0 / 3.0);
assert_f32_near!(slowdown.multiplier(10.0), 1.0 / 2.0);
assert_f32_near!(slowdown.multiplier(20.0), 1.0 / 3.0);
assert_f32_near!(slowdown.multiplier(100.0), 1.0 / 11.0);
}
#[test]
fn negative_weight_acts_as_zero_weight() {
let slowdown = WeightSlowdown { halfpoint: 10.0 };
assert_f32_near!(slowdown.multiplier(-10.0), 1.0);
}
}

View File

@ -1,80 +1,50 @@
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)]
pub enum Item { pub enum Item {
Wood, Wood,
Stone,
} }
impl Item { #[derive(Copy, Clone, Debug, Reflect)]
fn weight(&self) -> Kilograms {
match self {
Self::Wood => 1.0, // Paper birch, height 30cm, diameter 18cm ~= 6 kg, chopped in 6 pieces ~= 1kg
Self::Stone => 10.0, // Just a 10kg piece of rock
}
}
}
#[derive(Copy, Clone, Debug, Reflect, PartialEq, Eq)]
pub struct ItemStack { pub struct ItemStack {
pub item: Item, pub item: Item,
pub count: u32, pub count: u32,
} }
impl From<Item> for ItemStack { impl Default for ItemStack {
fn from(item: Item) -> Self { fn default() -> Self {
Self { item, count: 1 } Self {
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 struct ItemSource(pub ItemStack);
/// 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, PartialEq, Eq)] #[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>,
} }
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InventoryError {
IndexNotFound {
index: usize,
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, PartialEq, Eq)]
pub enum InsertResult { pub enum InsertResult {
/// Combine to an existing stack. Contains (`index`, `new_count`) /// Combine to an existing stack. Contains (`index`, `new_count`)
Combine { index: usize, new_count: u32 }, Combine(usize, u32),
/// No existing stackable stacks, push a new one. /// No existing stackable stacks, push a new one.
Push { stack: ItemStack }, Push(ItemStack),
} }
#[derive(Debug, PartialEq, Eq)]
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 { index: usize }, Empty(usize),
/// The stack will get only partially depleted. Contains (`index`, `new_count`) /// The stack will get only partially depleted. Contains (`index`, `new_count`)
Partial { index: usize, new_count: u32 }, Partial(usize, u32),
} }
impl Inventory { impl Inventory {
@ -85,30 +55,26 @@ impl Inventory {
} }
} }
pub fn weight(&self) -> Kilograms { pub fn is_empty(&self) -> bool {
self.items self.items.iter().all(|stack| stack.count == 0)
.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(InsertResult::Combine { Some((index, stack)) => {
index, Some(InsertResult::Combine(index, stack.count + item_stack.count))
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 { stack: item_stack }) Some(InsertResult::Push(*item_stack))
} else { } else {
None None
} }
@ -116,7 +82,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()
@ -126,9 +92,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
@ -142,7 +108,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)),
@ -150,58 +116,6 @@ 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);
@ -222,265 +136,6 @@ pub fn update_item_sprite(
for (mut sprite, item) in query.iter_mut() { for (mut sprite, item) in query.iter_mut() {
sprite.image = assets.load(match item { sprite.image = assets.load(match item {
Item::Wood => "sprites/wood.png", Item::Wood => "sprites/wood.png",
Item::Stone => "sprites/stone.png",
// _ => "sprites/missing.png",
}); });
} }
} }
#[derive(Event, Clone, Debug)]
pub struct SpawnItem {
pub item: Item,
pub to: Vec2,
pub velocity: Vec2,
}
#[cfg(test)]
mod tests {
use super::*;
use assert_float_eq::assert_f32_near;
#[test]
fn inventory_weight_calculation() {
let wood_weight = Item::Wood.weight();
let stone_weight = Item::Stone.weight();
let inventory = Inventory {
items: vec![
ItemStack {
item: Item::Wood,
count: 1,
},
ItemStack {
item: Item::Stone,
count: 5,
},
ItemStack {
item: Item::Wood,
count: 0,
},
],
..Default::default()
};
assert_f32_near!(inventory.weight(), wood_weight + stone_weight * 5.0);
}
#[test]
fn inventory_push_result_ok() {
let inventory = Inventory {
items: vec![ItemStack {
item: Item::Stone,
count: 1,
}],
..Default::default()
};
let stack = ItemStack {
item: Item::Wood,
count: 2,
};
assert_eq!(
inventory.try_insert(stack),
Some(InsertResult::Push { stack })
);
}
#[test]
fn inventory_combine_result_ok() {
let inventory = Inventory {
items: vec![
ItemStack {
item: Item::Stone,
count: 1,
},
ItemStack {
item: Item::Wood,
count: 5,
},
],
..Default::default()
};
let stack = ItemStack {
item: Item::Wood,
count: 1,
};
assert_eq!(
inventory.try_insert(stack),
Some(InsertResult::Combine {
index: 1,
new_count: 6
})
);
}
#[test]
fn inventory_empty_result() {
let inventory = Inventory {
items: vec![
ItemStack {
item: Item::Stone,
count: 1,
},
ItemStack {
item: Item::Wood,
count: 5,
},
],
..Default::default()
};
assert_eq!(
inventory.try_remove(ItemStack {
item: Item::Wood,
count: 5
}),
Some(RemoveResult::Empty { index: 1 })
);
}
#[test]
fn inventory_partial_remove_result() {
let inventory = Inventory {
items: vec![
ItemStack {
item: Item::Stone,
count: 1,
},
ItemStack {
item: Item::Wood,
count: 5,
},
],
..Default::default()
};
assert_eq!(
inventory.try_remove(ItemStack {
item: Item::Wood,
count: 1
}),
Some(RemoveResult::Partial {
index: 1,
new_count: 4
})
);
}
#[test]
fn inventory_invalid_transfer_results() {
let inventory = Inventory {
items: vec![ItemStack {
item: Item::Stone,
count: 1,
}],
capacity: Some(1),
};
let wood_stack = ItemStack {
item: Item::Wood,
count: 5,
};
assert_eq!(inventory.try_insert(wood_stack), None);
assert_eq!(inventory.try_remove(wood_stack), None);
let inventory = Inventory {
items: vec![ItemStack {
item: Item::Wood,
count: 1,
}],
capacity: None,
};
assert_eq!(inventory.try_remove(wood_stack), None);
}
#[test]
fn inventory_insert_applies_ok() {
let stack_a = ItemStack {
item: Item::Stone,
count: 1,
};
let stack_b = ItemStack {
item: Item::Wood,
count: 2,
};
let mut inventory = Inventory {
items: vec![stack_a],
..Default::default()
};
assert_eq!(
inventory.apply_insert(InsertResult::Push { stack: stack_b }),
Ok(())
);
assert_eq!(
inventory,
Inventory {
items: vec![stack_a, stack_b],
..Default::default()
}
);
assert_eq!(
inventory.apply_insert(InsertResult::Combine {
index: 1,
new_count: 4
}),
Ok(())
);
assert_eq!(
inventory,
Inventory {
items: vec![
stack_a,
ItemStack {
item: Item::Wood,
count: 4
}
],
capacity: None
}
);
}
#[test]
fn inventory_remove_applies_ok() {
let stack_a = ItemStack {
item: Item::Stone,
count: 5,
};
let stack_b = ItemStack {
item: Item::Wood,
count: 2,
};
let mut inventory = Inventory {
items: vec![stack_a, stack_b],
..Default::default()
};
assert_eq!(
inventory.apply_remove(RemoveResult::Partial {
index: 0,
new_count: 2
}),
Ok(())
);
assert_eq!(
inventory,
Inventory {
items: vec![
ItemStack {
item: Item::Stone,
count: 2
},
stack_b
],
..Default::default()
}
);
assert_eq!(
inventory.apply_remove(RemoveResult::Empty { index: 0 }),
Ok(())
);
assert_eq!(
inventory,
Inventory {
items: vec![stack_b],
..Default::default()
}
);
}
}

View File

@ -1,6 +1,5 @@
use bevy::{prelude::*, sprite::Anchor}; use bevy::{prelude::*, sprite::Anchor};
use bevy_rapier2d::prelude::*; use bevy_rapier2d::prelude::*;
use creature::{Mover, WeightSlowdown};
use item::{Inventory, Item, ItemSource, ItemStack, Stockpile}; use item::{Inventory, Item, ItemSource, ItemStack, Stockpile};
use work::{WorkDuration, Worker}; use work::{WorkDuration, Worker};
@ -18,9 +17,8 @@ use crate::util::SpriteLoader;
SpriteLoader(|| SpriteLoader::from("sprites/glorb.png")), SpriteLoader(|| SpriteLoader::from("sprites/glorb.png")),
Worker, Worker,
Inventory(|| Inventory::with_capacity(1)), Inventory(|| Inventory::with_capacity(1)),
WeightSlowdown(|| WeightSlowdown { halfpoint: 50.0 }), Velocity,
RigidBody(|| RigidBody::KinematicPositionBased), RigidBody(|| RigidBody::KinematicVelocityBased),
Mover,
)] )]
pub struct Glorb; pub struct Glorb;
@ -33,7 +31,7 @@ pub struct Glorb;
..default() ..default()
}), }),
SpriteLoader(|| SpriteLoader::from("sprites/tree.png")), SpriteLoader(|| SpriteLoader::from("sprites/tree.png")),
ItemSource(|| ItemSource { drops: vec![ItemStack { item: Item::Wood, count: 50 }], gather_limit: Some(1) }), ItemSource(|| ItemSource(ItemStack { item: Item::Wood, count: 1 })),
WorkDuration(|| WorkDuration(5.0)) WorkDuration(|| WorkDuration(5.0))
)] )]
pub struct Tree; pub struct Tree;
@ -47,7 +45,7 @@ pub struct Tree;
..default() ..default()
}), }),
SpriteLoader(|| SpriteLoader::from("sprites/box.png")), SpriteLoader(|| SpriteLoader::from("sprites/box.png")),
Inventory, Inventory(|| Inventory::with_capacity(25)),
Stockpile, Stockpile,
)] )]
pub struct Chest; pub struct Chest;

View File

@ -1,9 +1,5 @@
mod creature;
mod demo; mod demo;
mod item;
mod worker; mod worker;
pub use creature::*;
pub use demo::*; pub use demo::*;
pub use item::*;
pub use worker::*; pub use worker::*;

View File

@ -1,33 +0,0 @@
use bevy::prelude::*;
use crate::game::{
creature::{Mover, WeightSlowdown},
item::Inventory,
work::Worker,
};
pub fn worker_movement(
mut worker_query: Query<(Entity, &Mover, &Worker, &mut Transform)>,
slowdown_query: Query<(&Inventory, &WeightSlowdown)>,
global_query: Query<&GlobalTransform>,
time: Res<Time>,
) {
for (entity, mover, worker, mut transform) in worker_query.iter_mut() {
let Some(task) = worker.task.as_ref() else {
continue;
};
let (Ok(from), Ok(to)) = (global_query.get(entity), global_query.get(task.target)) else {
continue;
};
let diff = (to.translation() - from.translation()).xy();
let dist = diff.length().max(0.);
let dir = diff.normalize_or_zero();
let weight_mult = slowdown_query
.get(entity)
.map_or(1.0, |(inventory, slowdown)| {
slowdown.multiplier(inventory.weight())
});
let movement = dir.extend(0.) * mover.speed * weight_mult * time.delta_secs();
transform.translation += movement.clamp_length_max(dist);
}
}

View File

@ -29,7 +29,6 @@ pub fn setup_2d(mut commands: Commands) {
commands.spawn((Transform::from_xyz(200.0, 0.0, 0.0), prefab::Tree)); commands.spawn((Transform::from_xyz(200.0, 0.0, 0.0), prefab::Tree));
// commands.spawn((Name::from("Wood"), Item::Wood)); // commands.spawn((Name::from("Wood"), Item::Wood));
commands.spawn((Name::from("Stone"), Item::Stone));
commands.spawn((Transform::from_xyz(-200.0, -150.0, 0.0), prefab::Chest)); commands.spawn((Transform::from_xyz(-200.0, -150.0, 0.0), prefab::Chest));
} }

View File

@ -1,27 +0,0 @@
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()
},
));
}
}

View File

@ -1,104 +1,44 @@
use bevy::{color::palettes::css, prelude::*, utils::HashMap}; use bevy::{color::palettes::css, prelude::*};
use crate::{ use crate::{
debug::{ColoredShape, DebugDraw, Shape}, debug::{ColoredShape, DebugDraw, Shape},
game::{ game::{
item::{Inventory, Item, ItemSource, ItemStack, SpawnItem, Stockpile}, item::{Inventory, ItemSource, Stockpile},
work::{OnTaskFinish, Task, WorkDuration, WorkType, Worker}, work::{Task, WorkType, Worker},
}, },
util::random_vec2,
}; };
pub fn work_select( pub fn work_select(
workers: Query<(Entity, &Inventory, &GlobalTransform), With<Worker>>, mut worker_query: Query<(&mut Worker, &Inventory, &GlobalTransform)>,
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, &GlobalTransform), With<ItemSource>>, gather_query: Query<(Entity, &ItemSource, &GlobalTransform)>,
) { ) {
// What tasks are already targeting given entity for (mut worker, worker_inventory, worker_transform) in worker_query.iter_mut() {
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.task.is_some() { if worker.0.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 worker_inventory if let Some((_, _)) =
.try_transfer(stockpile_inventory, *worker_stack) worker_inventory.try_transfer(stockpile_inventory, worker_stack)
.is_some()
{ {
let dist_squared = worker_transform let stockpile_dist_squared = worker_transform
.translation() .translation()
.distance_squared(stockpile_transform.translation()); .distance_squared(stockpile_transform.translation());
if task_by_distance if task_by_distance.is_none_or(|(_, closest_task_dist)| {
.as_ref() stockpile_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: stockpile_entity, target: stockpile_entity,
work_type: WorkType::Store(*worker_stack), work_type: WorkType::Store(*worker_stack),
progress: 0.0, progress: 0.0,
}, },
dist_squared, stockpile_dist_squared,
)) ))
} }
} }
@ -106,174 +46,31 @@ pub fn work_select(
} }
if let Some((task, _)) = task_by_distance { if let Some((task, _)) = task_by_distance {
worker.task = Some(task.clone()); worker.0 = Some(task);
add_to_task_map(&mut target_tasks_map, task);
continue; continue;
} }
for (item_source_entity, item_source_transform) in gather_query.iter() { for (item_source_entity, item_source, item_source_transform) in gather_query.iter() {
let dist_squared = worker_transform if worker_inventory.try_insert(&item_source.0).is_some() {
.translation() let source_dist_squared = worker_transform
.distance_squared(item_source_transform.translation()); .translation()
if task_by_distance .distance_squared(item_source_transform.translation());
.as_ref() if task_by_distance
.is_none_or(|(_, closest_task_dist)| dist_squared < *closest_task_dist) .is_none_or(|(_, 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,
},
dist_squared,
))
}
}
// 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; task_by_distance = Some((
} Task {
} target: item_source_entity,
(worker_global, target_global) => { work_type: WorkType::Gather,
warn!("Either worker or target doesn't have a GlobalTransform component! Stopping the task. worker: {worker_global:?} target: {target_global:?}"); progress: 0.0,
worker.task = None; },
continue; source_dist_squared,
} ))
}
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}");
}
} }
} }
} }
worker.0 = task_by_distance.map(|(task, _)| task);
} }
} }
@ -284,39 +81,44 @@ 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 = match global_query.get(worker_entity) { let worker_global = global_query.get(worker_entity).unwrap();
Ok(global) => global, let colored_shapes = match worker.0 {
Err(_) => continue, Some(task) => match task.work_type {
}; 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, color: css::GREEN,
}, },
ColoredShape { ColoredShape {
shape: Shape::Line { shape: Shape::Line {
from: worker_global.translation().xy(), from: worker_global.translation().xy(),
to: target_global.translation().xy(), to: global_query.get(task.target).unwrap().translation().xy(),
}, },
color, color: css::GREEN,
}, },
] ],
} 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(),

View File

@ -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(Clone, Debug, Default, Component, Reflect)] #[derive(Copy, Clone, Debug, Default, Component, Reflect)]
#[reflect(Component)] #[reflect(Component)]
pub struct WorkDuration(pub Seconds); pub struct WorkDuration(pub Seconds);
@ -20,14 +20,13 @@ impl WorkDuration {
} }
} }
#[derive(Clone, Debug, Reflect)] #[derive(Copy, Clone, Debug, Reflect)]
pub enum WorkType { pub enum WorkType {
Pickup,
Gather, Gather,
Store(ItemStack), Store(ItemStack),
} }
#[derive(Clone, Debug, Reflect)] #[derive(Copy, Clone, Debug, Reflect)]
pub struct Task { pub struct Task {
pub progress: Seconds, pub progress: Seconds,
pub work_type: WorkType, pub work_type: WorkType,
@ -37,25 +36,12 @@ 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) => { Some(duration) => (self.progress + progress).div_euclid(duration).max(0.0) as u32,
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(Event, Clone, Debug)] #[derive(Copy, Clone, Debug, Default, Component, Reflect)]
pub struct OnTaskFinish {
pub worker: Entity,
pub task: Task,
}
#[derive(Clone, Debug, Default, Component, Reflect)]
#[reflect(Component)] #[reflect(Component)]
pub struct Worker { pub struct Worker(pub Option<Task>);
pub task: Option<Task>,
}

View File

@ -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::{f32::consts::PI, fs, path::Path}; use std::{fs, path::Path};
mod basis; mod basis;
mod plugin; mod plugin;
@ -153,10 +153,6 @@ 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");

View File

@ -1,2 +1 @@
pub type Seconds = f32; pub type Seconds = f32;
pub type Kilograms = f32;