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 0000000..fca6add Binary files /dev/null and b/assets/sprites/stone.aseprite differ diff --git a/assets/sprites/stone.png b/assets/sprites/stone.png new file mode 100644 index 0000000..3149d58 Binary files /dev/null and b/assets/sprites/stone.png differ diff --git a/assets/sprites/wood.aseprite b/assets/sprites/wood.aseprite index 6985d16..ae5a707 100644 Binary files a/assets/sprites/wood.aseprite and b/assets/sprites/wood.aseprite differ diff --git a/src/game/creature.rs b/src/game/creature.rs index 73acc85..3b22555 100644 --- a/src/game/creature.rs +++ b/src/game/creature.rs @@ -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); + } +} 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