underworld_core/components/
inventory.rs

1#[cfg(feature = "bevy_components")]
2use bevy_ecs::prelude::Component;
3#[cfg(feature = "openapi")]
4use poem_openapi::Object;
5#[cfg(feature = "serialization")]
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use super::{
10    items::{CharacterItem, CharacterItemView, Item},
11    Attack, Defense,
12};
13
14#[derive(Clone, Debug, Default)]
15#[cfg_attr(feature = "bevy_components", derive(Component))]
16#[cfg_attr(feature = "serialization", derive(Deserialize, Serialize))]
17pub struct Inventory {
18    pub equipment: Vec<CharacterItem>,
19}
20
21impl Inventory {
22    pub fn count_weapons_at_ready(&self) -> usize {
23        self.equipment
24            .iter()
25            .filter(|item| item.is_weapon() && item.at_the_ready)
26            .count()
27    }
28
29    pub fn count_wearables_at_ready(&self) -> usize {
30        self.equipment
31            .iter()
32            .filter(|item| item.is_wearable() && item.at_the_ready)
33            .count()
34    }
35
36    pub fn find_item(&self, item_id: &Uuid) -> Option<CharacterItem> {
37        self.equipment
38            .iter()
39            .find(|character_item| character_item.item.id.eq(item_id))
40            .cloned()
41    }
42
43    pub fn add_item(&mut self, character_item: CharacterItem) {
44        self.equipment.push(character_item)
45    }
46
47    pub fn remove_item(&mut self, item_id: &Uuid) -> Option<CharacterItem> {
48        let index = self
49            .equipment
50            .iter()
51            .enumerate()
52            .find(|(_, character_item)| character_item.item.id.eq(item_id))
53            .map(|(index, _)| index);
54
55        match index {
56            Some(it) => Some(self.equipment.remove(it)),
57            None => None,
58        }
59    }
60
61    pub fn equipped_wearables(&self) -> Vec<CharacterItem> {
62        self.equipment
63            .iter()
64            .filter(|item| item.is_wearable() && item.is_at_the_ready())
65            .cloned()
66            .collect()
67    }
68
69    pub fn readied_weapons(&self) -> Vec<CharacterItem> {
70        self.equipment
71            .iter()
72            .filter(|item| item.is_weapon() && item.is_at_the_ready())
73            .cloned()
74            .collect()
75    }
76
77    pub fn non_readied_weapons(&self) -> Vec<&CharacterItem> {
78        self.equipment
79            .iter()
80            .filter(|item| item.is_weapon() && !item.is_at_the_ready())
81            .collect()
82    }
83
84    pub fn strongest_non_readied_weapon(&self) -> Option<&CharacterItem> {
85        self.non_readied_weapons()
86            .into_iter()
87            .max_by(|a, b| a.item.num_attack_rolls().cmp(&b.item.num_attack_rolls()))
88    }
89
90    pub fn full_attack(&self) -> Option<Attack> {
91        self.equipment
92            .iter()
93            .filter_map(|character_item| {
94                if character_item.at_the_ready {
95                    character_item.item.attack.clone()
96                } else {
97                    None
98                }
99            })
100            .reduce(|accum, item| Attack {
101                num_rolls: accum.num_rolls + item.num_rolls,
102                modifier: accum.modifier + item.modifier,
103                effects: accum
104                    .effects
105                    .into_iter()
106                    .chain(item.effects.into_iter())
107                    .collect(),
108            })
109    }
110
111    pub fn full_defense(&self) -> Option<Defense> {
112        self.equipment
113            .iter()
114            .filter_map(|character_item| {
115                if character_item.at_the_ready {
116                    character_item.item.defense.clone()
117                } else {
118                    None
119                }
120            })
121            .reduce(|accum, item| Defense {
122                damage_resistance: accum.damage_resistance + item.damage_resistance,
123            })
124    }
125
126    pub fn drop_all(&mut self) -> Vec<Item> {
127        let mut items: Vec<CharacterItem> = Vec::new();
128        items.append(&mut self.equipment);
129        items.into_iter().map(|ci| ci.item).collect()
130    }
131}
132
133#[derive(Clone, Debug)]
134#[cfg_attr(feature = "bevy_components", derive(Component))]
135#[cfg_attr(feature = "serialization", derive(Deserialize, Serialize))]
136#[cfg_attr(feature = "openapi", derive(Object), oai(rename = "Inventory"))]
137pub struct InventoryView {
138    pub equipment: Vec<CharacterItemView>,
139}
140
141#[cfg(test)]
142mod tests {
143    use uuid::Uuid;
144
145    use crate::components::{
146        damage::AttackEffect,
147        items::{CharacterItem, Item, ItemType, LocationTag},
148        Attack, Defense,
149    };
150
151    use super::Inventory;
152
153    #[test]
154    fn drop_all() {
155        let mut inventory = Inventory {
156            equipment: vec![
157                CharacterItem {
158                    item: Item {
159                        id: Uuid::new_v4(),
160                        name: None,
161                        item_type: ItemType::Spear,
162                        tags: Vec::new(),
163                        descriptors: Vec::new(),
164                        material: None,
165                        attack: Some(Attack {
166                            num_rolls: 2,
167                            modifier: 2,
168                            effects: vec![AttackEffect::Crushing],
169                        }),
170                        defense: None,
171                        consumable: None,
172                        throwable: None,
173                    },
174                    equipped_location: LocationTag::Hand,
175                    at_the_ready: true,
176                },
177                CharacterItem {
178                    item: Item {
179                        id: Uuid::new_v4(),
180                        name: None,
181                        item_type: ItemType::LongSword,
182                        tags: Vec::new(),
183                        descriptors: Vec::new(),
184                        material: None,
185                        attack: Some(Attack {
186                            num_rolls: 1,
187                            modifier: -2,
188                            effects: vec![AttackEffect::Sharp],
189                        }),
190                        defense: None,
191                        consumable: None,
192                        throwable: None,
193                    },
194                    equipped_location: LocationTag::Hand,
195                    at_the_ready: true,
196                },
197            ],
198        };
199
200        let items = inventory.drop_all();
201
202        assert_eq!(0, inventory.equipment.len());
203        assert_eq!(2, items.len());
204    }
205
206    #[test]
207    fn full_attack() {
208        let inventory = Inventory {
209            equipment: vec![
210                CharacterItem {
211                    item: Item {
212                        id: Uuid::new_v4(),
213                        name: None,
214                        item_type: ItemType::Spear,
215                        tags: Vec::new(),
216                        descriptors: Vec::new(),
217                        material: None,
218                        attack: Some(Attack {
219                            num_rolls: 2,
220                            modifier: 2,
221                            effects: vec![AttackEffect::Crushing],
222                        }),
223                        defense: None,
224                        consumable: None,
225                        throwable: None,
226                    },
227                    equipped_location: LocationTag::Hand,
228                    at_the_ready: true,
229                },
230                CharacterItem {
231                    item: Item {
232                        id: Uuid::new_v4(),
233                        name: None,
234                        item_type: ItemType::LongSword,
235                        tags: Vec::new(),
236                        descriptors: Vec::new(),
237                        material: None,
238                        attack: Some(Attack {
239                            num_rolls: 1,
240                            modifier: -2,
241                            effects: vec![AttackEffect::Sharp],
242                        }),
243                        defense: None,
244                        consumable: None,
245                        throwable: None,
246                    },
247                    equipped_location: LocationTag::Hand,
248                    at_the_ready: true,
249                },
250            ],
251        };
252
253        let merged = inventory.full_attack();
254        assert!(merged.is_some());
255        let attack = merged.unwrap();
256        assert_eq!(attack.num_rolls, 3);
257        assert_eq!(attack.modifier, 0);
258        assert_eq!(
259            attack.effects,
260            vec![AttackEffect::Crushing, AttackEffect::Sharp]
261        );
262    }
263
264    #[test]
265    fn full_defense() {
266        let inventory = Inventory {
267            equipment: vec![
268                CharacterItem {
269                    item: Item {
270                        id: Uuid::new_v4(),
271                        name: None,
272                        item_type: ItemType::PlateBoots,
273                        tags: Vec::new(),
274                        descriptors: Vec::new(),
275                        material: None,
276                        attack: None,
277                        defense: Some(Defense {
278                            damage_resistance: 2,
279                        }),
280                        consumable: None,
281                        throwable: None,
282                    },
283                    equipped_location: LocationTag::Feet,
284                    at_the_ready: true,
285                },
286                CharacterItem {
287                    item: Item {
288                        id: Uuid::new_v4(),
289                        name: None,
290                        item_type: ItemType::PlateGauntlets,
291                        tags: Vec::new(),
292                        descriptors: Vec::new(),
293                        material: None,
294                        attack: None,
295                        defense: Some(Defense {
296                            damage_resistance: 6,
297                        }),
298                        consumable: None,
299                        throwable: None,
300                    },
301                    equipped_location: LocationTag::Hand,
302                    at_the_ready: true,
303                },
304            ],
305        };
306
307        let merged = inventory.full_defense();
308        assert!(merged.is_some());
309        let attack = merged.unwrap();
310        assert_eq!(attack.damage_resistance, 8);
311    }
312}