Skip to main content

proof_engine/editor/
inventory_editor.rs

1#[allow(dead_code, unused_variables, unused_mut, unused_imports)]
2
3use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
4use std::collections::{HashMap, VecDeque, HashSet, BTreeMap};
5
6// ============================================================
7// ITEM CATEGORY ENUM (40+ types)
8// ============================================================
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum ItemCategory {
12    // Melee Weapons
13    Sword,
14    Greatsword,
15    Dagger,
16    Axe,
17    Greataxe,
18    Mace,
19    Hammer,
20    Spear,
21    Polearm,
22    Whip,
23    // Ranged Weapons
24    Bow,
25    Crossbow,
26    Thrown,
27    Gun,
28    Rifle,
29    // Magic Weapons
30    Staff,
31    Wand,
32    Orb,
33    Tome,
34    // Shields
35    Shield,
36    Buckler,
37    TowerShield,
38    // Head Armor
39    Helmet,
40    Hood,
41    Crown,
42    // Body Armor
43    Chestplate,
44    Robe,
45    Leather,
46    // Leg Armor
47    Greaves,
48    Leggings,
49    Pants,
50    // Hand/Foot
51    Gauntlets,
52    Gloves,
53    Boots,
54    Sabatons,
55    // Accessories
56    Ring,
57    Amulet,
58    Belt,
59    Cloak,
60    // Consumables
61    Potion,
62    Food,
63    Scroll,
64    Bomb,
65    // Materials
66    Ore,
67    Gem,
68    Herb,
69    Hide,
70    Wood,
71    Cloth,
72    // Misc
73    QuestItem,
74    Key,
75    Currency,
76    Junk,
77}
78
79impl ItemCategory {
80    pub fn display_name(&self) -> &'static str {
81        match self {
82            ItemCategory::Sword => "Sword",
83            ItemCategory::Greatsword => "Greatsword",
84            ItemCategory::Dagger => "Dagger",
85            ItemCategory::Axe => "Axe",
86            ItemCategory::Greataxe => "Greataxe",
87            ItemCategory::Mace => "Mace",
88            ItemCategory::Hammer => "Hammer",
89            ItemCategory::Spear => "Spear",
90            ItemCategory::Polearm => "Polearm",
91            ItemCategory::Whip => "Whip",
92            ItemCategory::Bow => "Bow",
93            ItemCategory::Crossbow => "Crossbow",
94            ItemCategory::Thrown => "Thrown",
95            ItemCategory::Gun => "Gun",
96            ItemCategory::Rifle => "Rifle",
97            ItemCategory::Staff => "Staff",
98            ItemCategory::Wand => "Wand",
99            ItemCategory::Orb => "Orb",
100            ItemCategory::Tome => "Tome",
101            ItemCategory::Shield => "Shield",
102            ItemCategory::Buckler => "Buckler",
103            ItemCategory::TowerShield => "Tower Shield",
104            ItemCategory::Helmet => "Helmet",
105            ItemCategory::Hood => "Hood",
106            ItemCategory::Crown => "Crown",
107            ItemCategory::Chestplate => "Chestplate",
108            ItemCategory::Robe => "Robe",
109            ItemCategory::Leather => "Leather Armor",
110            ItemCategory::Greaves => "Greaves",
111            ItemCategory::Leggings => "Leggings",
112            ItemCategory::Pants => "Pants",
113            ItemCategory::Gauntlets => "Gauntlets",
114            ItemCategory::Gloves => "Gloves",
115            ItemCategory::Boots => "Boots",
116            ItemCategory::Sabatons => "Sabatons",
117            ItemCategory::Ring => "Ring",
118            ItemCategory::Amulet => "Amulet",
119            ItemCategory::Belt => "Belt",
120            ItemCategory::Cloak => "Cloak",
121            ItemCategory::Potion => "Potion",
122            ItemCategory::Food => "Food",
123            ItemCategory::Scroll => "Scroll",
124            ItemCategory::Bomb => "Bomb",
125            ItemCategory::Ore => "Ore",
126            ItemCategory::Gem => "Gem",
127            ItemCategory::Herb => "Herb",
128            ItemCategory::Hide => "Hide",
129            ItemCategory::Wood => "Wood",
130            ItemCategory::Cloth => "Cloth",
131            ItemCategory::QuestItem => "Quest Item",
132            ItemCategory::Key => "Key",
133            ItemCategory::Currency => "Currency",
134            ItemCategory::Junk => "Junk",
135        }
136    }
137
138    pub fn is_weapon(&self) -> bool {
139        matches!(self,
140            ItemCategory::Sword | ItemCategory::Greatsword | ItemCategory::Dagger |
141            ItemCategory::Axe | ItemCategory::Greataxe | ItemCategory::Mace |
142            ItemCategory::Hammer | ItemCategory::Spear | ItemCategory::Polearm |
143            ItemCategory::Whip | ItemCategory::Bow | ItemCategory::Crossbow |
144            ItemCategory::Thrown | ItemCategory::Gun | ItemCategory::Rifle |
145            ItemCategory::Staff | ItemCategory::Wand | ItemCategory::Orb | ItemCategory::Tome
146        )
147    }
148
149    pub fn is_armor(&self) -> bool {
150        matches!(self,
151            ItemCategory::Helmet | ItemCategory::Hood | ItemCategory::Crown |
152            ItemCategory::Chestplate | ItemCategory::Robe | ItemCategory::Leather |
153            ItemCategory::Greaves | ItemCategory::Leggings | ItemCategory::Pants |
154            ItemCategory::Gauntlets | ItemCategory::Gloves | ItemCategory::Boots |
155            ItemCategory::Sabatons | ItemCategory::Shield | ItemCategory::Buckler |
156            ItemCategory::TowerShield
157        )
158    }
159
160    pub fn is_accessory(&self) -> bool {
161        matches!(self,
162            ItemCategory::Ring | ItemCategory::Amulet | ItemCategory::Belt | ItemCategory::Cloak
163        )
164    }
165
166    pub fn is_consumable(&self) -> bool {
167        matches!(self,
168            ItemCategory::Potion | ItemCategory::Food | ItemCategory::Scroll | ItemCategory::Bomb
169        )
170    }
171
172    pub fn is_material(&self) -> bool {
173        matches!(self,
174            ItemCategory::Ore | ItemCategory::Gem | ItemCategory::Herb |
175            ItemCategory::Hide | ItemCategory::Wood | ItemCategory::Cloth
176        )
177    }
178
179    pub fn default_stack_size(&self) -> u32 {
180        if self.is_material() { 999 }
181        else if self.is_consumable() { 20 }
182        else if *self == ItemCategory::Currency { 99999 }
183        else if *self == ItemCategory::Junk { 10 }
184        else { 1 }
185    }
186
187    pub fn two_handed(&self) -> bool {
188        matches!(self,
189            ItemCategory::Greatsword | ItemCategory::Greataxe | ItemCategory::Hammer |
190            ItemCategory::Polearm | ItemCategory::Bow | ItemCategory::Rifle |
191            ItemCategory::Staff | ItemCategory::TowerShield
192        )
193    }
194}
195
196// ============================================================
197// RARITY SYSTEM
198// ============================================================
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
201pub enum Rarity {
202    Common,
203    Uncommon,
204    Rare,
205    Epic,
206    Legendary,
207    Mythic,
208}
209
210#[derive(Debug, Clone)]
211pub struct RarityConfig {
212    pub rarity: Rarity,
213    pub color: Vec4,
214    pub drop_weight: f32,
215    pub min_affixes: u32,
216    pub max_affixes: u32,
217    pub quality_min: f32,
218    pub quality_max: f32,
219    pub stat_multiplier: f32,
220    pub vendor_price_multiplier: f32,
221}
222
223impl RarityConfig {
224    pub fn for_rarity(rarity: Rarity) -> Self {
225        match rarity {
226            Rarity::Common => RarityConfig {
227                rarity,
228                color: Vec4::new(0.8, 0.8, 0.8, 1.0),
229                drop_weight: 10000.0,
230                min_affixes: 0,
231                max_affixes: 1,
232                quality_min: 0.5,
233                quality_max: 0.8,
234                stat_multiplier: 1.0,
235                vendor_price_multiplier: 1.0,
236            },
237            Rarity::Uncommon => RarityConfig {
238                rarity,
239                color: Vec4::new(0.1, 0.9, 0.1, 1.0),
240                drop_weight: 2000.0,
241                min_affixes: 1,
242                max_affixes: 2,
243                quality_min: 0.7,
244                quality_max: 0.9,
245                stat_multiplier: 1.2,
246                vendor_price_multiplier: 3.0,
247            },
248            Rarity::Rare => RarityConfig {
249                rarity,
250                color: Vec4::new(0.2, 0.4, 1.0, 1.0),
251                drop_weight: 400.0,
252                min_affixes: 2,
253                max_affixes: 4,
254                quality_min: 0.8,
255                quality_max: 1.0,
256                stat_multiplier: 1.5,
257                vendor_price_multiplier: 10.0,
258            },
259            Rarity::Epic => RarityConfig {
260                rarity,
261                color: Vec4::new(0.6, 0.1, 0.9, 1.0),
262                drop_weight: 80.0,
263                min_affixes: 3,
264                max_affixes: 5,
265                quality_min: 0.9,
266                quality_max: 1.1,
267                stat_multiplier: 2.0,
268                vendor_price_multiplier: 50.0,
269            },
270            Rarity::Legendary => RarityConfig {
271                rarity,
272                color: Vec4::new(1.0, 0.5, 0.0, 1.0),
273                drop_weight: 15.0,
274                min_affixes: 4,
275                max_affixes: 6,
276                quality_min: 1.0,
277                quality_max: 1.2,
278                stat_multiplier: 3.0,
279                vendor_price_multiplier: 200.0,
280            },
281            Rarity::Mythic => RarityConfig {
282                rarity,
283                color: Vec4::new(1.0, 0.0, 0.3, 1.0),
284                drop_weight: 2.0,
285                min_affixes: 5,
286                max_affixes: 8,
287                quality_min: 1.1,
288                quality_max: 1.5,
289                stat_multiplier: 5.0,
290                vendor_price_multiplier: 1000.0,
291            },
292        }
293    }
294
295    pub fn display_name(&self) -> &'static str {
296        match self.rarity {
297            Rarity::Common => "Common",
298            Rarity::Uncommon => "Uncommon",
299            Rarity::Rare => "Rare",
300            Rarity::Epic => "Epic",
301            Rarity::Legendary => "Legendary",
302            Rarity::Mythic => "Mythic",
303        }
304    }
305}
306
307// Alias method for O(1) weighted random sampling
308#[derive(Debug, Clone)]
309pub struct AliasTable {
310    pub prob: Vec<f64>,
311    pub alias: Vec<usize>,
312    pub n: usize,
313}
314
315impl AliasTable {
316    pub fn new(weights: &[f64]) -> Self {
317        let n = weights.len();
318        let total: f64 = weights.iter().sum();
319        let avg = total / n as f64;
320        let mut prob = vec![0.0f64; n];
321        let mut alias = vec![0usize; n];
322        let mut small: VecDeque<usize> = VecDeque::new();
323        let mut large: VecDeque<usize> = VecDeque::new();
324
325        for (i, &w) in weights.iter().enumerate() {
326            prob[i] = w / avg;
327            if prob[i] < 1.0 {
328                small.push_back(i);
329            } else {
330                large.push_back(i);
331            }
332        }
333
334        while !small.is_empty() && !large.is_empty() {
335            let s = small.pop_front().unwrap();
336            let l = large.pop_front().unwrap();
337            alias[s] = l;
338            prob[l] -= 1.0 - prob[s];
339            if prob[l] < 1.0 {
340                small.push_back(l);
341            } else {
342                large.push_back(l);
343            }
344        }
345        // remaining large/small due to floating point all set to 1
346        for l in large { prob[l] = 1.0; }
347        for s in small { prob[s] = 1.0; }
348
349        AliasTable { prob, alias, n }
350    }
351
352    pub fn sample(&self, u1: f64, u2: f64) -> usize {
353        let i = (u1 * self.n as f64) as usize % self.n;
354        if u2 < self.prob[i] { i } else { self.alias[i] }
355    }
356}
357
358// ============================================================
359// STAT MODIFIERS
360// ============================================================
361
362#[derive(Debug, Clone, PartialEq, Eq, Hash)]
363pub enum StatType {
364    Strength,
365    Dexterity,
366    Intelligence,
367    Vitality,
368    Endurance,
369    Luck,
370    AttackSpeed,
371    CastSpeed,
372    MoveSpeed,
373    CritChance,
374    CritMultiplier,
375    DodgeChance,
376    BlockChance,
377    LifeMax,
378    ManaMax,
379    StaminaMax,
380    LifeRegen,
381    ManaRegen,
382    FireResistance,
383    ColdResistance,
384    LightningResistance,
385    PoisonResistance,
386    PhysicalDamage,
387    FireDamage,
388    ColdDamage,
389    LightningDamage,
390    PoisonDamage,
391    PhysicalArmor,
392    MagicArmor,
393    ThornsMultiplier,
394    AreaRadius,
395    SkillDuration,
396    ResourceCostReduction,
397    CooldownReduction,
398    ExperienceGain,
399    GoldFind,
400    MagicFind,
401}
402
403impl StatType {
404    pub fn display_name(&self) -> &str {
405        match self {
406            StatType::Strength => "Strength",
407            StatType::Dexterity => "Dexterity",
408            StatType::Intelligence => "Intelligence",
409            StatType::Vitality => "Vitality",
410            StatType::Endurance => "Endurance",
411            StatType::Luck => "Luck",
412            StatType::AttackSpeed => "Attack Speed",
413            StatType::CastSpeed => "Cast Speed",
414            StatType::MoveSpeed => "Move Speed",
415            StatType::CritChance => "Critical Chance",
416            StatType::CritMultiplier => "Critical Multiplier",
417            StatType::DodgeChance => "Dodge Chance",
418            StatType::BlockChance => "Block Chance",
419            StatType::LifeMax => "Max Life",
420            StatType::ManaMax => "Max Mana",
421            StatType::StaminaMax => "Max Stamina",
422            StatType::LifeRegen => "Life Regen",
423            StatType::ManaRegen => "Mana Regen",
424            StatType::FireResistance => "Fire Resistance",
425            StatType::ColdResistance => "Cold Resistance",
426            StatType::LightningResistance => "Lightning Resistance",
427            StatType::PoisonResistance => "Poison Resistance",
428            StatType::PhysicalDamage => "Physical Damage",
429            StatType::FireDamage => "Fire Damage",
430            StatType::ColdDamage => "Cold Damage",
431            StatType::LightningDamage => "Lightning Damage",
432            StatType::PoisonDamage => "Poison Damage",
433            StatType::PhysicalArmor => "Physical Armor",
434            StatType::MagicArmor => "Magic Armor",
435            StatType::ThornsMultiplier => "Thorns",
436            StatType::AreaRadius => "Area Radius",
437            StatType::SkillDuration => "Skill Duration",
438            StatType::ResourceCostReduction => "Resource Cost Reduction",
439            StatType::CooldownReduction => "Cooldown Reduction",
440            StatType::ExperienceGain => "Experience Gain",
441            StatType::GoldFind => "Gold Find",
442            StatType::MagicFind => "Magic Find",
443        }
444    }
445
446    pub fn is_percentage(&self) -> bool {
447        matches!(self,
448            StatType::AttackSpeed | StatType::CastSpeed | StatType::MoveSpeed |
449            StatType::CritChance | StatType::CritMultiplier | StatType::DodgeChance |
450            StatType::BlockChance | StatType::LifeRegen | StatType::ManaRegen |
451            StatType::FireResistance | StatType::ColdResistance | StatType::LightningResistance |
452            StatType::PoisonResistance | StatType::CooldownReduction |
453            StatType::ResourceCostReduction | StatType::ExperienceGain |
454            StatType::GoldFind | StatType::MagicFind
455        )
456    }
457}
458
459#[derive(Debug, Clone)]
460pub struct StatModifier {
461    pub stat: StatType,
462    pub flat_value: f32,
463    pub percent_value: f32,
464    pub more_multiplier: f32, // multiplicative "more" modifier
465}
466
467impl StatModifier {
468    pub fn flat(stat: StatType, value: f32) -> Self {
469        StatModifier { stat, flat_value: value, percent_value: 0.0, more_multiplier: 1.0 }
470    }
471
472    pub fn percent(stat: StatType, value: f32) -> Self {
473        StatModifier { stat, flat_value: 0.0, percent_value: value, more_multiplier: 1.0 }
474    }
475
476    pub fn more(stat: StatType, multiplier: f32) -> Self {
477        StatModifier { stat, flat_value: 0.0, percent_value: 0.0, more_multiplier: multiplier }
478    }
479
480    pub fn apply(&self, base: f32) -> f32 {
481        (base + self.flat_value) * (1.0 + self.percent_value / 100.0) * self.more_multiplier
482    }
483}
484
485// ============================================================
486// REQUIREMENTS
487// ============================================================
488
489#[derive(Debug, Clone)]
490pub struct ItemRequirements {
491    pub level: u32,
492    pub strength: u32,
493    pub dexterity: u32,
494    pub intelligence: u32,
495    pub vitality: u32,
496    pub class_restrictions: Vec<String>,
497    pub quest_flag: Option<String>,
498    pub reputation_faction: Option<String>,
499    pub reputation_level: i32,
500}
501
502impl ItemRequirements {
503    pub fn none() -> Self {
504        ItemRequirements {
505            level: 1,
506            strength: 0,
507            dexterity: 0,
508            intelligence: 0,
509            vitality: 0,
510            class_restrictions: Vec::new(),
511            quest_flag: None,
512            reputation_faction: None,
513            reputation_level: 0,
514        }
515    }
516
517    pub fn meets_requirements(&self, player_level: u32, str: u32, dex: u32, int: u32, vit: u32) -> bool {
518        player_level >= self.level
519            && str >= self.strength
520            && dex >= self.dexterity
521            && int >= self.intelligence
522            && vit >= self.vitality
523    }
524}
525
526// ============================================================
527// ITEM DEFINITION — 30+ fields
528// ============================================================
529
530#[derive(Debug, Clone)]
531pub struct DamageRange {
532    pub min_damage: f32,
533    pub max_damage: f32,
534    pub damage_type: DamageType,
535    pub crit_multiplier: f32,
536}
537
538#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
539pub enum DamageType {
540    Physical,
541    Fire,
542    Cold,
543    Lightning,
544    Poison,
545    Arcane,
546    Holy,
547    Shadow,
548    Chaos,
549    True, // bypasses all resistance
550}
551
552impl DamageType {
553    pub fn color(&self) -> Vec4 {
554        match self {
555            DamageType::Physical => Vec4::new(0.8, 0.7, 0.6, 1.0),
556            DamageType::Fire => Vec4::new(1.0, 0.3, 0.0, 1.0),
557            DamageType::Cold => Vec4::new(0.3, 0.7, 1.0, 1.0),
558            DamageType::Lightning => Vec4::new(1.0, 1.0, 0.0, 1.0),
559            DamageType::Poison => Vec4::new(0.4, 0.9, 0.1, 1.0),
560            DamageType::Arcane => Vec4::new(0.8, 0.2, 1.0, 1.0),
561            DamageType::Holy => Vec4::new(1.0, 0.95, 0.7, 1.0),
562            DamageType::Shadow => Vec4::new(0.3, 0.0, 0.5, 1.0),
563            DamageType::Chaos => Vec4::new(0.6, 0.0, 0.2, 1.0),
564            DamageType::True => Vec4::new(1.0, 1.0, 1.0, 1.0),
565        }
566    }
567}
568
569#[derive(Debug, Clone)]
570pub struct ArmorValues {
571    pub physical_armor: f32,
572    pub magic_armor: f32,
573    pub fire_resist: f32,
574    pub cold_resist: f32,
575    pub lightning_resist: f32,
576    pub poison_resist: f32,
577    pub block_chance: f32,
578    pub block_amount: f32,
579    pub evasion_rating: f32,
580    pub energy_shield: f32,
581}
582
583impl ArmorValues {
584    pub fn zero() -> Self {
585        ArmorValues {
586            physical_armor: 0.0,
587            magic_armor: 0.0,
588            fire_resist: 0.0,
589            cold_resist: 0.0,
590            lightning_resist: 0.0,
591            poison_resist: 0.0,
592            block_chance: 0.0,
593            block_amount: 0.0,
594            evasion_rating: 0.0,
595            energy_shield: 0.0,
596        }
597    }
598
599    pub fn effective_mitigation(&self, damage_type: DamageType, raw_damage: f32) -> f32 {
600        let resist = match damage_type {
601            DamageType::Physical => self.physical_armor / (self.physical_armor + 300.0),
602            DamageType::Fire => (self.fire_resist / 100.0).min(0.75),
603            DamageType::Cold => (self.cold_resist / 100.0).min(0.75),
604            DamageType::Lightning => (self.lightning_resist / 100.0).min(0.75),
605            DamageType::Poison => (self.poison_resist / 100.0).min(0.75),
606            DamageType::Arcane => (self.magic_armor / (self.magic_armor + 200.0)).min(0.6),
607            DamageType::Holy | DamageType::Shadow | DamageType::Chaos => 0.0,
608            DamageType::True => 0.0,
609        };
610        raw_damage * (1.0 - resist)
611    }
612}
613
614#[derive(Debug, Clone)]
615pub struct ItemDefinition {
616    // Identity — fields 1–8
617    pub id: u64,
618    pub name: String,
619    pub description: String,
620    pub flavor_text: String,
621    pub category: ItemCategory,
622    pub rarity: Rarity,
623    pub icon_path: String,
624    pub model_path: String,
625    // Physical properties — fields 9–14
626    pub weight: f32,
627    pub volume: f32,
628    pub width: u8,   // grid width in cells
629    pub height: u8,  // grid height in cells
630    pub stack_size: u32,
631    pub durability_max: f32,
632    // Combat stats — fields 15–22
633    pub damage_ranges: Vec<DamageRange>,
634    pub attack_speed: f32,
635    pub range: f32,
636    pub armor_values: ArmorValues,
637    pub crit_chance: f32,
638    pub crit_multiplier: f32,
639    pub life_on_hit: f32,
640    pub mana_on_hit: f32,
641    // Level / economy — fields 23–28
642    pub item_level: u32,
643    pub required_level: u32,
644    pub base_price: u64,
645    pub sell_price_ratio: f32,
646    pub vendor_category: Option<String>,
647    pub bind_on_pickup: bool,
648    // Stat modifiers and requirements — fields 29–35
649    pub implicit_modifiers: Vec<StatModifier>,
650    pub explicit_modifiers: Vec<StatModifier>,
651    pub requirements: ItemRequirements,
652    pub set_id: Option<u64>,
653    pub unique_effect: Option<String>,
654    pub loot_filter_tier: u8,
655    pub drop_sound: String,
656}
657
658impl ItemDefinition {
659    pub fn new(id: u64, name: impl Into<String>, category: ItemCategory) -> Self {
660        let cat = category;
661        ItemDefinition {
662            id,
663            name: name.into(),
664            description: String::new(),
665            flavor_text: String::new(),
666            category: cat,
667            rarity: Rarity::Common,
668            icon_path: String::new(),
669            model_path: String::new(),
670            weight: 1.0,
671            volume: 1.0,
672            width: 1,
673            height: 1,
674            stack_size: cat.default_stack_size(),
675            durability_max: 100.0,
676            damage_ranges: Vec::new(),
677            attack_speed: 1.0,
678            range: 1.5,
679            armor_values: ArmorValues::zero(),
680            crit_chance: 5.0,
681            crit_multiplier: 1.5,
682            life_on_hit: 0.0,
683            mana_on_hit: 0.0,
684            item_level: 1,
685            required_level: 1,
686            base_price: 10,
687            sell_price_ratio: 0.25,
688            vendor_category: None,
689            bind_on_pickup: false,
690            implicit_modifiers: Vec::new(),
691            explicit_modifiers: Vec::new(),
692            requirements: ItemRequirements::none(),
693            set_id: None,
694            unique_effect: None,
695            loot_filter_tier: 1,
696            drop_sound: "item_drop_default".to_string(),
697        }
698    }
699
700    pub fn total_damage_per_second(&self) -> f32 {
701        let avg_damage: f32 = self.damage_ranges.iter().map(|d| {
702            (d.min_damage + d.max_damage) / 2.0
703        }).sum();
704        avg_damage * self.attack_speed
705    }
706
707    pub fn effective_sell_price(&self) -> u64 {
708        (self.base_price as f32 * self.sell_price_ratio) as u64
709    }
710
711    pub fn rarity_config(&self) -> RarityConfig {
712        RarityConfig::for_rarity(self.rarity)
713    }
714
715    pub fn total_stat_budget(&self) -> f32 {
716        // Compute weighted point-buy budget for balance validation
717        let mut budget = 0.0f32;
718        for m in &self.implicit_modifiers {
719            budget += stat_budget_cost(&m.stat) * (m.flat_value.abs() + m.percent_value.abs() * 2.0);
720        }
721        for m in &self.explicit_modifiers {
722            budget += stat_budget_cost(&m.stat) * (m.flat_value.abs() + m.percent_value.abs() * 2.0);
723        }
724        budget
725    }
726
727    pub fn validate(&self) -> Vec<String> {
728        let mut errors = Vec::new();
729        if self.name.is_empty() { errors.push("Name is empty".to_string()); }
730        if self.item_level == 0 { errors.push("Item level must be >= 1".to_string()); }
731        if self.weight < 0.0 { errors.push("Weight cannot be negative".to_string()); }
732        if self.volume < 0.0 { errors.push("Volume cannot be negative".to_string()); }
733        if self.stack_size == 0 { errors.push("Stack size must be >= 1".to_string()); }
734        if self.crit_chance < 0.0 || self.crit_chance > 100.0 {
735            errors.push("Crit chance must be 0–100".to_string());
736        }
737        if self.sell_price_ratio > 1.0 {
738            errors.push("Sell price ratio > 1 (item sells for more than it costs)".to_string());
739        }
740        for d in &self.damage_ranges {
741            if d.min_damage > d.max_damage {
742                errors.push(format!("Damage range min > max for {:?}", d.damage_type));
743            }
744        }
745        errors
746    }
747}
748
749fn stat_budget_cost(stat: &StatType) -> f32 {
750    match stat {
751        StatType::CritChance => 4.0,
752        StatType::CritMultiplier => 2.0,
753        StatType::PhysicalDamage | StatType::FireDamage | StatType::ColdDamage |
754        StatType::LightningDamage | StatType::PoisonDamage => 3.0,
755        StatType::LifeMax | StatType::ManaMax | StatType::StaminaMax => 1.5,
756        StatType::PhysicalArmor | StatType::MagicArmor => 1.2,
757        StatType::CooldownReduction | StatType::ResourceCostReduction => 5.0,
758        StatType::Strength | StatType::Dexterity | StatType::Intelligence |
759        StatType::Vitality | StatType::Endurance => 2.5,
760        _ => 1.0,
761    }
762}
763
764// ============================================================
765// AFFIX SYSTEM
766// ============================================================
767
768#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
769pub enum AffixType {
770    Prefix,
771    Suffix,
772    Implicit,
773}
774
775#[derive(Debug, Clone)]
776pub struct AffixDefinition {
777    pub id: u64,
778    pub name: String,
779    pub affix_type: AffixType,
780    pub modifier_template: StatModifier,
781    pub flat_min: f32,
782    pub flat_max: f32,
783    pub percent_min: f32,
784    pub percent_max: f32,
785    pub item_level_requirement: u32,
786    pub weight: f32,
787    pub applicable_categories: Vec<ItemCategory>,
788    pub conflicts_with: Vec<u64>, // affix IDs that cannot coexist
789    pub synergies_with: Vec<u64>, // affix IDs that grant bonus if co-present
790    pub synergy_bonus: f32,
791    pub tier: u32, // affix tier 1 = worst, 5 = best
792}
793
794impl AffixDefinition {
795    pub fn roll_flat_value(&self, t: f32) -> f32 {
796        // t is normalized [0,1] roll
797        self.flat_min + (self.flat_max - self.flat_min) * t
798    }
799
800    pub fn roll_percent_value(&self, t: f32) -> f32 {
801        self.percent_min + (self.percent_max - self.percent_min) * t
802    }
803
804    pub fn applies_to(&self, category: ItemCategory) -> bool {
805        self.applicable_categories.contains(&category)
806    }
807
808    pub fn has_conflict(&self, other_id: u64) -> bool {
809        self.conflicts_with.contains(&other_id)
810    }
811
812    pub fn has_synergy(&self, other_id: u64) -> bool {
813        self.synergies_with.contains(&other_id)
814    }
815
816    pub fn scaled_weight(&self, item_level: u32) -> f32 {
817        if item_level < self.item_level_requirement { return 0.0; }
818        let level_delta = (item_level - self.item_level_requirement) as f32;
819        // Weight decays as newer tiers become available: exponential decay
820        self.weight * (-0.05 * level_delta).exp().max(0.1)
821    }
822}
823
824#[derive(Debug, Clone)]
825pub struct AffixPool {
826    pub definitions: Vec<AffixDefinition>,
827}
828
829impl AffixPool {
830    pub fn new() -> Self {
831        AffixPool { definitions: Vec::new() }
832    }
833
834    pub fn add(&mut self, def: AffixDefinition) {
835        self.definitions.push(def);
836    }
837
838    pub fn eligible(&self, category: ItemCategory, item_level: u32, affix_type: AffixType) -> Vec<&AffixDefinition> {
839        self.definitions.iter().filter(|d|
840            d.affix_type == affix_type &&
841            d.applies_to(category) &&
842            d.scaled_weight(item_level) > 0.0
843        ).collect()
844    }
845
846    pub fn build_alias_table_for(&self, eligible: &[&AffixDefinition], item_level: u32) -> AliasTable {
847        let weights: Vec<f64> = eligible.iter().map(|d| d.scaled_weight(item_level) as f64).collect();
848        AliasTable::new(&weights)
849    }
850
851    pub fn check_conflicts(&self, selected: &[u64]) -> bool {
852        for &id_a in selected {
853            if let Some(def) = self.definitions.iter().find(|d| d.id == id_a) {
854                for &id_b in selected {
855                    if id_a != id_b && def.has_conflict(id_b) {
856                        return true; // conflict found
857                    }
858                }
859            }
860        }
861        false
862    }
863
864    pub fn compute_synergy_bonus(&self, selected_ids: &[u64]) -> f32 {
865        let mut total_bonus = 0.0f32;
866        for &id in selected_ids {
867            if let Some(def) = self.definitions.iter().find(|d| d.id == id) {
868                for &other_id in selected_ids {
869                    if id != other_id && def.has_synergy(other_id) {
870                        total_bonus += def.synergy_bonus;
871                    }
872                }
873            }
874        }
875        total_bonus
876    }
877}
878
879// ============================================================
880// CRAFTING SYSTEM
881// ============================================================
882
883#[derive(Debug, Clone)]
884pub struct CraftingIngredient {
885    pub item_id: u64,
886    pub quantity: u32,
887    pub quality_threshold: Option<f32>,
888    pub consumed: bool,
889}
890
891#[derive(Debug, Clone)]
892pub struct CraftingStation {
893    pub id: u64,
894    pub name: String,
895    pub tier: u32,
896    pub allowed_categories: Vec<ItemCategory>,
897}
898
899#[derive(Debug, Clone)]
900pub struct SkillRequirement {
901    pub skill_name: String,
902    pub required_level: u32,
903    pub consumed_xp: u32,
904}
905
906#[derive(Debug, Clone)]
907pub struct QualityOutcome {
908    pub quality_value: f32,
909    pub weight: f32,
910    pub label: String,
911}
912
913impl QualityOutcome {
914    pub fn standard_outcomes() -> Vec<Self> {
915        vec![
916            QualityOutcome { quality_value: 0.6, weight: 20.0, label: "Poor".to_string() },
917            QualityOutcome { quality_value: 0.8, weight: 40.0, label: "Normal".to_string() },
918            QualityOutcome { quality_value: 1.0, weight: 25.0, label: "Superior".to_string() },
919            QualityOutcome { quality_value: 1.1, weight: 10.0, label: "Masterwork".to_string() },
920            QualityOutcome { quality_value: 1.2, weight: 4.0,  label: "Flawless".to_string() },
921            QualityOutcome { quality_value: 1.5, weight: 1.0,  label: "Legendary".to_string() },
922        ]
923    }
924}
925
926#[derive(Debug, Clone)]
927pub struct RecipeDefinition {
928    pub id: u64,
929    pub name: String,
930    pub description: String,
931    pub ingredients: Vec<CraftingIngredient>,
932    pub output_item_id: u64,
933    pub output_count: u32,
934    pub required_station: CraftingStation,
935    pub skill_requirements: Vec<SkillRequirement>,
936    pub base_success_chance: f32,
937    pub skill_bonus_per_level: f32,   // success chance increase per skill level
938    pub quality_outcomes: Vec<QualityOutcome>,
939    pub byproducts: Vec<(u64, u32, f32)>, // (item_id, count, chance)
940    pub crafting_time_seconds: f32,
941    pub unlocked_by_default: bool,
942    pub unlock_source: Option<String>,
943    pub experience_reward: HashMap<String, u32>,
944}
945
946impl RecipeDefinition {
947    pub fn success_probability(&self, skill_level: u32) -> f32 {
948        let base = self.base_success_chance;
949        let bonus = self.skill_bonus_per_level * skill_level as f32;
950        (base + bonus).min(0.98).max(0.02)
951    }
952
953    pub fn expected_quality(&self, skill_level: u32) -> f32 {
954        // Weighted average quality, biased toward higher tiers by skill
955        let skill_bias = (skill_level as f32 / 100.0).min(1.0);
956        let total_weight: f32 = self.quality_outcomes.iter().map(|q| q.weight).sum();
957        let base_avg: f32 = self.quality_outcomes.iter().map(|q| q.quality_value * q.weight / total_weight).sum();
958        let max_quality = self.quality_outcomes.iter().map(|q| q.quality_value).fold(f32::NEG_INFINITY, f32::max);
959        base_avg + (max_quality - base_avg) * skill_bias * 0.3
960    }
961
962    pub fn crafting_time_with_speed(&self, crafting_speed_multiplier: f32) -> f32 {
963        self.crafting_time_seconds / crafting_speed_multiplier.max(0.1)
964    }
965
966    pub fn sample_quality(&self, roll: f32) -> f32 {
967        // roll is [0, total_weight)
968        let total: f32 = self.quality_outcomes.iter().map(|q| q.weight).sum();
969        let mut acc = 0.0f32;
970        let target = roll * total;
971        for q in &self.quality_outcomes {
972            acc += q.weight;
973            if target <= acc { return q.quality_value; }
974        }
975        self.quality_outcomes.last().map(|q| q.quality_value).unwrap_or(1.0)
976    }
977
978    pub fn validate_ingredients(&self, available: &HashMap<u64, u32>) -> bool {
979        for ing in &self.ingredients {
980            let have = available.get(&ing.item_id).copied().unwrap_or(0);
981            if have < ing.quantity { return false; }
982        }
983        true
984    }
985}
986
987// ============================================================
988// LOOT TABLES
989// ============================================================
990
991#[derive(Debug, Clone)]
992pub enum DropCondition {
993    Always,
994    PlayerLevelRange { min: u32, max: u32 },
995    QuestCompleted(String),
996    QuestActive(String),
997    FactionStanding { faction: String, min_rep: i32 },
998    ChancePercent(f32),
999    FirstKill,
1000    DifficultyAbove(u32),
1001}
1002
1003impl DropCondition {
1004    pub fn evaluate(&self, ctx: &LootContext) -> bool {
1005        match self {
1006            DropCondition::Always => true,
1007            DropCondition::PlayerLevelRange { min, max } =>
1008                ctx.player_level >= *min && ctx.player_level <= *max,
1009            DropCondition::QuestCompleted(q) => ctx.completed_quests.contains(q),
1010            DropCondition::QuestActive(q) => ctx.active_quests.contains(q),
1011            DropCondition::FactionStanding { faction, min_rep } =>
1012                ctx.faction_standings.get(faction).copied().unwrap_or(0) >= *min_rep,
1013            DropCondition::ChancePercent(c) => ctx.random_float < *c / 100.0,
1014            DropCondition::FirstKill => ctx.first_kill,
1015            DropCondition::DifficultyAbove(d) => ctx.difficulty >= *d,
1016        }
1017    }
1018}
1019
1020#[derive(Debug, Clone)]
1021pub struct LootContext {
1022    pub player_level: u32,
1023    pub magic_find: f32,
1024    pub completed_quests: HashSet<String>,
1025    pub active_quests: HashSet<String>,
1026    pub faction_standings: HashMap<String, i32>,
1027    pub random_float: f32,
1028    pub first_kill: bool,
1029    pub difficulty: u32,
1030}
1031
1032impl LootContext {
1033    pub fn default() -> Self {
1034        LootContext {
1035            player_level: 1,
1036            magic_find: 0.0,
1037            completed_quests: HashSet::new(),
1038            active_quests: HashSet::new(),
1039            faction_standings: HashMap::new(),
1040            random_float: 0.5,
1041            first_kill: false,
1042            difficulty: 1,
1043        }
1044    }
1045}
1046
1047#[derive(Debug, Clone)]
1048pub struct LootEntry {
1049    pub item_id: u64,
1050    pub weight: f32,
1051    pub min_quantity: u32,
1052    pub max_quantity: u32,
1053    pub condition: DropCondition,
1054    pub guaranteed: bool,
1055    pub rarity_override: Option<Rarity>,
1056}
1057
1058impl LootEntry {
1059    pub fn simple(item_id: u64, weight: f32) -> Self {
1060        LootEntry {
1061            item_id,
1062            weight,
1063            min_quantity: 1,
1064            max_quantity: 1,
1065            condition: DropCondition::Always,
1066            guaranteed: false,
1067            rarity_override: None,
1068        }
1069    }
1070
1071    pub fn roll_quantity(&self, t: f32) -> u32 {
1072        if self.min_quantity == self.max_quantity { return self.min_quantity; }
1073        let range = self.max_quantity - self.min_quantity;
1074        self.min_quantity + (t * range as f32) as u32
1075    }
1076}
1077
1078// Negative binomial distribution for drop count
1079fn negative_binomial_sample(r: f32, p: f32, u: f32) -> u32 {
1080    // PMF: P(X=k) = C(k+r-1, k) * (1-p)^r * p^k
1081    // Use CDF inversion
1082    let mut cdf = 0.0f64;
1083    let p64 = p as f64;
1084    let r64 = r as f64;
1085    let u64 = u as f64;
1086    for k in 0..=100u32 {
1087        let k64 = k as f64;
1088        // log(C(k+r-1, k)) = lgamma(k+r) - lgamma(r) - lgamma(k+1)
1089        let log_binom = lgamma(k64 + r64) - lgamma(r64) - lgamma(k64 + 1.0);
1090        let pmf = (log_binom + r64 * (1.0 - p64).ln() + k64 * p64.ln()).exp();
1091        cdf += pmf;
1092        if cdf >= u64 { return k; }
1093    }
1094    50 // fallback
1095}
1096
1097fn lgamma(x: f64) -> f64 {
1098    // Stirling's approximation for lgamma
1099    if x <= 0.0 { return 0.0; }
1100    if x < 0.5 {
1101        return std::f64::consts::PI.ln() - (std::f64::consts::PI * x).sin().ln() - lgamma(1.0 - x);
1102    }
1103    let x = x - 1.0;
1104    let tmp = x + 7.5;
1105    let ser: f64 = 0.999999999999997092
1106        + 57.1562356658629235 / (x + 1.0)
1107        - 59.5979603554754912 / (x + 2.0)
1108        + 14.1360979747417471 / (x + 3.0)
1109        - 0.491913816097620199 / (x + 4.0)
1110        + 0.339946499848118887e-4 / (x + 5.0)
1111        + 0.465236289270485756e-4 / (x + 6.0)
1112        - 0.983744753048795646e-4 / (x + 7.0)
1113        + 0.158088703224912494e-3 / (x + 8.0)
1114        - 0.210264441724104883e-3 / (x + 9.0)
1115        + 0.217439618115212643e-3 / (x + 10.0)
1116        - 0.164318106536763890e-3 / (x + 11.0)
1117        + 0.844182239838527433e-4 / (x + 12.0)
1118        - 0.261908384015814087e-4 / (x + 13.0)
1119        + 0.368991826595316234e-5 / (x + 14.0);
1120    0.5 * (2.0 * std::f64::consts::PI).ln() + (x + 0.5) * tmp.ln() - tmp + ser.ln()
1121}
1122
1123#[derive(Debug, Clone)]
1124pub struct LootTable {
1125    pub id: u64,
1126    pub name: String,
1127    pub entries: Vec<LootEntry>,
1128    pub drop_count_distribution: DropCountDistribution,
1129    pub magic_find_scaling: f32, // extra weight per 1% magic find
1130}
1131
1132#[derive(Debug, Clone)]
1133pub enum DropCountDistribution {
1134    Fixed(u32),
1135    Uniform { min: u32, max: u32 },
1136    NegativeBinomial { r: f32, p: f32 },
1137}
1138
1139impl DropCountDistribution {
1140    pub fn sample(&self, u: f32) -> u32 {
1141        match self {
1142            DropCountDistribution::Fixed(n) => *n,
1143            DropCountDistribution::Uniform { min, max } => {
1144                min + (u * (max - min + 1) as f32) as u32
1145            }
1146            DropCountDistribution::NegativeBinomial { r, p } => {
1147                negative_binomial_sample(*r, *p, u)
1148            }
1149        }
1150    }
1151}
1152
1153impl LootTable {
1154    pub fn roll_drops(&self, ctx: &LootContext, random_values: &[f32]) -> Vec<(u64, u32)> {
1155        let mut results: Vec<(u64, u32)> = Vec::new();
1156        let mut rv_index = 0usize;
1157        let next_r = |idx: &mut usize| -> f32 {
1158            let v = if *idx < random_values.len() { random_values[*idx] } else { 0.5 };
1159            *idx += 1; v
1160        };
1161
1162        // Guaranteed drops first
1163        for entry in &self.entries {
1164            if entry.guaranteed && entry.condition.evaluate(ctx) {
1165                let qty = entry.roll_quantity(next_r(&mut rv_index));
1166                results.push((entry.item_id, qty));
1167            }
1168        }
1169
1170        // Determine drop count
1171        let drop_count = self.drop_count_distribution.sample(next_r(&mut rv_index));
1172
1173        // Build alias table from eligible non-guaranteed entries
1174        let eligible: Vec<&LootEntry> = self.entries.iter().filter(|e|
1175            !e.guaranteed && e.condition.evaluate(ctx)
1176        ).collect();
1177
1178        if eligible.is_empty() { return results; }
1179
1180        let magic_find_multiplier = 1.0 + ctx.magic_find * self.magic_find_scaling / 100.0;
1181        let weights: Vec<f64> = eligible.iter().map(|e| {
1182            (e.weight * magic_find_multiplier) as f64
1183        }).collect();
1184        let alias = AliasTable::new(&weights);
1185
1186        for _ in 0..drop_count {
1187            let u1 = next_r(&mut rv_index) as f64;
1188            let u2 = next_r(&mut rv_index) as f64;
1189            let idx = alias.sample(u1, u2);
1190            let entry = eligible[idx];
1191            let qty = entry.roll_quantity(next_r(&mut rv_index));
1192            results.push((entry.item_id, qty));
1193        }
1194
1195        results
1196    }
1197}
1198
1199// ============================================================
1200// ECONOMY SYSTEM
1201// ============================================================
1202
1203#[derive(Debug, Clone)]
1204pub struct PriceElasticity {
1205    pub base_demand: f32,
1206    pub demand_slope: f32,   // price change per unit of supply/demand ratio
1207    pub min_price_ratio: f32,
1208    pub max_price_ratio: f32,
1209}
1210
1211impl PriceElasticity {
1212    pub fn price_ratio(&self, current_supply: f32, current_demand: f32) -> f32 {
1213        if current_supply <= 0.0 { return self.max_price_ratio; }
1214        let ratio = current_demand / current_supply;
1215        let price_ratio = 1.0 + self.demand_slope * (ratio - 1.0);
1216        price_ratio.clamp(self.min_price_ratio, self.max_price_ratio)
1217    }
1218}
1219
1220#[derive(Debug, Clone)]
1221pub struct VendorInventoryEntry {
1222    pub item_id: u64,
1223    pub stock: u32,
1224    pub max_stock: u32,
1225    pub restock_rate: f32, // units per in-game hour
1226    pub current_price_multiplier: f32,
1227    pub special_offer: bool,
1228    pub offer_expiry: f32,
1229}
1230
1231impl VendorInventoryEntry {
1232    pub fn restock_tick(&mut self, hours_elapsed: f32) {
1233        let new_stock = self.stock as f32 + self.restock_rate * hours_elapsed;
1234        self.stock = (new_stock as u32).min(self.max_stock);
1235    }
1236
1237    pub fn effective_buy_price(&self, base_price: u64, vendor_markup: f32) -> u64 {
1238        let multiplier = self.current_price_multiplier * vendor_markup;
1239        if self.special_offer {
1240            (base_price as f32 * multiplier * 0.75) as u64
1241        } else {
1242            (base_price as f32 * multiplier) as u64
1243        }
1244    }
1245}
1246
1247#[derive(Debug, Clone)]
1248pub struct VendorDefinition {
1249    pub id: u64,
1250    pub name: String,
1251    pub faction: String,
1252    pub inventory: Vec<VendorInventoryEntry>,
1253    pub buy_markup: f32,
1254    pub sell_markup: f32,
1255    pub barter_ratio: f32,
1256    pub refresh_interval_hours: f32,
1257    pub last_refresh_time: f32,
1258    pub price_elasticity: PriceElasticity,
1259    pub supply: f32,
1260    pub demand: f32,
1261}
1262
1263impl VendorDefinition {
1264    pub fn new(id: u64, name: impl Into<String>) -> Self {
1265        VendorDefinition {
1266            id,
1267            name: name.into(),
1268            faction: "Neutral".to_string(),
1269            inventory: Vec::new(),
1270            buy_markup: 2.5,
1271            sell_markup: 0.25,
1272            barter_ratio: 0.8,
1273            refresh_interval_hours: 24.0,
1274            last_refresh_time: 0.0,
1275            price_elasticity: PriceElasticity {
1276                base_demand: 1.0,
1277                demand_slope: 0.5,
1278                min_price_ratio: 0.5,
1279                max_price_ratio: 3.0,
1280            },
1281            supply: 1.0,
1282            demand: 1.0,
1283        }
1284    }
1285
1286    pub fn dynamic_price_ratio(&self) -> f32 {
1287        self.price_elasticity.price_ratio(self.supply, self.demand)
1288    }
1289
1290    pub fn buy_price(&self, item: &ItemDefinition) -> u64 {
1291        let rarity_config = item.rarity_config();
1292        let price = item.base_price as f32
1293            * self.buy_markup
1294            * rarity_config.vendor_price_multiplier
1295            * self.dynamic_price_ratio();
1296        price as u64
1297    }
1298
1299    pub fn sell_price(&self, item: &ItemDefinition) -> u64 {
1300        let rarity_config = item.rarity_config();
1301        let price = item.base_price as f32
1302            * self.sell_markup
1303            * rarity_config.vendor_price_multiplier;
1304        price as u64
1305    }
1306
1307    pub fn barter_value(&self, item: &ItemDefinition) -> u64 {
1308        let price = self.buy_price(item) as f32 * self.barter_ratio;
1309        price as u64
1310    }
1311
1312    pub fn refresh_inventory(&mut self, current_time: f32) {
1313        if current_time - self.last_refresh_time >= self.refresh_interval_hours {
1314            let hours = current_time - self.last_refresh_time;
1315            for entry in &mut self.inventory {
1316                entry.restock_tick(hours);
1317            }
1318            self.last_refresh_time = current_time;
1319        }
1320    }
1321
1322    pub fn adjust_supply_demand(&mut self, transaction: EconomyTransaction) {
1323        match transaction {
1324            EconomyTransaction::PlayerBought { quantity } => {
1325                self.supply -= quantity as f32 * 0.1;
1326                self.demand += quantity as f32 * 0.05;
1327            }
1328            EconomyTransaction::PlayerSold { quantity } => {
1329                self.supply += quantity as f32 * 0.1;
1330                self.demand -= quantity as f32 * 0.03;
1331            }
1332        }
1333        self.supply = self.supply.max(0.1);
1334        self.demand = self.demand.max(0.1);
1335    }
1336}
1337
1338#[derive(Debug, Clone)]
1339pub enum EconomyTransaction {
1340    PlayerBought { quantity: u32 },
1341    PlayerSold { quantity: u32 },
1342}
1343
1344// ============================================================
1345// EQUIPMENT SLOTS
1346// ============================================================
1347
1348#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1349pub enum EquipmentSlot {
1350    Head,
1351    Neck,
1352    LeftShoulder,
1353    RightShoulder,
1354    Chest,
1355    Back,
1356    Wrists,
1357    Hands,
1358    Waist,
1359    Legs,
1360    Feet,
1361    MainHand,
1362    OffHand,
1363    Ring1,
1364    Ring2,
1365    Trinket1,
1366    Trinket2,
1367}
1368
1369impl EquipmentSlot {
1370    pub fn all() -> &'static [EquipmentSlot] {
1371        &[
1372            EquipmentSlot::Head,
1373            EquipmentSlot::Neck,
1374            EquipmentSlot::LeftShoulder,
1375            EquipmentSlot::RightShoulder,
1376            EquipmentSlot::Chest,
1377            EquipmentSlot::Back,
1378            EquipmentSlot::Wrists,
1379            EquipmentSlot::Hands,
1380            EquipmentSlot::Waist,
1381            EquipmentSlot::Legs,
1382            EquipmentSlot::Feet,
1383            EquipmentSlot::MainHand,
1384            EquipmentSlot::OffHand,
1385            EquipmentSlot::Ring1,
1386            EquipmentSlot::Ring2,
1387            EquipmentSlot::Trinket1,
1388            EquipmentSlot::Trinket2,
1389        ]
1390    }
1391
1392    pub fn display_name(&self) -> &'static str {
1393        match self {
1394            EquipmentSlot::Head => "Head",
1395            EquipmentSlot::Neck => "Neck",
1396            EquipmentSlot::LeftShoulder => "Left Shoulder",
1397            EquipmentSlot::RightShoulder => "Right Shoulder",
1398            EquipmentSlot::Chest => "Chest",
1399            EquipmentSlot::Back => "Back",
1400            EquipmentSlot::Wrists => "Wrists",
1401            EquipmentSlot::Hands => "Hands",
1402            EquipmentSlot::Waist => "Waist",
1403            EquipmentSlot::Legs => "Legs",
1404            EquipmentSlot::Feet => "Feet",
1405            EquipmentSlot::MainHand => "Main Hand",
1406            EquipmentSlot::OffHand => "Off Hand",
1407            EquipmentSlot::Ring1 => "Ring 1",
1408            EquipmentSlot::Ring2 => "Ring 2",
1409            EquipmentSlot::Trinket1 => "Trinket 1",
1410            EquipmentSlot::Trinket2 => "Trinket 2",
1411        }
1412    }
1413
1414    pub fn compatible_categories(&self) -> &'static [ItemCategory] {
1415        match self {
1416            EquipmentSlot::Head => &[ItemCategory::Helmet, ItemCategory::Hood, ItemCategory::Crown],
1417            EquipmentSlot::Neck => &[ItemCategory::Amulet],
1418            EquipmentSlot::LeftShoulder | EquipmentSlot::RightShoulder => &[],
1419            EquipmentSlot::Chest => &[ItemCategory::Chestplate, ItemCategory::Robe, ItemCategory::Leather],
1420            EquipmentSlot::Back => &[ItemCategory::Cloak],
1421            EquipmentSlot::Wrists => &[],
1422            EquipmentSlot::Hands => &[ItemCategory::Gauntlets, ItemCategory::Gloves],
1423            EquipmentSlot::Waist => &[ItemCategory::Belt],
1424            EquipmentSlot::Legs => &[ItemCategory::Greaves, ItemCategory::Leggings, ItemCategory::Pants],
1425            EquipmentSlot::Feet => &[ItemCategory::Boots, ItemCategory::Sabatons],
1426            EquipmentSlot::MainHand => &[
1427                ItemCategory::Sword, ItemCategory::Greatsword, ItemCategory::Dagger,
1428                ItemCategory::Axe, ItemCategory::Greataxe, ItemCategory::Mace,
1429                ItemCategory::Hammer, ItemCategory::Spear, ItemCategory::Polearm,
1430                ItemCategory::Bow, ItemCategory::Crossbow, ItemCategory::Gun,
1431                ItemCategory::Rifle, ItemCategory::Staff, ItemCategory::Wand,
1432            ],
1433            EquipmentSlot::OffHand => &[
1434                ItemCategory::Shield, ItemCategory::Buckler, ItemCategory::TowerShield,
1435                ItemCategory::Orb, ItemCategory::Tome, ItemCategory::Dagger,
1436            ],
1437            EquipmentSlot::Ring1 | EquipmentSlot::Ring2 => &[ItemCategory::Ring],
1438            EquipmentSlot::Trinket1 | EquipmentSlot::Trinket2 => &[],
1439        }
1440    }
1441
1442    pub fn is_compatible(&self, category: ItemCategory) -> bool {
1443        let compat = self.compatible_categories();
1444        if compat.is_empty() {
1445            // Shoulders/Wrists/Trinkets accept special slot items
1446            return false;
1447        }
1448        compat.contains(&category)
1449    }
1450
1451    pub fn visual_attachment_bone(&self) -> &'static str {
1452        match self {
1453            EquipmentSlot::Head => "head_attach",
1454            EquipmentSlot::Neck => "neck_attach",
1455            EquipmentSlot::LeftShoulder => "shoulder_l_attach",
1456            EquipmentSlot::RightShoulder => "shoulder_r_attach",
1457            EquipmentSlot::Chest => "chest_attach",
1458            EquipmentSlot::Back => "back_attach",
1459            EquipmentSlot::Wrists => "wrist_attach",
1460            EquipmentSlot::Hands => "hand_attach",
1461            EquipmentSlot::Waist => "waist_attach",
1462            EquipmentSlot::Legs => "leg_attach",
1463            EquipmentSlot::Feet => "foot_attach",
1464            EquipmentSlot::MainHand => "weapon_r_attach",
1465            EquipmentSlot::OffHand => "weapon_l_attach",
1466            EquipmentSlot::Ring1 => "ring_r_attach",
1467            EquipmentSlot::Ring2 => "ring_l_attach",
1468            EquipmentSlot::Trinket1 | EquipmentSlot::Trinket2 => "trinket_attach",
1469        }
1470    }
1471}
1472
1473// ============================================================
1474// SET BONUS SYSTEM
1475// ============================================================
1476
1477#[derive(Debug, Clone)]
1478pub struct SetBonus {
1479    pub pieces_required: u32,
1480    pub description: String,
1481    pub modifiers: Vec<StatModifier>,
1482}
1483
1484#[derive(Debug, Clone)]
1485pub struct ItemSet {
1486    pub id: u64,
1487    pub name: String,
1488    pub item_ids: Vec<u64>,
1489    pub bonuses: Vec<SetBonus>, // sorted by pieces_required
1490    pub color: Vec4,
1491}
1492
1493impl ItemSet {
1494    pub fn count_equipped_pieces(&self, equipped: &HashMap<EquipmentSlot, u64>) -> u32 {
1495        let equipped_ids: HashSet<&u64> = equipped.values().collect();
1496        self.item_ids.iter().filter(|id| equipped_ids.contains(id)).count() as u32
1497    }
1498
1499    pub fn active_bonuses(&self, equipped: &HashMap<EquipmentSlot, u64>) -> Vec<&SetBonus> {
1500        let count = self.count_equipped_pieces(equipped);
1501        self.bonuses.iter().filter(|b| count >= b.pieces_required).collect()
1502    }
1503
1504    pub fn next_bonus(&self, equipped: &HashMap<EquipmentSlot, u64>) -> Option<(&SetBonus, u32)> {
1505        let count = self.count_equipped_pieces(equipped);
1506        self.bonuses.iter()
1507            .filter(|b| b.pieces_required > count)
1508            .min_by_key(|b| b.pieces_required)
1509            .map(|b| (b, b.pieces_required - count))
1510    }
1511}
1512
1513// ============================================================
1514// CONTAINER / GRID INVENTORY
1515// ============================================================
1516
1517#[derive(Debug, Clone)]
1518pub struct GridCell {
1519    pub item_instance_id: Option<u64>,
1520    pub is_anchor: bool, // top-left cell of item occupancy
1521}
1522
1523#[derive(Debug, Clone)]
1524pub struct ItemInstance {
1525    pub instance_id: u64,
1526    pub definition_id: u64,
1527    pub quantity: u32,
1528    pub durability: f32,
1529    pub quality: f32,
1530    pub affixes: Vec<(u64, f32)>, // (affix_id, rolled_value)
1531    pub position: Vec2,           // grid position (col, row)
1532    pub rotated: bool,
1533    pub custom_name: Option<String>,
1534    pub loot_filter_override: Option<u8>,
1535}
1536
1537impl ItemInstance {
1538    pub fn new(instance_id: u64, definition_id: u64) -> Self {
1539        ItemInstance {
1540            instance_id,
1541            definition_id,
1542            quantity: 1,
1543            durability: 100.0,
1544            quality: 1.0,
1545            affixes: Vec::new(),
1546            position: Vec2::ZERO,
1547            rotated: false,
1548            custom_name: None,
1549            loot_filter_override: None,
1550        }
1551    }
1552
1553    pub fn effective_width(&self, def: &ItemDefinition) -> u8 {
1554        if self.rotated { def.height } else { def.width }
1555    }
1556
1557    pub fn effective_height(&self, def: &ItemDefinition) -> u8 {
1558        if self.rotated { def.width } else { def.height }
1559    }
1560
1561    pub fn display_name<'a>(&'a self, def: &'a ItemDefinition) -> &'a str {
1562        self.custom_name.as_deref().unwrap_or(&def.name)
1563    }
1564
1565    pub fn durability_fraction(&self) -> f32 {
1566        if self.durability <= 0.0 { return 0.0; }
1567        (self.durability / 100.0).min(1.0)
1568    }
1569
1570    pub fn is_broken(&self) -> bool {
1571        self.durability <= 0.0
1572    }
1573
1574    pub fn repair_cost(&self, def: &ItemDefinition) -> u64 {
1575        let missing = (100.0 - self.durability).max(0.0);
1576        let fraction = missing / 100.0;
1577        (def.base_price as f32 * 0.1 * fraction * self.quality) as u64
1578    }
1579}
1580
1581#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1582pub enum BagFilterType {
1583    All,
1584    Weapons,
1585    Armor,
1586    Consumables,
1587    Materials,
1588    QuestItems,
1589    Custom,
1590}
1591
1592#[derive(Debug, Clone)]
1593pub struct Container {
1594    pub id: u64,
1595    pub name: String,
1596    pub grid_width: u32,
1597    pub grid_height: u32,
1598    pub cells: Vec<Vec<GridCell>>,
1599    pub items: HashMap<u64, ItemInstance>,
1600    pub max_weight: f32,
1601    pub current_weight: f32,
1602    pub max_volume: f32,
1603    pub current_volume: f32,
1604    pub filter_type: BagFilterType,
1605    pub is_locked: bool,
1606}
1607
1608impl Container {
1609    pub fn new(id: u64, name: impl Into<String>, width: u32, height: u32) -> Self {
1610        let cells: Vec<Vec<GridCell>> = (0..height).map(|_| {
1611            (0..width).map(|_| GridCell { item_instance_id: None, is_anchor: false }).collect()
1612        }).collect();
1613        Container {
1614            id,
1615            name: name.into(),
1616            grid_width: width,
1617            grid_height: height,
1618            cells,
1619            items: HashMap::new(),
1620            max_weight: 100.0,
1621            current_weight: 0.0,
1622            max_volume: 50.0,
1623            current_volume: 0.0,
1624            filter_type: BagFilterType::All,
1625            is_locked: false,
1626        }
1627    }
1628
1629    pub fn can_place(&self, col: u32, row: u32, w: u32, h: u32) -> bool {
1630        if col + w > self.grid_width || row + h > self.grid_height { return false; }
1631        for r in row..row+h {
1632            for c in col..col+w {
1633                if self.cells[r as usize][c as usize].item_instance_id.is_some() {
1634                    return false;
1635                }
1636            }
1637        }
1638        true
1639    }
1640
1641    pub fn find_free_slot(&self, w: u32, h: u32) -> Option<(u32, u32)> {
1642        for row in 0..self.grid_height {
1643            for col in 0..self.grid_width {
1644                if self.can_place(col, row, w, h) {
1645                    return Some((col, row));
1646                }
1647            }
1648        }
1649        None
1650    }
1651
1652    pub fn place_item(&mut self, inst: ItemInstance, col: u32, row: u32, def: &ItemDefinition) -> bool {
1653        let w = inst.effective_width(def) as u32;
1654        let h = inst.effective_height(def) as u32;
1655        if !self.can_place(col, row, w, h) { return false; }
1656        let iid = inst.instance_id;
1657        for r in row..row+h {
1658            for c in col..col+w {
1659                self.cells[r as usize][c as usize].item_instance_id = Some(iid);
1660                self.cells[r as usize][c as usize].is_anchor = r == row && c == col;
1661            }
1662        }
1663        self.current_weight += def.weight * inst.quantity as f32;
1664        self.current_volume += def.volume * inst.quantity as f32;
1665        self.items.insert(iid, inst);
1666        true
1667    }
1668
1669    pub fn remove_item(&mut self, instance_id: u64, def: &ItemDefinition) -> Option<ItemInstance> {
1670        if let Some(inst) = self.items.remove(&instance_id) {
1671            // Clear cells
1672            for row in &mut self.cells {
1673                for cell in row {
1674                    if cell.item_instance_id == Some(instance_id) {
1675                        cell.item_instance_id = None;
1676                        cell.is_anchor = false;
1677                    }
1678                }
1679            }
1680            self.current_weight -= def.weight * inst.quantity as f32;
1681            self.current_volume -= def.volume * inst.quantity as f32;
1682            Some(inst)
1683        } else {
1684            None
1685        }
1686    }
1687
1688    pub fn sort_by_category(&mut self, defs: &HashMap<u64, ItemDefinition>) {
1689        // Collect all items, clear grid, re-place sorted
1690        let mut all_items: Vec<ItemInstance> = self.items.drain().map(|(_, v)| v).collect();
1691        // Clear all cells
1692        for row in &mut self.cells {
1693            for cell in row {
1694                cell.item_instance_id = None;
1695                cell.is_anchor = false;
1696            }
1697        }
1698        self.current_weight = 0.0;
1699        self.current_volume = 0.0;
1700
1701        // Sort by category display name then by item name
1702        all_items.sort_by(|a, b| {
1703            let def_a = defs.get(&a.definition_id);
1704            let def_b = defs.get(&b.definition_id);
1705            match (def_a, def_b) {
1706                (Some(da), Some(db)) => {
1707                    let cat_cmp = da.category.display_name().cmp(db.category.display_name());
1708                    if cat_cmp == std::cmp::Ordering::Equal {
1709                        da.name.cmp(&db.name)
1710                    } else {
1711                        cat_cmp
1712                    }
1713                }
1714                _ => std::cmp::Ordering::Equal,
1715            }
1716        });
1717
1718        // Re-place items
1719        for mut item in all_items {
1720            if let Some(def) = defs.get(&item.definition_id) {
1721                let w = item.effective_width(def) as u32;
1722                let h = item.effective_height(def) as u32;
1723                if let Some((col, row)) = self.find_free_slot(w, h) {
1724                    item.position = Vec2::new(col as f32, row as f32);
1725                    self.place_item(item, col, row, def);
1726                }
1727            }
1728        }
1729    }
1730
1731    pub fn weight_fraction(&self) -> f32 {
1732        if self.max_weight <= 0.0 { return 0.0; }
1733        (self.current_weight / self.max_weight).min(1.0)
1734    }
1735
1736    pub fn is_overweight(&self) -> bool {
1737        self.current_weight > self.max_weight
1738    }
1739}
1740
1741// ============================================================
1742// PROCEDURAL ITEM GENERATION
1743// ============================================================
1744
1745const WEAPON_PREFIXES: &[&str] = &[
1746    "Ancient", "Brutal", "Cursed", "Dark", "Eldritch", "Fierce", "Gleaming",
1747    "Hallowed", "Infernal", "Jagged", "Keen", "Lethal", "Malevolent", "Null",
1748    "Ominous", "Piercing", "Quick", "Raging", "Savage", "Tempered", "Unholy",
1749    "Vicious", "Wicked", "Xeric", "Yearning", "Zealous", "Ashen", "Blazing",
1750    "Cracked", "Dreadful", "Enchanted", "Frostbitten", "Grim", "Haunted",
1751    "Icy", "Jade", "Kingsguard", "Luminous", "Molten", "Necrotic", "Obsidian",
1752    "Plagued", "Quenched", "Runed", "Spectral", "Twisted", "Umbral", "Volcanic",
1753    "Withered", "Ancient", "Baleful", "Corroded", "Defiled", "Energized",
1754    "Flickering", "Glacial", "Hexed", "Incandescent", "Jinxed", "Knotted",
1755    "Lightforged", "Mystic", "Nightshroud", "Oathbound", "Pristine", "Quivering",
1756    "Radiant", "Shadowed", "Thornbound", "Undying", "Venomous", "Whispering",
1757    "Xerograph", "Yearlong", "Zenith", "Abyss-touched", "Broken", "Chiseled",
1758    "Darkened", "Edged", "Flaming", "Gutted", "Heavy", "Inlaid", "Jagged",
1759    "Kindled", "Laced", "Marred", "Noble", "Outlaw", "Pitted", "Quenched",
1760    "Refined", "Serrated", "Tainted", "Uncanny", "Valiant", "Warped",
1761    "Xiphoid", "Yearner", "Zeal-wrought", "Accursed", "Battered", "Carven",
1762    "Dawnforged", "Ebonsteel", "Frosted", "Gilded", "Honed", "Ironclad",
1763    "Jeweled", "Krakenforged", "Lightweave", "Moonforged", "Nightfall",
1764    "Opal-set", "Petrified", "Quicksilver", "Runesteel", "Stormforged",
1765    "Titanforged", "Ursa", "Vindictive", "Wolfbound", "Xenos", "Yarrow",
1766    "Zenithborn", "Abyssal", "Bonded", "Condensed", "Deepsteel", "Ember",
1767    "Frostfire", "Gossamer", "Heartseeker", "Infused", "Judgement",
1768    "Kingslayer", "Lifesteel", "Mournful", "Noxious", "Outrider",
1769    "Purified", "Quickened", "Ruinous", "Soulbound", "Terrifying",
1770    "Undead", "Voidshard", "Wardancer", "Xenolith", "Yearning", "Zarathos",
1771    "Brightedge", "Crystalforged", "Desolation", "Ember-quenched", "Foxfire",
1772    "Galeforce", "Hexbound", "Ironweave", "Jaded", "Killerwhale", "Lodestar",
1773    "Mystical", "Nameless", "Otherworldly", "Parallax", "Quietus", "Riftborn",
1774    "Sanguine", "Tempestborn", "Unified", "Vesper", "Wanderer", "Xeric",
1775    "Yggdrasil", "Zero-point", "Aetheric", "Boneshatter", "Crimsonblade",
1776    "Dawnbreaker", "Eclipseborn", "Frostmantle", "Grimoire", "Hallowsteel",
1777    "Ironheart", "Jadeblade", "Knightfall", "Lifeguard", "Moonshatter",
1778    "Nightsong", "Oathkeeper", "Platinumcore", "Quickdraw", "Ragnarok",
1779    "Soulfire", "Thunderstrike", "Umbragen", "Veilwalker", "Worldender",
1780    "Xiphos", "Yieldless", "Zodiac", "Arcanite", "Bloodmire", "Cometfall",
1781    "Duskwalker", "Edenfall", "Fateweaver", "Ghoststeel", "Hellforged",
1782    "Immortal", "Jadefire", "Keysteel", "Lavaborn", "Moonfire", "Netherforged",
1783    "Overcharge", "Pyroborn", "Radiantfall", "Starborn", "Tidecaller",
1784    "Unbreakable", "Voidwalker", "Worldbreaker", "Xeric", "Youthful", "Zenith",
1785];
1786
1787const WEAPON_BASES: &[&str] = &[
1788    "Sword", "Blade", "Edge", "Cutter", "Slicer", "Cleaver", "Chopper", "Striker",
1789    "Biter", "Fang", "Talon", "Claw", "Piercer", "Impaler", "Skewer", "Lancer",
1790    "Prodder", "Stabber", "Poker", "Pricker", "Hammer", "Crusher", "Pounder",
1791    "Smasher", "Basher", "Bludgeon", "Club", "Maul", "Mallet", "Staff",
1792    "Rod", "Wand", "Scepter", "Orb", "Focus", "Conduit", "Channel", "Vessel",
1793    "Bow", "Stringer", "Launcher", "Thrower", "Shooter", "Axe", "Hatchet",
1794    "Cleaver", "Chopper", "Hewer", "Splitter", "Dagger", "Stiletto", "Dirk",
1795    "Kris", "Kukri", "Macuahuitl", "Naginata", "Odachi", "Partisan", "Ranseur",
1796    "Sabre", "Tulwar", "Ulfberht", "Vouge", "Warstaff", "Xiphos", "Yatagan",
1797    "Zweihander", "Arming Sword", "Bastard Sword", "Cavalry Sabre", "Dueling Blade",
1798    "Executioner", "Falchion", "Gladius", "Hauptmann", "Infantry Sword",
1799    "Jagged Shard", "Khopesh", "Longsword", "Montante", "Nightblade",
1800    "Officer's Sword", "Pallash", "Quickblade", "Reaper", "Shortsword",
1801    "Tomahawk", "Urgash", "Viking Sword", "Wakizashi", "Xenoblade",
1802    "Yellowback", "Zulfiqar", "Arcane Staff", "Bonechill Scepter", "Curse Wand",
1803    "Death Rod", "Ethereal Focus", "Fire Staff", "Grimoire Staff", "Hex Wand",
1804    "Ironwood Staff", "Jeweled Scepter", "Knotted Staff", "Lich Staff",
1805    "Moonstaff", "Null Rod", "Orb of Storms", "Plague Staff", "Quicksilver Rod",
1806    "Runestaff", "Shadow Wand", "Tome Staff", "Umbra Focus", "Venom Rod",
1807    "Wraith Staff", "Xenomancer's Orb", "Yew Wand", "Zephyr Staff",
1808    "Ancient Blade", "Battle Cleaver", "Champion's Edge", "Dragon Fang",
1809    "Elder Sword", "Flame Saber", "Ghost Blade", "Hero's Sword", "Ice Blade",
1810    "Judge's Sword", "Knight's Edge", "Lord's Blade", "Master Sword",
1811    "Noble Sword", "Old Blade", "Paladin's Sword", "Quest Blade", "Royal Edge",
1812    "Soldier's Sword", "Temple Blade", "Unknown Sword", "Veteran's Blade",
1813    "War Sword", "Xeric Blade", "Youth Cutter", "Zenith Sword",
1814    "Armageddon Blade", "Blood Sword", "Chaos Edge", "Death Sword",
1815    "Eternal Blade", "Fury Edge", "Glory Sword", "Honor Blade", "Inferno Sword",
1816    "Justice Edge", "King's Sword", "Legacy Blade", "Miracle Edge", "Night Sword",
1817    "Omega Blade", "Prophet's Sword", "Quiet Edge", "Ruin Sword", "Sacred Blade",
1818    "Truth Edge", "Unity Sword", "Victory Blade", "Warlord's Edge",
1819    "Xtreme Blade", "Yearning Sword", "Zero Blade", "Apex Sword",
1820    "Blazing Edge", "Crystal Sword", "Diamond Blade", "Emerald Edge",
1821    "Frost Sword", "Gale Blade", "Holy Sword", "Iron Edge", "Jade Sword",
1822    "Kraken Blade", "Lightning Edge", "Moon Sword", "Nova Blade",
1823    "Obsidian Edge", "Prism Sword", "Quartz Blade", "Rainbow Edge",
1824    "Solar Sword", "Thunder Blade", "Ultraviolet Edge", "Void Sword",
1825    "Wind Blade", "Xenon Edge", "Yellow Sword", "Zeal Blade",
1826];
1827
1828const WEAPON_SUFFIXES: &[&str] = &[
1829    "of Agony", "of Blight", "of Carnage", "of Destruction", "of Eternity",
1830    "of Flame", "of Glory", "of Havoc", "of Ice", "of Justice",
1831    "of Killing", "of Light", "of Madness", "of Night", "of Oblivion",
1832    "of Pestilence", "of Quake", "of Ruin", "of Storms", "of Thunder",
1833    "of Undeath", "of Vengeance", "of War", "of Xerxes", "of Yesterday",
1834    "of Zenith", "of the Abyss", "of the Bear", "of the Cosmos", "of the Deep",
1835    "of the Eagle", "of the Fox", "of the Gods", "of the Hawk", "of the Infinite",
1836    "of the Jaguar", "of the Kingdom", "of the Lion", "of the Moon", "of the Night",
1837    "of the Oracle", "of the Phoenix", "of the Queen", "of the Raven", "of the Sun",
1838    "of the Tiger", "of the Undying", "of the Void", "of the Wolf", "of the Wyrm",
1839    "of Xeric Power", "of Yearning", "of Zeal", "of Absolute Power", "of Balance",
1840    "of Chaos", "of Dominion", "of Elemental Force", "of Finality", "of Grace",
1841    "of Heroism", "of Immortality", "of Judgment", "of Kinship", "of Lore",
1842    "of Mastery", "of Nobility", "of Order", "of Purity", "of Quickness",
1843    "of Resolve", "of Supremacy", "of Time", "of Unity", "of Valor",
1844    "of Wrath", "of Xenomorph", "of Youth", "of Zealotry", "of Acrimony",
1845    "of Bliss", "of Cruelty", "of Daemonhood", "of Embers", "of Frost",
1846    "of Gales", "of Hatred", "of Inferno", "of Judgement", "of Kindness",
1847    "of Lunacy", "of Mirth", "of Nightfall", "of Oaths", "of Plague",
1848    "of Quickening", "of Rage", "of Sorrow", "of Torment", "of Utter Darkness",
1849    "of Virtue", "of Whispers", "of Xtreme Velocity", "of Yearlong Battle",
1850    "of Zero Mercy", "of Ancient Times", "of Battle Rage", "of Cold Fury",
1851    "of Deadly Precision", "of Endless War", "of Fallen Angels", "of Great Victory",
1852    "of Howling Wind", "of Inner Strength", "of Just Cause", "of Known Legends",
1853    "of Lost Souls", "of Mighty Blows", "of Noble Purpose", "of Old Magic",
1854    "of Perfect Form", "of Quiet Power", "of Righteous Fury", "of Sacred Ground",
1855    "of True Purpose", "of Undying Will", "of Vast Power", "of Warrior's Heart",
1856    "of Xeric Sands", "of Yearning Hearts", "of Zenith Power", "of Arcane Fury",
1857    "of Blazing Hearts", "of Crystal Clarity", "of Dark Purpose", "of Endless Night",
1858    "of Frozen Fury", "of Golden Light", "of Hidden Power", "of Iron Will",
1859    "of Jade Winds", "of King's Fury", "of Lasting Power", "of Moonlight",
1860    "of Nameless Dread", "of Otherworldly Power", "of Perfect Balance",
1861    "of Quick Strikes", "of Relentless Fury", "of Shining Light", "of True Power",
1862    "of Undying Power", "of Vast Destruction", "of World's Edge", "of Xenophobia",
1863    "of Youthful Energy", "of Zealous Fury", "of Absolute Destruction",
1864    "of Bright Horizons", "of Celestial Power", "of Deep Magic", "of Eternal Flame",
1865    "of Forgotten Lore", "of Grand Purpose", "of Hidden Depths", "of Ironbound Will",
1866    "of Just Rewards", "of King's Legacy", "of Lost Kingdoms", "of Many Battles",
1867    "of Noble Heritage", "of Old Kingdoms", "of Primal Fury", "of Quick Death",
1868    "of Red Fury", "of Starborn Power", "of Timeless Grace", "of Ultimate Power",
1869    "of Void Energy", "of War's End", "of Xenon Power", "of Younger Days",
1870    "of Zero Tolerance", "of Absolute Glory", "of Battle Hardened",
1871    "of Celestial Light", "of Dragon's Heart", "of Eternal Battle",
1872    "of Furious Heart", "of Ghost Steel", "of Hallowed Ground",
1873    "of Infinite Power", "of Just Battle", "of King's Honor",
1874];
1875
1876fn generate_item_name(prefix_idx: usize, base_idx: usize, suffix_idx: usize) -> String {
1877    let prefix = WEAPON_PREFIXES[prefix_idx % WEAPON_PREFIXES.len()];
1878    let base = WEAPON_BASES[base_idx % WEAPON_BASES.len()];
1879    let suffix = WEAPON_SUFFIXES[suffix_idx % WEAPON_SUFFIXES.len()];
1880    format!("{} {} {}", prefix, base, suffix)
1881}
1882
1883#[derive(Debug, Clone)]
1884pub struct ItemGenerationParams {
1885    pub item_level: u32,
1886    pub category: ItemCategory,
1887    pub rarity: Option<Rarity>,
1888    pub force_affixes: Vec<u64>,
1889    pub seed: u64,
1890}
1891
1892fn lcg_next(seed: &mut u64) -> u64 {
1893    *seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1894    *seed
1895}
1896
1897fn lcg_f32(seed: &mut u64) -> f32 {
1898    (lcg_next(seed) >> 32) as f32 / u32::MAX as f32
1899}
1900
1901pub fn generate_item(params: &ItemGenerationParams, affix_pool: &AffixPool, defs: &mut HashMap<u64, ItemDefinition>) -> ItemInstance {
1902    let mut seed = params.seed;
1903    let mut item_id = lcg_next(&mut seed);
1904    // Avoid collision
1905    while defs.contains_key(&item_id) { item_id = lcg_next(&mut seed); }
1906
1907    let rarity = params.rarity.unwrap_or_else(|| {
1908        let roll = lcg_f32(&mut seed);
1909        rarity_from_roll(roll)
1910    });
1911
1912    let rarity_cfg = RarityConfig::for_rarity(rarity);
1913    let num_affixes = rarity_cfg.min_affixes + (lcg_f32(&mut seed) * (rarity_cfg.max_affixes - rarity_cfg.min_affixes + 1) as f32) as u32;
1914    let quality = rarity_cfg.quality_min + lcg_f32(&mut seed) * (rarity_cfg.quality_max - rarity_cfg.quality_min);
1915
1916    let prefix_idx = lcg_next(&mut seed) as usize;
1917    let base_idx = lcg_next(&mut seed) as usize;
1918    let suffix_idx = lcg_next(&mut seed) as usize;
1919    let name = generate_item_name(prefix_idx, base_idx, suffix_idx);
1920
1921    let mut def = ItemDefinition::new(item_id, &name, params.category);
1922    def.rarity = rarity;
1923    def.item_level = params.item_level;
1924    def.required_level = (params.item_level as f32 * 0.8).floor() as u32;
1925    def.base_price = compute_base_price(params.item_level, rarity);
1926
1927    // Roll weapon damage if applicable
1928    if params.category.is_weapon() {
1929        let base_dps = 5.0 + params.item_level as f32 * 2.0 * rarity_cfg.stat_multiplier;
1930        let speed = 0.8 + lcg_f32(&mut seed) * 0.8;
1931        let avg_dmg = base_dps / speed;
1932        let variance = 0.2 + lcg_f32(&mut seed) * 0.3;
1933        let min_dmg = avg_dmg * (1.0 - variance);
1934        let max_dmg = avg_dmg * (1.0 + variance);
1935        def.damage_ranges.push(DamageRange {
1936            min_damage: min_dmg,
1937            max_damage: max_dmg,
1938            damage_type: DamageType::Physical,
1939            crit_multiplier: 1.5,
1940        });
1941        def.attack_speed = speed;
1942    }
1943
1944    // Roll armor if applicable
1945    if params.category.is_armor() {
1946        let base_armor = 10.0 + params.item_level as f32 * 3.0 * rarity_cfg.stat_multiplier;
1947        def.armor_values.physical_armor = base_armor * quality;
1948    }
1949
1950    // Roll explicit affixes
1951    let mut selected_affix_ids: Vec<u64> = params.force_affixes.clone();
1952    let eligible_prefixes = affix_pool.eligible(params.category, params.item_level, AffixType::Prefix);
1953    let eligible_suffixes = affix_pool.eligible(params.category, params.item_level, AffixType::Suffix);
1954
1955    let half_affixes = num_affixes / 2;
1956    roll_affixes_into(&mut def.explicit_modifiers, &mut selected_affix_ids, &eligible_prefixes, half_affixes, &mut seed, affix_pool);
1957    roll_affixes_into(&mut def.explicit_modifiers, &mut selected_affix_ids, &eligible_suffixes, num_affixes - half_affixes, &mut seed, affix_pool);
1958
1959    defs.insert(item_id, def);
1960
1961    let mut instance = ItemInstance::new(lcg_next(&mut seed), item_id);
1962    instance.quality = quality;
1963    instance
1964}
1965
1966fn roll_affixes_into(
1967    modifiers: &mut Vec<StatModifier>,
1968    selected_ids: &mut Vec<u64>,
1969    eligible: &[&AffixDefinition],
1970    count: u32,
1971    seed: &mut u64,
1972    pool: &AffixPool,
1973) {
1974    if eligible.is_empty() { return; }
1975    let item_level = 1; // dummy, actual is in eligible filter
1976    let weights: Vec<f64> = eligible.iter().map(|d| d.weight as f64).collect();
1977    if weights.is_empty() { return; }
1978    let alias = AliasTable::new(&weights);
1979
1980    let mut attempts = 0u32;
1981    let mut rolled = 0u32;
1982    while rolled < count && attempts < 50 {
1983        attempts += 1;
1984        let u1 = lcg_f32(seed) as f64;
1985        let u2 = lcg_f32(seed) as f64;
1986        let idx = alias.sample(u1, u2);
1987        let affix = eligible[idx];
1988        // Check conflicts
1989        if selected_ids.iter().any(|&id| affix.has_conflict(id)) { continue; }
1990        if selected_ids.contains(&affix.id) { continue; }
1991
1992        let roll_t = lcg_f32(seed);
1993        let mut m = affix.modifier_template.clone();
1994        m.flat_value = affix.roll_flat_value(roll_t);
1995        m.percent_value = affix.roll_percent_value(roll_t);
1996        modifiers.push(m);
1997        selected_ids.push(affix.id);
1998        rolled += 1;
1999    }
2000}
2001
2002fn rarity_from_roll(roll: f32) -> Rarity {
2003    // Cumulative probability thresholds (must sum to 1)
2004    const MYTHIC_THRESH: f32 = 0.0002;
2005    const LEGENDARY_THRESH: f32 = 0.0017;
2006    const EPIC_THRESH: f32 = 0.009;
2007    const RARE_THRESH: f32 = 0.041;
2008    const UNCOMMON_THRESH: f32 = 0.200;
2009    if roll < MYTHIC_THRESH { Rarity::Mythic }
2010    else if roll < LEGENDARY_THRESH { Rarity::Legendary }
2011    else if roll < EPIC_THRESH { Rarity::Epic }
2012    else if roll < RARE_THRESH { Rarity::Rare }
2013    else if roll < UNCOMMON_THRESH { Rarity::Uncommon }
2014    else { Rarity::Common }
2015}
2016
2017fn compute_base_price(item_level: u32, rarity: Rarity) -> u64 {
2018    let level_factor = (item_level as f64).powf(1.8);
2019    let rarity_mul = match rarity {
2020        Rarity::Common => 1.0,
2021        Rarity::Uncommon => 3.0,
2022        Rarity::Rare => 10.0,
2023        Rarity::Epic => 50.0,
2024        Rarity::Legendary => 250.0,
2025        Rarity::Mythic => 1000.0,
2026    };
2027    (level_factor * rarity_mul * 10.0) as u64
2028}
2029
2030// ============================================================
2031// POINT-BUY BALANCE VALIDATION
2032// ============================================================
2033
2034#[derive(Debug, Clone)]
2035pub struct BalanceReport {
2036    pub total_budget: f32,
2037    pub budget_per_slot: f32,
2038    pub budget_level_expected: f32,
2039    pub is_overpowered: bool,
2040    pub is_underpowered: bool,
2041    pub violations: Vec<String>,
2042    pub stat_breakdown: Vec<(String, f32)>,
2043}
2044
2045pub fn validate_item_balance(item: &ItemDefinition) -> BalanceReport {
2046    let total_budget = item.total_stat_budget();
2047    let expected_budget = budget_expectation(item.item_level, item.rarity);
2048    let tolerance = expected_budget * 0.25;
2049
2050    let mut violations = Vec::new();
2051    if total_budget > expected_budget + tolerance {
2052        violations.push(format!(
2053            "Item is overpowered: budget {:.1} exceeds expected {:.1} + tolerance {:.1}",
2054            total_budget, expected_budget, tolerance
2055        ));
2056    }
2057    if total_budget < expected_budget - tolerance {
2058        violations.push(format!(
2059            "Item is underpowered: budget {:.1} below expected {:.1} - tolerance {:.1}",
2060            total_budget, expected_budget, tolerance
2061        ));
2062    }
2063
2064    let mut stat_breakdown = Vec::new();
2065    for m in &item.implicit_modifiers {
2066        let cost = stat_budget_cost(&m.stat) * (m.flat_value.abs() + m.percent_value.abs() * 2.0);
2067        stat_breakdown.push((format!("{} (implicit)", m.stat.display_name()), cost));
2068    }
2069    for m in &item.explicit_modifiers {
2070        let cost = stat_budget_cost(&m.stat) * (m.flat_value.abs() + m.percent_value.abs() * 2.0);
2071        stat_breakdown.push((format!("{} (explicit)", m.stat.display_name()), cost));
2072    }
2073
2074    BalanceReport {
2075        total_budget,
2076        budget_per_slot: total_budget / EquipmentSlot::all().len() as f32,
2077        budget_level_expected: expected_budget,
2078        is_overpowered: total_budget > expected_budget + tolerance,
2079        is_underpowered: total_budget < expected_budget - tolerance,
2080        violations,
2081        stat_breakdown,
2082    }
2083}
2084
2085fn budget_expectation(item_level: u32, rarity: Rarity) -> f32 {
2086    let base = 5.0 + item_level as f32 * 1.5;
2087    let rarity_mul = match rarity {
2088        Rarity::Common => 1.0,
2089        Rarity::Uncommon => 1.5,
2090        Rarity::Rare => 2.5,
2091        Rarity::Epic => 4.0,
2092        Rarity::Legendary => 7.0,
2093        Rarity::Mythic => 12.0,
2094    };
2095    base * rarity_mul
2096}
2097
2098// ============================================================
2099// SEARCH / FILTER SYSTEM
2100// ============================================================
2101
2102#[derive(Debug, Clone)]
2103pub struct ItemFilter {
2104    pub name_query: String,
2105    pub categories: HashSet<ItemCategory>,
2106    pub rarities: HashSet<Rarity>,
2107    pub min_item_level: u32,
2108    pub max_item_level: u32,
2109    pub min_dps: f32,
2110    pub max_dps: f32,
2111    pub required_stats: Vec<(StatType, f32)>,
2112    pub show_stackable_only: bool,
2113    pub show_equippable_only: bool,
2114}
2115
2116impl ItemFilter {
2117    pub fn all() -> Self {
2118        ItemFilter {
2119            name_query: String::new(),
2120            categories: HashSet::new(),
2121            rarities: HashSet::new(),
2122            min_item_level: 0,
2123            max_item_level: u32::MAX,
2124            min_dps: 0.0,
2125            max_dps: f32::MAX,
2126            required_stats: Vec::new(),
2127            show_stackable_only: false,
2128            show_equippable_only: false,
2129        }
2130    }
2131
2132    pub fn matches(&self, item: &ItemDefinition) -> bool {
2133        // Name filter
2134        if !self.name_query.is_empty() {
2135            let query_lower = self.name_query.to_lowercase();
2136            if !item.name.to_lowercase().contains(&query_lower) &&
2137               !item.description.to_lowercase().contains(&query_lower) {
2138                return false;
2139            }
2140        }
2141
2142        // Category filter
2143        if !self.categories.is_empty() && !self.categories.contains(&item.category) {
2144            return false;
2145        }
2146
2147        // Rarity filter
2148        if !self.rarities.is_empty() && !self.rarities.contains(&item.rarity) {
2149            return false;
2150        }
2151
2152        // Item level range
2153        if item.item_level < self.min_item_level || item.item_level > self.max_item_level {
2154            return false;
2155        }
2156
2157        // DPS filter
2158        let dps = item.total_damage_per_second();
2159        if dps < self.min_dps || dps > self.max_dps {
2160            // Only apply dps filter for weapons
2161            if item.category.is_weapon() { return false; }
2162        }
2163
2164        // Stack filter
2165        if self.show_stackable_only && item.stack_size <= 1 {
2166            return false;
2167        }
2168
2169        // Equippable filter
2170        if self.show_equippable_only && !item.category.is_weapon() && !item.category.is_armor() && !item.category.is_accessory() {
2171            return false;
2172        }
2173
2174        // Required stat filter
2175        for (stat, min_val) in &self.required_stats {
2176            let total: f32 = item.explicit_modifiers.iter()
2177                .filter(|m| &m.stat == stat)
2178                .map(|m| m.flat_value + m.percent_value)
2179                .sum();
2180            if total < *min_val { return false; }
2181        }
2182
2183        true
2184    }
2185}
2186
2187// ============================================================
2188// IMPORT / EXPORT
2189// ============================================================
2190
2191#[derive(Debug, Clone)]
2192pub struct ItemExportRecord {
2193    pub id: u64,
2194    pub name: String,
2195    pub category: String,
2196    pub rarity: String,
2197    pub item_level: u32,
2198    pub base_price: u64,
2199    pub weight: f32,
2200    pub width: u8,
2201    pub height: u8,
2202    pub stack_size: u32,
2203    pub dps: f32,
2204    pub armor: f32,
2205    pub modifiers_csv: String,
2206}
2207
2208impl ItemExportRecord {
2209    pub fn from_definition(def: &ItemDefinition) -> Self {
2210        let mods_csv: String = def.explicit_modifiers.iter().map(|m| {
2211            format!("{}:{:.1}f:{:.1}p", m.stat.display_name(), m.flat_value, m.percent_value)
2212        }).collect::<Vec<_>>().join(";");
2213
2214        ItemExportRecord {
2215            id: def.id,
2216            name: def.name.clone(),
2217            category: def.category.display_name().to_string(),
2218            rarity: RarityConfig::for_rarity(def.rarity).display_name().to_string(),
2219            item_level: def.item_level,
2220            base_price: def.base_price,
2221            weight: def.weight,
2222            width: def.width,
2223            height: def.height,
2224            stack_size: def.stack_size,
2225            dps: def.total_damage_per_second(),
2226            armor: def.armor_values.physical_armor,
2227            modifiers_csv: mods_csv,
2228        }
2229    }
2230
2231    pub fn to_csv_row(&self) -> String {
2232        format!(
2233            "{},{},{},{},{},{},{},{},{},{},{:.2},{:.1},\"{}\"",
2234            self.id, self.name, self.category, self.rarity,
2235            self.item_level, self.base_price, self.weight,
2236            self.width, self.height, self.stack_size,
2237            self.dps, self.armor, self.modifiers_csv
2238        )
2239    }
2240
2241    pub fn csv_header() -> &'static str {
2242        "id,name,category,rarity,item_level,base_price,weight,width,height,stack_size,dps,armor,modifiers"
2243    }
2244}
2245
2246pub fn export_database_csv(defs: &HashMap<u64, ItemDefinition>) -> String {
2247    let mut lines = vec![ItemExportRecord::csv_header().to_string()];
2248    let mut sorted: Vec<&ItemDefinition> = defs.values().collect();
2249    sorted.sort_by_key(|d| d.id);
2250    for def in sorted {
2251        lines.push(ItemExportRecord::from_definition(def).to_csv_row());
2252    }
2253    lines.join("\n")
2254}
2255
2256// ============================================================
2257// ITEM DATABASE
2258// ============================================================
2259
2260#[derive(Debug, Clone)]
2261pub struct ItemDatabase {
2262    pub definitions: HashMap<u64, ItemDefinition>,
2263    pub recipes: HashMap<u64, RecipeDefinition>,
2264    pub loot_tables: HashMap<u64, LootTable>,
2265    pub item_sets: HashMap<u64, ItemSet>,
2266    pub affix_pool: AffixPool,
2267    pub vendors: HashMap<u64, VendorDefinition>,
2268    pub next_id: u64,
2269}
2270
2271impl ItemDatabase {
2272    pub fn new() -> Self {
2273        ItemDatabase {
2274            definitions: HashMap::new(),
2275            recipes: HashMap::new(),
2276            loot_tables: HashMap::new(),
2277            item_sets: HashMap::new(),
2278            affix_pool: AffixPool::new(),
2279            vendors: HashMap::new(),
2280            next_id: 1,
2281        }
2282    }
2283
2284    pub fn alloc_id(&mut self) -> u64 {
2285        let id = self.next_id;
2286        self.next_id += 1;
2287        id
2288    }
2289
2290    pub fn add_definition(&mut self, def: ItemDefinition) {
2291        self.definitions.insert(def.id, def);
2292    }
2293
2294    pub fn add_recipe(&mut self, recipe: RecipeDefinition) {
2295        self.recipes.insert(recipe.id, recipe);
2296    }
2297
2298    pub fn add_loot_table(&mut self, lt: LootTable) {
2299        self.loot_tables.insert(lt.id, lt);
2300    }
2301
2302    pub fn add_set(&mut self, set: ItemSet) {
2303        self.item_sets.insert(set.id, set);
2304    }
2305
2306    pub fn get_definition(&self, id: u64) -> Option<&ItemDefinition> {
2307        self.definitions.get(&id)
2308    }
2309
2310    pub fn search(&self, filter: &ItemFilter) -> Vec<&ItemDefinition> {
2311        let mut results: Vec<&ItemDefinition> = self.definitions.values()
2312            .filter(|d| filter.matches(d))
2313            .collect();
2314        results.sort_by(|a, b| a.name.cmp(&b.name));
2315        results
2316    }
2317
2318    pub fn bulk_update_prices(&mut self, factor: f32) {
2319        for def in self.definitions.values_mut() {
2320            def.base_price = (def.base_price as f32 * factor) as u64;
2321        }
2322    }
2323
2324    pub fn stats_summary(&self) -> DatabaseStats {
2325        let mut by_rarity: HashMap<Rarity, u32> = HashMap::new();
2326        let mut by_category: HashMap<String, u32> = HashMap::new();
2327        let mut total_value: u64 = 0;
2328        for def in self.definitions.values() {
2329            *by_rarity.entry(def.rarity).or_insert(0) += 1;
2330            *by_category.entry(def.category.display_name().to_string()).or_insert(0) += 1;
2331            total_value += def.base_price;
2332        }
2333        DatabaseStats {
2334            total_items: self.definitions.len(),
2335            total_recipes: self.recipes.len(),
2336            total_loot_tables: self.loot_tables.len(),
2337            total_sets: self.item_sets.len(),
2338            by_rarity,
2339            by_category,
2340            total_catalog_value: total_value,
2341        }
2342    }
2343}
2344
2345#[derive(Debug, Clone)]
2346pub struct DatabaseStats {
2347    pub total_items: usize,
2348    pub total_recipes: usize,
2349    pub total_loot_tables: usize,
2350    pub total_sets: usize,
2351    pub by_rarity: HashMap<Rarity, u32>,
2352    pub by_category: HashMap<String, u32>,
2353    pub total_catalog_value: u64,
2354}
2355
2356// ============================================================
2357// INVENTORY EDITOR STATE
2358// ============================================================
2359
2360#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2361pub enum EditorTab {
2362    ItemBrowser,
2363    ItemEditor,
2364    RecipeEditor,
2365    LootTableEditor,
2366    SetEditor,
2367    VendorEditor,
2368    AffixEditor,
2369    Statistics,
2370}
2371
2372#[derive(Debug, Clone)]
2373pub struct ItemEditorState {
2374    pub current_item: Option<ItemDefinition>,
2375    pub is_dirty: bool,
2376    pub validation_errors: Vec<String>,
2377    pub balance_report: Option<BalanceReport>,
2378    pub undo_stack: VecDeque<ItemDefinition>,
2379    pub redo_stack: VecDeque<ItemDefinition>,
2380}
2381
2382impl ItemEditorState {
2383    pub fn new() -> Self {
2384        ItemEditorState {
2385            current_item: None,
2386            is_dirty: false,
2387            validation_errors: Vec::new(),
2388            balance_report: None,
2389            undo_stack: VecDeque::new(),
2390            redo_stack: VecDeque::new(),
2391        }
2392    }
2393
2394    pub fn open_item(&mut self, item: ItemDefinition) {
2395        self.current_item = Some(item);
2396        self.is_dirty = false;
2397        self.validation_errors.clear();
2398        self.balance_report = None;
2399    }
2400
2401    pub fn mark_dirty(&mut self) {
2402        self.is_dirty = true;
2403        if let Some(ref item) = self.current_item {
2404            if self.undo_stack.len() >= 50 { self.undo_stack.pop_front(); }
2405            self.undo_stack.push_back(item.clone());
2406            self.redo_stack.clear();
2407        }
2408    }
2409
2410    pub fn undo(&mut self) {
2411        if let Some(prev) = self.undo_stack.pop_back() {
2412            if let Some(ref current) = self.current_item {
2413                if self.redo_stack.len() >= 50 { self.redo_stack.pop_front(); }
2414                self.redo_stack.push_back(current.clone());
2415            }
2416            self.current_item = Some(prev);
2417            self.is_dirty = true;
2418        }
2419    }
2420
2421    pub fn redo(&mut self) {
2422        if let Some(next) = self.redo_stack.pop_back() {
2423            if let Some(ref current) = self.current_item {
2424                if self.undo_stack.len() >= 50 { self.undo_stack.pop_front(); }
2425                self.undo_stack.push_back(current.clone());
2426            }
2427            self.current_item = Some(next);
2428            self.is_dirty = true;
2429        }
2430    }
2431
2432    pub fn validate(&mut self) {
2433        if let Some(ref item) = self.current_item {
2434            self.validation_errors = item.validate();
2435            self.balance_report = Some(validate_item_balance(item));
2436        }
2437    }
2438}
2439
2440#[derive(Debug, Clone)]
2441pub struct BrowserState {
2442    pub filter: ItemFilter,
2443    pub filtered_ids: Vec<u64>,
2444    pub selected_ids: HashSet<u64>,
2445    pub sort_column: BrowserSortColumn,
2446    pub sort_ascending: bool,
2447    pub scroll_offset: f32,
2448    pub row_height: f32,
2449    pub visible_rows: u32,
2450}
2451
2452#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2453pub enum BrowserSortColumn {
2454    Name,
2455    Category,
2456    Rarity,
2457    ItemLevel,
2458    DPS,
2459    Price,
2460}
2461
2462impl BrowserState {
2463    pub fn new() -> Self {
2464        BrowserState {
2465            filter: ItemFilter::all(),
2466            filtered_ids: Vec::new(),
2467            selected_ids: HashSet::new(),
2468            sort_column: BrowserSortColumn::Name,
2469            sort_ascending: true,
2470            scroll_offset: 0.0,
2471            row_height: 22.0,
2472            visible_rows: 30,
2473        }
2474    }
2475
2476    pub fn refresh(&mut self, db: &ItemDatabase) {
2477        let results = db.search(&self.filter);
2478        self.filtered_ids = results.iter().map(|d| d.id).collect();
2479        self.sort_results(db);
2480    }
2481
2482    pub fn sort_results(&mut self, db: &ItemDatabase) {
2483        let col = self.sort_column;
2484        let asc = self.sort_ascending;
2485        self.filtered_ids.sort_by(|&a, &b| {
2486            let da = db.definitions.get(&a);
2487            let db_def = db.definitions.get(&b);
2488            match (da, db_def) {
2489                (Some(da), Some(db)) => {
2490                    let cmp = match col {
2491                        BrowserSortColumn::Name => da.name.cmp(&db.name),
2492                        BrowserSortColumn::Category => da.category.display_name().cmp(db.category.display_name()),
2493                        BrowserSortColumn::Rarity => da.rarity.cmp(&db.rarity),
2494                        BrowserSortColumn::ItemLevel => da.item_level.cmp(&db.item_level),
2495                        BrowserSortColumn::DPS => da.total_damage_per_second().partial_cmp(&db.total_damage_per_second()).unwrap_or(std::cmp::Ordering::Equal),
2496                        BrowserSortColumn::Price => da.base_price.cmp(&db.base_price),
2497                    };
2498                    if asc { cmp } else { cmp.reverse() }
2499                }
2500                _ => std::cmp::Ordering::Equal,
2501            }
2502        });
2503    }
2504
2505    pub fn visible_item_ids(&self) -> &[u64] {
2506        let start = (self.scroll_offset / self.row_height) as usize;
2507        let end = (start + self.visible_rows as usize).min(self.filtered_ids.len());
2508        &self.filtered_ids[start..end]
2509    }
2510
2511    pub fn total_height(&self) -> f32 {
2512        self.filtered_ids.len() as f32 * self.row_height
2513    }
2514
2515    pub fn select_all(&mut self) {
2516        for &id in &self.filtered_ids {
2517            self.selected_ids.insert(id);
2518        }
2519    }
2520
2521    pub fn deselect_all(&mut self) {
2522        self.selected_ids.clear();
2523    }
2524
2525    pub fn toggle_select(&mut self, id: u64) {
2526        if self.selected_ids.contains(&id) {
2527            self.selected_ids.remove(&id);
2528        } else {
2529            self.selected_ids.insert(id);
2530        }
2531    }
2532}
2533
2534#[derive(Debug, Clone)]
2535pub struct RecipeEditorState {
2536    pub current_recipe: Option<RecipeDefinition>,
2537    pub is_dirty: bool,
2538    pub preview_quality: f32,
2539    pub preview_skill_level: u32,
2540    pub preview_success_chance: f32,
2541    pub preview_expected_quality: f32,
2542}
2543
2544impl RecipeEditorState {
2545    pub fn new() -> Self {
2546        RecipeEditorState {
2547            current_recipe: None,
2548            is_dirty: false,
2549            preview_quality: 1.0,
2550            preview_skill_level: 50,
2551            preview_success_chance: 0.75,
2552            preview_expected_quality: 1.0,
2553        }
2554    }
2555
2556    pub fn update_preview(&mut self) {
2557        if let Some(ref recipe) = self.current_recipe {
2558            self.preview_success_chance = recipe.success_probability(self.preview_skill_level);
2559            self.preview_expected_quality = recipe.expected_quality(self.preview_skill_level);
2560        }
2561    }
2562}
2563
2564#[derive(Debug, Clone)]
2565pub struct LootTableEditorState {
2566    pub current_table: Option<LootTable>,
2567    pub is_dirty: bool,
2568    pub simulate_context: LootContext,
2569    pub simulated_drops: Vec<(u64, u32)>,
2570    pub simulation_count: u32,
2571    pub drop_frequency: HashMap<u64, u32>,
2572}
2573
2574impl LootTableEditorState {
2575    pub fn new() -> Self {
2576        LootTableEditorState {
2577            current_table: None,
2578            is_dirty: false,
2579            simulate_context: LootContext::default(),
2580            simulated_drops: Vec::new(),
2581            simulation_count: 1000,
2582            drop_frequency: HashMap::new(),
2583        }
2584    }
2585
2586    pub fn run_simulation(&mut self) {
2587        self.drop_frequency.clear();
2588        if let Some(ref table) = self.current_table {
2589            let mut seed: u64 = 12345678;
2590            for _ in 0..self.simulation_count {
2591                let mut rvs = Vec::new();
2592                for _ in 0..20 {
2593                    rvs.push(lcg_f32(&mut seed));
2594                }
2595                let ctx = self.simulate_context.clone();
2596                let drops = table.roll_drops(&ctx, &rvs);
2597                for (item_id, qty) in drops {
2598                    *self.drop_frequency.entry(item_id).or_insert(0) += qty;
2599                }
2600            }
2601        }
2602    }
2603
2604    pub fn drop_rate_percent(&self, item_id: u64) -> f32 {
2605        let count = self.drop_frequency.get(&item_id).copied().unwrap_or(0);
2606        count as f32 / self.simulation_count as f32 * 100.0
2607    }
2608}
2609
2610#[derive(Debug, Clone)]
2611pub struct AffixEditorState {
2612    pub current_affix: Option<AffixDefinition>,
2613    pub is_dirty: bool,
2614    pub preview_rolls: Vec<f32>,
2615}
2616
2617impl AffixEditorState {
2618    pub fn new() -> Self {
2619        AffixEditorState {
2620            current_affix: None,
2621            is_dirty: false,
2622            preview_rolls: vec![0.0, 0.25, 0.5, 0.75, 1.0],
2623        }
2624    }
2625
2626    pub fn preview_values(&self) -> Vec<(f32, f32, f32)> {
2627        if let Some(ref affix) = self.current_affix {
2628            self.preview_rolls.iter().map(|&t| {
2629                (t, affix.roll_flat_value(t), affix.roll_percent_value(t))
2630            }).collect()
2631        } else {
2632            Vec::new()
2633        }
2634    }
2635}
2636
2637// ============================================================
2638// MAIN INVENTORY EDITOR
2639// ============================================================
2640
2641#[derive(Debug)]
2642pub struct InventoryEditor {
2643    pub database: ItemDatabase,
2644    pub active_tab: EditorTab,
2645    pub item_editor: ItemEditorState,
2646    pub browser: BrowserState,
2647    pub recipe_editor: RecipeEditorState,
2648    pub loot_table_editor: LootTableEditorState,
2649    pub affix_editor: AffixEditorState,
2650    pub generation_params: ItemGenerationParams,
2651    pub last_generated_instance: Option<ItemInstance>,
2652    pub window_size: Vec2,
2653    pub panel_split: f32,
2654    pub status_message: String,
2655    pub status_timer: f32,
2656    pub bulk_edit_field: String,
2657    pub bulk_edit_value: String,
2658    pub preview_item_id: Option<u64>,
2659    pub clipboard_item: Option<ItemDefinition>,
2660    pub search_history: VecDeque<String>,
2661    pub tag_registry: HashMap<String, Vec<u64>>,
2662    pub recently_modified: VecDeque<u64>,
2663}
2664
2665impl InventoryEditor {
2666    pub fn new() -> Self {
2667        let db = ItemDatabase::new();
2668        InventoryEditor {
2669            database: db,
2670            active_tab: EditorTab::ItemBrowser,
2671            item_editor: ItemEditorState::new(),
2672            browser: BrowserState::new(),
2673            recipe_editor: RecipeEditorState::new(),
2674            loot_table_editor: LootTableEditorState::new(),
2675            affix_editor: AffixEditorState::new(),
2676            generation_params: ItemGenerationParams {
2677                item_level: 1,
2678                category: ItemCategory::Sword,
2679                rarity: None,
2680                force_affixes: Vec::new(),
2681                seed: 42,
2682            },
2683            last_generated_instance: None,
2684            window_size: Vec2::new(1280.0, 720.0),
2685            panel_split: 0.35,
2686            status_message: String::new(),
2687            status_timer: 0.0,
2688            bulk_edit_field: String::new(),
2689            bulk_edit_value: String::new(),
2690            preview_item_id: None,
2691            clipboard_item: None,
2692            search_history: VecDeque::new(),
2693            tag_registry: HashMap::new(),
2694            recently_modified: VecDeque::new(),
2695        }
2696    }
2697
2698    pub fn set_status(&mut self, msg: impl Into<String>) {
2699        self.status_message = msg.into();
2700        self.status_timer = 3.0;
2701    }
2702
2703    pub fn tick(&mut self, dt: f32) {
2704        if self.status_timer > 0.0 {
2705            self.status_timer = (self.status_timer - dt).max(0.0);
2706        }
2707    }
2708
2709    pub fn open_item_by_id(&mut self, id: u64) {
2710        if let Some(def) = self.database.definitions.get(&id).cloned() {
2711            self.item_editor.open_item(def);
2712            self.active_tab = EditorTab::ItemEditor;
2713            self.recently_modified.push_front(id);
2714            if self.recently_modified.len() > 20 { self.recently_modified.pop_back(); }
2715        }
2716    }
2717
2718    pub fn save_current_item(&mut self) -> bool {
2719        if let Some(item) = self.item_editor.current_item.clone() {
2720            self.item_editor.validate();
2721            if !self.item_editor.validation_errors.is_empty() {
2722                self.set_status(format!("Cannot save: {} validation error(s)", self.item_editor.validation_errors.len()));
2723                return false;
2724            }
2725            let id = item.id;
2726            self.database.definitions.insert(id, item);
2727            self.item_editor.is_dirty = false;
2728            self.set_status("Item saved successfully.");
2729            self.browser.refresh(&self.database);
2730            true
2731        } else {
2732            false
2733        }
2734    }
2735
2736    pub fn create_new_item(&mut self, category: ItemCategory) {
2737        let id = self.database.alloc_id();
2738        let def = ItemDefinition::new(id, "New Item", category);
2739        self.item_editor.open_item(def);
2740        self.active_tab = EditorTab::ItemEditor;
2741    }
2742
2743    pub fn delete_selected_items(&mut self) {
2744        let to_delete: Vec<u64> = self.browser.selected_ids.iter().cloned().collect();
2745        for id in &to_delete {
2746            self.database.definitions.remove(id);
2747        }
2748        self.browser.selected_ids.clear();
2749        self.browser.refresh(&self.database);
2750        self.set_status(format!("Deleted {} item(s).", to_delete.len()));
2751    }
2752
2753    pub fn duplicate_item(&mut self, id: u64) {
2754        if let Some(def) = self.database.definitions.get(&id).cloned() {
2755            let new_id = self.database.alloc_id();
2756            let mut new_def = def;
2757            new_def.id = new_id;
2758            new_def.name = format!("{} (Copy)", new_def.name);
2759            self.database.definitions.insert(new_id, new_def);
2760            self.browser.refresh(&self.database);
2761            self.set_status("Item duplicated.");
2762        }
2763    }
2764
2765    pub fn generate_random_item(&mut self) {
2766        let params = self.generation_params.clone();
2767        let inst = generate_item(&params, &self.database.affix_pool, &mut self.database.definitions);
2768        self.last_generated_instance = Some(inst);
2769        self.browser.refresh(&self.database);
2770        self.set_status("Random item generated.");
2771        // Increment seed for next generation
2772        self.generation_params.seed = self.generation_params.seed.wrapping_add(1);
2773    }
2774
2775    pub fn copy_item(&mut self) {
2776        if let Some(ref item) = self.item_editor.current_item {
2777            self.clipboard_item = Some(item.clone());
2778            self.set_status("Item copied to clipboard.");
2779        }
2780    }
2781
2782    pub fn paste_item(&mut self) {
2783        if let Some(item) = self.clipboard_item.clone() {
2784            let new_id = self.database.alloc_id();
2785            let mut new_item = item;
2786            new_item.id = new_id;
2787            new_item.name = format!("{} (Paste)", new_item.name);
2788            self.database.definitions.insert(new_id, new_item.clone());
2789            self.item_editor.open_item(new_item);
2790            self.browser.refresh(&self.database);
2791            self.set_status("Item pasted from clipboard.");
2792        }
2793    }
2794
2795    pub fn bulk_update_selected(&mut self, field: &str, value_str: &str) -> usize {
2796        let ids: Vec<u64> = self.browser.selected_ids.iter().cloned().collect();
2797        let mut changed = 0usize;
2798        for id in ids {
2799            if let Some(def) = self.database.definitions.get_mut(&id) {
2800                match field {
2801                    "required_level" => {
2802                        if let Ok(v) = value_str.parse::<u32>() {
2803                            def.required_level = v; changed += 1;
2804                        }
2805                    }
2806                    "base_price" => {
2807                        if let Ok(v) = value_str.parse::<u64>() {
2808                            def.base_price = v; changed += 1;
2809                        }
2810                    }
2811                    "weight" => {
2812                        if let Ok(v) = value_str.parse::<f32>() {
2813                            def.weight = v; changed += 1;
2814                        }
2815                    }
2816                    "rarity" => {
2817                        match value_str {
2818                            "Common" => { def.rarity = Rarity::Common; changed += 1; }
2819                            "Uncommon" => { def.rarity = Rarity::Uncommon; changed += 1; }
2820                            "Rare" => { def.rarity = Rarity::Rare; changed += 1; }
2821                            "Epic" => { def.rarity = Rarity::Epic; changed += 1; }
2822                            "Legendary" => { def.rarity = Rarity::Legendary; changed += 1; }
2823                            "Mythic" => { def.rarity = Rarity::Mythic; changed += 1; }
2824                            _ => {}
2825                        }
2826                    }
2827                    "bind_on_pickup" => {
2828                        match value_str {
2829                            "true" | "1" => { def.bind_on_pickup = true; changed += 1; }
2830                            "false" | "0" => { def.bind_on_pickup = false; changed += 1; }
2831                            _ => {}
2832                        }
2833                    }
2834                    _ => {}
2835                }
2836            }
2837        }
2838        self.browser.refresh(&self.database);
2839        self.set_status(format!("Bulk updated {} items.", changed));
2840        changed
2841    }
2842
2843    pub fn export_to_csv(&self) -> String {
2844        export_database_csv(&self.database.definitions)
2845    }
2846
2847    pub fn add_tag_to_item(&mut self, item_id: u64, tag: String) {
2848        self.tag_registry.entry(tag).or_insert_with(Vec::new).push(item_id);
2849    }
2850
2851    pub fn items_with_tag(&self, tag: &str) -> Vec<u64> {
2852        self.tag_registry.get(tag).cloned().unwrap_or_default()
2853    }
2854
2855    pub fn search_with_history(&mut self, query: String) {
2856        if !query.is_empty() {
2857            self.search_history.push_front(query.clone());
2858            if self.search_history.len() > 20 { self.search_history.pop_back(); }
2859            self.search_history = self.search_history.drain(..).collect::<Vec<_>>().into_iter().fold(std::collections::VecDeque::new(), |mut acc, x| { if acc.back() != Some(&x) { acc.push_back(x); } acc });
2860        }
2861        self.browser.filter.name_query = query;
2862        self.browser.refresh(&self.database);
2863    }
2864
2865    pub fn get_item_tooltip(&self, id: u64) -> Option<ItemTooltip> {
2866        let def = self.database.definitions.get(&id)?;
2867        let cfg = RarityConfig::for_rarity(def.rarity);
2868        let mut lines = Vec::new();
2869        lines.push((def.name.clone(), cfg.color));
2870        lines.push((format!("{} | {}", def.category.display_name(), cfg.display_name()), Vec4::new(0.7,0.7,0.7,1.0)));
2871        lines.push((format!("Item Level: {}", def.item_level), Vec4::new(0.8,0.8,0.8,1.0)));
2872        if def.category.is_weapon() {
2873            let dps = def.total_damage_per_second();
2874            lines.push((format!("DPS: {:.1}", dps), Vec4::new(1.0,0.9,0.5,1.0)));
2875            for d in &def.damage_ranges {
2876                lines.push((format!("  {:?}: {:.0}-{:.0}", d.damage_type, d.min_damage, d.max_damage), d.damage_type.color()));
2877            }
2878            lines.push((format!("Speed: {:.2}", def.attack_speed), Vec4::new(0.7,0.7,0.7,1.0)));
2879        }
2880        if def.armor_values.physical_armor > 0.0 {
2881            lines.push((format!("Armor: {:.0}", def.armor_values.physical_armor), Vec4::new(0.7,0.9,1.0,1.0)));
2882        }
2883        for m in &def.implicit_modifiers {
2884            let sign = if m.flat_value >= 0.0 { "+" } else { "" };
2885            lines.push((format!("{}{:.0} {}", sign, m.flat_value, m.stat.display_name()), Vec4::new(0.8,0.8,0.6,1.0)));
2886        }
2887        for m in &def.explicit_modifiers {
2888            let sign = if m.flat_value >= 0.0 { "+" } else { "" };
2889            let color = if m.flat_value >= 0.0 { Vec4::new(0.5,1.0,0.5,1.0) } else { Vec4::new(1.0,0.5,0.5,1.0) };
2890            lines.push((format!("{}{:.0} {}", sign, m.flat_value, m.stat.display_name()), color));
2891        }
2892        if def.requirements.level > 1 {
2893            lines.push((format!("Required Level: {}", def.requirements.level), Vec4::new(0.9,0.6,0.3,1.0)));
2894        }
2895        if !def.flavor_text.is_empty() {
2896            lines.push((def.flavor_text.clone(), Vec4::new(0.6,0.6,0.8,1.0)));
2897        }
2898        lines.push((format!("Value: {} gold", def.base_price), Vec4::new(1.0,0.85,0.0,1.0)));
2899
2900        Some(ItemTooltip { lines })
2901    }
2902
2903    pub fn layout(&self) -> EditorLayout {
2904        let left_panel_width = self.window_size.x * self.panel_split;
2905        let right_panel_width = self.window_size.x - left_panel_width - 2.0;
2906        let header_height = 40.0;
2907        let footer_height = 24.0;
2908        let content_height = self.window_size.y - header_height - footer_height;
2909
2910        EditorLayout {
2911            header_rect: Vec4::new(0.0, 0.0, self.window_size.x, header_height),
2912            left_panel_rect: Vec4::new(0.0, header_height, left_panel_width, content_height),
2913            right_panel_rect: Vec4::new(left_panel_width + 2.0, header_height, right_panel_width, content_height),
2914            footer_rect: Vec4::new(0.0, self.window_size.y - footer_height, self.window_size.x, footer_height),
2915            divider_x: left_panel_width,
2916        }
2917    }
2918}
2919
2920#[derive(Debug, Clone)]
2921pub struct ItemTooltip {
2922    pub lines: Vec<(String, Vec4)>,
2923}
2924
2925#[derive(Debug, Clone)]
2926pub struct EditorLayout {
2927    pub header_rect: Vec4,
2928    pub left_panel_rect: Vec4,
2929    pub right_panel_rect: Vec4,
2930    pub footer_rect: Vec4,
2931    pub divider_x: f32,
2932}
2933
2934// ============================================================
2935// GRID INVENTORY PREVIEW RENDERER (coordinate math)
2936// ============================================================
2937
2938#[derive(Debug, Clone)]
2939pub struct GridRenderer {
2940    pub cell_size: f32,
2941    pub origin: Vec2,
2942    pub padding: f32,
2943}
2944
2945impl GridRenderer {
2946    pub fn new(cell_size: f32, origin: Vec2) -> Self {
2947        GridRenderer { cell_size, origin, padding: 2.0 }
2948    }
2949
2950    pub fn cell_rect(&self, col: u32, row: u32) -> Vec4 {
2951        let x = self.origin.x + col as f32 * (self.cell_size + self.padding);
2952        let y = self.origin.y + row as f32 * (self.cell_size + self.padding);
2953        Vec4::new(x, y, self.cell_size, self.cell_size)
2954    }
2955
2956    pub fn item_rect(&self, col: u32, row: u32, width: u32, height: u32) -> Vec4 {
2957        let x = self.origin.x + col as f32 * (self.cell_size + self.padding);
2958        let y = self.origin.y + row as f32 * (self.cell_size + self.padding);
2959        let w = width as f32 * (self.cell_size + self.padding) - self.padding;
2960        let h = height as f32 * (self.cell_size + self.padding) - self.padding;
2961        Vec4::new(x, y, w, h)
2962    }
2963
2964    pub fn total_size(&self, grid_width: u32, grid_height: u32) -> Vec2 {
2965        let w = grid_width as f32 * (self.cell_size + self.padding);
2966        let h = grid_height as f32 * (self.cell_size + self.padding);
2967        Vec2::new(w, h)
2968    }
2969
2970    pub fn hit_test(&self, point: Vec2, grid_width: u32, grid_height: u32) -> Option<(u32, u32)> {
2971        let rel = point - self.origin;
2972        if rel.x < 0.0 || rel.y < 0.0 { return None; }
2973        let col = (rel.x / (self.cell_size + self.padding)) as u32;
2974        let row = (rel.y / (self.cell_size + self.padding)) as u32;
2975        if col >= grid_width || row >= grid_height { return None; }
2976        Some((col, row))
2977    }
2978
2979    pub fn snap_to_grid(&self, point: Vec2) -> Vec2 {
2980        let step = self.cell_size + self.padding;
2981        let col = (point.x / step).floor();
2982        let row = (point.y / step).floor();
2983        Vec2::new(col * step + self.origin.x, row * step + self.origin.y)
2984    }
2985}
2986
2987// ============================================================
2988// SORTING ALGORITHMS (tetromino bin-packing)
2989// ============================================================
2990
2991// Skyline algorithm for 2D bin packing
2992#[derive(Debug, Clone)]
2993pub struct SkylinePacker {
2994    pub width: u32,
2995    pub skyline: Vec<u32>, // height at each column
2996}
2997
2998impl SkylinePacker {
2999    pub fn new(width: u32) -> Self {
3000        SkylinePacker { width, skyline: vec![0; width as usize] }
3001    }
3002
3003    pub fn find_placement(&self, item_w: u32, item_h: u32) -> Option<(u32, u32)> {
3004        if item_w > self.width { return None; }
3005        let max_col = self.width - item_w;
3006        let mut best: Option<(u32, u32, u32)> = None; // (col, row, cost)
3007
3008        for start_col in 0..=max_col {
3009            let max_height = (start_col..start_col + item_w)
3010                .map(|c| self.skyline[c as usize])
3011                .max()
3012                .unwrap_or(0);
3013            let placement_row = max_height;
3014            // Cost is placement_row (prefer lowest placements)
3015            if best.map(|(_, _, c)| placement_row < c).unwrap_or(true) {
3016                best = Some((start_col, placement_row, placement_row));
3017            }
3018        }
3019
3020        best.map(|(c, r, _)| (c, r))
3021    }
3022
3023    pub fn place(&mut self, col: u32, row: u32, item_w: u32, item_h: u32) {
3024        let new_height = row + item_h;
3025        for c in col..col + item_w {
3026            if (c as usize) < self.skyline.len() {
3027                self.skyline[c as usize] = self.skyline[c as usize].max(new_height);
3028            }
3029        }
3030    }
3031
3032    pub fn pack_items(grid_width: u32, items: &mut Vec<(u32, u32, u64)>) -> Vec<(u64, u32, u32)> {
3033        // items = Vec<(width, height, id)>
3034        // returns Vec<(id, col, row)>
3035        let mut packer = SkylinePacker::new(grid_width);
3036        let mut placements = Vec::new();
3037
3038        // Sort by area desc (larger items first for better packing)
3039        items.sort_by(|a, b| (b.0 * b.1).cmp(&(a.0 * a.1)));
3040
3041        for &(w, h, id) in items.iter() {
3042            if let Some((col, row)) = packer.find_placement(w, h) {
3043                packer.place(col, row, w, h);
3044                placements.push((id, col, row));
3045            }
3046        }
3047        placements
3048    }
3049}
3050
3051// ============================================================
3052// ITEM COMPARISON TOOL
3053// ============================================================
3054
3055#[derive(Debug, Clone)]
3056pub struct ItemComparison {
3057    pub item_a: ItemDefinition,
3058    pub item_b: ItemDefinition,
3059    pub stat_diffs: Vec<StatDiff>,
3060    pub armor_diff: ArmorDiff,
3061    pub dps_diff: f32,
3062    pub price_diff: i64,
3063    pub weight_diff: f32,
3064}
3065
3066#[derive(Debug, Clone)]
3067pub struct StatDiff {
3068    pub stat: StatType,
3069    pub value_a: f32,
3070    pub value_b: f32,
3071    pub delta: f32,
3072    pub is_better_on_b: bool,
3073}
3074
3075#[derive(Debug, Clone)]
3076pub struct ArmorDiff {
3077    pub physical_armor_delta: f32,
3078    pub magic_armor_delta: f32,
3079    pub fire_resist_delta: f32,
3080    pub cold_resist_delta: f32,
3081    pub lightning_resist_delta: f32,
3082    pub poison_resist_delta: f32,
3083}
3084
3085pub fn compare_items(a: &ItemDefinition, b: &ItemDefinition) -> ItemComparison {
3086    let mut stat_map_a: HashMap<String, f32> = HashMap::new();
3087    let mut stat_map_b: HashMap<String, f32> = HashMap::new();
3088
3089    for m in &a.explicit_modifiers {
3090        *stat_map_a.entry(m.stat.display_name().to_string()).or_insert(0.0) += m.flat_value + m.percent_value;
3091    }
3092    for m in &b.explicit_modifiers {
3093        *stat_map_b.entry(m.stat.display_name().to_string()).or_insert(0.0) += m.flat_value + m.percent_value;
3094    }
3095
3096    let mut all_stats: HashSet<String> = HashSet::new();
3097    all_stats.extend(stat_map_a.keys().cloned());
3098    all_stats.extend(stat_map_b.keys().cloned());
3099
3100    let mut stat_diffs: Vec<StatDiff> = all_stats.into_iter().filter_map(|stat_name| {
3101        let va = stat_map_a.get(&stat_name).copied().unwrap_or(0.0);
3102        let vb = stat_map_b.get(&stat_name).copied().unwrap_or(0.0);
3103        // Find the StatType enum value — use damage mod as fallback
3104        let st = StatType::PhysicalDamage; // placeholder, actual lookup would be needed
3105        Some(StatDiff {
3106            stat: st,
3107            value_a: va,
3108            value_b: vb,
3109            delta: vb - va,
3110            is_better_on_b: vb > va,
3111        })
3112    }).collect();
3113    stat_diffs.sort_by(|x, y| y.delta.abs().partial_cmp(&x.delta.abs()).unwrap_or(std::cmp::Ordering::Equal));
3114
3115    let armor_diff = ArmorDiff {
3116        physical_armor_delta: b.armor_values.physical_armor - a.armor_values.physical_armor,
3117        magic_armor_delta: b.armor_values.magic_armor - a.armor_values.magic_armor,
3118        fire_resist_delta: b.armor_values.fire_resist - a.armor_values.fire_resist,
3119        cold_resist_delta: b.armor_values.cold_resist - a.armor_values.cold_resist,
3120        lightning_resist_delta: b.armor_values.lightning_resist - a.armor_values.lightning_resist,
3121        poison_resist_delta: b.armor_values.poison_resist - a.armor_values.poison_resist,
3122    };
3123
3124    ItemComparison {
3125        item_a: a.clone(),
3126        item_b: b.clone(),
3127        stat_diffs,
3128        armor_diff,
3129        dps_diff: b.total_damage_per_second() - a.total_damage_per_second(),
3130        price_diff: b.base_price as i64 - a.base_price as i64,
3131        weight_diff: b.weight - a.weight,
3132    }
3133}
3134
3135// ============================================================
3136// STAT CALCULATOR (final stat sheet)
3137// ============================================================
3138
3139#[derive(Debug, Clone)]
3140pub struct CharacterStats {
3141    pub base_stats: HashMap<String, f32>,
3142    pub equipped_items: Vec<ItemDefinition>,
3143    pub active_set_bonuses: Vec<SetBonus>,
3144}
3145
3146impl CharacterStats {
3147    pub fn new() -> Self {
3148        let mut base_stats = HashMap::new();
3149        base_stats.insert("strength".to_string(), 10.0);
3150        base_stats.insert("dexterity".to_string(), 10.0);
3151        base_stats.insert("intelligence".to_string(), 10.0);
3152        base_stats.insert("vitality".to_string(), 10.0);
3153        CharacterStats {
3154            base_stats,
3155            equipped_items: Vec::new(),
3156            active_set_bonuses: Vec::new(),
3157        }
3158    }
3159
3160    pub fn compute_final_stat(&self, stat: &StatType) -> f32 {
3161        let base = match stat {
3162            StatType::LifeMax => 100.0 + self.base_stats.get("vitality").copied().unwrap_or(10.0) * 10.0,
3163            StatType::ManaMax => 50.0 + self.base_stats.get("intelligence").copied().unwrap_or(10.0) * 5.0,
3164            StatType::PhysicalDamage => self.base_stats.get("strength").copied().unwrap_or(10.0) * 2.0,
3165            _ => 0.0,
3166        };
3167
3168        let mut flat_sum = 0.0f32;
3169        let mut percent_sum = 0.0f32;
3170        let mut more_product = 1.0f32;
3171
3172        for item in &self.equipped_items {
3173            for m in &item.explicit_modifiers {
3174                if &m.stat == stat {
3175                    flat_sum += m.flat_value;
3176                    percent_sum += m.percent_value;
3177                    more_product *= m.more_multiplier;
3178                }
3179            }
3180            for m in &item.implicit_modifiers {
3181                if &m.stat == stat {
3182                    flat_sum += m.flat_value;
3183                    percent_sum += m.percent_value;
3184                    more_product *= m.more_multiplier;
3185                }
3186            }
3187        }
3188
3189        for bonus in &self.active_set_bonuses {
3190            for m in &bonus.modifiers {
3191                if &m.stat == stat {
3192                    flat_sum += m.flat_value;
3193                    percent_sum += m.percent_value;
3194                    more_product *= m.more_multiplier;
3195                }
3196            }
3197        }
3198
3199        (base + flat_sum) * (1.0 + percent_sum / 100.0) * more_product
3200    }
3201
3202    pub fn effective_dps_against(&self, target_armor: f32, target_resist: f32) -> f32 {
3203        let raw_dps: f32 = self.equipped_items.iter().map(|item| item.total_damage_per_second()).sum();
3204        let phys_dmg = self.compute_final_stat(&StatType::PhysicalDamage);
3205        let dmg_multiplier = 1.0 + phys_dmg / 100.0;
3206        let armor_reduction = target_armor / (target_armor + 300.0);
3207        raw_dps * dmg_multiplier * (1.0 - armor_reduction) * (1.0 - target_resist / 100.0)
3208    }
3209}
3210
3211// ============================================================
3212// DEFAULT ITEM DEFINITIONS (starter set)
3213// ============================================================
3214
3215pub fn create_starter_item_database() -> ItemDatabase {
3216    let mut db = ItemDatabase::new();
3217
3218    // Basic sword
3219    let sword_id = db.alloc_id();
3220    let mut sword = ItemDefinition::new(sword_id, "Iron Sword", ItemCategory::Sword);
3221    sword.item_level = 1;
3222    sword.required_level = 1;
3223    sword.base_price = 50;
3224    sword.weight = 2.5;
3225    sword.width = 1;
3226    sword.height = 3;
3227    sword.attack_speed = 1.2;
3228    sword.damage_ranges.push(DamageRange {
3229        min_damage: 5.0,
3230        max_damage: 12.0,
3231        damage_type: DamageType::Physical,
3232        crit_multiplier: 1.5,
3233    });
3234    sword.description = "A simple iron sword, reliable and well-balanced.".to_string();
3235    db.add_definition(sword);
3236
3237    // Iron helmet
3238    let helmet_id = db.alloc_id();
3239    let mut helmet = ItemDefinition::new(helmet_id, "Iron Helmet", ItemCategory::Helmet);
3240    helmet.item_level = 1;
3241    helmet.base_price = 40;
3242    helmet.weight = 3.0;
3243    helmet.armor_values.physical_armor = 15.0;
3244    helmet.description = "A sturdy iron helmet.".to_string();
3245    db.add_definition(helmet);
3246
3247    // Health potion
3248    let potion_id = db.alloc_id();
3249    let mut potion = ItemDefinition::new(potion_id, "Health Potion", ItemCategory::Potion);
3250    potion.item_level = 1;
3251    potion.base_price = 20;
3252    potion.weight = 0.3;
3253    potion.stack_size = 20;
3254    potion.implicit_modifiers.push(StatModifier::flat(StatType::LifeMax, 50.0));
3255    potion.description = "Restores 50 health points.".to_string();
3256    db.add_definition(potion);
3257
3258    // Iron ore material
3259    let ore_id = db.alloc_id();
3260    let mut ore = ItemDefinition::new(ore_id, "Iron Ore", ItemCategory::Ore);
3261    ore.item_level = 1;
3262    ore.base_price = 5;
3263    ore.weight = 0.5;
3264    ore.stack_size = 999;
3265    ore.description = "Raw iron ore, used in smithing.".to_string();
3266    db.add_definition(ore);
3267
3268    // Ring with magic find
3269    let ring_id = db.alloc_id();
3270    let mut ring = ItemDefinition::new(ring_id, "Ring of Fortune", ItemCategory::Ring);
3271    ring.item_level = 5;
3272    ring.rarity = Rarity::Uncommon;
3273    ring.base_price = 150;
3274    ring.weight = 0.1;
3275    ring.explicit_modifiers.push(StatModifier::percent(StatType::MagicFind, 10.0));
3276    ring.description = "A lucky ring that attracts rare loot.".to_string();
3277    db.add_definition(ring);
3278
3279    // Legendary greataxe
3280    let axe_id = db.alloc_id();
3281    let mut axe = ItemDefinition::new(axe_id, "Ravager's Greataxe", ItemCategory::Greataxe);
3282    axe.item_level = 20;
3283    axe.rarity = Rarity::Legendary;
3284    axe.base_price = 5000;
3285    axe.weight = 8.0;
3286    axe.width = 2;
3287    axe.height = 4;
3288    axe.attack_speed = 0.7;
3289    axe.damage_ranges.push(DamageRange {
3290        min_damage: 85.0,
3291        max_damage: 140.0,
3292        damage_type: DamageType::Physical,
3293        crit_multiplier: 2.0,
3294    });
3295    axe.damage_ranges.push(DamageRange {
3296        min_damage: 20.0,
3297        max_damage: 35.0,
3298        damage_type: DamageType::Fire,
3299        crit_multiplier: 1.8,
3300    });
3301    axe.explicit_modifiers.push(StatModifier::flat(StatType::Strength, 15.0));
3302    axe.explicit_modifiers.push(StatModifier::percent(StatType::PhysicalDamage, 40.0));
3303    axe.explicit_modifiers.push(StatModifier::flat(StatType::CritChance, 8.0));
3304    axe.unique_effect = Some("On kill: gain 10% attack speed for 5s".to_string());
3305    axe.description = "A massive axe that seems to hunger for combat.".to_string();
3306    axe.bind_on_pickup = true;
3307    db.add_definition(axe);
3308
3309    // Basic recipe
3310    let sword_recipe_id = db.alloc_id();
3311    let sword_recipe = RecipeDefinition {
3312        id: sword_recipe_id,
3313        name: "Forge Iron Sword".to_string(),
3314        description: "Create an iron sword at the forge.".to_string(),
3315        ingredients: vec![
3316            CraftingIngredient { item_id: ore_id, quantity: 5, quality_threshold: None, consumed: true },
3317        ],
3318        output_item_id: sword_id,
3319        output_count: 1,
3320        required_station: CraftingStation {
3321            id: 1,
3322            name: "Smithing Forge".to_string(),
3323            tier: 1,
3324            allowed_categories: vec![ItemCategory::Sword, ItemCategory::Axe, ItemCategory::Hammer],
3325        },
3326        skill_requirements: vec![
3327            SkillRequirement { skill_name: "Smithing".to_string(), required_level: 1, consumed_xp: 0 },
3328        ],
3329        base_success_chance: 0.9,
3330        skill_bonus_per_level: 0.005,
3331        quality_outcomes: QualityOutcome::standard_outcomes(),
3332        byproducts: vec![],
3333        crafting_time_seconds: 5.0,
3334        unlocked_by_default: true,
3335        unlock_source: None,
3336        experience_reward: {
3337            let mut m = HashMap::new();
3338            m.insert("Smithing".to_string(), 15);
3339            m
3340        },
3341    };
3342    db.add_recipe(sword_recipe);
3343
3344    // Basic loot table
3345    let loot_id = db.alloc_id();
3346    let loot_table = LootTable {
3347        id: loot_id,
3348        name: "Basic Goblin Loot".to_string(),
3349        entries: vec![
3350            LootEntry { item_id: sword_id, weight: 10.0, min_quantity: 1, max_quantity: 1, condition: DropCondition::Always, guaranteed: false, rarity_override: None },
3351            LootEntry { item_id: potion_id, weight: 50.0, min_quantity: 1, max_quantity: 3, condition: DropCondition::Always, guaranteed: false, rarity_override: None },
3352            LootEntry { item_id: ore_id, weight: 80.0, min_quantity: 1, max_quantity: 5, condition: DropCondition::Always, guaranteed: false, rarity_override: None },
3353            LootEntry { item_id: ring_id, weight: 5.0, min_quantity: 1, max_quantity: 1, condition: DropCondition::ChancePercent(25.0), guaranteed: false, rarity_override: None },
3354        ],
3355        drop_count_distribution: DropCountDistribution::NegativeBinomial { r: 2.0, p: 0.4 },
3356        magic_find_scaling: 0.01,
3357    };
3358    db.add_loot_table(loot_table);
3359
3360    // Item set
3361    let set = ItemSet {
3362        id: db.alloc_id(),
3363        name: "Iron Warrior's Set".to_string(),
3364        item_ids: vec![sword_id, helmet_id],
3365        bonuses: vec![
3366            SetBonus {
3367                pieces_required: 2,
3368                description: "+10% Physical Damage".to_string(),
3369                modifiers: vec![StatModifier::percent(StatType::PhysicalDamage, 10.0)],
3370            },
3371        ],
3372        color: Vec4::new(0.6, 0.6, 0.6, 1.0),
3373    };
3374    db.add_set(set);
3375
3376    // Vendor
3377    let mut vendor = VendorDefinition::new(db.alloc_id(), "Aldric the Merchant");
3378    vendor.inventory.push(VendorInventoryEntry {
3379        item_id: sword_id,
3380        stock: 3,
3381        max_stock: 5,
3382        restock_rate: 0.2,
3383        current_price_multiplier: 1.0,
3384        special_offer: false,
3385        offer_expiry: 0.0,
3386    });
3387    vendor.inventory.push(VendorInventoryEntry {
3388        item_id: potion_id,
3389        stock: 20,
3390        max_stock: 50,
3391        restock_rate: 2.0,
3392        current_price_multiplier: 1.0,
3393        special_offer: true,
3394        offer_expiry: 24.0,
3395    });
3396    db.vendors.insert(vendor.id, vendor);
3397
3398    // Sample affixes
3399    let affix_str = AffixDefinition {
3400        id: 1,
3401        name: "of Strength".to_string(),
3402        affix_type: AffixType::Suffix,
3403        modifier_template: StatModifier::flat(StatType::Strength, 0.0),
3404        flat_min: 1.0,
3405        flat_max: 15.0,
3406        percent_min: 0.0,
3407        percent_max: 0.0,
3408        item_level_requirement: 1,
3409        weight: 1000.0,
3410        applicable_categories: vec![
3411            ItemCategory::Sword, ItemCategory::Axe, ItemCategory::Hammer,
3412            ItemCategory::Chestplate, ItemCategory::Helmet, ItemCategory::Ring,
3413        ],
3414        conflicts_with: vec![],
3415        synergies_with: vec![2],
3416        synergy_bonus: 5.0,
3417        tier: 1,
3418    };
3419    db.affix_pool.add(affix_str);
3420
3421    let affix_dex = AffixDefinition {
3422        id: 2,
3423        name: "of Dexterity".to_string(),
3424        affix_type: AffixType::Suffix,
3425        modifier_template: StatModifier::flat(StatType::Dexterity, 0.0),
3426        flat_min: 1.0,
3427        flat_max: 15.0,
3428        percent_min: 0.0,
3429        percent_max: 0.0,
3430        item_level_requirement: 1,
3431        weight: 1000.0,
3432        applicable_categories: vec![
3433            ItemCategory::Dagger, ItemCategory::Bow, ItemCategory::Gloves,
3434            ItemCategory::Boots, ItemCategory::Ring,
3435        ],
3436        conflicts_with: vec![],
3437        synergies_with: vec![1],
3438        synergy_bonus: 5.0,
3439        tier: 1,
3440    };
3441    db.affix_pool.add(affix_dex);
3442
3443    let affix_fire_prefix = AffixDefinition {
3444        id: 3,
3445        name: "Flaming".to_string(),
3446        affix_type: AffixType::Prefix,
3447        modifier_template: StatModifier::flat(StatType::FireDamage, 0.0),
3448        flat_min: 5.0,
3449        flat_max: 30.0,
3450        percent_min: 0.0,
3451        percent_max: 0.0,
3452        item_level_requirement: 3,
3453        weight: 600.0,
3454        applicable_categories: vec![
3455            ItemCategory::Sword, ItemCategory::Dagger, ItemCategory::Staff, ItemCategory::Wand,
3456        ],
3457        conflicts_with: vec![4], // conflicts with Frozen prefix
3458        synergies_with: vec![],
3459        synergy_bonus: 0.0,
3460        tier: 1,
3461    };
3462    db.affix_pool.add(affix_fire_prefix);
3463
3464    let affix_cold_prefix = AffixDefinition {
3465        id: 4,
3466        name: "Frozen".to_string(),
3467        affix_type: AffixType::Prefix,
3468        modifier_template: StatModifier::flat(StatType::ColdDamage, 0.0),
3469        flat_min: 5.0,
3470        flat_max: 30.0,
3471        percent_min: 0.0,
3472        percent_max: 0.0,
3473        item_level_requirement: 3,
3474        weight: 600.0,
3475        applicable_categories: vec![
3476            ItemCategory::Sword, ItemCategory::Dagger, ItemCategory::Staff, ItemCategory::Wand,
3477        ],
3478        conflicts_with: vec![3], // conflicts with Flaming prefix
3479        synergies_with: vec![],
3480        synergy_bonus: 0.0,
3481        tier: 1,
3482    };
3483    db.affix_pool.add(affix_cold_prefix);
3484
3485    let affix_crit = AffixDefinition {
3486        id: 5,
3487        name: "Razor".to_string(),
3488        affix_type: AffixType::Prefix,
3489        modifier_template: StatModifier::flat(StatType::CritChance, 0.0),
3490        flat_min: 2.0,
3491        flat_max: 8.0,
3492        percent_min: 0.0,
3493        percent_max: 0.0,
3494        item_level_requirement: 5,
3495        weight: 400.0,
3496        applicable_categories: vec![
3497            ItemCategory::Sword, ItemCategory::Dagger, ItemCategory::Axe,
3498        ],
3499        conflicts_with: vec![],
3500        synergies_with: vec![],
3501        synergy_bonus: 0.0,
3502        tier: 2,
3503    };
3504    db.affix_pool.add(affix_crit);
3505
3506    db
3507}
3508
3509// ============================================================
3510// ITEM TOOLTIP RENDERER
3511// ============================================================
3512
3513#[derive(Debug, Clone)]
3514pub struct TooltipRect {
3515    pub x: f32,
3516    pub y: f32,
3517    pub width: f32,
3518    pub height: f32,
3519    pub lines: Vec<TooltipLine>,
3520}
3521
3522#[derive(Debug, Clone)]
3523pub struct TooltipLine {
3524    pub text: String,
3525    pub color: Vec4,
3526    pub indent: f32,
3527    pub font_size: f32,
3528    pub separator: bool,
3529}
3530
3531pub fn build_item_tooltip_rect(def: &ItemDefinition, anchor: Vec2, max_width: f32) -> TooltipRect {
3532    let line_height = 18.0;
3533    let padding = 8.0;
3534    let mut lines: Vec<TooltipLine> = Vec::new();
3535    let rarity_cfg = RarityConfig::for_rarity(def.rarity);
3536
3537    // Title
3538    lines.push(TooltipLine {
3539        text: def.name.clone(),
3540        color: rarity_cfg.color,
3541        indent: 0.0,
3542        font_size: 16.0,
3543        separator: false,
3544    });
3545
3546    // Category + Rarity
3547    lines.push(TooltipLine {
3548        text: format!("{} — {}", def.category.display_name(), rarity_cfg.display_name()),
3549        color: Vec4::new(0.75, 0.75, 0.75, 1.0),
3550        indent: 0.0,
3551        font_size: 14.0,
3552        separator: false,
3553    });
3554
3555    // Separator
3556    lines.push(TooltipLine { text: String::new(), color: Vec4::ONE, indent: 0.0, font_size: 0.0, separator: true });
3557
3558    // Combat stats
3559    if def.category.is_weapon() {
3560        let dps = def.total_damage_per_second();
3561        lines.push(TooltipLine {
3562            text: format!("DPS: {:.1}  ({:.2} attacks/sec)", dps, def.attack_speed),
3563            color: Vec4::new(1.0, 0.9, 0.5, 1.0),
3564            indent: 0.0, font_size: 14.0, separator: false,
3565        });
3566        for dmg in &def.damage_ranges {
3567            let avg = (dmg.min_damage + dmg.max_damage) / 2.0;
3568            lines.push(TooltipLine {
3569                text: format!("  {:?}: {:.0}–{:.0} ({:.1} avg)", dmg.damage_type, dmg.min_damage, dmg.max_damage, avg),
3570                color: dmg.damage_type.color(),
3571                indent: 8.0, font_size: 13.0, separator: false,
3572            });
3573        }
3574        lines.push(TooltipLine {
3575            text: format!("Crit: {:.1}% × {:.1}x", def.crit_chance, def.crit_multiplier),
3576            color: Vec4::new(1.0, 0.7, 0.4, 1.0),
3577            indent: 0.0, font_size: 13.0, separator: false,
3578        });
3579    }
3580
3581    if def.armor_values.physical_armor > 0.0 {
3582        lines.push(TooltipLine {
3583            text: format!("Armor: {:.0}", def.armor_values.physical_armor),
3584            color: Vec4::new(0.7, 0.85, 1.0, 1.0),
3585            indent: 0.0, font_size: 14.0, separator: false,
3586        });
3587    }
3588    if def.armor_values.evasion_rating > 0.0 {
3589        lines.push(TooltipLine {
3590            text: format!("Evasion: {:.0}", def.armor_values.evasion_rating),
3591            color: Vec4::new(0.5, 1.0, 0.7, 1.0),
3592            indent: 0.0, font_size: 14.0, separator: false,
3593        });
3594    }
3595    if def.armor_values.energy_shield > 0.0 {
3596        lines.push(TooltipLine {
3597            text: format!("Energy Shield: {:.0}", def.armor_values.energy_shield),
3598            color: Vec4::new(0.6, 0.8, 1.0, 1.0),
3599            indent: 0.0, font_size: 14.0, separator: false,
3600        });
3601    }
3602
3603    // Resistances
3604    let mut resists = Vec::new();
3605    if def.armor_values.fire_resist != 0.0 { resists.push(format!("Fire: {:.0}%", def.armor_values.fire_resist)); }
3606    if def.armor_values.cold_resist != 0.0 { resists.push(format!("Cold: {:.0}%", def.armor_values.cold_resist)); }
3607    if def.armor_values.lightning_resist != 0.0 { resists.push(format!("Lit: {:.0}%", def.armor_values.lightning_resist)); }
3608    if def.armor_values.poison_resist != 0.0 { resists.push(format!("Poi: {:.0}%", def.armor_values.poison_resist)); }
3609    if !resists.is_empty() {
3610        lines.push(TooltipLine {
3611            text: format!("Resist: {}", resists.join(" | ")),
3612            color: Vec4::new(1.0, 0.6, 0.2, 1.0),
3613            indent: 0.0, font_size: 13.0, separator: false,
3614        });
3615    }
3616
3617    if !def.implicit_modifiers.is_empty() || !def.explicit_modifiers.is_empty() {
3618        lines.push(TooltipLine { text: String::new(), color: Vec4::ONE, indent: 0.0, font_size: 0.0, separator: true });
3619    }
3620
3621    // Implicit modifiers
3622    for m in &def.implicit_modifiers {
3623        let (text, color) = format_modifier(m);
3624        lines.push(TooltipLine { text, color, indent: 0.0, font_size: 13.0, separator: false });
3625    }
3626
3627    if !def.implicit_modifiers.is_empty() && !def.explicit_modifiers.is_empty() {
3628        lines.push(TooltipLine { text: String::new(), color: Vec4::ONE, indent: 0.0, font_size: 0.0, separator: true });
3629    }
3630
3631    // Explicit modifiers
3632    for m in &def.explicit_modifiers {
3633        let (text, color) = format_modifier(m);
3634        lines.push(TooltipLine { text, color, indent: 0.0, font_size: 13.0, separator: false });
3635    }
3636
3637    // Unique effect
3638    if let Some(ref fx) = def.unique_effect {
3639        lines.push(TooltipLine { text: String::new(), color: Vec4::ONE, indent: 0.0, font_size: 0.0, separator: true });
3640        lines.push(TooltipLine {
3641            text: fx.clone(),
3642            color: Vec4::new(1.0, 0.6, 0.1, 1.0),
3643            indent: 0.0, font_size: 13.0, separator: false,
3644        });
3645    }
3646
3647    // Flavor text
3648    if !def.flavor_text.is_empty() {
3649        lines.push(TooltipLine { text: String::new(), color: Vec4::ONE, indent: 0.0, font_size: 0.0, separator: true });
3650        lines.push(TooltipLine {
3651            text: format!("\"{}\"", def.flavor_text),
3652            color: Vec4::new(0.6, 0.6, 0.8, 1.0),
3653            indent: 0.0, font_size: 12.0, separator: false,
3654        });
3655    }
3656
3657    // Requirements
3658    lines.push(TooltipLine { text: String::new(), color: Vec4::ONE, indent: 0.0, font_size: 0.0, separator: true });
3659    lines.push(TooltipLine {
3660        text: format!("Requires Level {}", def.requirements.level),
3661        color: Vec4::new(0.9, 0.6, 0.3, 1.0),
3662        indent: 0.0, font_size: 13.0, separator: false,
3663    });
3664    if def.requirements.strength > 0 {
3665        lines.push(TooltipLine {
3666            text: format!("  Strength {}", def.requirements.strength),
3667            color: Vec4::new(0.9, 0.6, 0.3, 1.0),
3668            indent: 8.0, font_size: 12.0, separator: false,
3669        });
3670    }
3671    if def.requirements.dexterity > 0 {
3672        lines.push(TooltipLine {
3673            text: format!("  Dexterity {}", def.requirements.dexterity),
3674            color: Vec4::new(0.9, 0.6, 0.3, 1.0),
3675            indent: 8.0, font_size: 12.0, separator: false,
3676        });
3677    }
3678
3679    // Weight and item level
3680    lines.push(TooltipLine {
3681        text: format!("Weight: {:.1}  |  Item Level: {}", def.weight, def.item_level),
3682        color: Vec4::new(0.6, 0.6, 0.6, 1.0),
3683        indent: 0.0, font_size: 12.0, separator: false,
3684    });
3685    lines.push(TooltipLine {
3686        text: format!("Value: {} gold  |  Sell: {} gold", def.base_price, def.effective_sell_price()),
3687        color: Vec4::new(1.0, 0.85, 0.0, 1.0),
3688        indent: 0.0, font_size: 12.0, separator: false,
3689    });
3690
3691    if def.bind_on_pickup {
3692        lines.push(TooltipLine {
3693            text: "Binds when Picked Up".to_string(),
3694            color: Vec4::new(0.8, 0.4, 0.4, 1.0),
3695            indent: 0.0, font_size: 12.0, separator: false,
3696        });
3697    }
3698
3699    // Compute total height
3700    let total_height = lines.iter().map(|l| {
3701        if l.separator { 8.0 } else { l.font_size * 1.3 }
3702    }).sum::<f32>() + padding * 2.0;
3703
3704    TooltipRect {
3705        x: anchor.x,
3706        y: anchor.y,
3707        width: max_width.min(260.0),
3708        height: total_height,
3709        lines,
3710    }
3711}
3712
3713fn format_modifier(m: &StatModifier) -> (String, Vec4) {
3714    let name = m.stat.display_name();
3715    let is_pct = m.stat.is_percentage();
3716    let positive = m.flat_value >= 0.0 && m.percent_value >= 0.0;
3717    let color = if positive { Vec4::new(0.5, 1.0, 0.5, 1.0) } else { Vec4::new(1.0, 0.4, 0.4, 1.0) };
3718    let sign = if positive { "+" } else { "" };
3719
3720    let text = if m.flat_value != 0.0 && m.percent_value != 0.0 {
3721        format!("{}{:.0} / {}{:.0}% {}", sign, m.flat_value, sign, m.percent_value, name)
3722    } else if m.percent_value != 0.0 {
3723        format!("{}{:.0}% {}", sign, m.percent_value, name)
3724    } else if is_pct {
3725        format!("{}{:.1}% {}", sign, m.flat_value, name)
3726    } else {
3727        format!("{}{:.0} {}", sign, m.flat_value, name)
3728    };
3729
3730    (text, color)
3731}
3732
3733// ============================================================
3734// LOOT TABLE PROBABILITY DISPLAY
3735// ============================================================
3736
3737#[derive(Debug, Clone)]
3738pub struct DropProbabilityEntry {
3739    pub item_id: u64,
3740    pub weight: f32,
3741    pub total_weight: f32,
3742    pub base_probability: f32,
3743    pub magic_find_adjusted: f32,
3744    pub conditional: bool,
3745}
3746
3747impl DropProbabilityEntry {
3748    pub fn probability_percent(&self) -> f32 {
3749        self.base_probability * 100.0
3750    }
3751
3752    pub fn magic_find_probability_percent(&self) -> f32 {
3753        self.magic_find_adjusted * 100.0
3754    }
3755}
3756
3757pub fn compute_drop_probabilities(table: &LootTable, magic_find: f32) -> Vec<DropProbabilityEntry> {
3758    let total_weight: f32 = table.entries.iter().filter(|e| !e.guaranteed).map(|e| e.weight).sum();
3759    let mf_mul = 1.0 + magic_find * table.magic_find_scaling / 100.0;
3760
3761    table.entries.iter().map(|entry| {
3762        let base_prob = if entry.guaranteed { 1.0 } else if total_weight > 0.0 { entry.weight / total_weight } else { 0.0 };
3763        let mf_total: f32 = table.entries.iter().filter(|e| !e.guaranteed).map(|e| e.weight * mf_mul).sum();
3764        let mf_prob = if entry.guaranteed { 1.0 } else if mf_total > 0.0 { entry.weight * mf_mul / mf_total } else { 0.0 };
3765        DropProbabilityEntry {
3766            item_id: entry.item_id,
3767            weight: entry.weight,
3768            total_weight,
3769            base_probability: base_prob,
3770            magic_find_adjusted: mf_prob,
3771            conditional: !matches!(entry.condition, DropCondition::Always),
3772        }
3773    }).collect()
3774}
3775
3776// ============================================================
3777// ECONOMY SIMULATION
3778// ============================================================
3779
3780#[derive(Debug, Clone)]
3781pub struct EconomySimulator {
3782    pub vendors: Vec<VendorDefinition>,
3783    pub transaction_log: VecDeque<EconomyEvent>,
3784    pub time: f32,
3785    pub price_history: HashMap<u64, Vec<(f32, u64)>>, // (time, price)
3786}
3787
3788#[derive(Debug, Clone)]
3789pub struct EconomyEvent {
3790    pub time: f32,
3791    pub vendor_id: u64,
3792    pub item_id: u64,
3793    pub transaction: EconomyTransaction,
3794    pub price: u64,
3795    pub quantity: u32,
3796}
3797
3798impl EconomySimulator {
3799    pub fn new() -> Self {
3800        EconomySimulator {
3801            vendors: Vec::new(),
3802            transaction_log: VecDeque::new(),
3803            time: 0.0,
3804            price_history: HashMap::new(),
3805        }
3806    }
3807
3808    pub fn simulate_step(&mut self, dt_hours: f32, item_defs: &HashMap<u64, ItemDefinition>) {
3809        self.time += dt_hours;
3810
3811        for vendor in &mut self.vendors {
3812            vendor.refresh_inventory(self.time);
3813
3814            // Simulate random player transactions
3815            let mut seed: u64 = (self.time * 1000.0) as u64 ^ vendor.id;
3816            let mut buy_events: Vec<(u64, u32)> = Vec::new();
3817            for entry in &vendor.inventory {
3818                let buy_p = lcg_f32(&mut seed);
3819                if buy_p < 0.1 && entry.stock > 0 {
3820                    buy_events.push((entry.item_id, 1u32));
3821                }
3822            }
3823            for (item_id, qty) in buy_events {
3824                if let Some(def) = item_defs.get(&item_id) {
3825                    let price = vendor.buy_price(def);
3826                    self.price_history
3827                        .entry(item_id)
3828                        .or_insert_with(Vec::new)
3829                        .push((self.time, price));
3830                    self.transaction_log.push_back(EconomyEvent {
3831                        time: self.time,
3832                        vendor_id: vendor.id,
3833                        item_id,
3834                        transaction: EconomyTransaction::PlayerBought { quantity: qty },
3835                        price,
3836                        quantity: qty,
3837                    });
3838                }
3839                vendor.adjust_supply_demand(EconomyTransaction::PlayerBought { quantity: qty });
3840            }
3841        }
3842
3843        // Trim transaction log to last 500 events
3844        while self.transaction_log.len() > 500 {
3845            self.transaction_log.pop_front();
3846        }
3847    }
3848
3849    pub fn average_price_for_item(&self, item_id: u64, last_n_events: usize) -> f32 {
3850        if let Some(history) = self.price_history.get(&item_id) {
3851            let slice = if history.len() > last_n_events {
3852                &history[history.len() - last_n_events..]
3853            } else {
3854                history.as_slice()
3855            };
3856            if slice.is_empty() { return 0.0; }
3857            let sum: u64 = slice.iter().map(|(_, p)| p).sum();
3858            sum as f32 / slice.len() as f32
3859        } else {
3860            0.0
3861        }
3862    }
3863
3864    pub fn price_trend(&self, item_id: u64) -> f32 {
3865        // Returns positive if price trending up, negative if down, 0 if stable
3866        if let Some(history) = self.price_history.get(&item_id) {
3867            if history.len() < 2 { return 0.0; }
3868            let n = history.len();
3869            let recent_avg = history[n/2..].iter().map(|(_, p)| *p as f32).sum::<f32>() / (n - n/2) as f32;
3870            let older_avg = history[..n/2].iter().map(|(_, p)| *p as f32).sum::<f32>() / (n/2) as f32;
3871            recent_avg - older_avg
3872        } else {
3873            0.0
3874        }
3875    }
3876}
3877
3878// ============================================================
3879// ITEM PREVIEW RENDERER (3D transform math)
3880// ============================================================
3881
3882#[derive(Debug, Clone)]
3883pub struct ItemPreviewTransform {
3884    pub translation: Vec3,
3885    pub rotation: Quat,
3886    pub scale: Vec3,
3887    pub auto_rotate: bool,
3888    pub auto_rotate_speed: f32,
3889    pub accumulated_angle: f32,
3890}
3891
3892impl ItemPreviewTransform {
3893    pub fn default_preview() -> Self {
3894        ItemPreviewTransform {
3895            translation: Vec3::ZERO,
3896            rotation: Quat::IDENTITY,
3897            scale: Vec3::ONE,
3898            auto_rotate: true,
3899            auto_rotate_speed: 0.5,
3900            accumulated_angle: 0.0,
3901        }
3902    }
3903
3904    pub fn update(&mut self, dt: f32) {
3905        if self.auto_rotate {
3906            self.accumulated_angle += self.auto_rotate_speed * dt;
3907            self.rotation = Quat::from_rotation_y(self.accumulated_angle);
3908        }
3909    }
3910
3911    pub fn model_matrix(&self) -> Mat4 {
3912        Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation)
3913    }
3914
3915    pub fn orbit(&mut self, delta_yaw: f32, delta_pitch: f32) {
3916        let yaw = Quat::from_rotation_y(delta_yaw);
3917        let right = self.rotation * Vec3::X;
3918        let pitch = Quat::from_axis_angle(right, delta_pitch);
3919        self.rotation = (pitch * yaw * self.rotation).normalize();
3920        self.auto_rotate = false;
3921    }
3922
3923    pub fn reset(&mut self) {
3924        *self = ItemPreviewTransform::default_preview();
3925    }
3926}
3927
3928// ============================================================
3929// INVENTORY DRAG-AND-DROP STATE
3930// ============================================================
3931
3932#[derive(Debug, Clone)]
3933pub enum DragState {
3934    Idle,
3935    Dragging {
3936        instance_id: u64,
3937        source_container: u64,
3938        cursor_offset: Vec2,
3939        current_pos: Vec2,
3940        drop_valid: bool,
3941        preview_col: Option<u32>,
3942        preview_row: Option<u32>,
3943    },
3944}
3945
3946impl DragState {
3947    pub fn start_drag(instance_id: u64, source: u64, offset: Vec2) -> Self {
3948        DragState::Dragging {
3949            instance_id,
3950            source_container: source,
3951            cursor_offset: offset,
3952            current_pos: Vec2::ZERO,
3953            drop_valid: false,
3954            preview_col: None,
3955            preview_row: None,
3956        }
3957    }
3958
3959    pub fn is_dragging(&self) -> bool {
3960        matches!(self, DragState::Dragging { .. })
3961    }
3962
3963    pub fn update_position(&mut self, pos: Vec2) {
3964        if let DragState::Dragging { ref mut current_pos, .. } = self {
3965            *current_pos = pos;
3966        }
3967    }
3968
3969    pub fn end_drag(&mut self) -> Option<(u64, u64, Option<(u32, u32)>)> {
3970        if let DragState::Dragging { instance_id, source_container, preview_col, preview_row, .. } = self.clone() {
3971            *self = DragState::Idle;
3972            let target = preview_col.and_then(|c| preview_row.map(|r| (c, r)));
3973            Some((instance_id, source_container, target))
3974        } else {
3975            None
3976        }
3977    }
3978}
3979
3980// ============================================================
3981// HOTKEY / KEYBINDING SYSTEM
3982// ============================================================
3983
3984#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3985pub enum EditorAction {
3986    NewItem,
3987    OpenItem,
3988    SaveItem,
3989    DeleteItem,
3990    DuplicateItem,
3991    Undo,
3992    Redo,
3993    Copy,
3994    Paste,
3995    Search,
3996    SelectAll,
3997    DeselectAll,
3998    GenerateRandom,
3999    ExportCsv,
4000    ToggleFilter,
4001    SwitchToBrowser,
4002    SwitchToItemEditor,
4003    SwitchToRecipes,
4004}
4005
4006#[derive(Debug, Clone)]
4007pub struct Keybinding {
4008    pub action: EditorAction,
4009    pub key: String,
4010    pub ctrl: bool,
4011    pub shift: bool,
4012    pub alt: bool,
4013    pub description: String,
4014}
4015
4016pub fn default_keybindings() -> Vec<Keybinding> {
4017    vec![
4018        Keybinding { action: EditorAction::NewItem, key: "N".to_string(), ctrl: true, shift: false, alt: false, description: "Create new item".to_string() },
4019        Keybinding { action: EditorAction::SaveItem, key: "S".to_string(), ctrl: true, shift: false, alt: false, description: "Save current item".to_string() },
4020        Keybinding { action: EditorAction::DeleteItem, key: "Delete".to_string(), ctrl: false, shift: false, alt: false, description: "Delete selected items".to_string() },
4021        Keybinding { action: EditorAction::DuplicateItem, key: "D".to_string(), ctrl: true, shift: false, alt: false, description: "Duplicate item".to_string() },
4022        Keybinding { action: EditorAction::Undo, key: "Z".to_string(), ctrl: true, shift: false, alt: false, description: "Undo".to_string() },
4023        Keybinding { action: EditorAction::Redo, key: "Y".to_string(), ctrl: true, shift: false, alt: false, description: "Redo".to_string() },
4024        Keybinding { action: EditorAction::Copy, key: "C".to_string(), ctrl: true, shift: false, alt: false, description: "Copy item".to_string() },
4025        Keybinding { action: EditorAction::Paste, key: "V".to_string(), ctrl: true, shift: false, alt: false, description: "Paste item".to_string() },
4026        Keybinding { action: EditorAction::Search, key: "F".to_string(), ctrl: true, shift: false, alt: false, description: "Focus search".to_string() },
4027        Keybinding { action: EditorAction::SelectAll, key: "A".to_string(), ctrl: true, shift: false, alt: false, description: "Select all".to_string() },
4028        Keybinding { action: EditorAction::GenerateRandom, key: "G".to_string(), ctrl: true, shift: false, alt: false, description: "Generate random item".to_string() },
4029        Keybinding { action: EditorAction::ExportCsv, key: "E".to_string(), ctrl: true, shift: false, alt: false, description: "Export to CSV".to_string() },
4030    ]
4031}
4032
4033// ============================================================
4034// ADDITIONAL CRAFTING HELPERS
4035// ============================================================
4036
4037pub fn list_recipes_for_output(db: &ItemDatabase, item_id: u64) -> Vec<&RecipeDefinition> {
4038    db.recipes.values().filter(|r| r.output_item_id == item_id).collect()
4039}
4040
4041pub fn list_recipes_using_ingredient(db: &ItemDatabase, ingredient_id: u64) -> Vec<&RecipeDefinition> {
4042    db.recipes.values().filter(|r| r.ingredients.iter().any(|i| i.item_id == ingredient_id)).collect()
4043}
4044
4045pub fn crafting_tree(db: &ItemDatabase, target_id: u64, depth: u32) -> Vec<(u32, u64, u32)> {
4046    // Returns (depth, item_id, quantity) for full dependency tree
4047    let mut result = Vec::new();
4048    let mut queue: VecDeque<(u32, u64, u32)> = VecDeque::new();
4049    queue.push_back((0, target_id, 1));
4050
4051    let mut visited: HashSet<u64> = HashSet::new();
4052
4053    while let Some((d, item_id, qty)) = queue.pop_front() {
4054        if d > depth { continue; }
4055        result.push((d, item_id, qty));
4056        if visited.contains(&item_id) { continue; }
4057        visited.insert(item_id);
4058
4059        if let Some(recipe) = db.recipes.values().find(|r| r.output_item_id == item_id) {
4060            for ing in &recipe.ingredients {
4061                queue.push_back((d + 1, ing.item_id, ing.quantity * qty));
4062            }
4063        }
4064    }
4065
4066    result
4067}
4068
4069// ============================================================
4070// ADVANCED SET BONUS DISPLAY
4071// ============================================================
4072
4073pub fn format_set_bonus_tooltip(set: &ItemSet, equipped_count: u32) -> Vec<String> {
4074    let mut lines = Vec::new();
4075    lines.push(format!("=== {} ===", set.name));
4076    lines.push(format!("{}/{} pieces equipped", equipped_count, set.item_ids.len()));
4077    for bonus in &set.bonuses {
4078        let active = equipped_count >= bonus.pieces_required;
4079        let prefix = if active { "[ACTIVE]" } else { "[LOCKED]" };
4080        lines.push(format!("{} ({} pc): {}", prefix, bonus.pieces_required, bonus.description));
4081    }
4082    lines
4083}
4084
4085// ============================================================
4086// STAT COMPARISON TABLE
4087// ============================================================
4088
4089pub fn stat_comparison_table(items: &[&ItemDefinition]) -> Vec<Vec<String>> {
4090    let all_stats = [
4091        StatType::Strength, StatType::Dexterity, StatType::Intelligence,
4092        StatType::Vitality, StatType::CritChance, StatType::CritMultiplier,
4093        StatType::PhysicalDamage, StatType::FireDamage, StatType::LifeMax,
4094        StatType::ManaMax, StatType::PhysicalArmor, StatType::CooldownReduction,
4095    ];
4096
4097    let mut header = vec!["Stat".to_string()];
4098    for item in items { header.push(item.name.clone()); }
4099
4100    let mut table = vec![header];
4101    for stat in &all_stats {
4102        let mut row = vec![stat.display_name().to_string()];
4103        for item in items {
4104            let total: f32 = item.explicit_modifiers.iter()
4105                .chain(item.implicit_modifiers.iter())
4106                .filter(|m| &m.stat == stat)
4107                .map(|m| m.flat_value + m.percent_value)
4108                .sum();
4109            if total == 0.0 { row.push("—".to_string()); }
4110            else { row.push(format!("{:.1}", total)); }
4111        }
4112        table.push(row);
4113    }
4114
4115    // DPS row
4116    let mut dps_row = vec!["DPS".to_string()];
4117    for item in items { dps_row.push(format!("{:.1}", item.total_damage_per_second())); }
4118    table.push(dps_row);
4119
4120    // Armor row
4121    let mut armor_row = vec!["Armor".to_string()];
4122    for item in items { armor_row.push(format!("{:.0}", item.armor_values.physical_armor)); }
4123    table.push(armor_row);
4124
4125    // Price row
4126    let mut price_row = vec!["Price".to_string()];
4127    for item in items { price_row.push(format!("{}", item.base_price)); }
4128    table.push(price_row);
4129
4130    table
4131}
4132
4133// ============================================================
4134// AFFIX GENERATION PRESET HELPERS
4135// ============================================================
4136
4137pub fn generate_affix_pool_preset() -> AffixPool {
4138    let mut pool = AffixPool::new();
4139    let all_weapon_cats = vec![
4140        ItemCategory::Sword, ItemCategory::Greatsword, ItemCategory::Dagger,
4141        ItemCategory::Axe, ItemCategory::Greataxe, ItemCategory::Mace,
4142        ItemCategory::Hammer, ItemCategory::Spear, ItemCategory::Staff, ItemCategory::Wand,
4143    ];
4144    let all_armor_cats = vec![
4145        ItemCategory::Helmet, ItemCategory::Chestplate, ItemCategory::Greaves,
4146        ItemCategory::Gauntlets, ItemCategory::Boots, ItemCategory::Shield,
4147    ];
4148    let all_acc_cats = vec![ItemCategory::Ring, ItemCategory::Amulet, ItemCategory::Belt];
4149
4150    // Generate a large batch of affixes for the pool
4151    let stat_pairs: &[(StatType, &str, f32, f32, f32, f32, AffixType, &[usize])] = &[
4152        // (stat, name, flat_min, flat_max, pct_min, pct_max, type, applicable_group_indices)
4153        // 0=weapons, 1=armor, 2=accessories
4154    ];
4155
4156    // Add weapon damage affixes
4157    for (i, &(dt, name)) in [
4158        (DamageType::Fire, "Fiery"), (DamageType::Cold, "Glacial"),
4159        (DamageType::Lightning, "Shocking"), (DamageType::Poison, "Toxic"),
4160        (DamageType::Arcane, "Arcane"),
4161    ].iter().enumerate() {
4162        let stat = match dt {
4163            DamageType::Fire => StatType::FireDamage,
4164            DamageType::Cold => StatType::ColdDamage,
4165            DamageType::Lightning => StatType::LightningDamage,
4166            DamageType::Poison => StatType::PoisonDamage,
4167            _ => StatType::PhysicalDamage,
4168        };
4169        pool.add(AffixDefinition {
4170            id: 100 + i as u64,
4171            name: name.to_string(),
4172            affix_type: AffixType::Prefix,
4173            modifier_template: StatModifier::flat(stat.clone(), 0.0),
4174            flat_min: 3.0 + i as f32 * 2.0,
4175            flat_max: 15.0 + i as f32 * 5.0,
4176            percent_min: 0.0,
4177            percent_max: 0.0,
4178            item_level_requirement: 1 + i as u32 * 2,
4179            weight: 500.0 - i as f32 * 50.0,
4180            applicable_categories: all_weapon_cats.clone(),
4181            conflicts_with: vec![],
4182            synergies_with: vec![],
4183            synergy_bonus: 0.0,
4184            tier: 1 + (i / 2) as u32,
4185        });
4186    }
4187
4188    // Add armor resistance affixes
4189    for (i, (stat, name)) in [
4190        (StatType::FireResistance, "of Fire Ward"),
4191        (StatType::ColdResistance, "of Cold Ward"),
4192        (StatType::LightningResistance, "of Lightning Ward"),
4193        (StatType::PoisonResistance, "of Toxin Ward"),
4194    ].iter().enumerate() {
4195        pool.add(AffixDefinition {
4196            id: 200 + i as u64,
4197            name: name.to_string(),
4198            affix_type: AffixType::Suffix,
4199            modifier_template: StatModifier::percent(stat.clone(), 0.0),
4200            flat_min: 0.0,
4201            flat_max: 0.0,
4202            percent_min: 5.0,
4203            percent_max: 25.0,
4204            item_level_requirement: 1,
4205            weight: 400.0,
4206            applicable_categories: all_armor_cats.clone(),
4207            conflicts_with: vec![],
4208            synergies_with: vec![],
4209            synergy_bonus: 0.0,
4210            tier: 1,
4211        });
4212    }
4213
4214    // Add life/mana modifiers for accessories
4215    pool.add(AffixDefinition {
4216        id: 300,
4217        name: "of Vitality".to_string(),
4218        affix_type: AffixType::Suffix,
4219        modifier_template: StatModifier::flat(StatType::LifeMax, 0.0),
4220        flat_min: 10.0,
4221        flat_max: 80.0,
4222        percent_min: 0.0,
4223        percent_max: 0.0,
4224        item_level_requirement: 1,
4225        weight: 800.0,
4226        applicable_categories: all_acc_cats.clone(),
4227        conflicts_with: vec![],
4228        synergies_with: vec![301],
4229        synergy_bonus: 10.0,
4230        tier: 1,
4231    });
4232    pool.add(AffixDefinition {
4233        id: 301,
4234        name: "of the Mind".to_string(),
4235        affix_type: AffixType::Suffix,
4236        modifier_template: StatModifier::flat(StatType::ManaMax, 0.0),
4237        flat_min: 10.0,
4238        flat_max: 60.0,
4239        percent_min: 0.0,
4240        percent_max: 0.0,
4241        item_level_requirement: 1,
4242        weight: 700.0,
4243        applicable_categories: all_acc_cats.clone(),
4244        conflicts_with: vec![],
4245        synergies_with: vec![300],
4246        synergy_bonus: 8.0,
4247        tier: 1,
4248    });
4249
4250    pool
4251}
4252
4253// ============================================================
4254// FINAL PUBLIC API / ENTRY POINTS
4255// ============================================================
4256
4257pub fn build_editor() -> InventoryEditor {
4258    let mut editor = InventoryEditor::new();
4259    editor.database = create_starter_item_database();
4260    // Augment with the full affix preset
4261    let more_affixes = generate_affix_pool_preset();
4262    for affix in more_affixes.definitions {
4263        editor.database.affix_pool.add(affix);
4264    }
4265    editor.browser.refresh(&editor.database);
4266    editor
4267}
4268
4269pub fn run_editor_frame(editor: &mut InventoryEditor, dt: f32, input: &EditorInput) {
4270    editor.tick(dt);
4271
4272    // Update preview transform
4273    // (would update if we had a preview transform stored in editor)
4274
4275    // Handle tab switching
4276    if let Some(ref action) = input.action {
4277        match action {
4278            EditorAction::NewItem => editor.create_new_item(ItemCategory::Sword),
4279            EditorAction::SaveItem => { editor.save_current_item(); }
4280            EditorAction::DeleteItem => editor.delete_selected_items(),
4281            EditorAction::Undo => editor.item_editor.undo(),
4282            EditorAction::Redo => editor.item_editor.redo(),
4283            EditorAction::Copy => editor.copy_item(),
4284            EditorAction::Paste => editor.paste_item(),
4285            EditorAction::GenerateRandom => editor.generate_random_item(),
4286            EditorAction::ExportCsv => { let _csv = editor.export_to_csv(); }
4287            EditorAction::SelectAll => editor.browser.select_all(),
4288            EditorAction::DeselectAll => editor.browser.deselect_all(),
4289            _ => {}
4290        }
4291    }
4292
4293    if let Some(search) = input.search_query.clone() {
4294        editor.search_with_history(search);
4295    }
4296
4297    if let Some(id) = input.open_item_id {
4298        editor.open_item_by_id(id);
4299    }
4300}
4301
4302#[derive(Debug, Clone)]
4303pub struct EditorInput {
4304    pub action: Option<EditorAction>,
4305    pub search_query: Option<String>,
4306    pub open_item_id: Option<u64>,
4307    pub mouse_pos: Vec2,
4308    pub delta_time: f32,
4309}
4310
4311impl EditorInput {
4312    pub fn idle() -> Self {
4313        EditorInput {
4314            action: None,
4315            search_query: None,
4316            open_item_id: None,
4317            mouse_pos: Vec2::ZERO,
4318            delta_time: 0.016,
4319        }
4320    }
4321}
4322
4323// ============================================================
4324// UNIT-TESTABLE MATH HELPERS
4325// ============================================================
4326
4327pub fn lerp_f32(a: f32, b: f32, t: f32) -> f32 {
4328    a + (b - a) * t.clamp(0.0, 1.0)
4329}
4330
4331pub fn inv_lerp_f32(a: f32, b: f32, v: f32) -> f32 {
4332    if (b - a).abs() < 1e-7 { return 0.0; }
4333    ((v - a) / (b - a)).clamp(0.0, 1.0)
4334}
4335
4336pub fn smooth_step(t: f32) -> f32 {
4337    let t = t.clamp(0.0, 1.0);
4338    t * t * (3.0 - 2.0 * t)
4339}
4340
4341pub fn smoother_step(t: f32) -> f32 {
4342    let t = t.clamp(0.0, 1.0);
4343    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
4344}
4345
4346pub fn damage_reduction_formula(armor: f32, attacker_level: f32, defender_level: f32) -> f32 {
4347    // WoW-style armor mitigation: armor / (armor + K) where K is level-based
4348    let k = 467.5 * defender_level - 22167.5;
4349    let k = k.max(1.0);
4350    let raw = armor / (armor + k);
4351    // Apply attacker level penalty
4352    let level_ratio = attacker_level / defender_level;
4353    (raw * level_ratio.min(1.0)).clamp(0.0, 0.85)
4354}
4355
4356pub fn critical_strike_damage(base: f32, crit_multiplier: f32, crit_chance: f32, roll: f32) -> f32 {
4357    if roll < crit_chance / 100.0 {
4358        base * crit_multiplier
4359    } else {
4360        base
4361    }
4362}
4363
4364pub fn resist_capped(resist: f32, cap: f32) -> f32 {
4365    resist.min(cap)
4366}
4367
4368pub fn elemental_damage_after_resist(raw: f32, resist_pct: f32) -> f32 {
4369    let r = resist_capped(resist_pct, 75.0) / 100.0;
4370    raw * (1.0 - r)
4371}
4372
4373pub fn diminishing_returns_cdr(base_cdr_pct: f32) -> f32 {
4374    // DR formula: effective = 1 - (1/(1 + cdr/100))
4375    let raw = base_cdr_pct / 100.0;
4376    (1.0 - 1.0 / (1.0 + raw)) * 100.0
4377}
4378
4379pub fn stat_scaling_value(base: f32, scaling_stat: f32, coefficient: f32, exponent: f32) -> f32 {
4380    base + coefficient * scaling_stat.powf(exponent)
4381}
4382
4383pub fn item_power_score(def: &ItemDefinition) -> f32 {
4384    let dps_score = def.total_damage_per_second() * 0.5;
4385    let armor_score = def.armor_values.physical_armor * 0.3;
4386    let stat_score = def.total_stat_budget();
4387    let rarity_bonus = match def.rarity {
4388        Rarity::Common => 0.0,
4389        Rarity::Uncommon => 5.0,
4390        Rarity::Rare => 15.0,
4391        Rarity::Epic => 30.0,
4392        Rarity::Legendary => 60.0,
4393        Rarity::Mythic => 120.0,
4394    };
4395    dps_score + armor_score + stat_score + rarity_bonus + def.item_level as f32 * 2.0
4396}
4397
4398// ============================================================
4399// RARITY DROP WEIGHT TABLE
4400// ============================================================
4401
4402pub struct RarityDropTable {
4403    pub alias: AliasTable,
4404    pub rarities: Vec<Rarity>,
4405}
4406
4407impl RarityDropTable {
4408    pub fn standard() -> Self {
4409        let rarities = vec![
4410            Rarity::Common,
4411            Rarity::Uncommon,
4412            Rarity::Rare,
4413            Rarity::Epic,
4414            Rarity::Legendary,
4415            Rarity::Mythic,
4416        ];
4417        let weights: Vec<f64> = rarities.iter().map(|r| {
4418            RarityConfig::for_rarity(*r).drop_weight as f64
4419        }).collect();
4420        RarityDropTable {
4421            alias: AliasTable::new(&weights),
4422            rarities,
4423        }
4424    }
4425
4426    pub fn magic_find_adjusted(magic_find: f32) -> Self {
4427        let base_rarities = vec![
4428            Rarity::Common,
4429            Rarity::Uncommon,
4430            Rarity::Rare,
4431            Rarity::Epic,
4432            Rarity::Legendary,
4433            Rarity::Mythic,
4434        ];
4435        let mf_mul = 1.0 + magic_find / 100.0;
4436        let weights: Vec<f64> = base_rarities.iter().map(|r| {
4437            let cfg = RarityConfig::for_rarity(*r);
4438            // Common items' weight is not boosted by magic find
4439            let boost = match r {
4440                Rarity::Common => 1.0f64,
4441                _ => mf_mul as f64,
4442            };
4443            cfg.drop_weight as f64 * boost
4444        }).collect();
4445        RarityDropTable {
4446            alias: AliasTable::new(&weights),
4447            rarities: base_rarities,
4448        }
4449    }
4450
4451    pub fn sample(&self, u1: f64, u2: f64) -> Rarity {
4452        let idx = self.alias.sample(u1, u2);
4453        self.rarities[idx]
4454    }
4455}
4456
4457// ============================================================
4458// EXTENDED: ITEM POWER CURVE ANALYSIS
4459// ============================================================
4460
4461#[derive(Debug, Clone)]
4462pub struct PowerCurvePoint {
4463    pub item_level: u32,
4464    pub rarity: Rarity,
4465    pub expected_dps: f32,
4466    pub expected_armor: f32,
4467    pub expected_stat_budget: f32,
4468}
4469
4470pub fn generate_power_curve(min_level: u32, max_level: u32) -> Vec<PowerCurvePoint> {
4471    let rarities = [Rarity::Common, Rarity::Uncommon, Rarity::Rare, Rarity::Epic, Rarity::Legendary, Rarity::Mythic];
4472    let mut points = Vec::new();
4473    for level in min_level..=max_level {
4474        for &rarity in &rarities {
4475            let cfg = RarityConfig::for_rarity(rarity);
4476            let expected_dps = (5.0 + level as f32 * 2.0) * cfg.stat_multiplier;
4477            let expected_armor = (10.0 + level as f32 * 3.0) * cfg.stat_multiplier;
4478            let expected_budget = budget_expectation(level, rarity);
4479            points.push(PowerCurvePoint {
4480                item_level: level,
4481                rarity,
4482                expected_dps,
4483                expected_armor,
4484                expected_stat_budget: expected_budget,
4485            });
4486        }
4487    }
4488    points
4489}
4490
4491pub fn item_level_from_dps(dps: f32, rarity: Rarity) -> u32 {
4492    let cfg = RarityConfig::for_rarity(rarity);
4493    let base_dps_at_1 = 5.0 * cfg.stat_multiplier;
4494    let dps_per_level = 2.0 * cfg.stat_multiplier;
4495    if dps_per_level <= 0.0 { return 1; }
4496    let level = (dps - base_dps_at_1) / dps_per_level;
4497    (level.max(1.0) as u32).min(100)
4498}
4499
4500pub fn recommended_item_level_for_zone(zone_difficulty: u32) -> u32 {
4501    let base = (zone_difficulty.saturating_sub(1)) * 10 + 1;
4502    base
4503}
4504
4505// ============================================================
4506// EXTENDED: ITEM ENCHANTMENT SYSTEM
4507// ============================================================
4508
4509#[derive(Debug, Clone)]
4510pub struct EnchantmentDefinition {
4511    pub id: u64,
4512    pub name: String,
4513    pub description: String,
4514    pub modifier: StatModifier,
4515    pub cost_gold: u64,
4516    pub cost_materials: Vec<(u64, u32)>,
4517    pub applicable_slots: Vec<EquipmentSlot>,
4518    pub max_enchant_level: u32,
4519    pub scaling_per_level: f32,
4520    pub visual_glow_color: Vec4,
4521    pub incompatible_with: Vec<u64>,
4522}
4523
4524impl EnchantmentDefinition {
4525    pub fn modifier_at_level(&self, level: u32) -> StatModifier {
4526        let mul = self.scaling_per_level.powf(level.saturating_sub(1) as f32);
4527        let mut m = self.modifier.clone();
4528        m.flat_value *= mul;
4529        m.percent_value *= mul;
4530        m
4531    }
4532
4533    pub fn cost_at_level(&self, level: u32) -> u64 {
4534        let mul = (2.0f32).powf(level as f32 - 1.0);
4535        (self.cost_gold as f32 * mul) as u64
4536    }
4537
4538    pub fn expected_dps_bonus(&self, attack_speed: f32) -> f32 {
4539        if self.modifier.stat == StatType::PhysicalDamage {
4540            self.modifier.flat_value * attack_speed
4541        } else {
4542            0.0
4543        }
4544    }
4545}
4546
4547#[derive(Debug, Clone)]
4548pub struct ItemEnchantment {
4549    pub enchant_id: u64,
4550    pub level: u32,
4551    pub applied_at_item_level: u32,
4552}
4553
4554impl ItemEnchantment {
4555    pub fn new(enchant_id: u64, level: u32, item_level: u32) -> Self {
4556        ItemEnchantment { enchant_id, level, applied_at_item_level: item_level }
4557    }
4558
4559    pub fn effective_modifier(&self, def: &EnchantmentDefinition) -> StatModifier {
4560        def.modifier_at_level(self.level)
4561    }
4562}
4563
4564// ============================================================
4565// EXTENDED: ITEM SOCKET SYSTEM
4566// ============================================================
4567
4568#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4569pub enum SocketColor {
4570    Red,
4571    Blue,
4572    Yellow,
4573    Green,
4574    White,
4575    Black,
4576}
4577
4578impl SocketColor {
4579    pub fn display_name(&self) -> &'static str {
4580        match self {
4581            SocketColor::Red => "Red",
4582            SocketColor::Blue => "Blue",
4583            SocketColor::Yellow => "Yellow",
4584            SocketColor::Green => "Green",
4585            SocketColor::White => "White",
4586            SocketColor::Black => "Black",
4587        }
4588    }
4589
4590    pub fn color(&self) -> Vec4 {
4591        match self {
4592            SocketColor::Red => Vec4::new(1.0, 0.1, 0.1, 1.0),
4593            SocketColor::Blue => Vec4::new(0.1, 0.3, 1.0, 1.0),
4594            SocketColor::Yellow => Vec4::new(1.0, 0.9, 0.0, 1.0),
4595            SocketColor::Green => Vec4::new(0.1, 0.8, 0.1, 1.0),
4596            SocketColor::White => Vec4::new(0.9, 0.9, 0.9, 1.0),
4597            SocketColor::Black => Vec4::new(0.1, 0.0, 0.2, 1.0),
4598        }
4599    }
4600}
4601
4602#[derive(Debug, Clone)]
4603pub struct GemDefinition {
4604    pub id: u64,
4605    pub name: String,
4606    pub socket_color: SocketColor,
4607    pub modifier: StatModifier,
4608    pub quality: f32,
4609    pub item_level_requirement: u32,
4610    pub icon_path: String,
4611}
4612
4613impl GemDefinition {
4614    pub fn effective_modifier(&self) -> StatModifier {
4615        let mut m = self.modifier.clone();
4616        m.flat_value *= self.quality;
4617        m.percent_value *= self.quality;
4618        m
4619    }
4620
4621    pub fn stat_value(&self) -> f32 {
4622        (self.modifier.flat_value.abs() + self.modifier.percent_value.abs()) * self.quality
4623    }
4624}
4625
4626#[derive(Debug, Clone)]
4627pub struct ItemSocket {
4628    pub color: SocketColor,
4629    pub gem_id: Option<u64>,
4630}
4631
4632impl ItemSocket {
4633    pub fn empty(color: SocketColor) -> Self {
4634        ItemSocket { color, gem_id: None }
4635    }
4636
4637    pub fn is_filled(&self) -> bool {
4638        self.gem_id.is_some()
4639    }
4640
4641    pub fn can_accept(&self, gem: &GemDefinition) -> bool {
4642        self.color == SocketColor::White
4643            || gem.socket_color == SocketColor::White
4644            || self.color == gem.socket_color
4645    }
4646
4647    pub fn insert_gem(&mut self, gem_id: u64) -> Option<u64> {
4648        let old = self.gem_id;
4649        self.gem_id = Some(gem_id);
4650        old
4651    }
4652
4653    pub fn remove_gem(&mut self) -> Option<u64> {
4654        self.gem_id.take()
4655    }
4656}
4657
4658#[derive(Debug, Clone)]
4659pub struct SocketBonus {
4660    pub description: String,
4661    pub modifier: StatModifier,
4662    pub requires_all_matching: bool,
4663}
4664
4665pub fn compute_socket_bonus(sockets: &[ItemSocket], gems: &HashMap<u64, GemDefinition>, bonus: &SocketBonus) -> Option<StatModifier> {
4666    if bonus.requires_all_matching {
4667        let all_match = sockets.iter().all(|s| {
4668            if let Some(gem_id) = s.gem_id {
4669                gems.get(&gem_id)
4670                    .map(|g| g.socket_color == s.color || g.socket_color == SocketColor::White || s.color == SocketColor::White)
4671                    .unwrap_or(false)
4672            } else {
4673                false
4674            }
4675        });
4676        if all_match { Some(bonus.modifier.clone()) } else { None }
4677    } else {
4678        let any_filled = sockets.iter().any(|s| s.gem_id.is_some());
4679        if any_filled { Some(bonus.modifier.clone()) } else { None }
4680    }
4681}
4682
4683pub fn compute_all_gem_bonuses(sockets: &[ItemSocket], gems: &HashMap<u64, GemDefinition>) -> Vec<StatModifier> {
4684    sockets.iter()
4685        .filter_map(|s| s.gem_id.and_then(|id| gems.get(&id)))
4686        .map(|g| g.effective_modifier())
4687        .collect()
4688}
4689
4690// ============================================================
4691// EXTENDED: ITEM UPGRADE SYSTEM
4692// ============================================================
4693
4694#[derive(Debug, Clone)]
4695pub struct UpgradePath {
4696    pub source_item_id: u64,
4697    pub result_item_id: u64,
4698    pub required_materials: Vec<(u64, u32)>,
4699    pub gold_cost: u64,
4700    pub required_smith_level: u32,
4701    pub preserves_affixes: bool,
4702    pub preserves_quality: bool,
4703    pub success_chance: f32,
4704    pub failure_downgrade: Option<u64>,
4705}
4706
4707impl UpgradePath {
4708    pub fn success_probability_with_level(&self, smith_level: u32) -> f32 {
4709        let bonus = ((smith_level as f32 - self.required_smith_level as f32).max(0.0)) * 0.01;
4710        (self.success_chance + bonus).min(0.99)
4711    }
4712
4713    pub fn expected_attempts_to_succeed(&self, smith_level: u32) -> f32 {
4714        let p = self.success_probability_with_level(smith_level);
4715        if p <= 0.0 { return f32::INFINITY; }
4716        1.0 / p
4717    }
4718
4719    pub fn expected_gold_cost_per_success(&self, smith_level: u32) -> u64 {
4720        let attempts = self.expected_attempts_to_succeed(smith_level);
4721        (self.gold_cost as f32 * attempts) as u64
4722    }
4723}
4724
4725// ============================================================
4726// EXTENDED: DISENCHANTING SYSTEM
4727// ============================================================
4728
4729#[derive(Debug, Clone)]
4730pub struct DisenchantOutcome {
4731    pub material_id: u64,
4732    pub min_quantity: u32,
4733    pub max_quantity: u32,
4734    pub chance: f32,
4735}
4736
4737pub fn disenchant_item(item: &ItemDefinition, rolls: &[f32]) -> Vec<(u64, u32)> {
4738    let outcomes: Vec<DisenchantOutcome> = match item.rarity {
4739        Rarity::Common => vec![
4740            DisenchantOutcome { material_id: 1001, min_quantity: 1, max_quantity: 3, chance: 1.0 },
4741        ],
4742        Rarity::Uncommon => vec![
4743            DisenchantOutcome { material_id: 1002, min_quantity: 1, max_quantity: 2, chance: 0.8 },
4744            DisenchantOutcome { material_id: 1001, min_quantity: 2, max_quantity: 5, chance: 0.2 },
4745        ],
4746        Rarity::Rare => vec![
4747            DisenchantOutcome { material_id: 1003, min_quantity: 1, max_quantity: 2, chance: 0.7 },
4748            DisenchantOutcome { material_id: 1002, min_quantity: 2, max_quantity: 4, chance: 0.3 },
4749        ],
4750        Rarity::Epic => vec![
4751            DisenchantOutcome { material_id: 1004, min_quantity: 1, max_quantity: 1, chance: 0.6 },
4752            DisenchantOutcome { material_id: 1003, min_quantity: 1, max_quantity: 3, chance: 0.4 },
4753        ],
4754        Rarity::Legendary => vec![
4755            DisenchantOutcome { material_id: 1005, min_quantity: 1, max_quantity: 1, chance: 0.5 },
4756            DisenchantOutcome { material_id: 1004, min_quantity: 1, max_quantity: 2, chance: 0.5 },
4757        ],
4758        Rarity::Mythic => vec![
4759            DisenchantOutcome { material_id: 1006, min_quantity: 1, max_quantity: 1, chance: 1.0 },
4760        ],
4761    };
4762
4763    let mut results = Vec::new();
4764    let mut roll_idx = 0usize;
4765
4766    for outcome in &outcomes {
4767        let roll = if roll_idx < rolls.len() { rolls[roll_idx] } else { 0.5 };
4768        roll_idx += 1;
4769        if roll < outcome.chance {
4770            let qty_roll = if roll_idx < rolls.len() { rolls[roll_idx] } else { 0.5 };
4771            roll_idx += 1;
4772            let range = outcome.max_quantity - outcome.min_quantity;
4773            let qty = outcome.min_quantity + (qty_roll * (range + 1) as f32) as u32;
4774            results.push((outcome.material_id, qty.min(outcome.max_quantity)));
4775        }
4776    }
4777
4778    results
4779}
4780
4781pub fn disenchant_expected_value(item: &ItemDefinition, material_prices: &HashMap<u64, u64>) -> f64 {
4782    let rolls: Vec<f32> = vec![0.5; 20];
4783    let outcomes = disenchant_item(item, &rolls);
4784    outcomes.iter().map(|(mat_id, qty)| {
4785        let price = material_prices.get(mat_id).copied().unwrap_or(1);
4786        price as f64 * *qty as f64
4787    }).sum()
4788}
4789
4790// ============================================================
4791// EXTENDED: WORLD DROP SCHEDULER
4792// ============================================================
4793
4794#[derive(Debug, Clone)]
4795pub struct WorldDropSchedule {
4796    pub zone_id: u64,
4797    pub zone_name: String,
4798    pub loot_table_id: u64,
4799    pub boss_loot_table_id: Option<u64>,
4800    pub respawn_time_minutes: f32,
4801    pub is_boss_zone: bool,
4802    pub min_player_level: u32,
4803    pub max_player_level: u32,
4804    pub guaranteed_drops: Vec<(u64, f32)>,
4805}
4806
4807impl WorldDropSchedule {
4808    pub fn should_reward(&self, player_level: u32) -> bool {
4809        player_level >= self.min_player_level && player_level <= self.max_player_level
4810    }
4811
4812    pub fn get_loot_table<'a>(&self, is_boss: bool, loot_tables: &'a HashMap<u64, LootTable>) -> Option<&'a LootTable> {
4813        if is_boss {
4814            self.boss_loot_table_id
4815                .and_then(|id| loot_tables.get(&id))
4816                .or_else(|| loot_tables.get(&self.loot_table_id))
4817        } else {
4818            loot_tables.get(&self.loot_table_id)
4819        }
4820    }
4821
4822    pub fn zone_difficulty(&self) -> u32 {
4823        (self.min_player_level + self.max_player_level) / 2
4824    }
4825
4826    pub fn is_level_appropriate(&self, level: u32) -> bool {
4827        let mid = self.zone_difficulty();
4828        let delta = if level > mid { level - mid } else { mid - level };
4829        delta <= 5
4830    }
4831}
4832
4833// ============================================================
4834// EXTENDED: MERCHANT HAGGLING
4835// ============================================================
4836
4837#[derive(Debug, Clone)]
4838pub struct HaggleState {
4839    pub base_price: u64,
4840    pub current_offer: u64,
4841    pub minimum_accept_price: u64,
4842    pub rounds_remaining: u32,
4843    pub vendor_patience: f32,
4844    pub player_charisma: u32,
4845    pub outcome: HaggleOutcome,
4846}
4847
4848#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4849pub enum HaggleOutcome {
4850    InProgress,
4851    Accepted,
4852    Rejected,
4853    Abandoned,
4854}
4855
4856impl HaggleState {
4857    pub fn new(base_price: u64, player_charisma: u32) -> Self {
4858        let charm_discount = 1.0 - (player_charisma as f32 * 0.01).min(0.3);
4859        let minimum = (base_price as f32 * charm_discount) as u64;
4860        HaggleState {
4861            base_price,
4862            current_offer: base_price,
4863            minimum_accept_price: minimum,
4864            rounds_remaining: 3 + player_charisma / 10,
4865            vendor_patience: 1.0,
4866            player_charisma,
4867            outcome: HaggleOutcome::InProgress,
4868        }
4869    }
4870
4871    pub fn make_offer(&mut self, offered_price: u64) -> HaggleResponse {
4872        if self.outcome != HaggleOutcome::InProgress {
4873            return HaggleResponse::AlreadyConcluded;
4874        }
4875        self.rounds_remaining = self.rounds_remaining.saturating_sub(1);
4876        let offer_ratio = offered_price as f32 / self.base_price as f32;
4877        let aggression = 1.0 - offer_ratio;
4878        self.vendor_patience -= aggression * 0.4;
4879        self.vendor_patience = self.vendor_patience.max(0.0);
4880
4881        if offered_price >= self.minimum_accept_price && self.vendor_patience > 0.1 {
4882            self.current_offer = offered_price;
4883            self.outcome = HaggleOutcome::Accepted;
4884            return HaggleResponse::Accept { final_price: offered_price };
4885        }
4886        if self.vendor_patience < 0.2 || self.rounds_remaining == 0 {
4887            self.outcome = HaggleOutcome::Rejected;
4888            return HaggleResponse::Reject { final_price: self.base_price };
4889        }
4890        let counter = ((offered_price as f32 + self.minimum_accept_price as f32) / 2.0) as u64;
4891        self.current_offer = counter;
4892        HaggleResponse::Counter { counter_price: counter, rounds_left: self.rounds_remaining }
4893    }
4894
4895    pub fn abandon(&mut self) {
4896        self.outcome = HaggleOutcome::Abandoned;
4897    }
4898
4899    pub fn savings_if_accepted(&self) -> i64 {
4900        self.base_price as i64 - self.current_offer as i64
4901    }
4902}
4903
4904#[derive(Debug, Clone)]
4905pub enum HaggleResponse {
4906    Accept { final_price: u64 },
4907    Reject { final_price: u64 },
4908    Counter { counter_price: u64, rounds_left: u32 },
4909    AlreadyConcluded,
4910}
4911
4912// ============================================================
4913// EXTENDED: ITEM HISTORY AND PROVENANCE
4914// ============================================================
4915
4916#[derive(Debug, Clone)]
4917pub struct ItemHistoryEntry {
4918    pub timestamp: f32,
4919    pub event: ItemEvent,
4920    pub player_id: Option<u64>,
4921    pub location: Option<String>,
4922}
4923
4924#[derive(Debug, Clone)]
4925pub enum ItemEvent {
4926    Generated { seed: u64 },
4927    Dropped { from_mob: String },
4928    Purchased { from_vendor: String, price: u64 },
4929    Crafted { recipe_name: String },
4930    Enchanted { enchant_name: String },
4931    Upgraded { from_tier: u32, to_tier: u32 },
4932    Repaired { cost: u64 },
4933    SocketGemInserted { gem_name: String },
4934    AffixRerolled { old_affix: String, new_affix: String },
4935    Identified,
4936    Destroyed,
4937    Traded { to_player: u64 },
4938}
4939
4940#[derive(Debug, Clone)]
4941pub struct ItemProvenance {
4942    pub instance_id: u64,
4943    pub history: Vec<ItemHistoryEntry>,
4944}
4945
4946impl ItemProvenance {
4947    pub fn new(instance_id: u64) -> Self {
4948        ItemProvenance { instance_id, history: Vec::new() }
4949    }
4950
4951    pub fn record(&mut self, event: ItemEvent, time: f32) {
4952        self.history.push(ItemHistoryEntry {
4953            timestamp: time,
4954            event,
4955            player_id: None,
4956            location: None,
4957        });
4958    }
4959
4960    pub fn origin_event(&self) -> Option<&ItemHistoryEntry> {
4961        self.history.first()
4962    }
4963
4964    pub fn times_repaired(&self) -> u32 {
4965        self.history.iter().filter(|e| matches!(e.event, ItemEvent::Repaired { .. })).count() as u32
4966    }
4967
4968    pub fn times_traded(&self) -> u32 {
4969        self.history.iter().filter(|e| matches!(e.event, ItemEvent::Traded { .. })).count() as u32
4970    }
4971
4972    pub fn age(&self, current_time: f32) -> f32 {
4973        self.history.first().map(|e| current_time - e.timestamp).unwrap_or(0.0)
4974    }
4975}
4976
4977// ============================================================
4978// EXTENDED: MATERIAL COST TABLES
4979// ============================================================
4980
4981pub fn material_cost_for_upgrade(item_level: u32, rarity: Rarity) -> HashMap<String, u32> {
4982    let mut costs = HashMap::new();
4983    let base = item_level;
4984    match rarity {
4985        Rarity::Common => {
4986            costs.insert("Iron Ore".to_string(), base * 3);
4987        }
4988        Rarity::Uncommon => {
4989            costs.insert("Iron Ore".to_string(), base * 5);
4990            costs.insert("Magic Dust".to_string(), base);
4991        }
4992        Rarity::Rare => {
4993            costs.insert("Mithril Ore".to_string(), base * 3);
4994            costs.insert("Arcane Shard".to_string(), base * 2);
4995        }
4996        Rarity::Epic => {
4997            costs.insert("Adamantite".to_string(), base * 2);
4998            costs.insert("Soul Fragment".to_string(), base);
4999            costs.insert("Arcane Shard".to_string(), base * 3);
5000        }
5001        Rarity::Legendary => {
5002            costs.insert("Void Steel".to_string(), base);
5003            costs.insert("Primordial Essence".to_string(), base);
5004            costs.insert("Dragon Scale".to_string(), base / 2 + 1);
5005        }
5006        Rarity::Mythic => {
5007            costs.insert("Aether Core".to_string(), base);
5008            costs.insert("Chaos Crystal".to_string(), base * 2);
5009        }
5010    }
5011    costs
5012}
5013
5014pub fn crafting_material_value(mat_name: &str, quantity: u32) -> u64 {
5015    let per_unit: u64 = match mat_name {
5016        "Iron Ore" => 2,
5017        "Magic Dust" => 8,
5018        "Mithril Ore" => 15,
5019        "Arcane Shard" => 25,
5020        "Adamantite" => 50,
5021        "Soul Fragment" => 75,
5022        "Void Steel" => 200,
5023        "Primordial Essence" => 350,
5024        "Dragon Scale" => 500,
5025        "Aether Core" => 1000,
5026        "Chaos Crystal" => 800,
5027        _ => 5,
5028    };
5029    per_unit * quantity as u64
5030}
5031
5032// ============================================================
5033// EXTENDED: INVENTORY TRANSACTION LOG
5034// ============================================================
5035
5036#[derive(Debug, Clone)]
5037pub struct InventoryTransaction {
5038    pub id: u64,
5039    pub timestamp: f32,
5040    pub transaction_type: TransactionType,
5041    pub item_instance_id: u64,
5042    pub item_def_id: u64,
5043    pub quantity: u32,
5044    pub source_container: Option<u64>,
5045    pub dest_container: Option<u64>,
5046    pub gold_delta: i64,
5047}
5048
5049#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5050pub enum TransactionType {
5051    Pickup,
5052    Drop,
5053    VendorBuy,
5054    VendorSell,
5055    Craft,
5056    Disenchant,
5057    Upgrade,
5058    Enchant,
5059    SocketGem,
5060    Move,
5061    Split,
5062    Merge,
5063    Destroy,
5064    QuestTurn,
5065    Trade,
5066    Loot,
5067}
5068
5069#[derive(Debug, Clone)]
5070pub struct InventoryTransactionLog {
5071    pub entries: VecDeque<InventoryTransaction>,
5072    pub max_entries: usize,
5073    pub next_id: u64,
5074    pub total_gold_earned: u64,
5075    pub total_gold_spent: u64,
5076    pub items_looted: u64,
5077    pub items_crafted: u64,
5078    pub items_sold: u64,
5079}
5080
5081impl InventoryTransactionLog {
5082    pub fn new() -> Self {
5083        InventoryTransactionLog {
5084            entries: VecDeque::new(),
5085            max_entries: 1000,
5086            next_id: 1,
5087            total_gold_earned: 0,
5088            total_gold_spent: 0,
5089            items_looted: 0,
5090            items_crafted: 0,
5091            items_sold: 0,
5092        }
5093    }
5094
5095    pub fn record(&mut self, txn: InventoryTransaction) {
5096        if txn.gold_delta > 0 {
5097            self.total_gold_earned += txn.gold_delta as u64;
5098        } else {
5099            self.total_gold_spent += (-txn.gold_delta) as u64;
5100        }
5101        match txn.transaction_type {
5102            TransactionType::Loot | TransactionType::Pickup => {
5103                self.items_looted += txn.quantity as u64;
5104            }
5105            TransactionType::Craft => {
5106                self.items_crafted += txn.quantity as u64;
5107            }
5108            TransactionType::VendorSell => {
5109                self.items_sold += txn.quantity as u64;
5110            }
5111            _ => {}
5112        }
5113        self.entries.push_back(txn);
5114        while self.entries.len() > self.max_entries {
5115            self.entries.pop_front();
5116        }
5117    }
5118
5119    pub fn alloc_id(&mut self) -> u64 {
5120        let id = self.next_id;
5121        self.next_id += 1;
5122        id
5123    }
5124
5125    pub fn filter_by_type(&self, tt: TransactionType) -> Vec<&InventoryTransaction> {
5126        self.entries.iter().filter(|e| e.transaction_type == tt).collect()
5127    }
5128
5129    pub fn net_gold(&self) -> i64 {
5130        self.total_gold_earned as i64 - self.total_gold_spent as i64
5131    }
5132
5133    pub fn total_for_item(&self, item_def_id: u64) -> (u32, u32) {
5134        let gained: u32 = self.entries.iter()
5135            .filter(|e| e.item_def_id == item_def_id && matches!(
5136                e.transaction_type,
5137                TransactionType::Loot | TransactionType::VendorBuy |
5138                TransactionType::Craft | TransactionType::Pickup
5139            ))
5140            .map(|e| e.quantity).sum();
5141        let lost: u32 = self.entries.iter()
5142            .filter(|e| e.item_def_id == item_def_id && matches!(
5143                e.transaction_type,
5144                TransactionType::VendorSell | TransactionType::Disenchant |
5145                TransactionType::Destroy | TransactionType::Drop
5146            ))
5147            .map(|e| e.quantity).sum();
5148        (gained, lost)
5149    }
5150}
5151
5152// ============================================================
5153// EXTENDED: ITEM CATALOG (sorted views)
5154// ============================================================
5155
5156#[derive(Debug, Clone)]
5157pub struct ItemCatalog {
5158    pub by_id: BTreeMap<u64, ItemDefinition>,
5159    pub by_name: BTreeMap<String, u64>,
5160    pub by_category_and_level: BTreeMap<(String, u32), Vec<u64>>,
5161    pub by_rarity_and_level: BTreeMap<(u8, u32), Vec<u64>>,
5162}
5163
5164impl ItemCatalog {
5165    pub fn build(db: &ItemDatabase) -> Self {
5166        let mut catalog = ItemCatalog {
5167            by_id: BTreeMap::new(),
5168            by_name: BTreeMap::new(),
5169            by_category_and_level: BTreeMap::new(),
5170            by_rarity_and_level: BTreeMap::new(),
5171        };
5172        for (id, def) in &db.definitions {
5173            catalog.by_id.insert(*id, def.clone());
5174            catalog.by_name.insert(def.name.clone(), *id);
5175            catalog.by_category_and_level
5176                .entry((def.category.display_name().to_string(), def.item_level))
5177                .or_insert_with(Vec::new)
5178                .push(*id);
5179            let rarity_ord: u8 = match def.rarity {
5180                Rarity::Common => 0, Rarity::Uncommon => 1, Rarity::Rare => 2,
5181                Rarity::Epic => 3, Rarity::Legendary => 4, Rarity::Mythic => 5,
5182            };
5183            catalog.by_rarity_and_level
5184                .entry((rarity_ord, def.item_level))
5185                .or_insert_with(Vec::new)
5186                .push(*id);
5187        }
5188        catalog
5189    }
5190
5191    pub fn items_in_level_range(&self, min: u32, max: u32, category: Option<&str>) -> Vec<&ItemDefinition> {
5192        let mut results = Vec::new();
5193        for ((cat, level), ids) in &self.by_category_and_level {
5194            if *level < min || *level > max { continue; }
5195            if let Some(c) = category { if cat != c { continue; } }
5196            for id in ids {
5197                if let Some(def) = self.by_id.get(id) { results.push(def); }
5198            }
5199        }
5200        results.sort_by_key(|d| d.item_level);
5201        results
5202    }
5203
5204    pub fn items_by_rarity(&self, rarity: Rarity) -> Vec<&ItemDefinition> {
5205        let rarity_ord: u8 = match rarity {
5206            Rarity::Common => 0, Rarity::Uncommon => 1, Rarity::Rare => 2,
5207            Rarity::Epic => 3, Rarity::Legendary => 4, Rarity::Mythic => 5,
5208        };
5209        let mut results = Vec::new();
5210        for ((r, _), ids) in &self.by_rarity_and_level {
5211            if *r != rarity_ord { continue; }
5212            for id in ids {
5213                if let Some(def) = self.by_id.get(id) { results.push(def); }
5214            }
5215        }
5216        results
5217    }
5218
5219    pub fn find_by_name_prefix(&self, prefix: &str) -> Vec<&ItemDefinition> {
5220        let lower = prefix.to_lowercase();
5221        self.by_name.iter()
5222            .filter(|(name, _)| name.to_lowercase().starts_with(&lower))
5223            .filter_map(|(_, id)| self.by_id.get(id))
5224            .collect()
5225    }
5226
5227    pub fn total_catalog_value(&self) -> u64 {
5228        self.by_id.values().map(|d| d.base_price).sum()
5229    }
5230
5231    pub fn most_valuable_items(&self, n: usize) -> Vec<&ItemDefinition> {
5232        let mut items: Vec<&ItemDefinition> = self.by_id.values().collect();
5233        items.sort_by(|a, b| b.base_price.cmp(&a.base_price));
5234        items.truncate(n);
5235        items
5236    }
5237
5238    pub fn average_item_level_by_rarity(&self) -> HashMap<u8, f32> {
5239        let mut sums: HashMap<u8, (u64, u32)> = HashMap::new();
5240        for (_, def) in &self.by_id {
5241            let r = match def.rarity {
5242                Rarity::Common => 0u8, Rarity::Uncommon => 1, Rarity::Rare => 2,
5243                Rarity::Epic => 3, Rarity::Legendary => 4, Rarity::Mythic => 5,
5244            };
5245            let e = sums.entry(r).or_insert((0, 0));
5246            e.0 += def.item_level as u64;
5247            e.1 += 1;
5248        }
5249        sums.iter().map(|(r, (s, c))| (*r, *s as f32 / (*c).max(1) as f32)).collect()
5250    }
5251}
5252
5253// ============================================================
5254// EXTENDED: LOOT FILTER
5255// ============================================================
5256
5257#[derive(Debug, Clone)]
5258pub struct LootFilterRule {
5259    pub id: u64,
5260    pub name: String,
5261    pub priority: u32,
5262    pub conditions: Vec<LootFilterCondition>,
5263    pub action: LootFilterAction,
5264    pub highlight_color: Option<Vec4>,
5265    pub play_sound: Option<String>,
5266    pub label_override: Option<String>,
5267}
5268
5269#[derive(Debug, Clone)]
5270pub enum LootFilterCondition {
5271    Rarity(Rarity),
5272    Category(ItemCategory),
5273    ItemLevelAbove(u32),
5274    ItemLevelBelow(u32),
5275    BasePriceAbove(u64),
5276    StatAbove { stat: StatType, value: f32 },
5277    DPSAbove(f32),
5278    WeightBelow(f32),
5279    IsBoundOnPickup,
5280}
5281
5282#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5283pub enum LootFilterAction {
5284    Show,
5285    Hide,
5286    Highlight,
5287    AutoPickup,
5288}
5289
5290impl LootFilterRule {
5291    pub fn matches(&self, item: &ItemDefinition) -> bool {
5292        self.conditions.iter().all(|cond| match cond {
5293            LootFilterCondition::Rarity(r) => item.rarity == *r,
5294            LootFilterCondition::Category(c) => item.category == *c,
5295            LootFilterCondition::ItemLevelAbove(l) => item.item_level > *l,
5296            LootFilterCondition::ItemLevelBelow(l) => item.item_level < *l,
5297            LootFilterCondition::BasePriceAbove(p) => item.base_price > *p,
5298            LootFilterCondition::StatAbove { stat, value } => {
5299                let total: f32 = item.explicit_modifiers.iter()
5300                    .chain(item.implicit_modifiers.iter())
5301                    .filter(|m| &m.stat == stat)
5302                    .map(|m| m.flat_value + m.percent_value)
5303                    .sum();
5304                total > *value
5305            }
5306            LootFilterCondition::DPSAbove(dps) => item.total_damage_per_second() > *dps,
5307            LootFilterCondition::WeightBelow(w) => item.weight < *w,
5308            LootFilterCondition::IsBoundOnPickup => item.bind_on_pickup,
5309        })
5310    }
5311}
5312
5313#[derive(Debug, Clone)]
5314pub struct LootFilterProfile {
5315    pub id: u64,
5316    pub name: String,
5317    pub rules: Vec<LootFilterRule>,
5318    pub default_action: LootFilterAction,
5319}
5320
5321impl LootFilterProfile {
5322    pub fn evaluate(&self, item: &ItemDefinition) -> LootFilterAction {
5323        let mut sorted_rules: Vec<&LootFilterRule> = self.rules.iter().collect();
5324        sorted_rules.sort_by_key(|r| r.priority);
5325        for rule in sorted_rules {
5326            if rule.matches(item) {
5327                return rule.action;
5328            }
5329        }
5330        self.default_action
5331    }
5332
5333    pub fn all_shown_items<'a>(&self, items: &'a [&ItemDefinition]) -> Vec<&'a ItemDefinition> {
5334        items.iter().filter(|&&item| {
5335            !matches!(self.evaluate(item), LootFilterAction::Hide)
5336        }).copied().collect()
5337    }
5338
5339    pub fn autopickup_items<'a>(&self, items: &'a [&ItemDefinition]) -> Vec<&'a ItemDefinition> {
5340        items.iter().filter(|&&item| {
5341            matches!(self.evaluate(item), LootFilterAction::AutoPickup)
5342        }).copied().collect()
5343    }
5344}
5345
5346// ============================================================
5347// EXTENDED: MULTI-PLAYER TRADE WINDOW
5348// ============================================================
5349
5350#[derive(Debug, Clone)]
5351pub struct TradeWindow {
5352    pub trade_id: u64,
5353    pub player_a_id: u64,
5354    pub player_b_id: u64,
5355    pub player_a_items: Vec<ItemInstance>,
5356    pub player_b_items: Vec<ItemInstance>,
5357    pub player_a_gold: u64,
5358    pub player_b_gold: u64,
5359    pub player_a_confirmed: bool,
5360    pub player_b_confirmed: bool,
5361    pub state: TradeState,
5362}
5363
5364#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5365pub enum TradeState {
5366    Open,
5367    Confirmed,
5368    Completed,
5369    Cancelled,
5370}
5371
5372impl TradeWindow {
5373    pub fn new(trade_id: u64, player_a: u64, player_b: u64) -> Self {
5374        TradeWindow {
5375            trade_id,
5376            player_a_id: player_a,
5377            player_b_id: player_b,
5378            player_a_items: Vec::new(),
5379            player_b_items: Vec::new(),
5380            player_a_gold: 0,
5381            player_b_gold: 0,
5382            player_a_confirmed: false,
5383            player_b_confirmed: false,
5384            state: TradeState::Open,
5385        }
5386    }
5387
5388    pub fn add_item_a(&mut self, item: ItemInstance, def: &ItemDefinition) -> bool {
5389        if def.bind_on_pickup { return false; }
5390        self.player_a_confirmed = false;
5391        self.player_b_confirmed = false;
5392        self.player_a_items.push(item);
5393        true
5394    }
5395
5396    pub fn add_item_b(&mut self, item: ItemInstance, def: &ItemDefinition) -> bool {
5397        if def.bind_on_pickup { return false; }
5398        self.player_a_confirmed = false;
5399        self.player_b_confirmed = false;
5400        self.player_b_items.push(item);
5401        true
5402    }
5403
5404    pub fn set_gold_a(&mut self, gold: u64) {
5405        self.player_a_gold = gold;
5406        self.player_a_confirmed = false;
5407        self.player_b_confirmed = false;
5408    }
5409
5410    pub fn set_gold_b(&mut self, gold: u64) {
5411        self.player_b_gold = gold;
5412        self.player_a_confirmed = false;
5413        self.player_b_confirmed = false;
5414    }
5415
5416    pub fn confirm(&mut self, player_id: u64) {
5417        if player_id == self.player_a_id { self.player_a_confirmed = true; }
5418        if player_id == self.player_b_id { self.player_b_confirmed = true; }
5419        if self.player_a_confirmed && self.player_b_confirmed {
5420            self.state = TradeState::Confirmed;
5421        }
5422    }
5423
5424    pub fn complete(&mut self) -> bool {
5425        if self.state == TradeState::Confirmed {
5426            self.state = TradeState::Completed;
5427            true
5428        } else {
5429            false
5430        }
5431    }
5432
5433    pub fn cancel(&mut self) {
5434        self.state = TradeState::Cancelled;
5435        self.player_a_confirmed = false;
5436        self.player_b_confirmed = false;
5437    }
5438
5439    pub fn value_a(&self, defs: &HashMap<u64, ItemDefinition>) -> u64 {
5440        let item_value: u64 = self.player_a_items.iter()
5441            .filter_map(|i| defs.get(&i.definition_id))
5442            .map(|d| d.base_price).sum();
5443        item_value + self.player_a_gold
5444    }
5445
5446    pub fn value_b(&self, defs: &HashMap<u64, ItemDefinition>) -> u64 {
5447        let item_value: u64 = self.player_b_items.iter()
5448            .filter_map(|i| defs.get(&i.definition_id))
5449            .map(|d| d.base_price).sum();
5450        item_value + self.player_b_gold
5451    }
5452
5453    pub fn value_ratio(&self, defs: &HashMap<u64, ItemDefinition>) -> f32 {
5454        let va = self.value_a(defs) as f32;
5455        let vb = self.value_b(defs) as f32;
5456        if vb < 1.0 { return f32::INFINITY; }
5457        va / vb
5458    }
5459
5460    pub fn is_fair_trade(&self, defs: &HashMap<u64, ItemDefinition>, tolerance: f32) -> bool {
5461        let ratio = self.value_ratio(defs);
5462        (ratio - 1.0).abs() <= tolerance
5463    }
5464}
5465
5466// ============================================================
5467// EXTENDED: ITEM STATISTICS GRAPHS
5468// ============================================================
5469
5470#[derive(Debug, Clone)]
5471pub struct ItemStatGraph {
5472    pub stat_type: StatType,
5473    pub data_points: Vec<(f32, f32)>,
5474    pub regression_slope: f32,
5475    pub regression_intercept: f32,
5476    pub r_squared: f32,
5477}
5478
5479impl ItemStatGraph {
5480    pub fn from_database(db: &ItemDatabase, stat_type: StatType) -> Self {
5481        let mut points: Vec<(f32, f32)> = Vec::new();
5482        for def in db.definitions.values() {
5483            let value: f32 = def.explicit_modifiers.iter()
5484                .chain(def.implicit_modifiers.iter())
5485                .filter(|m| m.stat == stat_type)
5486                .map(|m| m.flat_value + m.percent_value)
5487                .sum();
5488            if value > 0.0 {
5489                points.push((def.item_level as f32, value));
5490            }
5491        }
5492        let (slope, intercept, r2) = linear_regression(&points);
5493        ItemStatGraph {
5494            stat_type,
5495            data_points: points,
5496            regression_slope: slope,
5497            regression_intercept: intercept,
5498            r_squared: r2,
5499        }
5500    }
5501
5502    pub fn predict(&self, item_level: f32) -> f32 {
5503        self.regression_slope * item_level + self.regression_intercept
5504    }
5505
5506    pub fn outliers(&self, z_threshold: f32) -> Vec<(f32, f32)> {
5507        if self.data_points.len() < 3 { return Vec::new(); }
5508        let mean: f32 = self.data_points.iter().map(|(_, v)| v).sum::<f32>() / self.data_points.len() as f32;
5509        let variance: f32 = self.data_points.iter().map(|(_, v)| (v - mean).powi(2)).sum::<f32>() / self.data_points.len() as f32;
5510        let stddev = variance.sqrt();
5511        if stddev < 1e-6 { return Vec::new(); }
5512        self.data_points.iter().filter(|(_, v)| {
5513            ((v - mean) / stddev).abs() > z_threshold
5514        }).copied().collect()
5515    }
5516
5517    pub fn trend_description(&self) -> String {
5518        if self.regression_slope > 1.0 {
5519            format!("Strongly increasing: +{:.2} per level", self.regression_slope)
5520        } else if self.regression_slope > 0.1 {
5521            format!("Gently increasing: +{:.2} per level", self.regression_slope)
5522        } else if self.regression_slope < -1.0 {
5523            format!("Strongly decreasing: {:.2} per level", self.regression_slope)
5524        } else {
5525            format!("Roughly flat: {:.2} per level", self.regression_slope)
5526        }
5527    }
5528}
5529
5530fn linear_regression(points: &[(f32, f32)]) -> (f32, f32, f32) {
5531    let n = points.len() as f32;
5532    if n < 2.0 { return (0.0, 0.0, 0.0); }
5533    let sum_x: f32 = points.iter().map(|(x, _)| x).sum();
5534    let sum_y: f32 = points.iter().map(|(_, y)| y).sum();
5535    let sum_xx: f32 = points.iter().map(|(x, _)| x * x).sum();
5536    let sum_xy: f32 = points.iter().map(|(x, y)| x * y).sum();
5537    let denom = n * sum_xx - sum_x * sum_x;
5538    if denom.abs() < 1e-9 { return (0.0, sum_y / n, 0.0); }
5539    let slope = (n * sum_xy - sum_x * sum_y) / denom;
5540    let intercept = (sum_y - slope * sum_x) / n;
5541    let mean_y = sum_y / n;
5542    let ss_tot: f32 = points.iter().map(|(_, y)| (y - mean_y).powi(2)).sum();
5543    let ss_res: f32 = points.iter().map(|(x, y)| (y - (slope * x + intercept)).powi(2)).sum();
5544    let r2 = if ss_tot < 1e-9 { 1.0 } else { 1.0 - ss_res / ss_tot };
5545    (slope, intercept, r2)
5546}
5547
5548// ============================================================
5549// EXTENDED: CRAFTING QUEUE
5550// ============================================================
5551
5552#[derive(Debug, Clone)]
5553pub struct CraftingQueueEntry {
5554    pub recipe_id: u64,
5555    pub quantity: u32,
5556    pub started_at: f32,
5557    pub estimated_finish: f32,
5558    pub progress: f32,
5559    pub worker_id: Option<u64>,
5560}
5561
5562#[derive(Debug, Clone)]
5563pub struct CraftingQueue {
5564    pub entries: VecDeque<CraftingQueueEntry>,
5565    pub completed: Vec<CraftingQueueEntry>,
5566    pub max_concurrent: u32,
5567    pub current_time: f32,
5568}
5569
5570impl CraftingQueue {
5571    pub fn new(max_concurrent: u32) -> Self {
5572        CraftingQueue {
5573            entries: VecDeque::new(),
5574            completed: Vec::new(),
5575            max_concurrent,
5576            current_time: 0.0,
5577        }
5578    }
5579
5580    pub fn queue_recipe(&mut self, recipe_id: u64, quantity: u32, crafting_time: f32) -> bool {
5581        if self.entries.len() >= 20 { return false; }
5582        let start = self.current_time;
5583        let finish = start + crafting_time * quantity as f32;
5584        self.entries.push_back(CraftingQueueEntry {
5585            recipe_id,
5586            quantity,
5587            started_at: start,
5588            estimated_finish: finish,
5589            progress: 0.0,
5590            worker_id: None,
5591        });
5592        true
5593    }
5594
5595    pub fn tick(&mut self, dt: f32) -> Vec<CraftingQueueEntry> {
5596        self.current_time += dt;
5597        let active = self.max_concurrent as usize;
5598        for entry in self.entries.iter_mut().take(active) {
5599            let total_time = entry.estimated_finish - entry.started_at;
5600            if total_time > 0.0 {
5601                entry.progress = ((self.current_time - entry.started_at) / total_time).min(1.0);
5602            }
5603        }
5604        let mut finished = Vec::new();
5605        while let Some(front) = self.entries.front() {
5606            if self.current_time >= front.estimated_finish {
5607                if let Some(done) = self.entries.pop_front() {
5608                    self.completed.push(done.clone());
5609                    finished.push(done);
5610                }
5611            } else {
5612                break;
5613            }
5614        }
5615        finished
5616    }
5617
5618    pub fn estimated_completion_time(&self) -> f32 {
5619        self.entries.back().map(|e| e.estimated_finish).unwrap_or(self.current_time)
5620    }
5621
5622    pub fn total_remaining_time(&self) -> f32 {
5623        (self.estimated_completion_time() - self.current_time).max(0.0)
5624    }
5625
5626    pub fn cancel_recipe(&mut self, recipe_id: u64) -> bool {
5627        if let Some(pos) = self.entries.iter().position(|e| e.recipe_id == recipe_id) {
5628            self.entries.remove(pos);
5629            true
5630        } else {
5631            false
5632        }
5633    }
5634}
5635
5636// ============================================================
5637// EXTENDED: DROP ANALYTICS
5638// ============================================================
5639
5640#[derive(Debug, Clone)]
5641pub struct DropAnalytics {
5642    pub item_id: u64,
5643    pub times_seen: u32,
5644    pub times_picked_up: u32,
5645    pub times_sold: u32,
5646    pub times_crafted: u32,
5647    pub times_disenchanted: u32,
5648    pub average_item_level: f32,
5649    pub rarity_distribution: HashMap<Rarity, u32>,
5650    pub first_seen: f32,
5651    pub last_seen: f32,
5652}
5653
5654impl DropAnalytics {
5655    pub fn new(item_id: u64) -> Self {
5656        DropAnalytics {
5657            item_id,
5658            times_seen: 0,
5659            times_picked_up: 0,
5660            times_sold: 0,
5661            times_crafted: 0,
5662            times_disenchanted: 0,
5663            average_item_level: 0.0,
5664            rarity_distribution: HashMap::new(),
5665            first_seen: 0.0,
5666            last_seen: 0.0,
5667        }
5668    }
5669
5670    pub fn pickup_rate(&self) -> f32 {
5671        if self.times_seen == 0 { return 0.0; }
5672        self.times_picked_up as f32 / self.times_seen as f32
5673    }
5674
5675    pub fn keep_rate(&self) -> f32 {
5676        let disposed = self.times_sold + self.times_disenchanted;
5677        if self.times_picked_up == 0 { return 0.0; }
5678        1.0 - disposed as f32 / self.times_picked_up as f32
5679    }
5680
5681    pub fn predominant_rarity(&self) -> Option<Rarity> {
5682        self.rarity_distribution.iter()
5683            .max_by_key(|(_, &v)| v)
5684            .map(|(r, _)| *r)
5685    }
5686}
5687
5688#[derive(Debug, Clone)]
5689pub struct DropAnalyticsRegistry {
5690    pub data: HashMap<u64, DropAnalytics>,
5691    pub total_drops_processed: u64,
5692}
5693
5694impl DropAnalyticsRegistry {
5695    pub fn new() -> Self {
5696        DropAnalyticsRegistry { data: HashMap::new(), total_drops_processed: 0 }
5697    }
5698
5699    pub fn record_drop(&mut self, item_id: u64, item_level: f32, rarity: Rarity, time: f32) {
5700        self.total_drops_processed += 1;
5701        let entry = self.data.entry(item_id).or_insert_with(|| {
5702            let mut a = DropAnalytics::new(item_id);
5703            a.first_seen = time;
5704            a
5705        });
5706        entry.times_seen += 1;
5707        entry.last_seen = time;
5708        let n = entry.times_seen as f32;
5709        entry.average_item_level = entry.average_item_level * (n - 1.0) / n + item_level / n;
5710        *entry.rarity_distribution.entry(rarity).or_insert(0) += 1;
5711    }
5712
5713    pub fn record_pickup(&mut self, item_id: u64) {
5714        if let Some(entry) = self.data.get_mut(&item_id) {
5715            entry.times_picked_up += 1;
5716        }
5717    }
5718
5719    pub fn most_common_drops(&self, top_n: usize) -> Vec<(u64, u32)> {
5720        let mut sorted: Vec<(u64, u32)> = self.data.iter()
5721            .map(|(id, a)| (*id, a.times_seen)).collect();
5722        sorted.sort_by(|a, b| b.1.cmp(&a.1));
5723        sorted.truncate(top_n);
5724        sorted
5725    }
5726
5727    pub fn rarest_drops(&self, top_n: usize) -> Vec<(u64, u32)> {
5728        let mut sorted: Vec<(u64, u32)> = self.data.iter()
5729            .filter(|(_, a)| a.times_seen > 0)
5730            .map(|(id, a)| (*id, a.times_seen)).collect();
5731        sorted.sort_by(|a, b| a.1.cmp(&b.1));
5732        sorted.truncate(top_n);
5733        sorted
5734    }
5735
5736    pub fn average_pickup_rate(&self) -> f32 {
5737        if self.data.is_empty() { return 0.0; }
5738        let sum: f32 = self.data.values().map(|a| a.pickup_rate()).sum();
5739        sum / self.data.len() as f32
5740    }
5741}
5742
5743// ============================================================
5744// EXTENDED: NAMING HELPERS
5745// ============================================================
5746
5747pub fn generate_unique_item_name(base_name: &str, _suffix_tables: &[&str]) -> String {
5748    let titles = [
5749        "the Forsaken", "of Doom", "the Eternal", "of Legends", "the Undying",
5750        "of Chaos", "the Ancient", "the Merciless", "of the Fallen King",
5751        "of Twilight", "the Immortal", "of the Void", "the Cursed", "the Divine",
5752        "of the First Age", "the Reborn", "of Ruin", "the Unbroken", "of the Storm",
5753        "of Shadows",
5754    ];
5755    let idx = base_name.len() % titles.len();
5756    format!("{}, {}", base_name, titles[idx])
5757}
5758
5759pub fn apply_name_title(name: &str, title: &str) -> String {
5760    format!("{} {}", title, name)
5761}
5762
5763pub fn is_name_unique_in_database(name: &str, db: &ItemDatabase) -> bool {
5764    !db.definitions.values().any(|d| d.name == name)
5765}
5766
5767pub fn suggest_item_name(category: ItemCategory, rarity: Rarity, db: &ItemDatabase) -> String {
5768    let seed = category as u64 * 1000 + rarity as u64 * 100 + db.definitions.len() as u64;
5769    let name = generate_item_name(
5770        (seed as usize) % WEAPON_PREFIXES.len(),
5771        ((seed / 2) + 1) as usize % WEAPON_BASES.len(),
5772        ((seed / 4) + 3) as usize % WEAPON_SUFFIXES.len(),
5773    );
5774    if is_name_unique_in_database(&name, db) {
5775        name
5776    } else {
5777        format!("{} ({})", name, db.definitions.len())
5778    }
5779}
5780
5781// ============================================================
5782// EXTENDED: ITEM TAG SYSTEM
5783// ============================================================
5784
5785#[derive(Debug, Clone)]
5786pub struct ItemTagSystem {
5787    pub tags: HashMap<String, HashSet<u64>>,
5788    pub item_tags: HashMap<u64, HashSet<String>>,
5789}
5790
5791impl ItemTagSystem {
5792    pub fn new() -> Self {
5793        ItemTagSystem {
5794            tags: HashMap::new(),
5795            item_tags: HashMap::new(),
5796        }
5797    }
5798
5799    pub fn tag_item(&mut self, item_id: u64, tag: String) {
5800        self.tags.entry(tag.clone()).or_insert_with(HashSet::new).insert(item_id);
5801        self.item_tags.entry(item_id).or_insert_with(HashSet::new).insert(tag);
5802    }
5803
5804    pub fn untag_item(&mut self, item_id: u64, tag: &str) {
5805        if let Some(set) = self.tags.get_mut(tag) { set.remove(&item_id); }
5806        if let Some(set) = self.item_tags.get_mut(&item_id) { set.remove(tag); }
5807    }
5808
5809    pub fn items_with_tag(&self, tag: &str) -> Vec<u64> {
5810        self.tags.get(tag).map(|s| s.iter().copied().collect()).unwrap_or_default()
5811    }
5812
5813    pub fn tags_for_item(&self, item_id: u64) -> Vec<String> {
5814        self.item_tags.get(&item_id).map(|s| s.iter().cloned().collect()).unwrap_or_default()
5815    }
5816
5817    pub fn items_with_all_tags(&self, tags: &[&str]) -> Vec<u64> {
5818        if tags.is_empty() { return Vec::new(); }
5819        let mut result: Option<HashSet<u64>> = None;
5820        for &tag in tags {
5821            let set: HashSet<u64> = self.tags.get(tag).map(|s| s.clone()).unwrap_or_default();
5822            result = Some(match result {
5823                None => set,
5824                Some(prev) => prev.intersection(&set).copied().collect(),
5825            });
5826        }
5827        result.map(|s| s.into_iter().collect()).unwrap_or_default()
5828    }
5829
5830    pub fn all_tags(&self) -> Vec<String> {
5831        let mut tags: Vec<String> = self.tags.keys().cloned().collect();
5832        tags.sort();
5833        tags
5834    }
5835
5836    pub fn rename_tag(&mut self, old: &str, new: String) {
5837        if let Some(items) = self.tags.remove(old) {
5838            for &item_id in &items {
5839                if let Some(item_tags) = self.item_tags.get_mut(&item_id) {
5840                    item_tags.remove(old);
5841                    item_tags.insert(new.clone());
5842                }
5843            }
5844            self.tags.insert(new, items);
5845        }
5846    }
5847}
5848
5849// ============================================================
5850// FINAL ENTRY POINT
5851// ============================================================
5852
5853pub fn inventory_editor_main() {
5854    let mut editor = build_editor();
5855
5856    // Generate random items
5857    for i in 0..5u64 {
5858        editor.generation_params.seed = i * 1337 + 42;
5859        editor.generation_params.item_level = (i * 3 + 1) as u32;
5860        editor.generate_random_item();
5861    }
5862
5863    // Power curve analysis
5864    let _curve = generate_power_curve(1, 20);
5865
5866    // Build a catalog
5867    let catalog = ItemCatalog::build(&editor.database);
5868    let _common_items = catalog.items_by_rarity(Rarity::Common);
5869
5870    // Test loot table
5871    let ctx = LootContext::default();
5872    let random_values: Vec<f32> = (0..40).map(|i| (i as f32 * 0.617) % 1.0).collect();
5873    let tables: Vec<&LootTable> = editor.database.loot_tables.values().collect();
5874    for table in &tables {
5875        let _drops = table.roll_drops(&ctx, &random_values);
5876    }
5877
5878    // Stat comparison
5879    let all_defs: Vec<&ItemDefinition> = editor.database.definitions.values().collect();
5880    if all_defs.len() >= 2 {
5881        let _table = stat_comparison_table(&all_defs[..2]);
5882    }
5883
5884    // Rarity table test
5885    let rarity_table = RarityDropTable::standard();
5886    let mf_table = RarityDropTable::magic_find_adjusted(100.0);
5887    for i in 0u64..10 {
5888        let _r = rarity_table.sample(i as f64 / 10.0, (i as f64 + 0.5) / 10.0);
5889    }
5890
5891    // Tag system usage
5892    let mut tags = ItemTagSystem::new();
5893    for (id, def) in &editor.database.definitions {
5894        if def.rarity >= Rarity::Rare {
5895            tags.tag_item(*id, "high-value".to_string());
5896        }
5897        if def.category.is_weapon() {
5898            tags.tag_item(*id, "weapon".to_string());
5899        }
5900    }
5901
5902    // Crafting queue simulation
5903    let mut queue = CraftingQueue::new(2);
5904    for (id, recipe) in &editor.database.recipes {
5905        queue.queue_recipe(*id, 1, recipe.crafting_time_seconds);
5906    }
5907    for _ in 0..10 {
5908        let _finished = queue.tick(1.0);
5909    }
5910
5911    // Drop analytics
5912    let mut analytics = DropAnalyticsRegistry::new();
5913    for (id, def) in &editor.database.definitions {
5914        analytics.record_drop(*id, def.item_level as f32, def.rarity, 0.0);
5915    }
5916    let _most_common = analytics.most_common_drops(5);
5917}
5918
5919// ============================================================
5920// EXTENDED: ITEM MODIFIER REROLL SYSTEM
5921// ============================================================
5922
5923#[derive(Debug, Clone)]
5924pub struct RerollCost {
5925    pub gold: u64,
5926    pub materials: Vec<(u64, u32)>,
5927    pub randomizer_item_id: Option<u64>,
5928}
5929
5930impl RerollCost {
5931    pub fn for_item(def: &ItemDefinition) -> Self {
5932        let base = (def.base_price as f32 * 0.3) as u64;
5933        let mat_count = match def.rarity {
5934            Rarity::Common => 1,
5935            Rarity::Uncommon => 2,
5936            Rarity::Rare => 3,
5937            Rarity::Epic => 5,
5938            Rarity::Legendary => 8,
5939            Rarity::Mythic => 15,
5940        };
5941        RerollCost {
5942            gold: base,
5943            materials: vec![(2001, mat_count)],
5944            randomizer_item_id: None,
5945        }
5946    }
5947}
5948
5949pub fn reroll_explicit_modifiers(
5950    item: &mut ItemDefinition,
5951    affix_pool: &AffixPool,
5952    seed: u64,
5953) {
5954    let rarity_cfg = RarityConfig::for_rarity(item.rarity);
5955    item.explicit_modifiers.clear();
5956
5957    let mut s = seed;
5958    let num_affixes = rarity_cfg.min_affixes
5959        + (lcg_f32_local(&mut s) * (rarity_cfg.max_affixes - rarity_cfg.min_affixes + 1) as f32) as u32;
5960
5961    let eligible_prefixes = affix_pool.eligible(item.category, item.item_level, AffixType::Prefix);
5962    let eligible_suffixes = affix_pool.eligible(item.category, item.item_level, AffixType::Suffix);
5963
5964    let half = num_affixes / 2;
5965    let mut selected_ids: Vec<u64> = Vec::new();
5966    roll_affixes_into(&mut item.explicit_modifiers, &mut selected_ids, &eligible_prefixes, half, &mut s, affix_pool);
5967    roll_affixes_into(&mut item.explicit_modifiers, &mut selected_ids, &eligible_suffixes, num_affixes - half, &mut s, affix_pool);
5968}
5969
5970fn lcg_f32_local(seed: &mut u64) -> f32 {
5971    *seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
5972    ((*seed >> 32) as f32) / (u32::MAX as f32)
5973}
5974
5975pub fn reroll_single_modifier(
5976    item: &mut ItemDefinition,
5977    modifier_index: usize,
5978    affix_pool: &AffixPool,
5979    seed: u64,
5980) -> bool {
5981    if modifier_index >= item.explicit_modifiers.len() { return false; }
5982
5983    let mut s = seed;
5984    let eligible = affix_pool.eligible(item.category, item.item_level, AffixType::Suffix);
5985    if eligible.is_empty() { return false; }
5986
5987    let weights: Vec<f64> = eligible.iter().map(|d| d.weight as f64).collect();
5988    let alias = AliasTable::new(&weights);
5989    let u1 = lcg_f32_local(&mut s) as f64;
5990    let u2 = lcg_f32_local(&mut s) as f64;
5991    let idx = alias.sample(u1, u2);
5992    let affix = eligible[idx];
5993
5994    let roll_t = lcg_f32_local(&mut s);
5995    let mut m = affix.modifier_template.clone();
5996    m.flat_value = affix.roll_flat_value(roll_t);
5997    m.percent_value = affix.roll_percent_value(roll_t);
5998    item.explicit_modifiers[modifier_index] = m;
5999    true
6000}
6001
6002// ============================================================
6003// EXTENDED: ITEM CORRUPTION SYSTEM
6004// ============================================================
6005
6006#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6007pub enum CorruptionType {
6008    StatBuff,
6009    StatNerf,
6010    AddSocket,
6011    ChangeRarity,
6012    AddImplicit,
6013    CorruptedBlood, // ongoing damage penalty
6014    ChaosDamageBonus,
6015    BreakItem,
6016}
6017
6018#[derive(Debug, Clone)]
6019pub struct CorruptionOutcome {
6020    pub corruption_type: CorruptionType,
6021    pub weight: f32,
6022    pub description: String,
6023}
6024
6025pub fn corrupt_item(item: &mut ItemDefinition, seed: u64) -> CorruptionType {
6026    let outcomes = vec![
6027        CorruptionOutcome { corruption_type: CorruptionType::StatBuff, weight: 25.0, description: "One modifier is enhanced by 20-30%".to_string() },
6028        CorruptionOutcome { corruption_type: CorruptionType::StatNerf, weight: 25.0, description: "One modifier is reduced by 20-30%".to_string() },
6029        CorruptionOutcome { corruption_type: CorruptionType::AddSocket, weight: 15.0, description: "An additional socket is added".to_string() },
6030        CorruptionOutcome { corruption_type: CorruptionType::ChangeRarity, weight: 10.0, description: "Rarity shifts up or down one tier".to_string() },
6031        CorruptionOutcome { corruption_type: CorruptionType::AddImplicit, weight: 10.0, description: "A powerful but dangerous implicit is added".to_string() },
6032        CorruptionOutcome { corruption_type: CorruptionType::CorruptedBlood, weight: 8.0, description: "Wearer takes 1% of max health per second as damage".to_string() },
6033        CorruptionOutcome { corruption_type: CorruptionType::ChaosDamageBonus, weight: 5.0, description: "Adds 10-20% chaos damage bonus to all attacks".to_string() },
6034        CorruptionOutcome { corruption_type: CorruptionType::BreakItem, weight: 2.0, description: "Item is destroyed".to_string() },
6035    ];
6036
6037    let weights: Vec<f64> = outcomes.iter().map(|o| o.weight as f64).collect();
6038    let alias = AliasTable::new(&weights);
6039    let mut s = seed;
6040    let u1 = lcg_f32_local(&mut s) as f64;
6041    let u2 = lcg_f32_local(&mut s) as f64;
6042    let idx = alias.sample(u1, u2);
6043    let ct = outcomes[idx].corruption_type;
6044
6045    let roll = lcg_f32_local(&mut s);
6046    match ct {
6047        CorruptionType::StatBuff => {
6048            if let Some(m) = item.explicit_modifiers.first_mut() {
6049                let boost = 1.2 + roll * 0.1;
6050                m.flat_value *= boost;
6051                m.percent_value *= boost;
6052            }
6053        }
6054        CorruptionType::StatNerf => {
6055            if let Some(m) = item.explicit_modifiers.first_mut() {
6056                let reduce = 0.7 + roll * 0.1;
6057                m.flat_value *= reduce;
6058                m.percent_value *= reduce;
6059            }
6060        }
6061        CorruptionType::ChangeRarity => {
6062            item.rarity = if roll < 0.5 {
6063                match item.rarity {
6064                    Rarity::Common => Rarity::Uncommon,
6065                    Rarity::Uncommon => Rarity::Rare,
6066                    Rarity::Rare => Rarity::Epic,
6067                    Rarity::Epic => Rarity::Legendary,
6068                    Rarity::Legendary => Rarity::Mythic,
6069                    Rarity::Mythic => Rarity::Mythic,
6070                }
6071            } else {
6072                match item.rarity {
6073                    Rarity::Mythic => Rarity::Legendary,
6074                    Rarity::Legendary => Rarity::Epic,
6075                    Rarity::Epic => Rarity::Rare,
6076                    Rarity::Rare => Rarity::Uncommon,
6077                    Rarity::Uncommon => Rarity::Common,
6078                    Rarity::Common => Rarity::Common,
6079                }
6080            };
6081        }
6082        CorruptionType::AddImplicit => {
6083            item.implicit_modifiers.push(StatModifier::percent(StatType::CritChance, 5.0));
6084            // Add a downside
6085            item.implicit_modifiers.push(StatModifier::percent(StatType::LifeMax, -10.0));
6086        }
6087        CorruptionType::ChaosDamageBonus => {
6088            item.implicit_modifiers.push(StatModifier::percent(StatType::PhysicalDamage, 10.0 + roll * 10.0));
6089        }
6090        _ => {}
6091    }
6092
6093    ct
6094}
6095
6096// ============================================================
6097// EXTENDED: ITEM TRANSMUTATION TABLE
6098// ============================================================
6099
6100#[derive(Debug, Clone)]
6101pub struct TransmutationRule {
6102    pub source_category: ItemCategory,
6103    pub target_category: ItemCategory,
6104    pub cost_items: Vec<(u64, u32)>,
6105    pub success_rate: f32,
6106    pub preserves_level: bool,
6107    pub preserves_modifiers: bool,
6108    pub station_required: Option<String>,
6109}
6110
6111impl TransmutationRule {
6112    pub fn can_apply(&self, item: &ItemDefinition, station: Option<&str>) -> bool {
6113        if item.category != self.source_category { return false; }
6114        if let Some(req) = &self.station_required {
6115            if station.map(|s| s != req.as_str()).unwrap_or(true) { return false; }
6116        }
6117        true
6118    }
6119
6120    pub fn transmute(&self, item: &ItemDefinition, new_id: u64, seed: u64) -> ItemDefinition {
6121        let mut new_item = ItemDefinition::new(new_id, &item.name, self.target_category);
6122        if self.preserves_level {
6123            new_item.item_level = item.item_level;
6124            new_item.required_level = item.required_level;
6125        }
6126        new_item.rarity = item.rarity;
6127        if self.preserves_modifiers {
6128            new_item.explicit_modifiers = item.explicit_modifiers.clone();
6129            new_item.implicit_modifiers = item.implicit_modifiers.clone();
6130        }
6131        new_item
6132    }
6133}
6134
6135// ============================================================
6136// EXTENDED: ITEM DROP SOUND MAPPING
6137// ============================================================
6138
6139pub fn drop_sound_for_item(item: &ItemDefinition) -> &'static str {
6140    match item.category {
6141        ItemCategory::Currency => "drop_coin",
6142        ItemCategory::Gem => "drop_gem",
6143        ItemCategory::Potion => "drop_potion",
6144        ItemCategory::Sword | ItemCategory::Dagger | ItemCategory::Axe => "drop_blade",
6145        ItemCategory::Greatsword | ItemCategory::Greataxe | ItemCategory::Hammer => "drop_heavy_weapon",
6146        ItemCategory::Bow | ItemCategory::Crossbow => "drop_bow",
6147        ItemCategory::Staff | ItemCategory::Wand | ItemCategory::Orb | ItemCategory::Tome => "drop_magic",
6148        ItemCategory::Chestplate | ItemCategory::Helmet | ItemCategory::Greaves => "drop_armor_heavy",
6149        ItemCategory::Robe | ItemCategory::Hood | ItemCategory::Leggings => "drop_cloth",
6150        ItemCategory::Ring | ItemCategory::Amulet => "drop_jewel",
6151        _ => "drop_generic",
6152    }
6153}
6154
6155pub fn drop_visual_for_rarity(rarity: Rarity) -> (Vec4, f32, &'static str) {
6156    // (glow_color, glow_intensity, particle_vfx)
6157    match rarity {
6158        Rarity::Common => (Vec4::new(0.8, 0.8, 0.8, 0.3), 0.2, "drop_common"),
6159        Rarity::Uncommon => (Vec4::new(0.2, 0.8, 0.2, 0.5), 0.4, "drop_uncommon"),
6160        Rarity::Rare => (Vec4::new(0.2, 0.4, 1.0, 0.7), 0.6, "drop_rare"),
6161        Rarity::Epic => (Vec4::new(0.6, 0.1, 0.9, 0.8), 0.8, "drop_epic"),
6162        Rarity::Legendary => (Vec4::new(1.0, 0.5, 0.0, 1.0), 1.0, "drop_legendary"),
6163        Rarity::Mythic => (Vec4::new(1.0, 0.0, 0.3, 1.0), 1.5, "drop_mythic"),
6164    }
6165}
6166
6167// ============================================================
6168// EXTENDED: INVENTORY WEIGHT SIMULATION
6169// ============================================================
6170
6171pub fn simulate_inventory_weight(
6172    containers: &[&Container],
6173    travel_speed_normal: f32,
6174) -> InventoryWeightReport {
6175    let total_weight: f32 = containers.iter().map(|c| c.current_weight).sum();
6176    let max_weight: f32 = containers.iter().map(|c| c.max_weight).sum();
6177    let fraction = if max_weight > 0.0 { total_weight / max_weight } else { 0.0 };
6178
6179    // Speed penalty formula: linear drop, 50% speed at 100% encumbrance, 0% at 150%
6180    let speed_multiplier = if fraction <= 0.5 {
6181        1.0
6182    } else if fraction <= 1.0 {
6183        1.0 - (fraction - 0.5) * 0.5 / 0.5
6184    } else {
6185        0.0 // immobile
6186    };
6187
6188    let effective_speed = travel_speed_normal * speed_multiplier;
6189    let heavy_items: Vec<usize> = containers.iter().enumerate()
6190        .flat_map(|(i, c)| c.items.values().map(move |inst| i))
6191        .collect();
6192
6193    InventoryWeightReport {
6194        total_weight,
6195        max_weight,
6196        weight_fraction: fraction,
6197        speed_multiplier,
6198        effective_speed,
6199        is_encumbered: fraction > 0.7,
6200        is_overloaded: fraction > 1.0,
6201    }
6202}
6203
6204#[derive(Debug, Clone)]
6205pub struct InventoryWeightReport {
6206    pub total_weight: f32,
6207    pub max_weight: f32,
6208    pub weight_fraction: f32,
6209    pub speed_multiplier: f32,
6210    pub effective_speed: f32,
6211    pub is_encumbered: bool,
6212    pub is_overloaded: bool,
6213}
6214
6215// ============================================================
6216// EXTENDED: ITEM ATTRIBUTE INHERITANCE (set crafting)
6217// ============================================================
6218
6219pub fn inherit_attributes_from_parent(
6220    child: &mut ItemDefinition,
6221    parent: &ItemDefinition,
6222    inherit_mask: AttributeInheritMask,
6223) {
6224    if inherit_mask.rarity { child.rarity = parent.rarity; }
6225    if inherit_mask.item_level { child.item_level = parent.item_level; }
6226    if inherit_mask.implicit_modifiers { child.implicit_modifiers = parent.implicit_modifiers.clone(); }
6227    if inherit_mask.explicit_modifiers { child.explicit_modifiers = parent.explicit_modifiers.clone(); }
6228    if inherit_mask.sockets { /* would copy sockets */ }
6229    if inherit_mask.enchantments { /* would copy enchants */ }
6230    if inherit_mask.quality_tier {
6231        // Inherit quality via sell price ratio
6232        child.sell_price_ratio = parent.sell_price_ratio;
6233    }
6234}
6235
6236#[derive(Debug, Clone, Copy)]
6237pub struct AttributeInheritMask {
6238    pub rarity: bool,
6239    pub item_level: bool,
6240    pub implicit_modifiers: bool,
6241    pub explicit_modifiers: bool,
6242    pub sockets: bool,
6243    pub enchantments: bool,
6244    pub quality_tier: bool,
6245}
6246
6247impl AttributeInheritMask {
6248    pub fn all() -> Self {
6249        AttributeInheritMask {
6250            rarity: true, item_level: true, implicit_modifiers: true,
6251            explicit_modifiers: true, sockets: true, enchantments: true,
6252            quality_tier: true,
6253        }
6254    }
6255
6256    pub fn none() -> Self {
6257        AttributeInheritMask {
6258            rarity: false, item_level: false, implicit_modifiers: false,
6259            explicit_modifiers: false, sockets: false, enchantments: false,
6260            quality_tier: false,
6261        }
6262    }
6263
6264    pub fn affixes_only() -> Self {
6265        let mut m = AttributeInheritMask::none();
6266        m.implicit_modifiers = true;
6267        m.explicit_modifiers = true;
6268        m
6269    }
6270}
6271
6272// ============================================================
6273// EXTENDED: ITEM SEARCH TOKENIZER
6274// ============================================================
6275
6276pub struct ItemSearchTokenizer {
6277    pub stopwords: HashSet<String>,
6278}
6279
6280impl ItemSearchTokenizer {
6281    pub fn new() -> Self {
6282        let stops = ["the", "of", "a", "an", "and", "or", "in", "on", "at"]
6283            .iter().map(|s| s.to_string()).collect();
6284        ItemSearchTokenizer { stopwords: stops }
6285    }
6286
6287    pub fn tokenize(&self, text: &str) -> Vec<String> {
6288        text.split_whitespace()
6289            .map(|w| w.to_lowercase().trim_matches(|c: char| !c.is_alphanumeric()).to_string())
6290            .filter(|w| !w.is_empty() && !self.stopwords.contains(w))
6291            .collect()
6292    }
6293
6294    pub fn build_index(&self, defs: &HashMap<u64, ItemDefinition>) -> HashMap<String, Vec<u64>> {
6295        let mut idx: HashMap<String, Vec<u64>> = HashMap::new();
6296        for (id, def) in defs {
6297            for token in self.tokenize(&def.name) {
6298                idx.entry(token).or_insert_with(Vec::new).push(*id);
6299            }
6300            for token in self.tokenize(&def.description) {
6301                idx.entry(token).or_insert_with(Vec::new).push(*id);
6302            }
6303        }
6304        // Remove duplicates
6305        for list in idx.values_mut() {
6306            list.sort_unstable();
6307            list.dedup();
6308        }
6309        idx
6310    }
6311
6312    pub fn search_index(&self, index: &HashMap<String, Vec<u64>>, query: &str) -> Vec<u64> {
6313        let tokens = self.tokenize(query);
6314        if tokens.is_empty() { return Vec::new(); }
6315
6316        let sets: Vec<&Vec<u64>> = tokens.iter()
6317            .filter_map(|t| index.get(t))
6318            .collect();
6319        if sets.is_empty() { return Vec::new(); }
6320
6321        // Intersection
6322        let mut result: HashSet<u64> = sets[0].iter().copied().collect();
6323        for set in &sets[1..] {
6324            let s: HashSet<u64> = set.iter().copied().collect();
6325            result = result.intersection(&s).copied().collect();
6326        }
6327        let mut v: Vec<u64> = result.into_iter().collect();
6328        v.sort();
6329        v
6330    }
6331
6332    pub fn fuzzy_search(&self, index: &HashMap<String, Vec<u64>>, query: &str, max_edit_dist: usize) -> Vec<u64> {
6333        let query_tokens = self.tokenize(query);
6334        let mut results: HashSet<u64> = HashSet::new();
6335        for token in &query_tokens {
6336            for (key, ids) in index {
6337                if edit_distance(token, key) <= max_edit_dist {
6338                    results.extend(ids.iter().copied());
6339                }
6340            }
6341        }
6342        let mut v: Vec<u64> = results.into_iter().collect();
6343        v.sort();
6344        v
6345    }
6346}
6347
6348fn edit_distance(a: &str, b: &str) -> usize {
6349    let a: Vec<char> = a.chars().collect();
6350    let b: Vec<char> = b.chars().collect();
6351    let n = a.len();
6352    let m = b.len();
6353    if n == 0 { return m; }
6354    if m == 0 { return n; }
6355    let mut dp = vec![vec![0usize; m + 1]; n + 1];
6356    for i in 0..=n { dp[i][0] = i; }
6357    for j in 0..=m { dp[0][j] = j; }
6358    for i in 1..=n {
6359        for j in 1..=m {
6360            let cost = if a[i-1] == b[j-1] { 0 } else { 1 };
6361            dp[i][j] = (dp[i-1][j] + 1).min(dp[i][j-1] + 1).min(dp[i-1][j-1] + cost);
6362        }
6363    }
6364    dp[n][m]
6365}
6366
6367// ============================================================
6368// EXTENDED: VENDOR PRICE SIMULATION OVER TIME
6369// ============================================================
6370
6371#[derive(Debug, Clone)]
6372pub struct PriceSimPoint {
6373    pub time: f32,
6374    pub price: u64,
6375    pub supply: f32,
6376    pub demand: f32,
6377}
6378
6379pub fn simulate_vendor_prices(
6380    vendor: &mut VendorDefinition,
6381    item: &ItemDefinition,
6382    duration_hours: f32,
6383    events: &[(f32, EconomyTransaction)], // (time, event)
6384) -> Vec<PriceSimPoint> {
6385    let mut history = Vec::new();
6386    let step = 1.0f32;
6387    let steps = (duration_hours / step) as u32;
6388    let mut event_idx = 0;
6389
6390    for s in 0..steps {
6391        let t = s as f32 * step;
6392
6393        // Process events at this time
6394        while event_idx < events.len() && events[event_idx].0 <= t {
6395            vendor.adjust_supply_demand(events[event_idx].1.clone());
6396            event_idx += 1;
6397        }
6398
6399        vendor.refresh_inventory(t);
6400        let price = vendor.buy_price(item);
6401        history.push(PriceSimPoint {
6402            time: t,
6403            price,
6404            supply: vendor.supply,
6405            demand: vendor.demand,
6406        });
6407    }
6408
6409    history
6410}
6411
6412// ============================================================
6413// EXTENDED: ITEM DURABILITY DEGRADATION
6414// ============================================================
6415
6416#[derive(Debug, Clone)]
6417pub struct DurabilitySystem {
6418    pub base_loss_per_hit: f32,
6419    pub base_loss_per_death: f32,
6420    pub level_scaling: f32,  // higher level items lose less per hit
6421    pub repair_efficiency: f32, // fraction of max durability restored per repair
6422    pub indestructible_threshold: f32, // below this % the item breaks but isn't destroyed
6423}
6424
6425impl DurabilitySystem {
6426    pub fn standard() -> Self {
6427        DurabilitySystem {
6428            base_loss_per_hit: 0.01,
6429            base_loss_per_death: 5.0,
6430            level_scaling: 0.02,
6431            repair_efficiency: 1.0,
6432            indestructible_threshold: 0.0,
6433        }
6434    }
6435
6436    pub fn hit_damage(&self, item_level: u32) -> f32 {
6437        self.base_loss_per_hit * (1.0 - self.level_scaling * item_level as f32 * 0.01).max(0.1)
6438    }
6439
6440    pub fn apply_hit(&self, instance: &mut ItemInstance, item_level: u32) {
6441        let loss = self.hit_damage(item_level);
6442        instance.durability = (instance.durability - loss).max(0.0);
6443    }
6444
6445    pub fn apply_death(&self, instance: &mut ItemInstance) {
6446        instance.durability = (instance.durability - self.base_loss_per_death).max(0.0);
6447    }
6448
6449    pub fn repair(&self, instance: &mut ItemInstance) {
6450        instance.durability = (instance.durability + 100.0 * self.repair_efficiency).min(100.0);
6451    }
6452
6453    pub fn partial_repair(&self, instance: &mut ItemInstance, fraction: f32) {
6454        let gain = 100.0 * self.repair_efficiency * fraction.clamp(0.0, 1.0);
6455        instance.durability = (instance.durability + gain).min(100.0);
6456    }
6457}
6458
6459// ============================================================
6460// EXTENDED: CRAFTING SKILL PROGRESSION
6461// ============================================================
6462
6463#[derive(Debug, Clone)]
6464pub struct CraftingSkill {
6465    pub name: String,
6466    pub current_level: u32,
6467    pub current_xp: u64,
6468    pub xp_to_next_level: u64,
6469    pub total_xp: u64,
6470    pub max_level: u32,
6471    pub recipes_unlocked_at: HashMap<u32, Vec<u64>>, // level -> recipe ids
6472    pub special_perks_at: HashMap<u32, String>, // level -> perk description
6473}
6474
6475impl CraftingSkill {
6476    pub fn new(name: impl Into<String>) -> Self {
6477        let mut skill = CraftingSkill {
6478            name: name.into(),
6479            current_level: 1,
6480            current_xp: 0,
6481            xp_to_next_level: 100,
6482            total_xp: 0,
6483            max_level: 100,
6484            recipes_unlocked_at: HashMap::new(),
6485            special_perks_at: HashMap::new(),
6486        };
6487        skill.special_perks_at.insert(10, "+5% crafting success rate".to_string());
6488        skill.special_perks_at.insert(25, "Can craft Rare quality items".to_string());
6489        skill.special_perks_at.insert(50, "+10% crafting speed".to_string());
6490        skill.special_perks_at.insert(75, "Can craft Epic quality items".to_string());
6491        skill.special_perks_at.insert(100, "Grandmaster: Can craft Legendary quality items".to_string());
6492        skill
6493    }
6494
6495    pub fn xp_required_for_level(level: u32) -> u64 {
6496        // Exponential growth: xp = 100 * 1.15^(level-1)
6497        (100.0 * (1.15f64).powi(level as i32 - 1)) as u64
6498    }
6499
6500    pub fn add_xp(&mut self, xp: u64) -> Vec<u32> {
6501        let mut levels_gained = Vec::new();
6502        self.current_xp += xp;
6503        self.total_xp += xp;
6504
6505        while self.current_xp >= self.xp_to_next_level && self.current_level < self.max_level {
6506            self.current_xp -= self.xp_to_next_level;
6507            self.current_level += 1;
6508            levels_gained.push(self.current_level);
6509            self.xp_to_next_level = Self::xp_required_for_level(self.current_level + 1);
6510        }
6511
6512        levels_gained
6513    }
6514
6515    pub fn level_fraction(&self) -> f32 {
6516        if self.xp_to_next_level == 0 { return 1.0; }
6517        self.current_xp as f32 / self.xp_to_next_level as f32
6518    }
6519
6520    pub fn success_rate_bonus(&self) -> f32 {
6521        // +0.5% per 10 levels
6522        (self.current_level as f32 / 10.0) * 0.5
6523    }
6524
6525    pub fn quality_bonus(&self) -> f32 {
6526        // +1% quality multiplier per 5 levels
6527        self.current_level as f32 * 0.01
6528    }
6529}
6530
6531// ============================================================
6532// EXTENDED: LOADOUT SYSTEM
6533// ============================================================
6534
6535#[derive(Debug, Clone)]
6536pub struct EquipmentLoadout {
6537    pub id: u64,
6538    pub name: String,
6539    pub slots: HashMap<EquipmentSlot, Option<ItemInstance>>,
6540    pub total_weight: f32,
6541    pub stat_totals: HashMap<StatType, f32>,
6542    pub active_set_piece_counts: HashMap<u64, u32>,
6543}
6544
6545impl EquipmentLoadout {
6546    pub fn new(id: u64, name: impl Into<String>) -> Self {
6547        let mut slots = HashMap::new();
6548        for &slot in EquipmentSlot::all() {
6549            slots.insert(slot, None);
6550        }
6551        EquipmentLoadout {
6552            id,
6553            name: name.into(),
6554            slots,
6555            total_weight: 0.0,
6556            stat_totals: HashMap::new(),
6557            active_set_piece_counts: HashMap::new(),
6558        }
6559    }
6560
6561    pub fn equip(&mut self, slot: EquipmentSlot, inst: ItemInstance, def: &ItemDefinition) -> Option<ItemInstance> {
6562        let old = self.slots.insert(slot, Some(inst));
6563        self.recalculate_totals_simple(def);
6564        old.flatten()
6565    }
6566
6567    pub fn unequip(&mut self, slot: EquipmentSlot) -> Option<ItemInstance> {
6568        self.slots.get_mut(&slot).and_then(|s| s.take())
6569    }
6570
6571    pub fn recalculate_totals_simple(&mut self, last_def: &ItemDefinition) {
6572        // Simplified: just add the last def's modifiers
6573        for m in &last_def.explicit_modifiers {
6574            *self.stat_totals.entry(m.stat.clone()).or_insert(0.0) += m.flat_value;
6575        }
6576        self.total_weight += last_def.weight;
6577    }
6578
6579    pub fn is_slot_filled(&self, slot: EquipmentSlot) -> bool {
6580        self.slots.get(&slot).and_then(|s| s.as_ref()).is_some()
6581    }
6582
6583    pub fn empty_slots(&self) -> Vec<EquipmentSlot> {
6584        EquipmentSlot::all().iter()
6585            .filter(|&&slot| !self.is_slot_filled(slot))
6586            .copied()
6587            .collect()
6588    }
6589
6590    pub fn equipped_count(&self) -> u32 {
6591        self.slots.values().filter(|s| s.is_some()).count() as u32
6592    }
6593
6594    pub fn loadout_score(&self, defs: &HashMap<u64, ItemDefinition>) -> f32 {
6595        let mut score = 0.0f32;
6596        for inst in self.slots.values().filter_map(|s| s.as_ref()) {
6597            if let Some(def) = defs.get(&inst.definition_id) {
6598                score += item_power_score(def) * inst.quality;
6599            }
6600        }
6601        score
6602    }
6603}
6604
6605// ============================================================
6606// EXTENDED: LOOT TABLE VISUAL EDITOR HELPERS
6607// ============================================================
6608
6609#[derive(Debug, Clone)]
6610pub struct LootTableEditorLayout {
6611    pub entry_rects: Vec<Vec4>,       // one per entry
6612    pub probability_bars: Vec<Vec4>,  // visual bar for each entry's probability
6613    pub add_button_rect: Vec4,
6614    pub simulate_button_rect: Vec4,
6615    pub total_weight: f32,
6616}
6617
6618impl LootTableEditorLayout {
6619    pub fn compute(table: &LootTable, panel_rect: Vec4) -> Self {
6620        let x = panel_rect.x + 8.0;
6621        let mut y = panel_rect.y + 40.0;
6622        let row_h = 30.0;
6623        let w = panel_rect.z - 16.0;
6624        let total_weight: f32 = table.entries.iter().filter(|e| !e.guaranteed).map(|e| e.weight).sum();
6625
6626        let mut entry_rects = Vec::new();
6627        let mut prob_bars = Vec::new();
6628
6629        for entry in &table.entries {
6630            entry_rects.push(Vec4::new(x, y, w, row_h));
6631            let prob = if entry.guaranteed { 1.0 } else if total_weight > 0.0 { entry.weight / total_weight } else { 0.0 };
6632            let bar_w = (w - 120.0) * prob;
6633            prob_bars.push(Vec4::new(x + 120.0, y + 8.0, bar_w, row_h - 16.0));
6634            y += row_h + 4.0;
6635        }
6636
6637        let add_btn = Vec4::new(x, y + 8.0, 80.0, 24.0);
6638        let sim_btn = Vec4::new(x + 90.0, y + 8.0, 100.0, 24.0);
6639
6640        LootTableEditorLayout {
6641            entry_rects,
6642            probability_bars: prob_bars,
6643            add_button_rect: add_btn,
6644            simulate_button_rect: sim_btn,
6645            total_weight,
6646        }
6647    }
6648}
6649
6650// ============================================================
6651// EXTENDED: STAT PROGRESSION PREVIEW
6652// ============================================================
6653
6654pub fn stat_projection_over_levels(
6655    stat: StatType,
6656    min_level: u32,
6657    max_level: u32,
6658    rarity: Rarity,
6659) -> Vec<(u32, f32)> {
6660    (min_level..=max_level).map(|lvl| {
6661        let expected = budget_expectation(lvl, rarity);
6662        let cost = stat_budget_cost(&stat);
6663        let allocated_to_stat = expected / (4.0 * cost); // rough portion
6664        (lvl, allocated_to_stat)
6665    }).collect()
6666}
6667
6668pub fn compute_ideal_item_set_for_level(level: u32) -> HashMap<EquipmentSlot, (Rarity, f32)> {
6669    // Returns (rarity, expected_power) for each slot at given level
6670    let mut recommendation = HashMap::new();
6671    let rarity = if level < 10 { Rarity::Common }
6672        else if level < 25 { Rarity::Uncommon }
6673        else if level < 50 { Rarity::Rare }
6674        else if level < 75 { Rarity::Epic }
6675        else { Rarity::Legendary };
6676
6677    for &slot in EquipmentSlot::all() {
6678        let power = budget_expectation(level, rarity);
6679        recommendation.insert(slot, (rarity, power));
6680    }
6681    recommendation
6682}
6683
6684// ============================================================
6685// EXTENDED: GOLD SINK SIMULATION
6686// ============================================================
6687
6688#[derive(Debug, Clone)]
6689pub struct GoldSinkSimulator {
6690    pub repair_cost_per_session: u64,
6691    pub vendor_purchases_per_session: u64,
6692    pub crafting_cost_per_session: u64,
6693    pub enchanting_cost_per_session: u64,
6694    pub trading_fees_per_session: u64,
6695    pub tax_rate: f32, // fraction of vendor sales taken as tax
6696}
6697
6698impl GoldSinkSimulator {
6699    pub fn default_economy() -> Self {
6700        GoldSinkSimulator {
6701            repair_cost_per_session: 50,
6702            vendor_purchases_per_session: 200,
6703            crafting_cost_per_session: 150,
6704            enchanting_cost_per_session: 100,
6705            trading_fees_per_session: 30,
6706            tax_rate: 0.05,
6707        }
6708    }
6709
6710    pub fn total_gold_sunk_per_session(&self, vendor_sales: u64) -> u64 {
6711        let tax = (vendor_sales as f32 * self.tax_rate) as u64;
6712        self.repair_cost_per_session
6713            + self.vendor_purchases_per_session
6714            + self.crafting_cost_per_session
6715            + self.enchanting_cost_per_session
6716            + self.trading_fees_per_session
6717            + tax
6718    }
6719
6720    pub fn gold_income_per_session(&self, mobs_killed: u32, avg_gold_per_mob: u64, vendor_sales: u64) -> u64 {
6721        mobs_killed as u64 * avg_gold_per_mob + vendor_sales
6722    }
6723
6724    pub fn net_gold_per_session(&self, mobs_killed: u32, avg_gold_per_mob: u64, vendor_sales: u64) -> i64 {
6725        let income = self.gold_income_per_session(mobs_killed, avg_gold_per_mob, vendor_sales) as i64;
6726        let sunk = self.total_gold_sunk_per_session(vendor_sales) as i64;
6727        income - sunk
6728    }
6729
6730    pub fn inflation_factor(&self, sessions: u32, mobs_per_session: u32, avg_gold: u64, vendor_sales: u64) -> f32 {
6731        let net = self.net_gold_per_session(mobs_per_session, avg_gold, vendor_sales);
6732        // Positive net = inflation over time
6733        let total_net = net * sessions as i64;
6734        if total_net > 0 {
6735            1.0 + (total_net as f32 / (1_000_000.0 * sessions as f32)).min(2.0)
6736        } else {
6737            1.0
6738        }
6739    }
6740}
6741
6742// ============================================================
6743// EXTENDED: ITEM COMPARISON MATRIX
6744// ============================================================
6745
6746pub fn build_comparison_matrix(items: &[&ItemDefinition]) -> Vec<Vec<f32>> {
6747    let n = items.len();
6748    let mut matrix = vec![vec![0.0f32; n]; n];
6749    for i in 0..n {
6750        for j in 0..n {
6751            if i == j { matrix[i][j] = 1.0; continue; }
6752            let score_i = item_power_score(items[i]);
6753            let score_j = item_power_score(items[j]);
6754            if score_j > 0.0 {
6755                matrix[i][j] = score_i / score_j; // ratio > 1 means i is better than j
6756            }
6757        }
6758    }
6759    matrix
6760}
6761
6762pub fn best_item_for_slot<'a>(
6763    slot: EquipmentSlot,
6764    available: &[&'a ItemDefinition],
6765    player_level: u32,
6766) -> Option<&'a ItemDefinition> {
6767    available.iter()
6768        .filter(|&&def| {
6769            slot.is_compatible(def.category) &&
6770            def.requirements.level <= player_level
6771        })
6772        .max_by(|a, b| {
6773            item_power_score(a).partial_cmp(&item_power_score(b)).unwrap_or(std::cmp::Ordering::Equal)
6774        })
6775        .copied()
6776}
6777
6778// ============================================================
6779// EXTENDED: FULL DATABASE PERSISTENCE HELPERS
6780// ============================================================
6781
6782pub fn serialize_item_to_string(item: &ItemDefinition) -> String {
6783    let mods: String = item.explicit_modifiers.iter().map(|m| {
6784        format!("{}:{}:{}", m.stat.display_name(), m.flat_value, m.percent_value)
6785    }).collect::<Vec<_>>().join(",");
6786
6787    format!(
6788        "id={};name={};cat={};rarity={};ilvl={};price={};weight={};w={};h={};mods=[{}]",
6789        item.id,
6790        item.name,
6791        item.category.display_name(),
6792        RarityConfig::for_rarity(item.rarity).display_name(),
6793        item.item_level,
6794        item.base_price,
6795        item.weight,
6796        item.width,
6797        item.height,
6798        mods
6799    )
6800}
6801
6802pub fn count_items_by_slot_compatibility(db: &ItemDatabase) -> HashMap<String, u32> {
6803    let mut counts: HashMap<String, u32> = HashMap::new();
6804    for def in db.definitions.values() {
6805        for &slot in EquipmentSlot::all() {
6806            if slot.is_compatible(def.category) {
6807                *counts.entry(slot.display_name().to_string()).or_insert(0) += 1;
6808                break;
6809            }
6810        }
6811    }
6812    counts
6813}
6814
6815pub fn database_integrity_check(db: &ItemDatabase) -> Vec<String> {
6816    let mut issues = Vec::new();
6817
6818    // Check recipes reference valid items
6819    for recipe in db.recipes.values() {
6820        if !db.definitions.contains_key(&recipe.output_item_id) {
6821            issues.push(format!("Recipe {} references non-existent output item {}", recipe.name, recipe.output_item_id));
6822        }
6823        for ing in &recipe.ingredients {
6824            if !db.definitions.contains_key(&ing.item_id) {
6825                issues.push(format!("Recipe {} references non-existent ingredient {}", recipe.name, ing.item_id));
6826            }
6827        }
6828    }
6829
6830    // Check loot tables reference valid items
6831    for table in db.loot_tables.values() {
6832        for entry in &table.entries {
6833            if !db.definitions.contains_key(&entry.item_id) {
6834                issues.push(format!("Loot table {} references non-existent item {}", table.name, entry.item_id));
6835            }
6836        }
6837    }
6838
6839    // Check item sets reference valid items
6840    for set in db.item_sets.values() {
6841        for &item_id in &set.item_ids {
6842            if !db.definitions.contains_key(&item_id) {
6843                issues.push(format!("Set {} references non-existent item {}", set.name, item_id));
6844            }
6845        }
6846    }
6847
6848    // Check item ID uniqueness
6849    let mut ids: HashSet<u64> = HashSet::new();
6850    for &id in db.definitions.keys() {
6851        if !ids.insert(id) {
6852            issues.push(format!("Duplicate item ID: {}", id));
6853        }
6854    }
6855
6856    issues
6857}
6858
6859// ============================================================
6860// EXTENDED: ITEM LOCALIZATION SUPPORT
6861// ============================================================
6862
6863#[derive(Debug, Clone)]
6864pub struct ItemLocalization {
6865    pub item_id: u64,
6866    pub locale: String,
6867    pub name: String,
6868    pub description: String,
6869    pub flavor_text: String,
6870}
6871
6872#[derive(Debug, Clone)]
6873pub struct LocalizationDatabase {
6874    pub entries: HashMap<(u64, String), ItemLocalization>,
6875    pub supported_locales: Vec<String>,
6876    pub fallback_locale: String,
6877}
6878
6879impl LocalizationDatabase {
6880    pub fn new() -> Self {
6881        LocalizationDatabase {
6882            entries: HashMap::new(),
6883            supported_locales: vec!["en".to_string(), "fr".to_string(), "de".to_string(), "ja".to_string()],
6884            fallback_locale: "en".to_string(),
6885        }
6886    }
6887
6888    pub fn add_entry(&mut self, entry: ItemLocalization) {
6889        self.entries.insert((entry.item_id, entry.locale.clone()), entry);
6890    }
6891
6892    pub fn get_name(&self, item_id: u64, locale: &str) -> Option<&str> {
6893        self.entries.get(&(item_id, locale.to_string()))
6894            .map(|e| e.name.as_str())
6895    }
6896
6897    pub fn get_localized_name(&self, item: &ItemDefinition, locale: &str) -> String {
6898        self.get_name(item.id, locale)
6899            .or_else(|| self.get_name(item.id, &self.fallback_locale))
6900            .map(|s| s.to_string())
6901            .unwrap_or_else(|| item.name.clone())
6902    }
6903
6904    pub fn missing_translations(&self, db: &ItemDatabase, locale: &str) -> Vec<u64> {
6905        db.definitions.keys()
6906            .filter(|&&id| !self.entries.contains_key(&(id, locale.to_string())))
6907            .copied()
6908            .collect()
6909    }
6910
6911    pub fn localization_coverage(&self, db: &ItemDatabase, locale: &str) -> f32 {
6912        let missing = self.missing_translations(db, locale).len();
6913        let total = db.definitions.len();
6914        if total == 0 { return 1.0; }
6915        1.0 - missing as f32 / total as f32
6916    }
6917}
6918
6919// ============================================================
6920// EXTENDED: BATCH IMPORT VALIDATION
6921// ============================================================
6922
6923#[derive(Debug, Clone)]
6924pub struct BatchImportRecord {
6925    pub row_index: usize,
6926    pub raw_data: String,
6927    pub parsed_item: Option<ItemDefinition>,
6928    pub errors: Vec<String>,
6929    pub warnings: Vec<String>,
6930}
6931
6932pub fn validate_batch_import(records: &mut Vec<BatchImportRecord>, db: &ItemDatabase) {
6933    for record in records.iter_mut() {
6934        if let Some(ref item) = record.parsed_item {
6935            // Check for duplicate IDs
6936            if db.definitions.contains_key(&item.id) {
6937                record.warnings.push(format!("Item ID {} already exists — will overwrite", item.id));
6938            }
6939            // Run item validation
6940            let errs = item.validate();
6941            record.errors.extend(errs);
6942            // Check balance
6943            let report = validate_item_balance(item);
6944            if report.is_overpowered {
6945                record.warnings.push(format!("Item appears overpowered: budget={:.1}, expected={:.1}", report.total_budget, report.budget_level_expected));
6946            }
6947            if report.is_underpowered {
6948                record.warnings.push(format!("Item appears underpowered: budget={:.1}, expected={:.1}", report.total_budget, report.budget_level_expected));
6949            }
6950        } else {
6951            record.errors.push("Failed to parse item data".to_string());
6952        }
6953    }
6954}
6955
6956pub fn batch_import_summary(records: &[BatchImportRecord]) -> BatchImportSummary {
6957    let total = records.len();
6958    let valid = records.iter().filter(|r| r.errors.is_empty() && r.parsed_item.is_some()).count();
6959    let with_warnings = records.iter().filter(|r| !r.warnings.is_empty()).count();
6960    let failed = records.iter().filter(|r| !r.errors.is_empty()).count();
6961
6962    BatchImportSummary { total, valid, with_warnings, failed }
6963}
6964
6965#[derive(Debug, Clone)]
6966pub struct BatchImportSummary {
6967    pub total: usize,
6968    pub valid: usize,
6969    pub with_warnings: usize,
6970    pub failed: usize,
6971}
6972
6973
6974#[derive(Debug, Clone)]
6975pub struct EnchantmentRegistry {
6976    pub enchantments: HashMap<u64, EnchantmentDefinition>,
6977}
6978
6979impl EnchantmentRegistry {
6980    pub fn new() -> Self { Self { enchantments: HashMap::new() } }
6981
6982    pub fn register(&mut self, enchant: EnchantmentDefinition) {
6983        self.enchantments.insert(enchant.id, enchant);
6984    }
6985
6986    pub fn applicable_to_slot(&self, slot: &EquipmentSlot) -> Vec<&EnchantmentDefinition> {
6987        self.enchantments.values().filter(|e| e.applicable_slots.contains(slot)).collect()
6988    }
6989
6990    pub fn can_apply(&self, enchant_id: u64, current_enchants: &[u64]) -> bool {
6991        let enchant = match self.enchantments.get(&enchant_id) { Some(e) => e, None => return false };
6992        for existing_id in current_enchants {
6993            if enchant.incompatible_with.contains(existing_id) { return false; }
6994            if let Some(existing) = self.enchantments.get(existing_id) {
6995                if existing.incompatible_with.contains(&enchant_id) { return false; }
6996            }
6997        }
6998        true
6999    }
7000
7001    pub fn total_cost(&self, enchant_ids: &[u64]) -> f32 {
7002        enchant_ids.iter().filter_map(|id| self.enchantments.get(id)).map(|e| e.cost_gold as f32).sum()
7003    }
7004}
7005
7006// ============================================================
7007// SECTION: ITEM AUDIT LOG
7008// ============================================================
7009
7010#[derive(Debug, Clone)]
7011pub enum ItemHistoryEvent {
7012    Created { seed: u64 },
7013    ModifiedField { field: String, old_value: String, new_value: String },
7014    Corrupted { corruption_type: String },
7015    Enchanted { enchant_id: u64 },
7016    Socketed { socket_index: usize, gem_id: u64 },
7017    Repaired { durability_restored: f32 },
7018    Upgraded { upgrade_level: u32 },
7019    Traded { from_player: u64, to_player: u64 },
7020}
7021
7022#[derive(Debug, Clone)]
7023pub struct ItemAuditEntry {
7024    pub timestamp: u64,
7025    pub event: ItemHistoryEvent,
7026    pub actor_id: u64,
7027}
7028
7029#[derive(Debug, Clone)]
7030pub struct ItemAuditLog {
7031    pub item_id: u64,
7032    pub entries: Vec<ItemAuditEntry>,
7033}
7034
7035impl ItemAuditLog {
7036    pub fn new(item_id: u64) -> Self { Self { item_id, entries: Vec::new() } }
7037
7038    pub fn record(&mut self, timestamp: u64, event: ItemHistoryEvent, actor_id: u64) {
7039        self.entries.push(ItemAuditEntry { timestamp, event, actor_id });
7040    }
7041
7042    pub fn count_event_type(&self, event_type: &str) -> usize {
7043        self.entries.iter().filter(|e| {
7044            let ty = match &e.event {
7045                ItemHistoryEvent::Created { .. } => "Created",
7046                ItemHistoryEvent::ModifiedField { .. } => "ModifiedField",
7047                ItemHistoryEvent::Corrupted { .. } => "Corrupted",
7048                ItemHistoryEvent::Enchanted { .. } => "Enchanted",
7049                ItemHistoryEvent::Socketed { .. } => "Socketed",
7050                ItemHistoryEvent::Repaired { .. } => "Repaired",
7051                ItemHistoryEvent::Upgraded { .. } => "Upgraded",
7052                ItemHistoryEvent::Traded { .. } => "Traded",
7053            };
7054            ty == event_type
7055        }).count()
7056    }
7057
7058    pub fn last_traded(&self) -> Option<(u64, u64)> {
7059        self.entries.iter().rev().find_map(|e| {
7060            if let ItemHistoryEvent::Traded { from_player, to_player } = &e.event {
7061                Some((*from_player, *to_player))
7062            } else { None }
7063        })
7064    }
7065}
7066
7067// ============================================================
7068// SECTION: ARMOR NAME GENERATION TABLES
7069// ============================================================
7070
7071pub const ARMOR_PREFIXES: &[&str] = &[
7072    "Adamantine", "Ancient", "Arcane", "Astral", "Awakened", "Azure", "Battleforged",
7073    "Blessed", "Bound", "Brilliant", "Bronze", "Burning", "Carved", "Celestial",
7074    "Chained", "Chromatic", "Cobalt", "Consecrated", "Corrupted", "Crystalline",
7075    "Cursed", "Dark", "Dawnforged", "Deadened", "Defiant", "Dense", "Divine",
7076    "Draconic", "Dread", "Dusk", "Echoing", "Elder", "Embossed", "Empowered",
7077    "Enchanted", "Ethereal", "Exalted", "Fabled", "Fallen", "Flared", "Flickering",
7078    "Forged", "Frostborn", "Ghostly", "Gilded", "Glorious", "Glowing", "Golden",
7079    "Granite", "Hallowed", "Hardened", "Haunted", "Heavy", "Heralded", "Hollow",
7080    "Holy", "Honored", "Horned", "Imbued", "Immortal", "Imperial", "Infernal",
7081    "Inlaid", "Iron", "Jade", "Jagged", "Jeweled", "Kingly", "Lacquered",
7082    "Layered", "Leathered", "Light", "Lightning", "Linked", "Living", "Lunar",
7083    "Lustrous", "Mageweaved", "Marbled", "Marked", "Massive", "Merciless",
7084    "Metallic", "Midnight", "Misted", "Moonlit", "Mottled", "Mystic",
7085    "Nimble", "Noble", "Oaken", "Obsidian", "Opal", "Ornate", "Overlaid",
7086    "Pale", "Patched", "Phantom", "Plated", "Polished", "Pristine", "Radiant",
7087    "Raging", "Refined", "Reinforced", "Resplendent", "Rimed", "Rippling",
7088    "Risen", "Rooted", "Royal", "Runed", "Sacred", "Sainted", "Scaled",
7089    "Scorched", "Searing", "Shadow", "Shattered", "Shielded", "Shining",
7090    "Silver", "Singed", "Skeletal", "Spiked", "Spirit", "Stalwart", "Steel",
7091    "Stoneborn", "Storm", "Sturdy", "Sublime", "Sunforged", "Tempest",
7092    "Tempered", "Thorned", "Thunder", "Titanium", "Torn", "Towering",
7093    "Tranquil", "True", "Twisted", "Umbral", "Undead", "Unholy", "Unrelenting",
7094    "Verdant", "Vesper", "Vibrant", "Vile", "Violet", "Void", "Volcanic",
7095    "Warded", "Warlord", "Wicked", "Wild", "Woven", "Wyrmscale", "Zenith",
7096];
7097
7098pub const ARMOR_BASES: &[&str] = &[
7099    "Breastplate", "Chainmail", "Coat", "Cowl", "Cuirass", "Doublet", "Gambeson",
7100    "Gauntlets", "Gorget", "Greaves", "Hauberk", "Helm", "Hood", "Jacket",
7101    "Jerkin", "Kite Shield", "Lamellar", "Leggings", "Lorica", "Mantle",
7102    "Pauldrons", "Pavise", "Plate Armor", "Robe", "Round Shield", "Sabatons",
7103    "Scale Mail", "Sallet", "Sash", "Shoulderguards", "Skirt", "Skull Cap",
7104    "Splint Mail", "Surcoat", "Tower Shield", "Tunic", "Vambraces", "Vest",
7105    "Visage", "Warplate", "Wrap", "Brigandine", "Barbute", "Buckler",
7106    "Camail", "Chausses", "Coif", "Cuisse", "Elbow Cops", "Fauld",
7107    "Full Plate", "Harness", "Haubergeon", "Kettle Hat", "Knee Cops",
7108    "Lames", "Laminar", "Mail Coif", "Mitten Gauntlets", "Nasal Helm",
7109    "Pavis", "Poleyn", "Pot Helm", "Rerebrace", "Roundel", "Sabaton",
7110    "Spangenhelm", "Spaulders", "Tasset", "Vambrace", "War Mask",
7111    "Winged Helm", "Wyrm Coil", "Leather Vest", "Silk Robe", "Wool Cloak",
7112];
7113
7114pub const ARMOR_SUFFIXES: &[&str] = &[
7115    "of Absorption", "of Aegis", "of Agility", "of Atonement", "of Aversion",
7116    "of Bravery", "of Bulwark", "of Calm", "of Castigation", "of Clarity",
7117    "of Composure", "of Conviction", "of Courage", "of Dauntlessness", "of Defense",
7118    "of Deflection", "of Denial", "of Devotion", "of Dominance", "of Durability",
7119    "of Endurance", "of Fortification", "of Fortitude", "of Fury", "of Grace",
7120    "of Hardiness", "of Immunity", "of Impenetrability", "of Indomitability",
7121    "of Iron Will", "of Justice", "of Might", "of Persistence", "of Power",
7122    "of Protection", "of Purity", "of Readiness", "of Recovery", "of Resilience",
7123    "of Resolve", "of Retaliation", "of Retribution", "of Salvation", "of Sanctity",
7124    "of Serenity", "of Shelter", "of Shielding", "of Solace", "of Solidarity",
7125    "of Stability", "of Steadfastness", "of Stone", "of Strength", "of Stubbornness",
7126    "of Tenacity", "of the Bear", "of the Bull", "of the Colossus", "of the Defender",
7127    "of the Dragon", "of the Earth", "of the Fortress", "of the Giant", "of the Guardian",
7128    "of the Iron Giant", "of the Knight", "of the Leviathan", "of the Lion",
7129    "of the Mountain", "of the Paladin", "of the Rampart", "of the Rock",
7130    "of the Sentinel", "of the Titan", "of the Tortoise", "of the Unyielding",
7131    "of the Vanguard", "of the Wall", "of the Warden", "of the Warrior",
7132    "of Toughness", "of Valor", "of Vengeance", "of Vigilance", "of Vitality",
7133    "of Ward", "of Warding", "of Will", "of Withstanding", "of Wrath", "of Zeal",
7134];
7135
7136pub fn generate_armor_name(seed: &mut u64) -> String {
7137    let prefix_idx = (lcg_next(seed) as usize) % ARMOR_PREFIXES.len();
7138    let base_idx = (lcg_next(seed) as usize) % ARMOR_BASES.len();
7139    let suffix_roll = lcg_next(seed) % 3;
7140    if suffix_roll == 0 {
7141        let suffix_idx = (lcg_next(seed) as usize) % ARMOR_SUFFIXES.len();
7142        format!("{} {} {}", ARMOR_PREFIXES[prefix_idx], ARMOR_BASES[base_idx], ARMOR_SUFFIXES[suffix_idx])
7143    } else {
7144        format!("{} {}", ARMOR_PREFIXES[prefix_idx], ARMOR_BASES[base_idx])
7145    }
7146}
7147
7148// ============================================================
7149// SECTION: SET BONUS CALCULATOR
7150// ============================================================
7151
7152#[derive(Debug, Clone)]
7153pub struct SetBonusTier {
7154    pub pieces_required: usize,
7155    pub modifiers: Vec<StatModifier>,
7156    pub special_effect: Option<String>,
7157}
7158
7159#[derive(Debug, Clone)]
7160pub struct ItemSetDefinition {
7161    pub id: u64,
7162    pub name: String,
7163    pub members: Vec<u64>,
7164    pub bonus_tiers: Vec<SetBonusTier>,
7165}
7166
7167impl ItemSetDefinition {
7168    pub fn active_bonuses(&self, equipped_count: usize) -> Vec<&SetBonusTier> {
7169        self.bonus_tiers.iter().filter(|t| equipped_count >= t.pieces_required).collect()
7170    }
7171
7172    pub fn total_modifiers_at_count(&self, equipped_count: usize) -> Vec<StatModifier> {
7173        let mut mods = Vec::new();
7174        for tier in self.active_bonuses(equipped_count) {
7175            mods.extend(tier.modifiers.clone());
7176        }
7177        mods
7178    }
7179}
7180
7181pub fn detect_active_sets(equipped: &[&ItemDefinition], sets: &HashMap<u64, ItemSetDefinition>) -> Vec<(u64, usize)> {
7182    let mut set_counts: HashMap<u64, usize> = HashMap::new();
7183    for item in equipped {
7184        if let Some(set_id) = item.set_id {
7185            *set_counts.entry(set_id).or_insert(0) += 1;
7186        }
7187    }
7188    set_counts.into_iter()
7189        .filter(|(set_id, count)| {
7190            sets.get(set_id).map(|s| !s.active_bonuses(*count).is_empty()).unwrap_or(false)
7191        })
7192        .collect()
7193}
7194
7195// ============================================================
7196// SECTION: UPGRADE MATERIAL SYSTEM
7197// ============================================================
7198
7199#[derive(Debug, Clone)]
7200pub struct UpgradeMaterial {
7201    pub id: u64,
7202    pub name: String,
7203    pub tier: u32,
7204    pub material_type: String,
7205    pub stack_size: u32,
7206    pub base_value: f32,
7207}
7208
7209#[derive(Debug, Clone)]
7210pub struct UpgradeRecipe {
7211    pub target_item_category: ItemCategory,
7212    pub current_upgrade_level: u32,
7213    pub materials: Vec<(u64, u32)>,
7214    pub gold_cost: u64,
7215    pub success_probability: f32,
7216    pub stat_gain_per_upgrade: Vec<StatModifier>,
7217}
7218
7219pub fn compute_upgrade_success_probability(base_prob: f32, upgrade_level: u32, blessing_bonus: f32) -> f32 {
7220    let level_penalty = (upgrade_level as f32 * 0.05).min(0.4);
7221    ((base_prob - level_penalty) + blessing_bonus).clamp(0.05, 1.0)
7222}
7223
7224pub fn apply_upgrade(item: &mut ItemDefinition, recipe: &UpgradeRecipe, success: bool, upgrade_level: &mut u32) {
7225    if success {
7226        *upgrade_level += 1;
7227        for modifier in &recipe.stat_gain_per_upgrade {
7228            item.implicit_modifiers.push(modifier.clone());
7229        }
7230    }
7231    // Failure: no change (item not destroyed at lower levels typically)
7232}
7233
7234// ============================================================
7235// SECTION: INVENTORY WEIGHT BUDGET PLANNER
7236// ============================================================
7237
7238#[derive(Debug, Clone)]
7239pub struct WeightBudgetPlanner {
7240    pub max_carry_weight: f32,
7241    pub current_items: Vec<(u64, f32)>,
7242}
7243
7244impl WeightBudgetPlanner {
7245    pub fn new(max_carry_weight: f32) -> Self {
7246        Self { max_carry_weight, current_items: Vec::new() }
7247    }
7248
7249    pub fn add_item(&mut self, item_id: u64, weight: f32) -> bool {
7250        let total: f32 = self.current_items.iter().map(|(_, w)| w).sum::<f32>() + weight;
7251        if total <= self.max_carry_weight {
7252            self.current_items.push((item_id, weight));
7253            true
7254        } else { false }
7255    }
7256
7257    pub fn remove_item(&mut self, item_id: u64) {
7258        self.current_items.retain(|(id, _)| *id != item_id);
7259    }
7260
7261    pub fn current_weight(&self) -> f32 {
7262        self.current_items.iter().map(|(_, w)| w).sum()
7263    }
7264
7265    pub fn remaining_capacity(&self) -> f32 {
7266        self.max_carry_weight - self.current_weight()
7267    }
7268
7269    pub fn encumbrance_penalty(&self) -> f32 {
7270        let ratio = self.current_weight() / self.max_carry_weight;
7271        if ratio < 0.5 { 0.0 }
7272        else if ratio < 0.75 { (ratio - 0.5) * 0.4 }
7273        else if ratio < 1.0 { 0.1 + (ratio - 0.75) * 0.8 }
7274        else { 0.3 + (ratio - 1.0) * 2.0 }
7275    }
7276
7277    pub fn knapsack_optimal_subset(&self, target_weight: f32) -> Vec<u64> {
7278        // Greedy by weight approximation
7279        let mut sorted = self.current_items.clone();
7280        sorted.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
7281        let mut result = Vec::new();
7282        let mut remaining = target_weight;
7283        for (id, w) in sorted {
7284            if w <= remaining {
7285                remaining -= w;
7286                result.push(id);
7287            }
7288        }
7289        result
7290    }
7291}
7292
7293// ============================================================
7294// SECTION: ITEM GENERATION VALIDATION
7295// ============================================================
7296
7297#[derive(Debug, Clone)]
7298pub struct ItemGenerationReport {
7299    pub item_id: u64,
7300    pub passed: bool,
7301    pub warnings: Vec<String>,
7302    pub errors: Vec<String>,
7303    pub balance_score: f32,
7304}
7305
7306pub fn full_generation_validation(item: &ItemDefinition, item_level: u32) -> ItemGenerationReport {
7307    let mut warnings = Vec::new();
7308    let mut errors = Vec::new();
7309
7310    if item.name.is_empty() { errors.push("Item has no name".to_string()); }
7311    if item.base_price == 0 { errors.push("Zero base price".to_string()); }
7312    if item.weight < 0.0 { errors.push("Negative weight".to_string()); }
7313    if item.explicit_modifiers.len() > 6 { warnings.push("Item has more than 6 explicit modifiers".to_string()); }
7314    if item.requirements.level > item_level + 5 { warnings.push("Item level requirement significantly exceeds item_level".to_string()); }
7315
7316    let balance = validate_item_balance(item);
7317    let balance_ok = !balance.is_overpowered && !balance.is_underpowered;
7318    let balance_score = if balance_ok { 1.0 } else { 0.5 };
7319    if !balance_ok { warnings.push(format!("Balance check failed: {:?}", balance.violations)); }
7320
7321    ItemGenerationReport {
7322        item_id: item.id,
7323        passed: errors.is_empty(),
7324        warnings,
7325        errors,
7326        balance_score,
7327    }
7328}
7329
7330// ============================================================
7331// SECTION: VENDOR INVENTORY ROTATION
7332// ============================================================
7333
7334#[derive(Debug, Clone)]
7335pub struct VendorSlot {
7336    pub item_id: u64,
7337    pub stock: u32,
7338    pub listed_price: f32,
7339    pub refresh_at_tick: u64,
7340}
7341
7342#[derive(Debug, Clone)]
7343pub struct VendorState {
7344    pub vendor_id: u64,
7345    pub slots: Vec<VendorSlot>,
7346    pub gold: u64,
7347    pub refresh_interval_ticks: u64,
7348    pub last_refresh_tick: u64,
7349    pub specialty_category: Option<ItemCategory>,
7350}
7351
7352impl VendorState {
7353    pub fn new(vendor_id: u64, refresh_interval_ticks: u64) -> Self {
7354        Self { vendor_id, slots: Vec::new(), gold: 1000, refresh_interval_ticks, last_refresh_tick: 0, specialty_category: None }
7355    }
7356
7357    pub fn add_slot(&mut self, item_id: u64, stock: u32, price: f32, current_tick: u64) {
7358        self.slots.push(VendorSlot { item_id, stock, listed_price: price, refresh_at_tick: current_tick + self.refresh_interval_ticks });
7359    }
7360
7361    pub fn buy_item(&mut self, slot_index: usize, player_gold: &mut u64) -> Option<u64> {
7362        if slot_index >= self.slots.len() { return None; }
7363        let slot = &mut self.slots[slot_index];
7364        let price = slot.listed_price as u64;
7365        if *player_gold < price || slot.stock == 0 { return None; }
7366        *player_gold -= price;
7367        self.gold += price;
7368        slot.stock -= 1;
7369        Some(slot.item_id)
7370    }
7371
7372    pub fn sell_item_to_vendor(&mut self, item_id: u64, sell_price: f32, player_gold: &mut u64) {
7373        let gain = (sell_price * 0.3) as u64;
7374        if self.gold >= gain {
7375            self.gold -= gain;
7376            *player_gold += gain;
7377            self.slots.push(VendorSlot { item_id, stock: 1, listed_price: sell_price * 1.2, refresh_at_tick: u64::MAX });
7378        }
7379    }
7380
7381    pub fn needs_refresh(&self, current_tick: u64) -> bool {
7382        current_tick >= self.last_refresh_tick + self.refresh_interval_ticks
7383    }
7384
7385    pub fn refresh(&mut self, current_tick: u64, new_items: Vec<(u64, u32, f32)>) {
7386        self.slots.retain(|s| s.refresh_at_tick > current_tick);
7387        for (item_id, stock, price) in new_items {
7388            self.add_slot(item_id, stock, price, current_tick);
7389        }
7390        self.last_refresh_tick = current_tick;
7391    }
7392
7393    pub fn apply_reputation_discount(&mut self, reputation: f32) {
7394        // reputation 0..100 gives up to 20% discount
7395        let discount = (reputation / 100.0 * 0.2).min(0.2);
7396        for slot in &mut self.slots {
7397            slot.listed_price *= 1.0 - discount;
7398        }
7399    }
7400}
7401
7402// ============================================================
7403// SECTION: STASH TAB MANAGEMENT
7404// ============================================================
7405
7406#[derive(Debug, Clone)]
7407pub struct StashTab {
7408    pub id: u64,
7409    pub name: String,
7410    pub tab_type: StashTabType,
7411    pub items: Vec<ItemInstance>,
7412    pub grid_width: u32,
7413    pub grid_height: u32,
7414    pub color: Vec4,
7415    pub is_premium: bool,
7416}
7417
7418#[derive(Debug, Clone, PartialEq)]
7419pub enum StashTabType { Normal, Currency, Map, Fragment, Quad, Divination, Gem, Flask, Unique }
7420
7421impl StashTab {
7422    pub fn new(id: u64, name: &str, tab_type: StashTabType) -> Self {
7423        let (w, h) = match &tab_type {
7424            StashTabType::Quad => (24, 24),
7425            _ => (12, 12),
7426        };
7427        Self { id, name: name.to_string(), tab_type, items: Vec::new(), grid_width: w, grid_height: h, color: Vec4::new(0.5, 0.5, 0.5, 1.0), is_premium: false }
7428    }
7429
7430    pub fn add_item(&mut self, item: ItemInstance) { self.items.push(item); }
7431
7432    pub fn remove_item(&mut self, instance_id: u64) -> Option<ItemInstance> {
7433        if let Some(pos) = self.items.iter().position(|i| i.instance_id == instance_id) {
7434            Some(self.items.remove(pos))
7435        } else { None }
7436    }
7437
7438    pub fn item_count(&self) -> usize { self.items.len() }
7439    pub fn is_full(&self) -> bool { self.items.len() >= (self.grid_width * self.grid_height) as usize }
7440
7441    pub fn total_value(&self, db: &ItemDatabase) -> f32 {
7442        self.items.iter()
7443            .filter_map(|inst| db.definitions.get(&inst.definition_id))
7444            .map(|d| d.base_price as f32)
7445            .sum()
7446    }
7447
7448    pub fn search(&self, query: &str) -> Vec<&ItemInstance> {
7449        let q = query.to_lowercase();
7450        self.items.iter().filter(|inst| {
7451            inst.instance_id.to_string().contains(&q)
7452        }).collect()
7453    }
7454}
7455
7456#[derive(Debug)]
7457pub struct StashManager {
7458    pub tabs: Vec<StashTab>,
7459    pub active_tab_id: u64,
7460}
7461
7462impl StashManager {
7463    pub fn new() -> Self { Self { tabs: Vec::new(), active_tab_id: 0 } }
7464
7465    pub fn add_tab(&mut self, tab: StashTab) { self.tabs.push(tab); }
7466
7467    pub fn active_tab(&self) -> Option<&StashTab> {
7468        self.tabs.iter().find(|t| t.id == self.active_tab_id)
7469    }
7470
7471    pub fn active_tab_mut(&mut self) -> Option<&mut StashTab> {
7472        self.tabs.iter_mut().find(|t| t.id == self.active_tab_id)
7473    }
7474
7475    pub fn find_item(&self, instance_id: u64) -> Option<(&StashTab, &ItemInstance)> {
7476        for tab in &self.tabs {
7477            if let Some(item) = tab.items.iter().find(|i| i.instance_id == instance_id) {
7478                return Some((tab, item));
7479            }
7480        }
7481        None
7482    }
7483
7484    pub fn move_item(&mut self, instance_id: u64, to_tab_id: u64) -> bool {
7485        let from_tab_idx = self.tabs.iter().position(|t| t.items.iter().any(|i| i.instance_id == instance_id));
7486        if let Some(from_idx) = from_tab_idx {
7487            let item = self.tabs[from_idx].remove_item(instance_id);
7488            if let Some(item) = item {
7489                if let Some(to_tab) = self.tabs.iter_mut().find(|t| t.id == to_tab_id) {
7490                    to_tab.add_item(item);
7491                    return true;
7492                }
7493            }
7494        }
7495        false
7496    }
7497
7498    pub fn total_value_all_tabs(&self, db: &ItemDatabase) -> f32 {
7499        self.tabs.iter().map(|t| t.total_value(db)).sum()
7500    }
7501}
7502
7503// ============================================================
7504// END OF FILE
7505// ============================================================