underworld_core 0.6.3

Small library to either play a game or assist in generating rooms
Documentation
#[cfg(feature = "bevy_components")]
use bevy_ecs::prelude::Component;
#[cfg(feature = "openapi")]
use poem_openapi::Object;
#[cfg(feature = "serialization")]
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use super::{
    items::{CharacterItem, CharacterItemView, Item},
    Attack, Defense,
};

#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "bevy_components", derive(Component))]
#[cfg_attr(feature = "serialization", derive(Deserialize, Serialize))]
pub struct Inventory {
    pub equipment: Vec<CharacterItem>,
}

impl Inventory {
    pub fn count_weapons_at_ready(&self) -> usize {
        self.equipment
            .iter()
            .filter(|item| item.is_weapon() && item.at_the_ready)
            .count()
    }

    pub fn count_wearables_at_ready(&self) -> usize {
        self.equipment
            .iter()
            .filter(|item| item.is_wearable() && item.at_the_ready)
            .count()
    }

    pub fn find_item(&self, item_id: &Uuid) -> Option<CharacterItem> {
        self.equipment
            .iter()
            .find(|character_item| character_item.item.id.eq(item_id))
            .cloned()
    }

    pub fn add_item(&mut self, character_item: CharacterItem) {
        self.equipment.push(character_item)
    }

    pub fn remove_item(&mut self, item_id: &Uuid) -> Option<CharacterItem> {
        let index = self
            .equipment
            .iter()
            .enumerate()
            .find(|(_, character_item)| character_item.item.id.eq(item_id))
            .map(|(index, _)| index);

        match index {
            Some(it) => Some(self.equipment.remove(it)),
            None => None,
        }
    }

    pub fn equipped_wearables(&self) -> Vec<CharacterItem> {
        self.equipment
            .iter()
            .filter(|item| item.is_wearable() && item.is_at_the_ready())
            .cloned()
            .collect()
    }

    pub fn readied_weapons(&self) -> Vec<CharacterItem> {
        self.equipment
            .iter()
            .filter(|item| item.is_weapon() && item.is_at_the_ready())
            .cloned()
            .collect()
    }

    pub fn non_readied_weapons(&self) -> Vec<&CharacterItem> {
        self.equipment
            .iter()
            .filter(|item| item.is_weapon() && !item.is_at_the_ready())
            .collect()
    }

    pub fn strongest_non_readied_weapon(&self) -> Option<&CharacterItem> {
        self.non_readied_weapons()
            .into_iter()
            .max_by(|a, b| a.item.num_attack_rolls().cmp(&b.item.num_attack_rolls()))
    }

    pub fn full_attack(&self) -> Option<Attack> {
        self.equipment
            .iter()
            .filter_map(|character_item| {
                if character_item.at_the_ready {
                    character_item.item.attack.clone()
                } else {
                    None
                }
            })
            .reduce(|accum, item| Attack {
                num_rolls: accum.num_rolls + item.num_rolls,
                modifier: accum.modifier + item.modifier,
                effects: accum
                    .effects
                    .into_iter()
                    .chain(item.effects.into_iter())
                    .collect(),
            })
    }

    pub fn full_defense(&self) -> Option<Defense> {
        self.equipment
            .iter()
            .filter_map(|character_item| {
                if character_item.at_the_ready {
                    character_item.item.defense.clone()
                } else {
                    None
                }
            })
            .reduce(|accum, item| Defense {
                damage_resistance: accum.damage_resistance + item.damage_resistance,
            })
    }

    pub fn drop_all(&mut self) -> Vec<Item> {
        let mut items: Vec<CharacterItem> = Vec::new();
        items.append(&mut self.equipment);
        items.into_iter().map(|ci| ci.item).collect()
    }
}

#[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_components", derive(Component))]
#[cfg_attr(feature = "serialization", derive(Deserialize, Serialize))]
#[cfg_attr(feature = "openapi", derive(Object), oai(rename = "Inventory"))]
pub struct InventoryView {
    pub equipment: Vec<CharacterItemView>,
}

#[cfg(test)]
mod tests {
    use uuid::Uuid;

    use crate::components::{
        damage::AttackEffect,
        items::{CharacterItem, Item, ItemType, LocationTag},
        Attack, Defense,
    };

