Added weight slowdown for workers and unit tests for inventory & slowdown

master
hheik 2025-04-09 16:07:19 +03:00
parent d9319bfd5c
commit f422d0afe4
10 changed files with 337 additions and 32 deletions

7
Cargo.lock generated
View File

@ -230,6 +230,12 @@ 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"
@ -2419,6 +2425,7 @@ 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",

View File

@ -14,6 +14,9 @@ bevy_rapier2d = "0.28.0"
fastrand = "2.3.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.

BIN
assets/sprites/stone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

View File

@ -1,7 +1,12 @@
use bevy::prelude::*; use bevy::prelude::*;
#[derive(Copy, Clone, Debug, Component, Reflect)] use crate::util::Kilograms;
use super::item::Inventory;
#[derive(Clone, Debug, Component, Reflect)]
#[reflect(Component)] #[reflect(Component)]
#[require(Transform)]
pub struct Mover { pub struct Mover {
pub speed: f32, pub speed: f32,
} }
@ -11,3 +16,47 @@ impl Default for Mover {
Self { speed: 100.0 } 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

@ -7,25 +7,19 @@ use crate::util::Kilograms;
#[require(Sprite)] #[require(Sprite)]
pub enum Item { pub enum Item {
Wood, Wood,
Stone,
} }
impl Item { impl Item {
// TODO: implement
fn stack_max_size(&self) -> Option<u32> {
match self {
_ => Some(100),
}
}
// TODO: implement
fn weight(&self) -> Kilograms { fn weight(&self) -> Kilograms {
match self { match self {
Self::Wood => 1.0, // Paper birch, height 30cm, diameter 18cm ~= 6 kg, chopped in 6 pieces ~= 1kg 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)] #[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,
@ -46,7 +40,7 @@ pub struct ItemSource {
pub gather_limit: Option<u32>, pub gather_limit: Option<u32>,
} }
#[derive(Clone, Debug, Default, Component, Reflect)] #[derive(Clone, Debug, Default, Component, Reflect, PartialEq, Eq)]
#[reflect(Component)] #[reflect(Component)]
pub struct Inventory { pub struct Inventory {
pub capacity: Option<usize>, pub capacity: Option<usize>,
@ -55,7 +49,7 @@ pub struct Inventory {
pub items: Vec<ItemStack>, pub items: Vec<ItemStack>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum InventoryError { pub enum InventoryError {
IndexNotFound { IndexNotFound {
index: usize, index: usize,
@ -67,7 +61,7 @@ pub enum InventoryError {
/// Result for trying to insert an item to inventory. Should be used and discarded immediately, as /// 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 /// 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! /// the valid inventory state that it aplies to!
#[derive(Debug)] #[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 { index: usize, new_count: u32 },
@ -75,7 +69,7 @@ pub enum InsertResult {
Push { stack: ItemStack }, Push { stack: ItemStack },
} }
#[derive(Debug)] #[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 { index: usize },
@ -228,6 +222,8 @@ 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",
}); });
} }
} }
@ -238,3 +234,253 @@ pub struct SpawnItem {
pub to: Vec2, pub to: Vec2,
pub velocity: 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,6 @@
use bevy::{prelude::*, sprite::Anchor}; use bevy::{prelude::*, sprite::Anchor};
use bevy_rapier2d::prelude::*; use bevy_rapier2d::prelude::*;
use creature::Mover; 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,6 +18,7 @@ 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 }),
RigidBody(|| RigidBody::KinematicPositionBased), RigidBody(|| RigidBody::KinematicPositionBased),
Mover, Mover,
)] )]
@ -32,7 +33,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: 1 }], gather_limit: None }), ItemSource(|| ItemSource { drops: vec![ItemStack { item: Item::Wood, count: 50 }], gather_limit: Some(1) }),
WorkDuration(|| WorkDuration(5.0)) WorkDuration(|| WorkDuration(5.0))
)] )]
pub struct Tree; pub struct Tree;
@ -46,7 +47,7 @@ pub struct Tree;
..default() ..default()
}), }),
SpriteLoader(|| SpriteLoader::from("sprites/box.png")), SpriteLoader(|| SpriteLoader::from("sprites/box.png")),
Inventory(|| Inventory::with_capacity(25)), Inventory,
Stockpile, Stockpile,
)] )]
pub struct Chest; pub struct Chest;

View File

@ -1,16 +1,18 @@
use bevy::prelude::*; use bevy::prelude::*;
use crate::{ use crate::game::{
game::{creature::Mover, item::Inventory, work::Worker}, creature::{Mover, WeightSlowdown},
util::{inverse_lerp, lerp}, item::Inventory,
work::Worker,
}; };
pub fn worker_movement( pub fn worker_movement(
mut worker_query: Query<(Entity, &Mover, &Worker, &mut Transform, Option<&Inventory>)>, mut worker_query: Query<(Entity, &Mover, &Worker, &mut Transform)>,
slowdown_query: Query<(&Inventory, &WeightSlowdown)>,
global_query: Query<&GlobalTransform>, global_query: Query<&GlobalTransform>,
time: Res<Time>, time: Res<Time>,
) { ) {
for (entity, mover, worker, mut transform, inventory) in worker_query.iter_mut() { for (entity, mover, worker, mut transform) in worker_query.iter_mut() {
let Some(task) = worker.task.as_ref() else { let Some(task) = worker.task.as_ref() else {
continue; continue;
}; };
@ -20,16 +22,12 @@ pub fn worker_movement(
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.) let weight_mult = slowdown_query
* mover.speed .get(entity)
* inventory.map_or(1.0, |inventory| { .map_or(1.0, |(inventory, slowdown)| {
lerp( slowdown.multiplier(inventory.weight())
1.0, });
0.2, let movement = dir.extend(0.) * mover.speed * weight_mult * time.delta_secs();
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);
} }
} }

View File

@ -29,6 +29,7 @@ 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));
} }