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",
]
[[package]]
name = "assert_float_eq"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10d2119f741b79fe9907f5396d19bffcb46568cfcc315e78677d731972ac7085"
[[package]]
name = "assert_type_match"
version = "0.1.1"
@ -2419,6 +2425,7 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
name = "glorbs"
version = "0.1.0"
dependencies = [
"assert_float_eq",
"bevy",
"bevy-inspector-egui",
"bevy_mod_debugdump",

View File

@ -14,6 +14,9 @@ bevy_rapier2d = "0.28.0"
fastrand = "2.3.0"
num-traits = "0.2.19"
[dev-dependencies]
assert_float_eq = "1.1.4"
# Enable a small amount of optimization in debug mode
[profile.dev]
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::*;
#[derive(Copy, Clone, Debug, Component, Reflect)]
use crate::util::Kilograms;
use super::item::Inventory;
#[derive(Clone, Debug, Component, Reflect)]
#[reflect(Component)]
#[require(Transform)]
pub struct Mover {
pub speed: f32,
}
@ -11,3 +16,47 @@ impl Default for Mover {
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)]
pub enum Item {
Wood,
Stone,
}
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
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 item: Item,
pub count: u32,
@ -46,7 +40,7 @@ pub struct ItemSource {
pub gather_limit: Option<u32>,
}
#[derive(Clone, Debug, Default, Component, Reflect)]
#[derive(Clone, Debug, Default, Component, Reflect, PartialEq, Eq)]
#[reflect(Component)]
pub struct Inventory {
pub capacity: Option<usize>,
@ -55,7 +49,7 @@ pub struct Inventory {
pub items: Vec<ItemStack>,
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InventoryError {
IndexNotFound {
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
/// 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)]
#[derive(Debug, PartialEq, Eq)]
pub enum InsertResult {
/// Combine to an existing stack. Contains (`index`, `new_count`)
Combine { index: usize, new_count: u32 },
@ -75,7 +69,7 @@ pub enum InsertResult {
Push { stack: ItemStack },
}
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq)]
pub enum RemoveResult {
/// The stack will get depleted, so it can be removed. Contains the `index` of empty stack.
Empty { index: usize },
@ -228,6 +222,8 @@ pub fn update_item_sprite(
for (mut sprite, item) in query.iter_mut() {
sprite.image = assets.load(match item {
Item::Wood => "sprites/wood.png",
Item::Stone => "sprites/stone.png",
// _ => "sprites/missing.png",
});
}
}
@ -238,3 +234,253 @@ pub struct SpawnItem {
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,6 @@
use bevy::{prelude::*, sprite::Anchor};
use bevy_rapier2d::prelude::*;
use creature::Mover;
use creature::{Mover, WeightSlowdown};
use item::{Inventory, Item, ItemSource, ItemStack, Stockpile};
use work::{WorkDuration, Worker};
@ -18,6 +18,7 @@ use crate::util::SpriteLoader;
SpriteLoader(|| SpriteLoader::from("sprites/glorb.png")),
Worker,
Inventory(|| Inventory::with_capacity(1)),
WeightSlowdown(|| WeightSlowdown { halfpoint: 50.0 }),
RigidBody(|| RigidBody::KinematicPositionBased),
Mover,
)]
@ -32,7 +33,7 @@ pub struct Glorb;
..default()
}),
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))
)]
pub struct Tree;
@ -46,7 +47,7 @@ pub struct Tree;
..default()
}),
SpriteLoader(|| SpriteLoader::from("sprites/box.png")),
Inventory(|| Inventory::with_capacity(25)),
Inventory,
Stockpile,
)]
pub struct Chest;

View File

@ -1,16 +1,18 @@
use bevy::prelude::*;
use crate::{
game::{creature::Mover, item::Inventory, work::Worker},
util::{inverse_lerp, lerp},
use crate::game::{
creature::{Mover, WeightSlowdown},
item::Inventory,
work::Worker,
};
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>,
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 {
continue;
};
@ -20,16 +22,12 @@ pub fn worker_movement(
let diff = (to.translation() - from.translation()).xy();
let dist = diff.length().max(0.);
let dir = diff.normalize_or_zero();
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();
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,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((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));
}