    use super::Inventory;

    #[test]
    fn drop_all() {
        let mut inventory = Inventory {
            equipment: vec![
                CharacterItem {
                    item: Item {
                        id: Uuid::new_v4(),
                        name: None,
                        item_type: ItemType::Spear,
                        tags: Vec::new(),
                        descriptors: Vec::new(),
                        material: None,
                        attack: Some(Attack {
                            num_rolls: 2,
                            modifier: 2,
                            effects: vec![AttackEffect::Crushing],
                        }),
                        defense: None,
                        consumable: None,
                        throwable: None,
                    },
                    equipped_location: LocationTag::Hand,
                    at_the_ready: true,
                },
                CharacterItem {
                    item: Item {
                        id: Uuid::new_v4(),
                        name: None,
                        item_type: ItemType::LongSword,
                        tags: Vec::new(),
                        descriptors: Vec::new(),
                        material: None,
                        attack: Some(Attack {
                            num_rolls: 1,
                            modifier: -2,
                            effects: vec![AttackEffect::Sharp],
                        }),
                        defense: None,
                        consumable: None,
                        throwable: None,
                    },
                    equipped_location: LocationTag::Hand,
                    at_the_ready: true,
                },
            ],
        };

        let items = inventory.drop_all();

        assert_eq!(0, inventory.equipment.len());
        assert_eq!(2, items.len());
    }

    #[test]
    fn full_attack() {
        let inventory = Inventory {
            equipment: vec![
                CharacterItem {
                    item: Item {
                        id: Uuid::new_v4(),
                        name: None,
                        item_type: ItemType::Spear,
                        tags: Vec::new(),
                        descriptors: Vec::new(),
                        material: None,
                        attack: Some(Attack {
                            num_rolls: 2,
                            modifier: 2,
                            effects: vec![AttackEffect::Crushing],
                        }),
                        defense: None,
                        consumable: None,
                        throwable: None,
                    },
                    equipped_location: LocationTag::Hand,
                    at_the_ready: true,
                },
                CharacterItem {
                    item: Item {
                        id: Uuid::new_v4(),
                        name: None,
                        item_type: ItemType::LongSword,
                        tags: Vec::new(),
                        descriptors: Vec::new(),
                        material: None,
                        attack: Some(Attack {
                            num_rolls: 1,
                            modifier: -2,
                            effects: vec![AttackEffect::Sharp],
                        }),
                        defense: None,
                        consumable: None,
                        throwable: None,
                    },
                    equipped_location: LocationTag::Hand,
                    at_the_ready: true,
                },
            ],
        };

        let merged = inventory.full_attack();
        assert!(merged.is_some());
        let attack = merged.unwrap();
        assert_eq!(attack.num_rolls, 3);
        assert_eq!(attack.modifier, 0);
        assert_eq!(
            attack.effects,
            vec![AttackEffect::Crushing, AttackEffect::Sharp]
        );
    }

    #[test]
    fn full_defense() {
        let inventory = Inventory {
            equipment: vec![
                CharacterItem {
                    item: Item {
                        id: Uuid::new_v4(),
                        name: None,
                        item_type: ItemType::PlateBoots,
                        tags: Vec::new(),
                        descriptors: Vec::new(),
                        material: None,
                        attack: None,
                        defense: Some(Defense {
                            damage_resistance: 2,
                        }),
                        consumable: None,
                        throwable: None,
                    },
                    equipped_location: LocationTag::Feet,
                    at_the_ready: true,
                },
                CharacterItem {
                    item: Item {
                        id: Uuid::new_v4(),
                        name: None,
                        item_type: ItemType::PlateGauntlets,
                        tags: Vec::new(),
                        descriptors: Vec::new(),
                        material: None,
                        attack: None,
                        defense: Some(Defense {
                            damage_resistance: 6,
                        }),
                        consumable: None,
                        throwable: None,
                    },
                    equipped_location: LocationTag::Hand,
                    at_the_ready: true,
                },
            ],
        };

        let merged = inventory.full_defense();
        assert!(merged.is_some());
        let attack = merged.unwrap();
        assert_eq!(attack.damage_resistance, 8);
    }
}