underworld_core/components/
inventory.rs1#[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}