1use std::collections::HashMap;
7use super::{Element, CombatStats};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub enum Rarity {
13 Common,
14 Uncommon,
15 Rare,
16 Epic,
17 Legendary,
18 Mythic,
19 Unique,
20}
21
22impl Rarity {
23 pub fn color(self) -> glam::Vec4 {
24 use Rarity::*;
25 match self {
26 Common => glam::Vec4::new(0.80, 0.80, 0.80, 1.0),
27 Uncommon => glam::Vec4::new(0.30, 0.90, 0.30, 1.0),
28 Rare => glam::Vec4::new(0.20, 0.40, 1.00, 1.0),
29 Epic => glam::Vec4::new(0.65, 0.20, 0.95, 1.0),
30 Legendary => glam::Vec4::new(1.00, 0.65, 0.00, 1.0),
31 Mythic => glam::Vec4::new(1.00, 0.10, 0.10, 1.0),
32 Unique => glam::Vec4::new(0.90, 0.75, 0.20, 1.0),
33 }
34 }
35
36 pub fn glyph(self) -> char {
37 use Rarity::*;
38 match self {
39 Common => '·',
40 Uncommon => '◆',
41 Rare => '★',
42 Epic => '✦',
43 Legendary => '⬡',
44 Mythic => '⚜',
45 Unique => '◉',
46 }
47 }
48
49 pub fn stat_multiplier(self) -> f32 {
50 use Rarity::*;
51 match self {
52 Common => 1.00,
53 Uncommon => 1.20,
54 Rare => 1.50,
55 Epic => 1.90,
56 Legendary => 2.50,
57 Mythic => 3.50,
58 Unique => 5.00,
59 }
60 }
61
62 pub fn name(self) -> &'static str {
63 use Rarity::*;
64 match self {
65 Common => "Common",
66 Uncommon => "Uncommon",
67 Rare => "Rare",
68 Epic => "Epic",
69 Legendary => "Legendary",
70 Mythic => "Mythic",
71 Unique => "Unique",
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
79pub enum ItemCategory {
80 Weapon,
81 Offhand,
82 Helm,
83 Chest,
84 Legs,
85 Boots,
86 Gloves,
87 Ring,
88 Amulet,
89 Belt,
90 Consumable,
91 Material,
92 QuestItem,
93 Rune,
94 Gem,
95}
96
97impl ItemCategory {
98 pub fn is_equippable(self) -> bool {
99 !matches!(self, ItemCategory::Consumable | ItemCategory::Material | ItemCategory::QuestItem)
100 }
101
102 pub fn slot(self) -> Option<EquipSlot> {
103 use ItemCategory::*;
104 match self {
105 Weapon => Some(EquipSlot::MainHand),
106 Offhand => Some(EquipSlot::OffHand),
107 Helm => Some(EquipSlot::Head),
108 Chest => Some(EquipSlot::Chest),
109 Legs => Some(EquipSlot::Legs),
110 Boots => Some(EquipSlot::Feet),
111 Gloves => Some(EquipSlot::Hands),
112 Ring => Some(EquipSlot::Ring1), Amulet => Some(EquipSlot::Neck),
114 Belt => Some(EquipSlot::Belt),
115 _ => None,
116 }
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
123pub enum EquipSlot {
124 MainHand,
125 OffHand,
126 Head,
127 Chest,
128 Legs,
129 Feet,
130 Hands,
131 Ring1,
132 Ring2,
133 Neck,
134 Belt,
135}
136
137impl EquipSlot {
138 pub const ALL: &'static [EquipSlot] = &[
139 EquipSlot::MainHand, EquipSlot::OffHand, EquipSlot::Head,
140 EquipSlot::Chest, EquipSlot::Legs, EquipSlot::Feet,
141 EquipSlot::Hands, EquipSlot::Ring1, EquipSlot::Ring2,
142 EquipSlot::Neck, EquipSlot::Belt,
143 ];
144
145 pub fn name(self) -> &'static str {
146 use EquipSlot::*;
147 match self {
148 MainHand => "Main Hand", OffHand => "Off Hand", Head => "Head",
149 Chest => "Chest", Legs => "Legs", Feet => "Feet",
150 Hands => "Hands", Ring1 => "Ring 1", Ring2 => "Ring 2",
151 Neck => "Neck", Belt => "Belt",
152 }
153 }
154}
155
156#[derive(Debug, Clone)]
159pub enum ModifierType {
160 FlatAdd(f32),
161 PercentAdd(f32), PercentMul(f32), FlatSet(f32), }
165
166#[derive(Debug, Clone)]
167pub struct StatModifier {
168 pub stat: StatId,
169 pub kind: ModifierType,
170 pub source: String, }
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
174pub enum StatId {
175 Attack,
176 Defense,
177 MaxHp,
178 HpRegen,
179 CritChance,
180 CritMult,
181 Penetration,
182 DodgeChance,
183 BlockChance,
184 BlockAmount,
185 MoveSpeed,
186 AttackSpeed,
187 SkillCooldown,
188 ManaMax,
189 ManaRegen,
190 EntropyAmp,
191}
192
193impl StatId {
194 pub fn name(self) -> &'static str {
195 use StatId::*;
196 match self {
197 Attack => "Attack", Defense => "Defense", MaxHp => "Max HP",
198 HpRegen => "HP Regen", CritChance => "Crit Chance",
199 CritMult => "Crit Multiplier", Penetration => "Penetration",
200 DodgeChance => "Dodge Chance", BlockChance => "Block Chance",
201 BlockAmount => "Block Amount", MoveSpeed => "Move Speed",
202 AttackSpeed => "Attack Speed", SkillCooldown => "Skill Cooldown",
203 ManaMax => "Max Mana", ManaRegen => "Mana Regen",
204 EntropyAmp => "Entropy Amplification",
205 }
206 }
207}
208
209#[derive(Debug, Clone)]
212pub struct ItemAffix {
213 pub name: String,
214 pub modifiers: Vec<StatModifier>,
215 pub is_prefix: bool,
216}
217
218impl ItemAffix {
219 pub fn new(name: impl Into<String>, is_prefix: bool) -> Self {
220 Self { name: name.into(), modifiers: Vec::new(), is_prefix }
221 }
222
223 pub fn add_modifier(mut self, stat: StatId, kind: ModifierType) -> Self {
224 self.modifiers.push(StatModifier { stat, kind, source: self.name.clone() });
225 self
226 }
227
228 pub fn of_the_berserker() -> Self {
230 Self::new("of the Berserker", false)
231 .add_modifier(StatId::Attack, ModifierType::PercentAdd(0.25))
232 .add_modifier(StatId::CritChance, ModifierType::FlatAdd(0.08))
233 }
234
235 pub fn of_fortification() -> Self {
236 Self::new("of Fortification", false)
237 .add_modifier(StatId::Defense, ModifierType::PercentAdd(0.30))
238 .add_modifier(StatId::MaxHp, ModifierType::PercentAdd(0.15))
239 }
240
241 pub fn enchanted() -> Self {
242 Self::new("Enchanted", true)
243 .add_modifier(StatId::EntropyAmp, ModifierType::PercentAdd(0.20))
244 }
245
246 pub fn swift() -> Self {
247 Self::new("Swift", true)
248 .add_modifier(StatId::MoveSpeed, ModifierType::PercentAdd(0.15))
249 .add_modifier(StatId::AttackSpeed, ModifierType::PercentAdd(0.10))
250 }
251
252 pub fn arcane() -> Self {
253 Self::new("Arcane", true)
254 .add_modifier(StatId::ManaMax, ModifierType::FlatAdd(50.0))
255 .add_modifier(StatId::ManaRegen, ModifierType::PercentAdd(0.20))
256 }
257}
258
259#[derive(Debug, Clone)]
262pub enum ItemEffect {
263 OnHitElement { element: Element, amount: f32 },
265 OnKillHeal { amount: f32 },
267 LowHpBoost { threshold: f32, multiplier: f32 },
269 ProcOnHit { chance: f32, effect: Box<ItemEffect> },
271 Aura { radius: f32, stat: StatId, value: f32 },
273 Thorns { reflect_pct: f32 },
275 Lifesteal { pct: f32 },
277 OnCritStatus { status_name: String, duration: f32 },
279}
280
281#[derive(Debug, Clone)]
284pub struct Item {
285 pub id: u64,
286 pub name: String,
287 pub description: String,
288 pub category: ItemCategory,
289 pub rarity: Rarity,
290 pub level: u32,
291 pub base_stats: Vec<StatModifier>,
292 pub affixes: Vec<ItemAffix>,
293 pub effects: Vec<ItemEffect>,
294 pub stack_size: u32,
295 pub max_stack: u32,
296 pub value: u64, pub weight: f32,
298 pub glyph: char,
299 pub sockets: Vec<Option<Item>>, pub element: Option<Element>,
301 pub set_id: Option<u32>, }
303
304impl Item {
305 pub fn new(name: impl Into<String>, category: ItemCategory, rarity: Rarity, level: u32) -> Self {
306 let name = name.into();
307 let glyph = match category {
308 ItemCategory::Weapon => '⚔',
309 ItemCategory::Offhand => '🛡',
310 ItemCategory::Helm => '⛑',
311 ItemCategory::Chest => '🛡',
312 ItemCategory::Boots => '👟',
313 ItemCategory::Ring => '◎',
314 ItemCategory::Amulet => '◉',
315 ItemCategory::Gem => '◆',
316 ItemCategory::Rune => '✦',
317 _ => '·',
318 };
319 Item {
320 id: 0,
321 name,
322 description: String::new(),
323 category,
324 rarity,
325 level,
326 base_stats: Vec::new(),
327 affixes: Vec::new(),
328 effects: Vec::new(),
329 stack_size: 1,
330 max_stack: if matches!(category, ItemCategory::Consumable | ItemCategory::Material) { 99 } else { 1 },
331 value: (level as u64 * 10) * rarity.stat_multiplier() as u64,
332 weight: 1.0,
333 glyph,
334 sockets: Vec::new(),
335 element: None,
336 set_id: None,
337 }
338 }
339
340 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
341 self.description = desc.into();
342 self
343 }
344
345 pub fn with_stat(mut self, stat: StatId, kind: ModifierType) -> Self {
346 self.base_stats.push(StatModifier { stat, kind, source: self.name.clone() });
347 self
348 }
349
350 pub fn with_affix(mut self, affix: ItemAffix) -> Self {
351 self.affixes.push(affix);
352 self
353 }
354
355 pub fn with_effect(mut self, effect: ItemEffect) -> Self {
356 self.effects.push(effect);
357 self
358 }
359
360 pub fn with_element(mut self, el: Element) -> Self {
361 self.element = Some(el);
362 self
363 }
364
365 pub fn add_socket(mut self) -> Self {
366 self.sockets.push(None);
367 self
368 }
369
370 pub fn socket_gem(&mut self, slot: usize, gem: Item) -> bool {
371 if slot < self.sockets.len() {
372 self.sockets[slot] = Some(gem);
373 true
374 } else {
375 false
376 }
377 }
378
379 pub fn display_name(&self) -> String {
380 let prefix = self.affixes.iter()
381 .find(|a| a.is_prefix)
382 .map(|a| format!("{} ", a.name))
383 .unwrap_or_default();
384 let suffix = self.affixes.iter()
385 .find(|a| !a.is_prefix)
386 .map(|a| format!(" {}", a.name))
387 .unwrap_or_default();
388 format!("{}{}{}", prefix, self.name, suffix)
389 }
390
391 pub fn all_modifiers(&self) -> Vec<&StatModifier> {
392 let mut mods: Vec<&StatModifier> = self.base_stats.iter().collect();
393 for affix in &self.affixes {
394 mods.extend(affix.modifiers.iter());
395 }
396 for socket in &self.sockets {
397 if let Some(gem) = socket {
398 mods.extend(gem.base_stats.iter());
399 }
400 }
401 mods
402 }
403
404 pub fn tooltip(&self) -> String {
405 let mut lines = vec![
406 format!("{} [{}]", self.display_name(), self.rarity.name()),
407 format!("Level {} {} | {} glyph", self.level, format!("{:?}", self.category), self.glyph),
408 ];
409 if !self.description.is_empty() {
410 lines.push(self.description.clone());
411 }
412 lines.push(String::from("---"));
413 for m in self.all_modifiers() {
414 let val_str = match &m.kind {
415 ModifierType::FlatAdd(v) => format!("+{:.0}", v),
416 ModifierType::PercentAdd(v) => format!("+{:.0}%", v * 100.0),
417 ModifierType::PercentMul(v) => format!("×{:.2}", v),
418 ModifierType::FlatSet(v) => format!("={:.0}", v),
419 };
420 lines.push(format!(" {} {}", val_str, m.stat.name()));
421 }
422 if !self.effects.is_empty() {
423 lines.push("Effects:".to_string());
424 for eff in &self.effects {
425 lines.push(format!(" {:?}", eff));
426 }
427 }
428 lines.push(format!("Value: {} gold | Weight: {:.1}", self.value, self.weight));
429 lines.join("\n")
430 }
431
432 pub fn stat_bonus(&self, stat: StatId, base: f32) -> f32 {
434 let mut flat = 0.0f32;
435 let mut pct_add = 0.0f32;
436 let mut pct_mul = 1.0f32;
437 let mut override_val: Option<f32> = None;
438 for m in self.all_modifiers() {
439 if m.stat == stat {
440 match m.kind {
441 ModifierType::FlatAdd(v) => flat += v,
442 ModifierType::PercentAdd(v) => pct_add += v,
443 ModifierType::PercentMul(v) => pct_mul *= 1.0 + v,
444 ModifierType::FlatSet(v) => override_val = Some(v),
445 }
446 }
447 }
448 if let Some(v) = override_val { return v; }
449 (base + flat) * (1.0 + pct_add) * pct_mul
450 }
451}
452
453#[derive(Debug, Clone)]
456pub struct ItemStack {
457 pub item: Item,
458 pub quantity: u32,
459}
460
461impl ItemStack {
462 pub fn single(item: Item) -> Self {
463 Self { item, quantity: 1 }
464 }
465
466 pub fn stacked(item: Item, qty: u32) -> Self {
467 let qty = qty.min(item.max_stack);
468 Self { item, quantity: qty }
469 }
470
471 pub fn can_stack_with(&self, other: &ItemStack) -> bool {
472 self.item.id == other.item.id && self.item.max_stack > 1
473 }
474
475 pub fn add_to_stack(&mut self, qty: u32) -> u32 {
476 let space = self.item.max_stack - self.quantity;
477 let added = qty.min(space);
478 self.quantity += added;
479 qty - added }
481
482 pub fn remove_from_stack(&mut self, qty: u32) -> u32 {
483 let removed = qty.min(self.quantity);
484 self.quantity -= removed;
485 removed
486 }
487
488 pub fn is_empty(&self) -> bool { self.quantity == 0 }
489}
490
491#[derive(Debug, Clone)]
494pub struct Inventory {
495 slots: Vec<Option<ItemStack>>,
496 capacity: usize,
497 gold: u64,
498 weight: f32,
499 max_weight: f32,
500}
501
502impl Inventory {
503 pub fn new(capacity: usize, max_weight: f32) -> Self {
504 Inventory {
505 slots: vec![None; capacity],
506 capacity,
507 gold: 0,
508 weight: 0.0,
509 max_weight,
510 }
511 }
512
513 pub fn add_item(&mut self, item: Item) -> bool {
514 if self.weight + item.weight > self.max_weight {
515 return false;
516 }
517 if item.max_stack > 1 {
519 for slot in self.slots.iter_mut().flatten() {
520 if slot.item.id == item.id {
521 let leftover = slot.add_to_stack(1);
522 if leftover == 0 {
523 self.weight += item.weight;
524 return true;
525 }
526 }
527 }
528 }
529 for slot in &mut self.slots {
531 if slot.is_none() {
532 self.weight += item.weight;
533 *slot = Some(ItemStack::single(item));
534 return true;
535 }
536 }
537 false }
539
540 pub fn remove_item(&mut self, slot_idx: usize) -> Option<Item> {
541 if slot_idx >= self.capacity { return None; }
542 let stack = self.slots[slot_idx].take()?;
543 self.weight -= stack.item.weight;
544 Some(stack.item)
545 }
546
547 pub fn remove_count(&mut self, slot_idx: usize, count: u32) -> u32 {
548 if slot_idx >= self.capacity { return 0; }
549 if let Some(stack) = &mut self.slots[slot_idx] {
550 let removed = stack.remove_from_stack(count);
551 self.weight -= stack.item.weight * removed as f32;
552 if stack.is_empty() { self.slots[slot_idx] = None; }
553 return removed;
554 }
555 0
556 }
557
558 pub fn get(&self, slot_idx: usize) -> Option<&ItemStack> {
559 self.slots.get(slot_idx)?.as_ref()
560 }
561
562 pub fn get_mut(&mut self, slot_idx: usize) -> Option<&mut ItemStack> {
563 self.slots.get_mut(slot_idx)?.as_mut()
564 }
565
566 pub fn find_item_by_id(&self, id: u64) -> Option<usize> {
567 self.slots.iter().position(|s| s.as_ref().map(|st| st.item.id == id).unwrap_or(false))
568 }
569
570 pub fn find_items_by_category(&self, cat: ItemCategory) -> Vec<usize> {
571 self.slots.iter().enumerate()
572 .filter(|(_, s)| s.as_ref().map(|st| st.item.category == cat).unwrap_or(false))
573 .map(|(i, _)| i)
574 .collect()
575 }
576
577 pub fn used_slots(&self) -> usize {
578 self.slots.iter().filter(|s| s.is_some()).count()
579 }
580
581 pub fn free_slots(&self) -> usize {
582 self.capacity - self.used_slots()
583 }
584
585 pub fn is_full(&self) -> bool { self.free_slots() == 0 }
586
587 pub fn total_value(&self) -> u64 {
588 self.slots.iter().flatten()
589 .map(|s| s.item.value * s.quantity as u64)
590 .sum()
591 }
592
593 pub fn add_gold(&mut self, amount: u64) { self.gold += amount; }
594 pub fn remove_gold(&mut self, amount: u64) -> bool {
595 if self.gold >= amount { self.gold -= amount; true } else { false }
596 }
597 pub fn gold(&self) -> u64 { self.gold }
598 pub fn weight(&self) -> f32 { self.weight }
599 pub fn max_weight(&self) -> f32 { self.max_weight }
600 pub fn capacity(&self) -> usize { self.capacity }
601
602 pub fn sort(&mut self) {
604 let mut items: Vec<ItemStack> = self.slots.iter_mut()
605 .filter_map(|s| s.take())
606 .collect();
607 items.sort_by(|a, b| {
608 b.item.rarity.cmp(&a.item.rarity)
609 .then(b.item.level.cmp(&a.item.level))
610 .then(a.item.name.cmp(&b.item.name))
611 });
612 for (i, item) in items.into_iter().enumerate() {
613 if i < self.capacity { self.slots[i] = Some(item); }
614 }
615 }
616
617 pub fn transfer(&mut self, slot_idx: usize, target: &mut Inventory) -> bool {
619 if let Some(item) = self.remove_item(slot_idx) {
620 if target.add_item(item.clone()) {
621 return true;
622 }
623 let _ = self.add_item(item);
625 }
626 false
627 }
628
629 pub fn iter(&self) -> impl Iterator<Item = (usize, &ItemStack)> {
630 self.slots.iter().enumerate()
631 .filter_map(|(i, s)| s.as_ref().map(|st| (i, st)))
632 }
633}
634
635#[derive(Debug, Clone, Default)]
638pub struct Equipment {
639 pub slots: HashMap<EquipSlot, Item>,
640}
641
642impl Equipment {
643 pub fn new() -> Self {
644 Self { slots: HashMap::new() }
645 }
646
647 pub fn equip(&mut self, item: Item) -> Option<Item> {
648 let slot = item.category.slot()?;
649 let old = self.slots.remove(&slot);
650 self.slots.insert(slot, item);
651 old
652 }
653
654 pub fn unequip(&mut self, slot: EquipSlot) -> Option<Item> {
655 self.slots.remove(&slot)
656 }
657
658 pub fn get(&self, slot: EquipSlot) -> Option<&Item> {
659 self.slots.get(&slot)
660 }
661
662 pub fn is_slot_filled(&self, slot: EquipSlot) -> bool {
663 self.slots.contains_key(&slot)
664 }
665
666 pub fn all_modifiers(&self) -> Vec<&StatModifier> {
668 self.slots.values()
669 .flat_map(|item| item.all_modifiers())
670 .collect()
671 }
672
673 pub fn apply_to_stats(&self, base: &CombatStats) -> CombatStats {
675 let mut result = base.clone();
676 let mods = self.all_modifiers();
677 result.attack = apply_mods(&mods, StatId::Attack, base.attack);
678 result.armor = apply_mods(&mods, StatId::Defense, base.armor);
679 result.max_hp = apply_mods(&mods, StatId::MaxHp, base.max_hp);
680 result.crit_chance = apply_mods(&mods, StatId::CritChance, base.crit_chance);
681 result.crit_mult = apply_mods(&mods, StatId::CritMult, base.crit_mult);
682 result.penetration = apply_mods(&mods, StatId::Penetration, base.penetration);
683 result.dodge_chance = apply_mods(&mods, StatId::DodgeChance, base.dodge_chance);
684 result.entropy_amp = apply_mods(&mods, StatId::EntropyAmp, base.entropy_amp);
685 result
686 }
687
688 pub fn total_weight(&self) -> f32 {
690 self.slots.values().map(|i| i.weight).sum()
691 }
692
693 pub fn summary(&self) -> Vec<String> {
695 EquipSlot::ALL.iter().map(|&slot| {
696 if let Some(item) = self.slots.get(&slot) {
697 format!("[{}] {} ({})", slot.name(), item.display_name(), item.rarity.name())
698 } else {
699 format!("[{}] — empty —", slot.name())
700 }
701 }).collect()
702 }
703}
704
705fn apply_mods(mods: &[&StatModifier], target: StatId, base: f32) -> f32 {
706 let mut flat = 0.0f32;
707 let mut pct_add = 0.0f32;
708 let mut pct_mul = 1.0f32;
709 let mut override_val: Option<f32> = None;
710 for m in mods {
711 if m.stat == target {
712 match m.kind {
713 ModifierType::FlatAdd(v) => flat += v,
714 ModifierType::PercentAdd(v) => pct_add += v,
715 ModifierType::PercentMul(v) => pct_mul *= 1.0 + v,
716 ModifierType::FlatSet(v) => override_val = Some(v),
717 }
718 }
719 }
720 if let Some(v) = override_val { return v; }
721 (base + flat) * (1.0 + pct_add) * pct_mul
722}
723
724#[derive(Debug, Clone)]
727pub struct LootEntry {
728 pub item_builder: fn(u32) -> Item, pub weight: f32,
730 pub min_qty: u32,
731 pub max_qty: u32,
732 pub min_level: u32,
733 pub requires_rare_roll: bool,
734}
735
736#[derive(Debug, Clone, Default)]
737pub struct LootTable {
738 pub entries: Vec<LootEntry>,
739 pub gold_min: u64,
740 pub gold_max: u64,
741 pub drop_chance: f32, pub item_count_min: u32,
743 pub item_count_max: u32,
744}
745
746impl LootTable {
747 pub fn new() -> Self {
748 LootTable {
749 entries: Vec::new(),
750 gold_min: 0,
751 gold_max: 0,
752 drop_chance: 1.0,
753 item_count_min: 1,
754 item_count_max: 3,
755 }
756 }
757
758 pub fn add_entry(&mut self, builder: fn(u32) -> Item, weight: f32, min_qty: u32, max_qty: u32) {
759 self.entries.push(LootEntry {
760 item_builder: builder, weight, min_qty, max_qty,
761 min_level: 0, requires_rare_roll: false,
762 });
763 }
764
765 pub fn set_gold(&mut self, min: u64, max: u64) {
766 self.gold_min = min;
767 self.gold_max = max;
768 }
769
770 pub fn roll(&self, level: u32, luck_bonus: f32, seed: u64) -> LootDrop {
772 let mut rng = SimpleRng::new(seed);
773
774 if rng.next_f32() > self.drop_chance * (1.0 + luck_bonus) {
775 return LootDrop::empty();
776 }
777
778 let gold = if self.gold_max > 0 {
779 self.gold_min + rng.next_u64() % (self.gold_max - self.gold_min + 1)
780 } else { 0 };
781
782 let count = if self.item_count_max > self.item_count_min {
783 self.item_count_min + (rng.next_u32() % (self.item_count_max - self.item_count_min + 1))
784 } else {
785 self.item_count_min
786 };
787
788 let total_weight: f32 = self.entries.iter()
789 .filter(|e| level >= e.min_level)
790 .map(|e| e.weight)
791 .sum();
792
793 let mut items = Vec::new();
794 for _ in 0..count {
795 if total_weight <= 0.0 { break; }
796 let roll = rng.next_f32() * total_weight;
797 let mut accum = 0.0;
798 for entry in &self.entries {
799 if level < entry.min_level { continue; }
800 accum += entry.weight;
801 if roll <= accum {
802 let qty = if entry.max_qty > entry.min_qty {
803 entry.min_qty + rng.next_u32() % (entry.max_qty - entry.min_qty + 1)
804 } else {
805 entry.min_qty
806 };
807 for _ in 0..qty {
808 let mut item = (entry.item_builder)(level);
809 item.id = rng.next_u64();
810 items.push(item);
811 }
812 break;
813 }
814 }
815 }
816
817 LootDrop { items, gold }
818 }
819}
820
821#[derive(Debug, Clone)]
822pub struct LootDrop {
823 pub items: Vec<Item>,
824 pub gold: u64,
825}
826
827impl LootDrop {
828 pub fn empty() -> Self { Self { items: Vec::new(), gold: 0 } }
829 pub fn is_empty(&self) -> bool { self.items.is_empty() && self.gold == 0 }
830}
831
832struct SimpleRng { state: u64 }
835
836impl SimpleRng {
837 fn new(seed: u64) -> Self { Self { state: seed ^ 0x6a09e667f3bcc908 } }
838
839 fn next_u64(&mut self) -> u64 {
840 self.state ^= self.state << 13;
841 self.state ^= self.state >> 7;
842 self.state ^= self.state << 17;
843 self.state
844 }
845
846 fn next_u32(&mut self) -> u32 { (self.next_u64() >> 32) as u32 }
847
848 fn next_f32(&mut self) -> f32 {
849 (self.next_u64() >> 11) as f32 / (1u64 << 53) as f32
850 }
851}
852
853#[cfg(test)]
856mod tests {
857 use super::*;
858
859 fn make_sword(level: u32) -> Item {
860 Item::new("Iron Sword", ItemCategory::Weapon, Rarity::Common, level)
861 .with_stat(StatId::Attack, ModifierType::FlatAdd(10.0))
862 }
863
864 #[test]
865 fn test_item_display_name() {
866 let item = Item::new("Blade", ItemCategory::Weapon, Rarity::Rare, 10)
867 .with_affix(ItemAffix::swift())
868 .with_affix(ItemAffix::of_the_berserker());
869 assert!(item.display_name().contains("Swift"));
870 assert!(item.display_name().contains("Berserker"));
871 }
872
873 #[test]
874 fn test_inventory_add_remove() {
875 let mut inv = Inventory::new(10, 100.0);
876 let sword = make_sword(1);
877 assert!(inv.add_item(sword));
878 assert_eq!(inv.used_slots(), 1);
879 let removed = inv.remove_item(0);
880 assert!(removed.is_some());
881 assert_eq!(inv.used_slots(), 0);
882 }
883
884 #[test]
885 fn test_equipment_stat_apply() {
886 let mut equip = Equipment::new();
887 let item = Item::new("Power Helm", ItemCategory::Helm, Rarity::Rare, 5)
888 .with_stat(StatId::MaxHp, ModifierType::FlatAdd(100.0))
889 .with_stat(StatId::Defense, ModifierType::PercentAdd(0.20));
890 equip.equip(item);
891
892 let base = CombatStats::default();
893 let boosted = equip.apply_to_stats(&base);
894 assert!(boosted.max_hp > base.max_hp);
895 assert!(boosted.armor >= base.armor);
896 }
897
898 #[test]
899 fn test_loot_table_roll() {
900 let mut table = LootTable::new();
901 table.add_entry(|lvl| make_sword(lvl), 1.0, 1, 1);
902 table.set_gold(10, 50);
903 let drop = table.roll(5, 0.0, 12345);
904 let _ = drop.is_empty();
906 }
907
908 #[test]
909 fn test_rarity_ordering() {
910 assert!(Rarity::Legendary > Rarity::Rare);
911 assert!(Rarity::Common < Rarity::Epic);
912 }
913}