From f422d0afe4a1f9bb5f5df0719503eb724857eb4f Mon Sep 17 00:00:00 2001 From: hheik <4469778+hheik@users.noreply.github.com> Date: Wed, 9 Apr 2025 16:07:19 +0300 Subject: [PATCH] Added weight slowdown for workers and unit tests for inventory & slowdown --- Cargo.lock | 7 + Cargo.toml | 3 + assets/sprites/stone.aseprite | Bin 0 -> 484 bytes assets/sprites/stone.png | Bin 0 -> 509 bytes assets/sprites/wood.aseprite | Bin 545 -> 467 bytes src/game/creature.rs | 51 ++++++- src/game/item.rs | 272 ++++++++++++++++++++++++++++++++-- src/game/prefab.rs | 7 +- src/game/systems/creature.rs | 28 ++-- src/game/systems/demo.rs | 1 + 10 files changed, 337 insertions(+), 32 deletions(-) create mode 100644 assets/sprites/stone.aseprite create mode 100644 assets/sprites/stone.png diff --git a/Cargo.lock b/Cargo.lock index 7ac3dd7..517ccf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 3bff232..346a773 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/assets/sprites/stone.aseprite b/assets/sprites/stone.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..fca6add22c02d19064aeccca8cc054ff6c505f74 GIT binary patch literal 484 zcmaFD$iVPmDImv);Ru-TIVnEEU0Jajz zI*=&PB_M)s=&a&#URS?p9!W) zgF%6zV$R(ehJ4KiJgz(4#HY!vk`_)+NPe?3nzK{ONoCvq{f}38-0P_PX;%0q^U(a` zfBwXreb{%Z@Yc5d_1CKOoAw@Sl$bsFsOUk-qh-e}m1ehZ6qi19BiOpdu=(sEiId-> zt{o0=)7-Ya{%m>H>n&DQTX%2rI@VE~_Rdo(?eF#|UiYNkmj#|?Kb>a!KC4DK>$`r< zJ&DRs&c)wd&gpZ{-Zrn|?}n|LKe!rX{f#`Ix+QI4wd-&BEytq1AG1nUiF5hB>Db2C z>%a2_XPMuUU3j%V%{Z%i!Wy}Aw~iY=m)2E#f5zEZdeeq4GfF)-Yl)o}mwM{y3jprg Biw^(* literal 0 HcmV?d00001 diff --git a/assets/sprites/stone.png b/assets/sprites/stone.png new file mode 100644 index 0000000000000000000000000000000000000000..3149d584a620c45bd18d4470f51e6e371db9e5ff GIT binary patch literal 509 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D%zw|lxchE&XX zJJUPwkbyv3`>N?D)C5oM6cA!}cACSzaKl51m#3ymAMO8WGilrZ?M@B0^X@F7ybm;GR$Vf&oxDH0`+(Sj6bp-hB9*Is9T^gRH~(k3-B0&ke23xD!D3ep%Y92; zACKH+Y{D79eO-}n`RAn-OV%}RW!ZM(pX!q2q~{H~o9elGH|}`DwA1FmNkOp??h9WZ z-&s|mCm!*vQ>v_-?}M8w!)pV*ulk-XS%L|wPAlUxl|lp+)+B2S>U?gAVu|Q@u+e$N zJeKP&8FTmEs%{hsW4R({+y3x}GtaxHCr(!?xv}ht?`Jl@ttT5HoLE`uwbfT~FaL(T zpj+=7vIP&${rdZ@R+V;&`Ij1=vkSlG+`CecEx2T}+-L21N14Tr3Qaxa(UaQR;j!QB xL6FQ^QPrsz%cG@MH|{Qdw={=<_cPU#NC-LE5{+vwnXvoB1>#YoCGnlsxZD{UC1@BO6P5`x1kMrs%wf$+Q2* zpM7%r_MiX%|Ic2#y``=_Gb<}HEU3(>YmJf7&G6*w)w}wqdv)a-$b)QBU 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); + } +} diff --git a/src/game/item.rs b/src/game/item.rs index 208f87a..b283d97 100644 --- a/src/game/item.rs +++ b/src/game/item.rs @@ -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 { - 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, } -#[derive(Clone, Debug, Default, Component, Reflect)] +#[derive(Clone, Debug, Default, Component, Reflect, PartialEq, Eq)] #[reflect(Component)] pub struct Inventory { pub capacity: Option, @@ -55,7 +49,7 @@ pub struct Inventory { pub items: Vec, } -#[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() + } + ); + } +} diff --git a/src/game/prefab.rs b/src/game/prefab.rs index d60e977..6998893 100644 --- a/src/game/prefab.rs +++ b/src/game/prefab.rs @@ -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; diff --git a/src/game/systems/creature.rs b/src/game/systems/creature.rs index 8753873..883a512 100644 --- a/src/game/systems/creature.rs +++ b/src/game/systems/creature.rs @@ -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