1use std::collections::HashMap;
5use crate::character::stats::{StatKind, StatModifier, ModifierKind};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
12pub enum ItemRarity {
13 Common,
14 Uncommon,
15 Rare,
16 Epic,
17 Legendary,
18 Mythic,
19}
20
21impl ItemRarity {
22 pub fn color(&self) -> (u8, u8, u8) {
23 match self {
24 ItemRarity::Common => (200, 200, 200),
25 ItemRarity::Uncommon => (30, 200, 30),
26 ItemRarity::Rare => (0, 100, 255),
27 ItemRarity::Epic => (160, 0, 255),
28 ItemRarity::Legendary => (255, 165, 0),
29 ItemRarity::Mythic => (255, 50, 50),
30 }
31 }
32
33 pub fn display_name(&self) -> &'static str {
34 match self {
35 ItemRarity::Common => "Common",
36 ItemRarity::Uncommon => "Uncommon",
37 ItemRarity::Rare => "Rare",
38 ItemRarity::Epic => "Epic",
39 ItemRarity::Legendary => "Legendary",
40 ItemRarity::Mythic => "Mythic",
41 }
42 }
43
44 pub fn affix_count_range(&self) -> (usize, usize) {
45 match self {
46 ItemRarity::Common => (0, 0),
47 ItemRarity::Uncommon => (1, 2),
48 ItemRarity::Rare => (3, 6),
49 ItemRarity::Epic => (5, 8),
50 ItemRarity::Legendary => (6, 10),
51 ItemRarity::Mythic => (8, 12),
52 }
53 }
54
55 pub fn sell_multiplier(&self) -> f32 {
56 match self {
57 ItemRarity::Common => 1.0,
58 ItemRarity::Uncommon => 2.0,
59 ItemRarity::Rare => 5.0,
60 ItemRarity::Epic => 15.0,
61 ItemRarity::Legendary => 50.0,
62 ItemRarity::Mythic => 200.0,
63 }
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
72pub enum ItemType {
73 Weapon,
74 Armor,
75 Accessory,
76 Consumable,
77 Material,
78 Quest,
79 Spell,
80 Trap,
81 Tool,
82 Container,
83 Currency,
84 Key,
85 Ammunition,
86}
87
88impl ItemType {
89 pub fn is_equippable(&self) -> bool {
90 matches!(self, ItemType::Weapon | ItemType::Armor | ItemType::Accessory)
91 }
92
93 pub fn is_stackable(&self) -> bool {
94 matches!(self, ItemType::Material | ItemType::Consumable | ItemType::Currency | ItemType::Ammunition)
95 }
96}
97
98#[allow(dead_code)]
100enum _AmmunitionHelper { Ammunition }
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107pub enum WeaponType {
108 Sword,
109 Dagger,
110 Axe,
111 Mace,
112 Staff,
113 Wand,
114 Bow,
115 Crossbow,
116 Spear,
117 Shield,
118 Fist,
119 Whip,
120 Hammer,
121 Scythe,
122 Tome,
123}
124
125#[derive(Debug, Clone)]
126pub struct StatScaling {
127 pub strength_ratio: f32,
128 pub dexterity_ratio: f32,
129 pub intelligence_ratio: f32,
130}
131
132impl StatScaling {
133 pub fn strength_weapon() -> Self {
134 Self { strength_ratio: 1.0, dexterity_ratio: 0.2, intelligence_ratio: 0.0 }
135 }
136 pub fn dex_weapon() -> Self {
137 Self { strength_ratio: 0.3, dexterity_ratio: 1.0, intelligence_ratio: 0.0 }
138 }
139 pub fn int_weapon() -> Self {
140 Self { strength_ratio: 0.0, dexterity_ratio: 0.2, intelligence_ratio: 1.0 }
141 }
142 pub fn balanced() -> Self {
143 Self { strength_ratio: 0.5, dexterity_ratio: 0.5, intelligence_ratio: 0.0 }
144 }
145 pub fn total_bonus(&self, str_val: f32, dex_val: f32, int_val: f32) -> f32 {
146 str_val * self.strength_ratio + dex_val * self.dexterity_ratio + int_val * self.intelligence_ratio
147 }
148}
149
150#[derive(Debug, Clone)]
151pub struct WeaponData {
152 pub damage_min: f32,
153 pub damage_max: f32,
154 pub attack_speed: f32,
155 pub range: f32,
156 pub weapon_type: WeaponType,
157 pub stat_scaling: StatScaling,
158 pub two_handed: bool,
159 pub element: Option<DamageElement>,
160}
161
162impl WeaponData {
163 pub fn new(weapon_type: WeaponType) -> Self {
164 let (min, max, spd, range, two_handed) = match weapon_type {
165 WeaponType::Sword => (8.0, 14.0, 1.0, 1.5, false),
166 WeaponType::Dagger => (4.0, 8.0, 1.6, 1.2, false),
167 WeaponType::Axe => (12.0, 20.0, 0.8, 1.5, true),
168 WeaponType::Mace => (10.0, 16.0, 0.9, 1.4, false),
169 WeaponType::Staff => (6.0, 12.0, 0.8, 2.0, true),
170 WeaponType::Wand => (4.0, 8.0, 1.2, 8.0, false),
171 WeaponType::Bow => (10.0, 18.0, 1.1, 15.0, true),
172 WeaponType::Crossbow => (14.0, 22.0, 0.7, 18.0, true),
173 WeaponType::Spear => (11.0, 17.0, 0.9, 2.5, true),
174 WeaponType::Shield => (3.0, 6.0, 0.8, 1.2, false),
175 WeaponType::Fist => (5.0, 9.0, 1.8, 1.0, false),
176 WeaponType::Whip => (7.0, 11.0, 1.3, 3.0, false),
177 WeaponType::Hammer => (15.0, 25.0, 0.6, 1.5, true),
178 WeaponType::Scythe => (13.0, 21.0, 0.75, 2.0, true),
179 WeaponType::Tome => (3.0, 7.0, 1.0, 6.0, false),
180 };
181 let scaling = match weapon_type {
182 WeaponType::Wand | WeaponType::Staff | WeaponType::Tome => StatScaling::int_weapon(),
183 WeaponType::Dagger | WeaponType::Bow | WeaponType::Crossbow => StatScaling::dex_weapon(),
184 _ => StatScaling::strength_weapon(),
185 };
186 Self {
187 damage_min: min,
188 damage_max: max,
189 attack_speed: spd,
190 range,
191 weapon_type,
192 stat_scaling: scaling,
193 two_handed,
194 element: None,
195 }
196 }
197
198 pub fn average_damage(&self) -> f32 {
199 (self.damage_min + self.damage_max) * 0.5
200 }
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
204pub enum DamageElement {
205 Fire,
206 Ice,
207 Lightning,
208 Poison,
209 Holy,
210 Dark,
211 Physical,
212 Arcane,
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
220pub enum ArmorWeightClass {
221 Light,
222 Medium,
223 Heavy,
224 Robes,
225}
226
227#[derive(Debug, Clone)]
228pub struct ArmorData {
229 pub defense: f32,
230 pub magic_resist: f32,
231 pub weight_class: ArmorWeightClass,
232 pub set_id: Option<u32>,
233}
234
235impl ArmorData {
236 pub fn new(defense: f32, magic_resist: f32, weight_class: ArmorWeightClass) -> Self {
237 Self { defense, magic_resist, weight_class, set_id: None }
238 }
239
240 pub fn move_speed_penalty(&self) -> f32 {
241 match self.weight_class {
242 ArmorWeightClass::Robes => 0.0,
243 ArmorWeightClass::Light => 0.0,
244 ArmorWeightClass::Medium => 5.0,
245 ArmorWeightClass::Heavy => 15.0,
246 }
247 }
248}
249
250#[derive(Debug, Clone)]
255pub struct ArmorSet {
256 pub id: u32,
257 pub name: String,
258 pub pieces: Vec<u64>, pub bonuses: Vec<(usize, Vec<StatModifier>)>, }
261
262impl ArmorSet {
263 pub fn new(id: u32, name: impl Into<String>) -> Self {
264 Self { id, name: name.into(), pieces: Vec::new(), bonuses: Vec::new() }
265 }
266
267 pub fn add_piece(mut self, item_id: u64) -> Self {
268 self.pieces.push(item_id);
269 self
270 }
271
272 pub fn add_bonus(mut self, pieces_required: usize, modifiers: Vec<StatModifier>) -> Self {
273 self.bonuses.push((pieces_required, modifiers));
274 self
275 }
276
277 pub fn active_bonuses(&self, equipped_count: usize) -> Vec<&StatModifier> {
278 let mut result = Vec::new();
279 for (required, mods) in &self.bonuses {
280 if equipped_count >= *required {
281 result.extend(mods.iter());
282 }
283 }
284 result
285 }
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
293pub enum EquipSlot {
294 Head,
295 Chest,
296 Legs,
297 Feet,
298 Hands,
299 MainHand,
300 OffHand,
301 Ring1,
302 Ring2,
303 Amulet,
304 Cape,
305 Belt,
306}
307
308impl EquipSlot {
309 pub fn all() -> &'static [EquipSlot] {
310 &[
311 EquipSlot::Head, EquipSlot::Chest, EquipSlot::Legs, EquipSlot::Feet,
312 EquipSlot::Hands, EquipSlot::MainHand, EquipSlot::OffHand,
313 EquipSlot::Ring1, EquipSlot::Ring2, EquipSlot::Amulet,
314 EquipSlot::Cape, EquipSlot::Belt,
315 ]
316 }
317
318 pub fn display_name(&self) -> &'static str {
319 match self {
320 EquipSlot::Head => "Head",
321 EquipSlot::Chest => "Chest",
322 EquipSlot::Legs => "Legs",
323 EquipSlot::Feet => "Feet",
324 EquipSlot::Hands => "Hands",
325 EquipSlot::MainHand => "Main Hand",
326 EquipSlot::OffHand => "Off Hand",
327 EquipSlot::Ring1 => "Ring (Left)",
328 EquipSlot::Ring2 => "Ring (Right)",
329 EquipSlot::Amulet => "Amulet",
330 EquipSlot::Cape => "Cape",
331 EquipSlot::Belt => "Belt",
332 }
333 }
334}
335
336#[derive(Debug, Clone)]
341pub enum ConsumableEffect {
342 HealHp(f32),
343 HealMp(f32),
344 HealStamina(f32),
345 Buff { modifier: StatModifier, duration_secs: f32 },
346 Teleport { x: f32, y: f32, z: f32 },
347 Revive { hp_fraction: f32 },
348 Antidote,
349 Invisibility { duration_secs: f32 },
350 Invulnerability { duration_secs: f32 },
351 Transform { entity_type: String, duration_secs: f32 },
352 LevelUp,
353 GrantXp(u64),
354}
355
356#[derive(Debug, Clone)]
361pub struct Item {
362 pub id: u64,
363 pub name: String,
364 pub description: String,
365 pub icon_char: char,
366 pub weight: f32,
367 pub value: u32,
368 pub rarity: ItemRarity,
369 pub item_type: ItemType,
370 pub tags: Vec<String>,
371 pub stack_size: u32,
372 pub max_stack: u32,
373 pub equip_slot: Option<EquipSlot>,
374 pub weapon_data: Option<WeaponData>,
375 pub armor_data: Option<ArmorData>,
376 pub consumable_effects: Vec<ConsumableEffect>,
377 pub stat_modifiers: Vec<StatModifier>,
378 pub required_level: u32,
379 pub affixes: Vec<Affix>,
380}
381
382impl Item {
383 pub fn new(id: u64, name: impl Into<String>, item_type: ItemType) -> Self {
384 Self {
385 id,
386 name: name.into(),
387 description: String::new(),
388 icon_char: '?',
389 weight: 1.0,
390 value: 10,
391 rarity: ItemRarity::Common,
392 item_type,
393 tags: Vec::new(),
394 stack_size: 1,
395 max_stack: 1,
396 equip_slot: None,
397 weapon_data: None,
398 armor_data: None,
399 consumable_effects: Vec::new(),
400 stat_modifiers: Vec::new(),
401 required_level: 1,
402 affixes: Vec::new(),
403 }
404 }
405
406 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
407 self.description = desc.into();
408 self
409 }
410
411 pub fn with_icon(mut self, c: char) -> Self {
412 self.icon_char = c;
413 self
414 }
415
416 pub fn with_rarity(mut self, rarity: ItemRarity) -> Self {
417 self.rarity = rarity;
418 self
419 }
420
421 pub fn with_value(mut self, value: u32) -> Self {
422 self.value = value;
423 self
424 }
425
426 pub fn with_weight(mut self, weight: f32) -> Self {
427 self.weight = weight;
428 self
429 }
430
431 pub fn with_slot(mut self, slot: EquipSlot) -> Self {
432 self.equip_slot = Some(slot);
433 self
434 }
435
436 pub fn with_weapon(mut self, data: WeaponData) -> Self {
437 self.weapon_data = Some(data);
438 self.equip_slot = Some(EquipSlot::MainHand);
439 self
440 }
441
442 pub fn with_armor(mut self, data: ArmorData) -> Self {
443 self.armor_data = Some(data);
444 self
445 }
446
447 pub fn add_modifier(mut self, m: StatModifier) -> Self {
448 self.stat_modifiers.push(m);
449 self
450 }
451
452 pub fn add_tag(mut self, tag: impl Into<String>) -> Self {
453 self.tags.push(tag.into());
454 self
455 }
456
457 pub fn sell_value(&self) -> u32 {
458 (self.value as f32 * self.rarity.sell_multiplier()) as u32
459 }
460
461 pub fn total_stat_bonus(&self, kind: StatKind) -> f32 {
462 self.stat_modifiers.iter()
463 .filter(|m| m.stat == kind && m.kind == ModifierKind::FlatAdd)
464 .map(|m| m.value)
465 .sum()
466 }
467
468 pub fn is_stackable(&self) -> bool {
469 self.max_stack > 1
470 }
471
472 pub fn can_stack_with(&self, other: &Item) -> bool {
473 self.id == other.id && self.is_stackable() && self.stack_size < self.max_stack
474 }
475}
476
477#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
482pub enum AffixKind {
483 Prefix,
484 Suffix,
485 Implicit,
486}
487
488#[derive(Debug, Clone)]
489pub struct Affix {
490 pub kind: AffixKind,
491 pub tier: u8,
492 pub name: String,
493 pub stat_modifiers: Vec<StatModifier>,
494}
495
496impl Affix {
497 pub fn new(kind: AffixKind, tier: u8, name: impl Into<String>) -> Self {
498 Self { kind, tier, name: name.into(), stat_modifiers: Vec::new() }
499 }
500
501 pub fn add_modifier(mut self, m: StatModifier) -> Self {
502 self.stat_modifiers.push(m);
503 self
504 }
505}
506
507#[derive(Debug, Clone, Default)]
512pub struct EquippedItems {
513 pub slots: HashMap<EquipSlot, Item>,
514}
515
516impl EquippedItems {
517 pub fn new() -> Self {
518 Self { slots: HashMap::new() }
519 }
520
521 pub fn equip(&mut self, item: Item) -> Option<Item> {
523 let slot = item.equip_slot?;
524 let old = self.slots.remove(&slot);
525 self.slots.insert(slot, item);
526 old
527 }
528
529 pub fn unequip(&mut self, slot: EquipSlot) -> Option<Item> {
531 self.slots.remove(&slot)
532 }
533
534 pub fn get(&self, slot: EquipSlot) -> Option<&Item> {
535 self.slots.get(&slot)
536 }
537
538 pub fn weapon_damage(&self) -> f32 {
539 self.slots.get(&EquipSlot::MainHand)
540 .and_then(|i| i.weapon_data.as_ref())
541 .map(|w| w.average_damage())
542 .unwrap_or(0.0)
543 }
544
545 pub fn total_defense(&self) -> f32 {
546 self.slots.values()
547 .filter_map(|i| i.armor_data.as_ref())
548 .map(|a| a.defense)
549 .sum()
550 }
551
552 pub fn total_magic_resist(&self) -> f32 {
553 self.slots.values()
554 .filter_map(|i| i.armor_data.as_ref())
555 .map(|a| a.magic_resist)
556 .sum()
557 }
558
559 pub fn all_stat_modifiers(&self) -> Vec<&StatModifier> {
560 self.slots.values()
561 .flat_map(|i| i.stat_modifiers.iter())
562 .collect()
563 }
564
565 pub fn total_weight(&self) -> f32 {
566 self.slots.values().map(|i| i.weight).sum()
567 }
568
569 pub fn count_set_pieces(&self, set_id: u32) -> usize {
570 self.slots.values()
571 .filter(|i| i.armor_data.as_ref().and_then(|a| a.set_id) == Some(set_id))
572 .count()
573 }
574}
575
576#[derive(Debug, Clone)]
581pub struct Inventory {
582 pub items: Vec<Option<Item>>,
583 pub capacity: usize,
584 pub max_weight: f32,
585}
586
587impl Inventory {
588 pub fn new(capacity: usize, max_weight: f32) -> Self {
589 Self {
590 items: vec![None; capacity],
591 capacity,
592 max_weight,
593 }
594 }
595
596 pub fn add_item(&mut self, mut item: Item) -> Result<usize, Item> {
597 if item.is_stackable() {
599 for (idx, slot) in self.items.iter_mut().enumerate() {
600 if let Some(existing) = slot {
601 if existing.can_stack_with(&item) {
602 let space = existing.max_stack - existing.stack_size;
603 if item.stack_size <= space {
604 existing.stack_size += item.stack_size;
605 return Ok(idx);
606 } else {
607 item.stack_size -= space;
608 existing.stack_size = existing.max_stack;
609 }
610 }
611 }
612 }
613 }
614
615 if self.current_weight() + item.weight > self.max_weight {
617 return Err(item);
618 }
619
620 for (idx, slot) in self.items.iter_mut().enumerate() {
622 if slot.is_none() {
623 *slot = Some(item);
624 return Ok(idx);
625 }
626 }
627
628 Err(item) }
630
631 pub fn remove_item(&mut self, slot: usize) -> Option<Item> {
632 if slot < self.capacity {
633 self.items[slot].take()
634 } else {
635 None
636 }
637 }
638
639 pub fn get(&self, slot: usize) -> Option<&Item> {
640 self.items.get(slot).and_then(|s| s.as_ref())
641 }
642
643 pub fn current_weight(&self) -> f32 {
644 self.items.iter().flatten().map(|i| i.weight * i.stack_size as f32).sum()
645 }
646
647 pub fn is_full(&self) -> bool {
648 self.items.iter().all(|s| s.is_some())
649 }
650
651 pub fn free_slots(&self) -> usize {
652 self.items.iter().filter(|s| s.is_none()).count()
653 }
654
655 pub fn item_count(&self) -> usize {
656 self.items.iter().flatten().count()
657 }
658
659 pub fn find_by_id(&self, id: u64) -> Option<(usize, &Item)> {
660 self.items.iter().enumerate()
661 .find_map(|(i, s)| s.as_ref().filter(|item| item.id == id).map(|item| (i, item)))
662 }
663
664 pub fn find_all_by_id(&self, id: u64) -> Vec<usize> {
665 self.items.iter().enumerate()
666 .filter_map(|(i, s)| s.as_ref().filter(|item| item.id == id).map(|_| i))
667 .collect()
668 }
669
670 pub fn sort(&mut self) {
672 let mut filled: Vec<Item> = self.items.iter_mut().filter_map(|s| s.take()).collect();
673 filled.sort_by(|a, b| {
674 b.rarity.cmp(&a.rarity).then(a.name.cmp(&b.name))
675 });
676 for (i, item) in filled.into_iter().enumerate() {
677 if i < self.capacity {
678 self.items[i] = Some(item);
679 }
680 }
681 }
682
683 pub fn stack_items(&mut self) {
685 let mut stacks: HashMap<u64, (u32, u32)> = HashMap::new(); let mut non_stackable: Vec<Item> = Vec::new();
688 for slot in self.items.iter_mut() {
689 if let Some(item) = slot.take() {
690 if item.is_stackable() {
691 let entry = stacks.entry(item.id).or_insert((0, item.max_stack));
692 entry.0 += item.stack_size;
693 non_stackable.push(item); } else {
696 non_stackable.push(item);
697 }
698 }
699 }
700 self.items = vec![None; self.capacity];
703 let mut idx = 0;
704 for item in non_stackable {
705 if idx < self.capacity {
706 self.items[idx] = Some(item);
707 idx += 1;
708 }
709 }
710 }
711
712 pub fn total_value(&self) -> u32 {
713 self.items.iter().flatten().map(|i| i.value * i.stack_size).sum()
714 }
715}
716
717#[derive(Debug, Clone)]
722pub struct StashTab {
723 pub name: String,
724 pub inventory: Inventory,
725}
726
727impl StashTab {
728 pub fn new(name: impl Into<String>, capacity: usize) -> Self {
729 Self {
730 name: name.into(),
731 inventory: Inventory::new(capacity, f32::MAX),
732 }
733 }
734}
735
736#[derive(Debug, Clone)]
737pub struct Stash {
738 pub tabs: Vec<StashTab>,
739 pub gold: u64,
740}
741
742impl Stash {
743 pub fn new() -> Self {
744 Self {
745 tabs: vec![
746 StashTab::new("Main", 120),
747 StashTab::new("Equipment", 120),
748 StashTab::new("Materials", 120),
749 ],
750 gold: 0,
751 }
752 }
753
754 pub fn add_tab(&mut self, name: impl Into<String>) {
755 self.tabs.push(StashTab::new(name, 120));
756 }
757
758 pub fn get_tab(&self, idx: usize) -> Option<&StashTab> {
759 self.tabs.get(idx)
760 }
761
762 pub fn get_tab_mut(&mut self, idx: usize) -> Option<&mut StashTab> {
763 self.tabs.get_mut(idx)
764 }
765
766 pub fn deposit(&mut self, tab: usize, item: Item) -> Result<usize, Item> {
767 if let Some(t) = self.tabs.get_mut(tab) {
768 t.inventory.add_item(item)
769 } else {
770 Err(item)
771 }
772 }
773
774 pub fn withdraw(&mut self, tab: usize, slot: usize) -> Option<Item> {
775 self.tabs.get_mut(tab)?.inventory.remove_item(slot)
776 }
777}
778
779impl Default for Stash {
780 fn default() -> Self {
781 Self::new()
782 }
783}
784
785static PREFIX_NAMES: &[&str] = &[
790 "Flaming", "Frozen", "Blessed", "Cursed", "Ancient", "Shadow", "Thunder",
791 "Venom", "Sacred", "Ethereal", "Forged", "Runic", "Arcane", "Void",
792 "Spectral", "Iron", "Golden", "Silver", "Storm", "Blood",
793];
794
795static SUFFIX_NAMES: &[&str] = &[
796 "of Power", "of the Titan", "of Swiftness", "of the Sage", "of Fortitude",
797 "of the Gods", "of Destruction", "of Protection", "of Wisdom", "of the Ages",
798 "of Fury", "of the Dragon", "of the Phoenix", "of Doom", "of Light",
799];
800
801pub struct ItemGenerator {
802 next_id: u64,
803 seed: u64,
804}
805
806impl ItemGenerator {
807 pub fn new(seed: u64) -> Self {
808 Self { next_id: 1000, seed }
809 }
810
811 fn next_rand(&mut self) -> u64 {
812 self.seed ^= self.seed << 13;
814 self.seed ^= self.seed >> 7;
815 self.seed ^= self.seed << 17;
816 self.seed
817 }
818
819 fn rand_range(&mut self, min: u64, max: u64) -> u64 {
820 if max <= min { return min; }
821 min + self.next_rand() % (max - min)
822 }
823
824 fn rand_f32(&mut self, min: f32, max: f32) -> f32 {
825 let r = (self.next_rand() % 100000) as f32 / 100000.0;
826 min + r * (max - min)
827 }
828
829 fn next_id(&mut self) -> u64 {
830 let id = self.next_id;
831 self.next_id += 1;
832 id
833 }
834
835 fn pick_prefix(&mut self) -> &'static str {
836 let idx = self.next_rand() as usize % PREFIX_NAMES.len();
837 PREFIX_NAMES[idx]
838 }
839
840 fn pick_suffix(&mut self) -> &'static str {
841 let idx = self.next_rand() as usize % SUFFIX_NAMES.len();
842 SUFFIX_NAMES[idx]
843 }
844
845 fn roll_rarity(&mut self, level: u32, magic_find: f32) -> ItemRarity {
846 let mf_bonus = magic_find * 0.001;
847 let r = self.rand_f32(0.0, 1.0) - mf_bonus;
848 let adjust = 1.0 - (level as f32 * 0.005).min(0.3);
849 if r < 0.001 * adjust { ItemRarity::Mythic }
850 else if r < 0.01 * adjust { ItemRarity::Legendary }
851 else if r < 0.05 * adjust { ItemRarity::Epic }
852 else if r < 0.15 * adjust { ItemRarity::Rare }
853 else if r < 0.35 * adjust { ItemRarity::Uncommon }
854 else { ItemRarity::Common }
855 }
856
857 fn make_affix_for_weapon(&mut self, kind: AffixKind, tier: u8) -> Affix {
858 let stats = [
859 StatKind::Strength, StatKind::Dexterity, StatKind::PhysicalAttack,
860 StatKind::CritChance, StatKind::AttackSpeed, StatKind::LifeSteal,
861 ];
862 let stat = stats[self.next_rand() as usize % stats.len()];
863 let value = self.rand_f32(1.0 * tier as f32, 5.0 * tier as f32);
864 let name = if kind == AffixKind::Prefix {
865 self.pick_prefix().to_string()
866 } else {
867 self.pick_suffix().to_string()
868 };
869 Affix::new(kind, tier, name)
870 .add_modifier(StatModifier::flat("item_affix", stat, value))
871 }
872
873 fn make_affix_for_armor(&mut self, kind: AffixKind, tier: u8) -> Affix {
874 let stats = [
875 StatKind::Constitution, StatKind::Vitality, StatKind::Defense,
876 StatKind::MagicResist, StatKind::BlockChance, StatKind::Evasion,
877 ];
878 let stat = stats[self.next_rand() as usize % stats.len()];
879 let value = self.rand_f32(1.0 * tier as f32, 5.0 * tier as f32);
880 let name = if kind == AffixKind::Prefix {
881 self.pick_prefix().to_string()
882 } else {
883 self.pick_suffix().to_string()
884 };
885 Affix::new(kind, tier, name)
886 .add_modifier(StatModifier::flat("item_affix", stat, value))
887 }
888
889 pub fn generate_weapon(&mut self, level: u32, magic_find: f32) -> Item {
890 let weapon_types = [
891 WeaponType::Sword, WeaponType::Dagger, WeaponType::Axe, WeaponType::Mace,
892 WeaponType::Staff, WeaponType::Wand, WeaponType::Bow, WeaponType::Spear,
893 ];
894 let wt = weapon_types[self.next_rand() as usize % weapon_types.len()];
895 let mut weapon_data = WeaponData::new(wt);
896 let scale = 1.0 + level as f32 * 0.1;
897 weapon_data.damage_min *= scale;
898 weapon_data.damage_max *= scale;
899
900 let rarity = self.roll_rarity(level, magic_find);
901 let (min_aff, max_aff) = rarity.affix_count_range();
902 let affix_count = self.rand_range(min_aff as u64, (max_aff + 1) as u64) as usize;
903
904 let tier = ((level / 10) as u8).max(1);
905 let base_name = format!("{:?}", wt);
906 let prefix = if affix_count > 0 { format!("{} ", self.pick_prefix()) } else { String::new() };
907 let suffix = if affix_count > 1 { format!(" {}", self.pick_suffix()) } else { String::new() };
908 let full_name = format!("{}{}{}", prefix, base_name, suffix);
909
910 let id = self.next_id();
911 let base_value = (level as u32 * 10 + 50) * rarity.sell_multiplier() as u32;
912
913 let mut item = Item::new(id, full_name, ItemType::Weapon)
914 .with_icon('†')
915 .with_rarity(rarity)
916 .with_value(base_value)
917 .with_weight(weapon_data.two_handed.then_some(3.5).unwrap_or(1.5))
918 .with_weapon(weapon_data);
919
920 for i in 0..affix_count {
921 let kind = if i % 2 == 0 { AffixKind::Prefix } else { AffixKind::Suffix };
922 let affix = self.make_affix_for_weapon(kind, tier);
923 for m in affix.stat_modifiers.clone() {
924 item.stat_modifiers.push(m);
925 }
926 item.affixes.push(affix);
927 }
928
929 item
930 }
931
932 pub fn generate_armor(&mut self, level: u32, slot: EquipSlot, magic_find: f32) -> Item {
933 let defense_base = level as f32 * 3.0 + 5.0;
934 let mr_base = level as f32 * 1.5 + 2.0;
935 let weight_class = match self.next_rand() % 4 {
936 0 => ArmorWeightClass::Robes,
937 1 => ArmorWeightClass::Light,
938 2 => ArmorWeightClass::Medium,
939 _ => ArmorWeightClass::Heavy,
940 };
941 let armor_data = ArmorData::new(defense_base, mr_base, weight_class);
942
943 let rarity = self.roll_rarity(level, magic_find);
944 let (min_aff, max_aff) = rarity.affix_count_range();
945 let affix_count = self.rand_range(min_aff as u64, (max_aff + 1) as u64) as usize;
946
947 let tier = ((level / 10) as u8).max(1);
948 let slot_name = slot.display_name();
949 let prefix = if affix_count > 0 { format!("{} ", self.pick_prefix()) } else { String::new() };
950 let suffix = if affix_count > 1 { format!(" {}", self.pick_suffix()) } else { String::new() };
951 let full_name = format!("{}{}{}", prefix, slot_name, suffix);
952
953 let id = self.next_id();
954 let icon = match slot {
955 EquipSlot::Head => '^',
956 EquipSlot::Chest => 'Ω',
957 EquipSlot::Legs => '|',
958 EquipSlot::Feet => '_',
959 _ => '#',
960 };
961
962 let base_value = (level as u32 * 8 + 30) * rarity.sell_multiplier() as u32;
963
964 let mut item = Item::new(id, full_name, ItemType::Armor)
965 .with_icon(icon)
966 .with_rarity(rarity)
967 .with_value(base_value)
968 .with_weight(match weight_class {
969 ArmorWeightClass::Robes => 0.5,
970 ArmorWeightClass::Light => 1.0,
971 ArmorWeightClass::Medium => 2.0,
972 ArmorWeightClass::Heavy => 4.0,
973 })
974 .with_slot(slot)
975 .with_armor(armor_data);
976
977 for i in 0..affix_count {
978 let kind = if i % 2 == 0 { AffixKind::Prefix } else { AffixKind::Suffix };
979 let affix = self.make_affix_for_armor(kind, tier);
980 for m in affix.stat_modifiers.clone() {
981 item.stat_modifiers.push(m);
982 }
983 item.affixes.push(affix);
984 }
985
986 item
987 }
988
989 pub fn generate_consumable(&mut self, level: u32) -> Item {
990 let id = self.next_id();
991 let heal_amount = level as f32 * 15.0 + 50.0;
992 let (name, icon, effect) = match self.next_rand() % 4 {
993 0 => ("Health Potion", '!', ConsumableEffect::HealHp(heal_amount)),
994 1 => ("Mana Potion", '!', ConsumableEffect::HealMp(heal_amount * 0.8)),
995 2 => ("Stamina Draught", '!', ConsumableEffect::HealStamina(heal_amount)),
996 _ => ("Elixir of Strength", '!', ConsumableEffect::Buff {
997 modifier: StatModifier::flat("elixir", StatKind::Strength, 5.0 + level as f32),
998 duration_secs: 60.0,
999 }),
1000 };
1001 let mut item = Item::new(id, name, ItemType::Consumable)
1002 .with_icon(icon)
1003 .with_rarity(ItemRarity::Common)
1004 .with_value(level as u32 * 5 + 10)
1005 .with_weight(0.1);
1006 item.max_stack = 99;
1007 item.consumable_effects.push(effect);
1008 item
1009 }
1010}
1011
1012#[derive(Debug, Clone)]
1017pub struct LootEntry {
1018 pub item_id: Option<u64>,
1019 pub weight: f32,
1020 pub min_count: u32,
1021 pub max_count: u32,
1022 pub generator_type: Option<String>,
1023}
1024
1025impl LootEntry {
1026 pub fn item(item_id: u64, weight: f32) -> Self {
1027 Self { item_id: Some(item_id), weight, min_count: 1, max_count: 1, generator_type: None }
1028 }
1029
1030 pub fn generated(kind: impl Into<String>, weight: f32) -> Self {
1031 Self { item_id: None, weight, min_count: 1, max_count: 1, generator_type: Some(kind.into()) }
1032 }
1033
1034 pub fn with_count(mut self, min: u32, max: u32) -> Self {
1035 self.min_count = min;
1036 self.max_count = max;
1037 self
1038 }
1039}
1040
1041#[derive(Debug, Clone)]
1042pub struct LootTable {
1043 pub entries: Vec<LootEntry>,
1044 pub min_drops: u32,
1045 pub max_drops: u32,
1046 pub gold_min: u32,
1047 pub gold_max: u32,
1048}
1049
1050impl LootTable {
1051 pub fn new(min_drops: u32, max_drops: u32) -> Self {
1052 Self {
1053 entries: Vec::new(),
1054 min_drops,
1055 max_drops,
1056 gold_min: 0,
1057 gold_max: 0,
1058 }
1059 }
1060
1061 pub fn add_entry(mut self, entry: LootEntry) -> Self {
1062 self.entries.push(entry);
1063 self
1064 }
1065
1066 pub fn with_gold(mut self, min: u32, max: u32) -> Self {
1067 self.gold_min = min;
1068 self.gold_max = max;
1069 self
1070 }
1071
1072 pub fn total_weight(&self) -> f32 {
1073 self.entries.iter().map(|e| e.weight).sum()
1074 }
1075
1076 pub fn roll(&self, seed: u64, magic_find: f32) -> Vec<usize> {
1078 let mut s = seed;
1079 let xorshift = |s: &mut u64| {
1080 *s ^= *s << 13; *s ^= *s >> 7; *s ^= *s << 17; *s
1081 };
1082
1083 let mf_scale = 1.0 + magic_find * 0.001;
1084 let drops_range = self.max_drops.saturating_sub(self.min_drops) + 1;
1085 let drops = self.min_drops + (xorshift(&mut s) % drops_range as u64) as u32;
1086
1087 let mut result = Vec::new();
1088 let total_w = self.total_weight();
1089 if total_w <= 0.0 { return result; }
1090
1091 for _ in 0..drops {
1092 let r = (xorshift(&mut s) % 100000) as f32 / 100000.0 * total_w / mf_scale;
1093 let mut acc = 0.0f32;
1094 for (i, entry) in self.entries.iter().enumerate() {
1095 acc += entry.weight;
1096 if r < acc {
1097 result.push(i);
1098 break;
1099 }
1100 }
1101 }
1102 result
1103 }
1104}
1105
1106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1111pub enum CraftingStation {
1112 Forge,
1113 AlchemyTable,
1114 EnchantingTable,
1115 CookingFire,
1116 Workbench,
1117 ArcaneAnvil,
1118}
1119
1120impl CraftingStation {
1121 pub fn display_name(&self) -> &'static str {
1122 match self {
1123 CraftingStation::Forge => "Forge",
1124 CraftingStation::AlchemyTable => "Alchemy Table",
1125 CraftingStation::EnchantingTable => "Enchanting Table",
1126 CraftingStation::CookingFire => "Cooking Fire",
1127 CraftingStation::Workbench => "Workbench",
1128 CraftingStation::ArcaneAnvil => "Arcane Anvil",
1129 }
1130 }
1131}
1132
1133#[derive(Debug, Clone)]
1134pub struct RecipeIngredient {
1135 pub item_id: u64,
1136 pub count: u32,
1137}
1138
1139impl RecipeIngredient {
1140 pub fn new(item_id: u64, count: u32) -> Self {
1141 Self { item_id, count }
1142 }
1143}
1144
1145#[derive(Debug, Clone)]
1146pub struct Recipe {
1147 pub id: u64,
1148 pub name: String,
1149 pub ingredients: Vec<RecipeIngredient>,
1150 pub result_item_id: u64,
1151 pub result_count: u32,
1152 pub required_level: u32,
1153 pub success_chance: f32,
1154 pub station: CraftingStation,
1155 pub xp_reward: u64,
1156}
1157
1158impl Recipe {
1159 pub fn new(id: u64, name: impl Into<String>, station: CraftingStation) -> Self {
1160 Self {
1161 id,
1162 name: name.into(),
1163 ingredients: Vec::new(),
1164 result_item_id: 0,
1165 result_count: 1,
1166 required_level: 1,
1167 success_chance: 1.0,
1168 station,
1169 xp_reward: 10,
1170 }
1171 }
1172
1173 pub fn add_ingredient(mut self, item_id: u64, count: u32) -> Self {
1174 self.ingredients.push(RecipeIngredient::new(item_id, count));
1175 self
1176 }
1177
1178 pub fn with_result(mut self, item_id: u64, count: u32) -> Self {
1179 self.result_item_id = item_id;
1180 self.result_count = count;
1181 self
1182 }
1183
1184 pub fn with_success_chance(mut self, chance: f32) -> Self {
1185 self.success_chance = chance.clamp(0.0, 1.0);
1186 self
1187 }
1188
1189 pub fn with_level(mut self, level: u32) -> Self {
1190 self.required_level = level;
1191 self
1192 }
1193
1194 pub fn can_craft(&self, inventory: &Inventory, player_level: u32) -> bool {
1195 if player_level < self.required_level { return false; }
1196 for ingredient in &self.ingredients {
1197 let total: u32 = inventory.items.iter().flatten()
1198 .filter(|i| i.id == ingredient.item_id)
1199 .map(|i| i.stack_size)
1200 .sum();
1201 if total < ingredient.count { return false; }
1202 }
1203 true
1204 }
1205}
1206
1207#[derive(Debug, Clone, Default)]
1208pub struct CraftingSystem {
1209 pub recipes: HashMap<u64, Recipe>,
1210}
1211
1212impl CraftingSystem {
1213 pub fn new() -> Self {
1214 Self { recipes: HashMap::new() }
1215 }
1216
1217 pub fn register_recipe(&mut self, recipe: Recipe) {
1218 self.recipes.insert(recipe.id, recipe);
1219 }
1220
1221 pub fn available_recipes(&self, station: CraftingStation, inventory: &Inventory, level: u32) -> Vec<&Recipe> {
1222 self.recipes.values()
1223 .filter(|r| r.station == station && r.can_craft(inventory, level))
1224 .collect()
1225 }
1226
1227 pub fn all_for_station(&self, station: CraftingStation) -> Vec<&Recipe> {
1228 self.recipes.values()
1229 .filter(|r| r.station == station)
1230 .collect()
1231 }
1232
1233 pub fn try_craft(&self, recipe_id: u64, inventory: &mut Inventory, level: u32, seed: u64) -> bool {
1236 let recipe = match self.recipes.get(&recipe_id) {
1237 Some(r) => r.clone(),
1238 None => return false,
1239 };
1240 if !recipe.can_craft(inventory, level) { return false; }
1241
1242 let mut s = seed;
1244 s ^= s << 13; s ^= s >> 7; s ^= s << 17;
1245 let r = (s % 100000) as f32 / 100000.0;
1246 if r > recipe.success_chance { return false; }
1247
1248 for ingredient in &recipe.ingredients {
1250 let mut remaining = ingredient.count;
1251 for slot in inventory.items.iter_mut() {
1252 if remaining == 0 { break; }
1253 if let Some(item) = slot {
1254 if item.id == ingredient.item_id {
1255 if item.stack_size <= remaining {
1256 remaining -= item.stack_size;
1257 *slot = None;
1258 } else {
1259 item.stack_size -= remaining;
1260 remaining = 0;
1261 }
1262 }
1263 }
1264 }
1265 }
1266 true
1267 }
1268}
1269
1270#[derive(Debug, Clone)]
1275pub struct ShopItem {
1276 pub item: Item,
1277 pub stock: Option<u32>, pub buy_price_override: Option<u32>,
1279}
1280
1281impl ShopItem {
1282 pub fn new(item: Item) -> Self {
1283 Self { item, stock: None, buy_price_override: None }
1284 }
1285
1286 pub fn with_stock(mut self, count: u32) -> Self {
1287 self.stock = Some(count);
1288 self
1289 }
1290
1291 pub fn buy_price(&self) -> u32 {
1292 self.buy_price_override.unwrap_or(self.item.value)
1293 }
1294
1295 pub fn sell_price(&self) -> u32 {
1296 (self.item.sell_value() as f32 * 0.3) as u32
1297 }
1298}
1299
1300#[derive(Debug, Clone)]
1301pub struct TradeSystem {
1302 pub shop_name: String,
1303 pub stock: Vec<ShopItem>,
1304 pub buy_multiplier: f32, pub sell_multiplier: f32, pub reputation_discount: f32, }
1308
1309impl TradeSystem {
1310 pub fn new(name: impl Into<String>) -> Self {
1311 Self {
1312 shop_name: name.into(),
1313 stock: Vec::new(),
1314 buy_multiplier: 1.2,
1315 sell_multiplier: 0.3,
1316 reputation_discount: 0.001,
1317 }
1318 }
1319
1320 pub fn add_item(&mut self, item: ShopItem) {
1321 self.stock.push(item);
1322 }
1323
1324 pub fn price_to_buy(&self, item: &Item, reputation: i32) -> u32 {
1325 let discount = (reputation as f32 * self.reputation_discount).min(0.5);
1326 ((item.value as f32 * self.buy_multiplier) * (1.0 - discount)) as u32
1327 }
1328
1329 pub fn price_to_sell(&self, item: &Item, reputation: i32) -> u32 {
1330 let bonus = (reputation as f32 * self.reputation_discount * 0.5).min(0.3);
1331 ((item.value as f32 * self.sell_multiplier) * (1.0 + bonus)) as u32
1332 }
1333
1334 pub fn can_afford(price: u32, gold: u64) -> bool {
1335 gold >= price as u64
1336 }
1337
1338 pub fn buy_from_shop(&mut self, stock_idx: usize, gold: &mut u64, inventory: &mut Inventory, reputation: i32) -> bool {
1339 if stock_idx >= self.stock.len() { return false; }
1340 let price = self.price_to_buy(&self.stock[stock_idx].item, reputation);
1341 if *gold < price as u64 { return false; }
1342
1343 let item = self.stock[stock_idx].item.clone();
1344 match inventory.add_item(item) {
1345 Ok(_) => {
1346 *gold -= price as u64;
1347 if let Some(ref mut stock) = self.stock[stock_idx].stock {
1348 if *stock > 0 { *stock -= 1; }
1349 }
1350 true
1351 }
1352 Err(_) => false,
1353 }
1354 }
1355
1356 pub fn sell_to_shop(&mut self, item: Item, gold: &mut u64, reputation: i32) {
1357 let price = self.price_to_sell(&item, reputation);
1358 *gold += price as u64;
1359 let mut shop_item = ShopItem::new(item);
1361 shop_item.stock = Some(1);
1362 self.stock.push(shop_item);
1363 }
1364}
1365
1366#[cfg(test)]
1371mod tests {
1372 use super::*;
1373
1374 fn make_sword() -> Item {
1375 Item::new(1, "Iron Sword", ItemType::Weapon)
1376 .with_icon('†')
1377 .with_weapon(WeaponData::new(WeaponType::Sword))
1378 .with_value(50)
1379 }
1380
1381 fn make_potion() -> Item {
1382 let mut item = Item::new(2, "Health Potion", ItemType::Consumable)
1383 .with_icon('!')
1384 .with_value(20);
1385 item.max_stack = 10;
1386 item.consumable_effects.push(ConsumableEffect::HealHp(50.0));
1387 item
1388 }
1389
1390 #[test]
1391 fn test_item_sell_value_scales_with_rarity() {
1392 let common = Item::new(1, "A", ItemType::Material).with_rarity(ItemRarity::Common).with_value(100);
1393 let legendary = Item::new(2, "B", ItemType::Material).with_rarity(ItemRarity::Legendary).with_value(100);
1394 assert!(legendary.sell_value() > common.sell_value());
1395 }
1396
1397 #[test]
1398 fn test_inventory_add_and_remove() {
1399 let mut inv = Inventory::new(10, 100.0);
1400 let sword = make_sword();
1401 let slot = inv.add_item(sword).unwrap();
1402 assert!(inv.get(slot).is_some());
1403 let removed = inv.remove_item(slot);
1404 assert!(removed.is_some());
1405 assert!(inv.get(slot).is_none());
1406 }
1407
1408 #[test]
1409 fn test_inventory_weight_limit() {
1410 let mut inv = Inventory::new(10, 1.0); let mut heavy = make_sword();
1412 heavy.weight = 2.0;
1413 assert!(inv.add_item(heavy).is_err());
1414 }
1415
1416 #[test]
1417 fn test_inventory_stacking() {
1418 let mut inv = Inventory::new(10, 1000.0);
1419 let potion = make_potion();
1420 let mut potion2 = make_potion();
1421 potion2.stack_size = 3;
1422 inv.add_item(potion).unwrap();
1423 inv.add_item(potion2).unwrap();
1424 let stacked = inv.get(0).unwrap();
1426 assert_eq!(stacked.stack_size, 4);
1427 }
1428
1429 #[test]
1430 fn test_inventory_sort() {
1431 let mut inv = Inventory::new(10, 1000.0);
1432 let common = Item::new(1, "Z Common", ItemType::Material).with_rarity(ItemRarity::Common).with_value(1);
1433 let rare = Item::new(2, "A Rare", ItemType::Material).with_rarity(ItemRarity::Rare).with_value(10);
1434 inv.add_item(common).unwrap();
1435 inv.add_item(rare).unwrap();
1436 inv.sort();
1437 assert_eq!(inv.get(0).unwrap().rarity, ItemRarity::Rare);
1439 }
1440
1441 #[test]
1442 fn test_equipped_items_weapon_damage() {
1443 let mut equipped = EquippedItems::new();
1444 let sword = make_sword();
1445 equipped.equip(sword);
1446 assert!(equipped.weapon_damage() > 0.0);
1447 }
1448
1449 #[test]
1450 fn test_equipped_items_unequip() {
1451 let mut equipped = EquippedItems::new();
1452 let sword = make_sword();
1453 equipped.equip(sword);
1454 let removed = equipped.unequip(EquipSlot::MainHand);
1455 assert!(removed.is_some());
1456 assert!(equipped.get(EquipSlot::MainHand).is_none());
1457 }
1458
1459 #[test]
1460 fn test_equipped_items_swap() {
1461 let mut equipped = EquippedItems::new();
1462 let sword1 = make_sword();
1463 let mut sword2 = make_sword();
1464 sword2.name = "Better Sword".to_string();
1465 sword2.id = 99;
1466 equipped.equip(sword1);
1467 let old = equipped.equip(sword2);
1468 assert!(old.is_some());
1469 assert_eq!(old.unwrap().name, "Iron Sword");
1470 }
1471
1472 #[test]
1473 fn test_item_generator_weapon() {
1474 let mut gen = ItemGenerator::new(42);
1475 let weapon = gen.generate_weapon(10, 0.0);
1476 assert!(weapon.weapon_data.is_some());
1477 assert!(weapon.value > 0);
1478 }
1479
1480 #[test]
1481 fn test_item_generator_rarity_scaling() {
1482 let mut gen = ItemGenerator::new(999);
1483 let mut legendary_count = 0;
1484 for i in 0..1000 {
1485 let w = gen.generate_weapon(100, 500.0);
1486 if w.rarity >= ItemRarity::Epic { legendary_count += 1; }
1487 let _ = i;
1488 }
1489 assert!(legendary_count > 0, "Expected at least some epic+ items at high level/magic find");
1490 }
1491
1492 #[test]
1493 fn test_loot_table_roll() {
1494 let table = LootTable::new(1, 3)
1495 .add_entry(LootEntry::item(1, 50.0))
1496 .add_entry(LootEntry::item(2, 30.0))
1497 .add_entry(LootEntry::item(3, 20.0));
1498 let drops = table.roll(12345, 0.0);
1499 assert!(!drops.is_empty());
1500 }
1501
1502 #[test]
1503 fn test_crafting_can_craft() {
1504 let mut sys = CraftingSystem::new();
1505 let recipe = Recipe::new(1, "Iron Blade", CraftingStation::Forge)
1506 .add_ingredient(10, 3)
1507 .with_result(20, 1);
1508 sys.register_recipe(recipe);
1509
1510 let mut inv = Inventory::new(10, 100.0);
1511 let mut ore = Item::new(10, "Iron Ore", ItemType::Material);
1512 ore.max_stack = 99;
1513 ore.stack_size = 5;
1514 inv.add_item(ore).unwrap();
1515
1516 let available = sys.available_recipes(CraftingStation::Forge, &inv, 1);
1517 assert_eq!(available.len(), 1);
1518 }
1519
1520 #[test]
1521 fn test_trade_buy_sell() {
1522 let mut shop = TradeSystem::new("Blacksmith");
1523 let sword = make_sword();
1524 shop.add_item(ShopItem::new(sword));
1525 let mut gold = 1000u64;
1526 let mut inv = Inventory::new(10, 100.0);
1527 let bought = shop.buy_from_shop(0, &mut gold, &mut inv, 0);
1528 assert!(bought);
1529 assert!(gold < 1000);
1530 }
1531
1532 #[test]
1533 fn test_stash_deposit_withdraw() {
1534 let mut stash = Stash::new();
1535 let sword = make_sword();
1536 let slot = stash.deposit(0, sword).unwrap();
1537 let item = stash.withdraw(0, slot);
1538 assert!(item.is_some());
1539 }
1540
1541 #[test]
1542 fn test_armor_set_bonuses() {
1543 let set = ArmorSet::new(1, "Warrior Set")
1544 .add_piece(100)
1545 .add_piece(101)
1546 .add_bonus(2, vec![StatModifier::flat("set", StatKind::Strength, 20.0)]);
1547 assert_eq!(set.active_bonuses(1).len(), 0);
1548 assert_eq!(set.active_bonuses(2).len(), 1);
1549 }
1550}