Skip to main content

proof_engine/character/
inventory.rs

1// src/character/inventory.rs
2// Item system, equipment, crafting, loot tables, and trade.
3
4use std::collections::HashMap;
5use crate::character::stats::{StatKind, StatModifier, ModifierKind};
6
7// ---------------------------------------------------------------------------
8// ItemRarity
9// ---------------------------------------------------------------------------
10
11#[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// ---------------------------------------------------------------------------
68// ItemType
69// ---------------------------------------------------------------------------
70
71#[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// Needed for the match above — add Ammunition variant
99#[allow(dead_code)]
100enum _AmmunitionHelper { Ammunition }
101
102// ---------------------------------------------------------------------------
103// WeaponType & WeaponData
104// ---------------------------------------------------------------------------
105
106#[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// ---------------------------------------------------------------------------
216// ArmorData
217// ---------------------------------------------------------------------------
218
219#[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// ---------------------------------------------------------------------------
251// ArmorSet — set bonuses
252// ---------------------------------------------------------------------------
253
254#[derive(Debug, Clone)]
255pub struct ArmorSet {
256    pub id: u32,
257    pub name: String,
258    pub pieces: Vec<u64>, // item IDs in the set
259    pub bonuses: Vec<(usize, Vec<StatModifier>)>, // (pieces_required, modifiers)
260}
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// ---------------------------------------------------------------------------
289// EquipSlot
290// ---------------------------------------------------------------------------
291
292#[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// ---------------------------------------------------------------------------
337// ConsumableEffect
338// ---------------------------------------------------------------------------
339
340#[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// ---------------------------------------------------------------------------
357// Item
358// ---------------------------------------------------------------------------
359
360#[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// ---------------------------------------------------------------------------
478// Affix system for procedural items
479// ---------------------------------------------------------------------------
480
481#[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// ---------------------------------------------------------------------------
508// EquippedItems
509// ---------------------------------------------------------------------------
510
511#[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    /// Equip an item. Returns the item that was previously in the slot, if any.
522    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    /// Unequip slot, returning the item.
530    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// ---------------------------------------------------------------------------
577// Inventory — slot-based item storage
578// ---------------------------------------------------------------------------
579
580#[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        // Try stacking first
598        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        // Check weight
616        if self.current_weight() + item.weight > self.max_weight {
617            return Err(item);
618        }
619
620        // Find first empty slot
621        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) // No space
629    }
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    /// Sort inventory: by rarity desc, then name
671    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    /// Try to stack all stackable items together
684    pub fn stack_items(&mut self) {
685        // Collect all stackable items
686        let mut stacks: HashMap<u64, (u32, u32)> = HashMap::new(); // id -> (total, max_stack)
687        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                    // Preserve the template by keeping the item around (we re-create stacks below)
694                    non_stackable.push(item); // temporarily hold it
695                } else {
696                    non_stackable.push(item);
697                }
698            }
699        }
700        // Rebuild: non-stackable first, then stacks
701        // This is simplified — in practice we'd preserve items properly
702        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// ---------------------------------------------------------------------------
718// Stash — extended storage with named tabs
719// ---------------------------------------------------------------------------
720
721#[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
785// ---------------------------------------------------------------------------
786// Procedural Item Generation
787// ---------------------------------------------------------------------------
788
789static 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        // Simple xorshift64
813        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// ---------------------------------------------------------------------------
1013// LootTable
1014// ---------------------------------------------------------------------------
1015
1016#[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    /// Roll the loot table with a seed, returns indices of selected entries.
1077    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// ---------------------------------------------------------------------------
1107// CraftingSystem
1108// ---------------------------------------------------------------------------
1109
1110#[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    /// Attempt to craft. Returns true on success.
1234    /// On success, ingredients are consumed from inventory and result added.
1235    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        // Check success
1243        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        // Consume ingredients
1249        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// ---------------------------------------------------------------------------
1271// TradeSystem — shop buying/selling
1272// ---------------------------------------------------------------------------
1273
1274#[derive(Debug, Clone)]
1275pub struct ShopItem {
1276    pub item: Item,
1277    pub stock: Option<u32>, // None = unlimited
1278    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, // How much the shop charges relative to value
1305    pub sell_multiplier: f32, // How much the shop pays relative to value
1306    pub reputation_discount: f32, // Per reputation point
1307}
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        // Add to shop stock with sell price as value
1360        let mut shop_item = ShopItem::new(item);
1361        shop_item.stock = Some(1);
1362        self.stock.push(shop_item);
1363    }
1364}
1365
1366// ---------------------------------------------------------------------------
1367// Tests
1368// ---------------------------------------------------------------------------
1369
1370#[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); // only 1 unit of weight
1411        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        // Should be stacked in slot 0
1425        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        // Rare should come first
1438        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}