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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum ItemCategory {
12 Sword,
14 Greatsword,
15 Dagger,
16 Axe,
17 Greataxe,
18 Mace,
19 Hammer,
20 Spear,
21 Polearm,
22 Whip,
23 Bow,
25 Crossbow,
26 Thrown,
27 Gun,
28 Rifle,
29 Staff,
31 Wand,
32 Orb,
33 Tome,
34 Shield,
36 Buckler,
37 TowerShield,
38 Helmet,
40 Hood,
41 Crown,
42 Chestplate,
44 Robe,
45 Leather,
46 Greaves,
48 Leggings,
49 Pants,
50 Gauntlets,
52 Gloves,
53 Boots,
54 Sabatons,
55 Ring,
57 Amulet,
58 Belt,
59 Cloak,
60 Potion,
62 Food,
63 Scroll,
64 Bomb,
65 Ore,
67 Gem,
68 Herb,
69 Hide,
70 Wood,
71 Cloth,
72 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#[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#[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 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#[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, }
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#[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#[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, }
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 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 pub weight: f32,
627 pub volume: f32,
628 pub width: u8, pub height: u8, pub stack_size: u32,
631 pub durability_max: f32,
632 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 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 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 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#[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>, pub synergies_with: Vec<u64>, pub synergy_bonus: f32,
791 pub tier: u32, }
793
794impl AffixDefinition {
795 pub fn roll_flat_value(&self, t: f32) -> f32 {
796 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 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; }
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#[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, pub quality_outcomes: Vec<QualityOutcome>,
939 pub byproducts: Vec<(u64, u32, f32)>, 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 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 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#[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
1078fn negative_binomial_sample(r: f32, p: f32, u: f32) -> u32 {
1080 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 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 }
1096
1097fn lgamma(x: f64) -> f64 {
1098 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, }
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 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 let drop_count = self.drop_count_distribution.sample(next_r(&mut rv_index));
1172
1173 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#[derive(Debug, Clone)]
1204pub struct PriceElasticity {
1205 pub base_demand: f32,
1206 pub demand_slope: f32, 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, 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#[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 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#[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>, 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#[derive(Debug, Clone)]
1518pub struct GridCell {
1519 pub item_instance_id: Option<u64>,
1520 pub is_anchor: bool, }
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)>, pub position: Vec2, 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 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 let mut all_items: Vec<ItemInstance> = self.items.drain().map(|(_, v)| v).collect();
1691 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 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 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
1741const 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 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 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 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 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; 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 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 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#[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#[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 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 if !self.categories.is_empty() && !self.categories.contains(&item.category) {
2144 return false;
2145 }
2146
2147 if !self.rarities.is_empty() && !self.rarities.contains(&item.rarity) {
2149 return false;
2150 }
2151
2152 if item.item_level < self.min_item_level || item.item_level > self.max_item_level {
2154 return false;
2155 }
2156
2157 let dps = item.total_damage_per_second();
2159 if dps < self.min_dps || dps > self.max_dps {
2160 if item.category.is_weapon() { return false; }
2162 }
2163
2164 if self.show_stackable_only && item.stack_size <= 1 {
2166 return false;
2167 }
2168
2169 if self.show_equippable_only && !item.category.is_weapon() && !item.category.is_armor() && !item.category.is_accessory() {
2171 return false;
2172 }
2173
2174 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#[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#[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#[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#[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(¶ms, &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 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#[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#[derive(Debug, Clone)]
2993pub struct SkylinePacker {
2994 pub width: u32,
2995 pub skyline: Vec<u32>, }
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; 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 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 let mut packer = SkylinePacker::new(grid_width);
3036 let mut placements = Vec::new();
3037
3038 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#[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 let st = StatType::PhysicalDamage; 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#[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
3211pub fn create_starter_item_database() -> ItemDatabase {
3216 let mut db = ItemDatabase::new();
3217
3218 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 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 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 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 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 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 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 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 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 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 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], 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], 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#[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 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 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 lines.push(TooltipLine { text: String::new(), color: Vec4::ONE, indent: 0.0, font_size: 0.0, separator: true });
3557
3558 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 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 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 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 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 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 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 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 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#[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#[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)>>, }
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 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 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 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#[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#[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#[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
4033pub 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 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
4069pub 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
4085pub 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 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 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 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
4133pub 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 let stat_pairs: &[(StatType, &str, f32, f32, f32, f32, AffixType, &[usize])] = &[
4152 ];
4155
4156 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 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 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
4253pub fn build_editor() -> InventoryEditor {
4258 let mut editor = InventoryEditor::new();
4259 editor.database = create_starter_item_database();
4260 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 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
4323pub 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 let k = 467.5 * defender_level - 22167.5;
4349 let k = k.max(1.0);
4350 let raw = armor / (armor + k);
4351 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 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
4398pub 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 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#[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#[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#[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#[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#[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#[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#[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#[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
4977pub 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#[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#[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#[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#[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#[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#[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#[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
5743pub 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#[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
5849pub fn inventory_editor_main() {
5854 let mut editor = build_editor();
5855
5856 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 let _curve = generate_power_curve(1, 20);
5865
5866 let catalog = ItemCatalog::build(&editor.database);
5868 let _common_items = catalog.items_by_rarity(Rarity::Common);
5869
5870 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 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 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 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 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 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6007pub enum CorruptionType {
6008 StatBuff,
6009 StatNerf,
6010 AddSocket,
6011 ChangeRarity,
6012 AddImplicit,
6013 CorruptedBlood, 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 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#[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
6135pub 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 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
6167pub 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 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 };
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
6215pub 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 { }
6229 if inherit_mask.enchantments { }
6230 if inherit_mask.quality_tier {
6231 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
6272pub 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 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 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#[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)], ) -> 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 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#[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, pub repair_efficiency: f32, pub indestructible_threshold: f32, }
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#[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>>, pub special_perks_at: HashMap<u32, String>, }
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 (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 (self.current_level as f32 / 10.0) * 0.5
6523 }
6524
6525 pub fn quality_bonus(&self) -> f32 {
6526 self.current_level as f32 * 0.01
6528 }
6529}
6530
6531#[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 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#[derive(Debug, Clone)]
6610pub struct LootTableEditorLayout {
6611 pub entry_rects: Vec<Vec4>, pub probability_bars: Vec<Vec4>, 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
6650pub 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); (lvl, allocated_to_stat)
6665 }).collect()
6666}
6667
6668pub fn compute_ideal_item_set_for_level(level: u32) -> HashMap<EquipmentSlot, (Rarity, f32)> {
6669 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#[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, }
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 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
6742pub 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; }
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
6778pub 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 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 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 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 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#[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#[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 if db.definitions.contains_key(&item.id) {
6937 record.warnings.push(format!("Item ID {} already exists — will overwrite", item.id));
6938 }
6939 let errs = item.validate();
6941 record.errors.extend(errs);
6942 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#[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
7067pub 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#[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#[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 }
7233
7234#[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 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#[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#[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 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#[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