terminal_rpg/
items.rs

1use rand::{thread_rng, Rng};
2use serde::{Deserialize, Serialize};
3use std::borrow::Cow;
4use uuid::Uuid;
5
6use crate::{character::CharacterClass, session::PlayerCharacter};
7
8pub const ITEM_RARITY_DROP_RATES: ItemRarityDropRates = ItemRarityDropRates {
9    common: 0.43,
10    uncommon: 0.30,
11    rare: 0.14,
12    epic: 0.08,
13    legendary: 0.05,
14};
15pub const WEAPON_BASE_VALUES: WeaponBaseValues = WeaponBaseValues {
16    min_damage: 12,
17    max_damage: 15,
18    min_crit_hit_rate: 0.12,
19    max_crit_hit_rate: 0.15,
20};
21pub const ARMOR_BASE_VALUES: ArmorBaseValues = ArmorBaseValues {
22    min_health: 15,
23    max_health: 20,
24    min_defense: 1,
25    max_defense: 2,
26};
27pub const RING_BASE_VALUES: RingBaseValues = RingBaseValues {
28    min_mana: 20,
29    max_mana: 25,
30};
31pub const ENCHANTMENT_BASE_VALUES: EnchantmentBaseValues = EnchantmentBaseValues {
32    min_damage: 3,
33    max_damage: 5,
34    min_crit_hit_rate: 0.03,
35    max_crit_hit_rate: 0.05,
36    min_health: 7,
37    max_health: 10,
38    min_defense: 1,
39    max_defense: 2,
40    min_mana: 10,
41    max_mana: 15,
42};
43
44//-------------------//
45// Consumable items //
46//-----------------//
47
48pub const ITEM_HEALTH_POTION_NAME: &str = "Health Potion";
49pub const ITEM_HEALTH_POTION: ItemInfo = ItemInfo {
50    name: Cow::Borrowed(ITEM_HEALTH_POTION_NAME),
51    description: Cow::Borrowed("A magical potion that restores health points."),
52    category: ItemCategory::Consumable,
53};
54
55pub const ITEM_MANA_POTION_NAME: &str = "Mana Potion";
56pub const ITEM_MANA_POTION: ItemInfo = ItemInfo {
57    name: Cow::Borrowed(ITEM_MANA_POTION_NAME),
58    description: Cow::Borrowed("A magical potion that restores mana points."),
59    category: ItemCategory::Consumable,
60};
61
62//---------------//
63// Weapon items //
64//-------------//
65
66pub const ITEM_SWORD: ItemInfo = ItemInfo {
67    name: Cow::Borrowed("Sword"),
68    description: Cow::Borrowed("A sword that increases offensive stats."),
69    category: ItemCategory::Weapon,
70};
71
72pub const ITEM_AXE: ItemInfo = ItemInfo {
73    name: Cow::Borrowed("Axe"),
74    description: Cow::Borrowed("An axe that increases offensive stats."),
75    category: ItemCategory::Weapon,
76};
77
78pub const ITEM_STAFF: ItemInfo = ItemInfo {
79    name: Cow::Borrowed("Staff"),
80    description: Cow::Borrowed("A staff that increases offensive stats."),
81    category: ItemCategory::Weapon,
82};
83
84pub const ITEM_DAGGER: ItemInfo = ItemInfo {
85    name: Cow::Borrowed("Dagger"),
86    description: Cow::Borrowed("A dagger that increases offensive stats."),
87    category: ItemCategory::Weapon,
88};
89
90pub const ITEM_HALBERD: ItemInfo = ItemInfo {
91    name: Cow::Borrowed("Halberd"),
92    description: Cow::Borrowed("A halberd that increases offensive stats."),
93    category: ItemCategory::Weapon,
94};
95
96//--------------//
97// Armor items //
98//------------//
99
100pub const ITEM_ARMOR: ItemInfo = ItemInfo {
101    name: Cow::Borrowed("Armor"),
102    description: Cow::Borrowed("An armor that increases defensive stats."),
103    category: ItemCategory::Armor,
104};
105
106//-------------//
107// Ring items //
108//-----------//
109
110pub const ITEM_RING: ItemInfo = ItemInfo {
111    name: Cow::Borrowed("Ring"),
112    description: Cow::Borrowed("A ring that increases some stats."),
113    category: ItemCategory::Ring,
114};
115
116//-----------------------------------//
117
118#[derive(Serialize, Deserialize, Clone)]
119pub struct ItemInfo {
120    pub name: Cow<'static, str>,
121    pub description: Cow<'static, str>,
122    pub category: ItemCategory,
123}
124
125#[derive(Serialize, Deserialize, Clone)]
126pub struct ConsumableItem {
127    pub info: ItemInfo,
128    pub effect: String,
129    pub rarity: ItemRarity,
130    pub amount_in_inventory: u32,
131}
132
133impl ConsumableItem {
134    pub fn new_health_potion(rarity: ItemRarity) -> Self {
135        Self {
136            info: ITEM_HEALTH_POTION,
137            effect: get_health_potion_effect(&rarity),
138            rarity,
139            amount_in_inventory: 0,
140        }
141    }
142
143    pub fn new_mana_potion(rarity: ItemRarity) -> Self {
144        Self {
145            info: ITEM_MANA_POTION,
146            effect: get_mana_potion_effect(&rarity),
147            rarity,
148            amount_in_inventory: 0,
149        }
150    }
151
152    /// Returns text telling what the item did.
153    pub fn use_item(&self, character: &mut PlayerCharacter) -> (String, ItemRarity, String) {
154        let display_name = get_item_display_name(CharacterItem::Consumable(&self));
155        let mut item_name = "Player used an unknown item.".to_string();
156        let mut effect = "Nothing happened".to_string();
157        let mut rarity = ItemRarity::Unknown;
158        match self.info.name.as_ref() {
159            ITEM_HEALTH_POTION_NAME => {
160                let heal_percentage = get_potion_effect_percentage(&self.rarity) as f64 / 100.0;
161                let restored_health = character
162                    .restore_health((heal_percentage * character.get_total_health() as f64) as u32);
163                item_name = format!("{}", &display_name);
164                rarity = self.rarity.clone();
165                effect = format!("Player restored {} health points", restored_health);
166            }
167            ITEM_MANA_POTION_NAME => {
168                let heal_percentage = get_potion_effect_percentage(&self.rarity) as f64 / 100.0;
169                let restored_mana = character
170                    .restore_mana((heal_percentage * character.get_total_mana() as f64) as u32);
171                item_name = format!("{}", &display_name);
172                rarity = self.rarity.clone();
173                effect = format!("Player restored {} mana points", restored_mana);
174            }
175            _ => {}
176        }
177        if self.amount_in_inventory > 1 {
178            character.decrease_consumable_inventory_amount(&display_name, 1);
179        } else {
180            character.delete_consumable(&display_name);
181        }
182        (item_name, rarity, effect)
183    }
184}
185
186#[derive(Serialize, Deserialize, Clone)]
187pub struct ArmorItemStats {
188    pub health: u32,
189    pub defense: u32,
190}
191
192#[derive(Serialize, Deserialize, Clone)]
193pub struct ArmorItem {
194    pub info: ItemInfo,
195    pub id: String,
196    pub level: u32,
197    pub rarity: ItemRarity,
198    pub stats: ArmorItemStats,
199    pub enchantments: Vec<Enchantment>,
200}
201
202impl ArmorItem {
203    pub fn new(
204        info: ItemInfo,
205        level: u32,
206        rarity: ItemRarity,
207        stats: ArmorItemStats,
208        enchantments: Vec<Enchantment>,
209    ) -> Self {
210        Self {
211            info,
212            id: Uuid::new_v4().to_string(),
213            level,
214            rarity,
215            stats,
216            enchantments,
217        }
218    }
219
220    pub fn is_equipped(&self, character: &PlayerCharacter) -> bool {
221        if let Some(id) = &character.equipped_items.armor {
222            if let Some(armor) = character.data.inventory.armors.get(id) {
223                if armor.id.eq(&self.id) {
224                    return true;
225                }
226            }
227        }
228        false
229    }
230}
231
232#[derive(Serialize, Deserialize, Clone)]
233pub struct WeaponItemStats {
234    pub damage: u32,
235    pub crit_hit_rate: f64,
236}
237
238#[derive(Serialize, Deserialize, Clone)]
239pub struct WeaponItem {
240    pub info: ItemInfo,
241    pub id: String,
242    pub level: u32,
243    pub rarity: ItemRarity,
244    pub stats: WeaponItemStats,
245    pub enchantments: Vec<Enchantment>,
246}
247
248impl WeaponItem {
249    pub fn new(
250        info: ItemInfo,
251        level: u32,
252        rarity: ItemRarity,
253        stats: WeaponItemStats,
254        enchantments: Vec<Enchantment>,
255    ) -> Self {
256        Self {
257            info,
258            id: Uuid::new_v4().to_string(),
259            level,
260            rarity,
261            stats,
262            enchantments,
263        }
264    }
265
266    pub fn is_equipped(&self, character: &PlayerCharacter) -> bool {
267        if let Some(id) = &character.equipped_items.weapon {
268            if let Some(weapon) = character.data.inventory.weapons.get(id) {
269                if weapon.id.eq(&self.id) {
270                    return true;
271                }
272            }
273        }
274        false
275    }
276}
277
278#[derive(Serialize, Deserialize, Clone)]
279pub struct RingItemStats {
280    pub mana: u32,
281}
282
283#[derive(Serialize, Deserialize, Clone)]
284pub struct RingItem {
285    pub info: ItemInfo,
286    pub id: String,
287    pub level: u32,
288    pub rarity: ItemRarity,
289    pub stats: RingItemStats,
290    pub enchantments: Vec<Enchantment>,
291}
292
293impl RingItem {
294    pub fn new(
295        info: ItemInfo,
296        level: u32,
297        rarity: ItemRarity,
298        stats: RingItemStats,
299        enchantments: Vec<Enchantment>,
300    ) -> Self {
301        Self {
302            info,
303            id: Uuid::new_v4().to_string(),
304            level,
305            rarity,
306            stats,
307            enchantments,
308        }
309    }
310
311    pub fn is_equipped(&self, character: &PlayerCharacter) -> bool {
312        if let Some(id) = &character.equipped_items.ring {
313            if let Some(ring) = character.data.inventory.rings.get(id) {
314                if ring.id.eq(&self.id) {
315                    return true;
316                }
317            }
318        }
319        false
320    }
321}
322
323pub struct ItemRarityDropRates {
324    pub common: f64,
325    pub uncommon: f64,
326    pub rare: f64,
327    pub epic: f64,
328    pub legendary: f64,
329}
330
331pub struct WeaponBaseValues {
332    pub min_damage: u32,
333    pub max_damage: u32,
334    pub min_crit_hit_rate: f64,
335    pub max_crit_hit_rate: f64,
336}
337
338pub struct ArmorBaseValues {
339    pub min_health: u32,
340    pub max_health: u32,
341    pub min_defense: u32,
342    pub max_defense: u32,
343}
344
345pub struct RingBaseValues {
346    pub min_mana: u32,
347    pub max_mana: u32,
348}
349
350pub struct EnchantmentBaseValues {
351    pub min_damage: u32,
352    pub max_damage: u32,
353    pub min_crit_hit_rate: f64,
354    pub max_crit_hit_rate: f64,
355    pub min_health: u32,
356    pub max_health: u32,
357    pub min_defense: u32,
358    pub max_defense: u32,
359    pub min_mana: u32,
360    pub max_mana: u32,
361}
362
363pub enum CharacterItem<'a> {
364    Consumable(&'a ConsumableItem),
365    Weapon(&'a WeaponItem),
366    Armor(&'a ArmorItem),
367    Ring(&'a RingItem),
368    Unknown,
369}
370
371pub enum CharacterItemOwned {
372    Consumable(ConsumableItem),
373    Weapon(WeaponItem),
374    Armor(ArmorItem),
375    Ring(RingItem),
376    Unknown,
377}
378
379#[derive(Serialize, Deserialize, Clone)]
380pub enum Enchantment {
381    Damage(u32),
382    CritHitRate(f64),
383    Health(u32),
384    Defense(u32),
385    Mana(u32),
386    Unknown,
387}
388
389#[derive(Debug, Serialize, Deserialize, Clone)]
390pub enum ItemRarity {
391    Common,
392    Uncommon,
393    Rare,
394    Epic,
395    Legendary,
396    Mythical,
397    Unknown,
398}
399
400#[derive(Debug, Serialize, Deserialize, Clone)]
401pub enum ItemCategory {
402    Consumable,
403    Weapon,
404    Armor,
405    Ring,
406    Unknown,
407}
408
409/// Returns the effect percentage of potions.
410/// For example, returns 50 if the percentage is 50%.
411/// 50 can be divided by 100 to get the decimal for calculations: 50/100 = 0.5.
412/// E.g. for health potions, the amount of restored health is then 0.5 * MAX_HEALTH.
413pub fn get_potion_effect_percentage(rarity: &ItemRarity) -> i32 {
414    match rarity {
415        ItemRarity::Common => 20,
416        ItemRarity::Uncommon => 40,
417        ItemRarity::Rare => 60,
418        ItemRarity::Epic => 80,
419        ItemRarity::Legendary => 100,
420        _ => 0,
421    }
422}
423
424pub fn get_health_potion_effect(rarity: &ItemRarity) -> String {
425    format!(
426        "Restores {}% of your maximum health points.",
427        get_potion_effect_percentage(rarity)
428    )
429}
430
431pub fn get_mana_potion_effect(rarity: &ItemRarity) -> String {
432    format!(
433        "Restores {}% of your maximum mana points.",
434        get_potion_effect_percentage(rarity)
435    )
436}
437
438/// Returns a string representation of an item.
439/// The string is used to display the item in menus.
440pub fn get_item_display_name<'a>(item: CharacterItem<'a>) -> String {
441    match item {
442        CharacterItem::Consumable(consumable) => {
443            format!("{:?} {}", consumable.rarity, consumable.info.name)
444        }
445        CharacterItem::Weapon(weapon) => {
446            format!("{:?} {}", weapon.rarity, weapon.info.name)
447        }
448        CharacterItem::Armor(armor) => {
449            format!("{:?} {}", armor.rarity, armor.info.name)
450        }
451        CharacterItem::Ring(ring) => {
452            format!("{:?} {}", ring.rarity, ring.info.name)
453        }
454        _ => format!("?Unknown?"),
455    }
456}
457
458pub fn get_item_level_display<'a>(level: u32) -> String {
459    format!("(Level {})", level)
460}
461
462/// Returns the purchase value of an item in gold.
463pub fn get_item_purchase_value(rarity: &ItemRarity) -> u32 {
464    match rarity {
465        ItemRarity::Common => 200,
466        ItemRarity::Uncommon => 400,
467        ItemRarity::Rare => 600,
468        ItemRarity::Epic => 800,
469        ItemRarity::Legendary => 1000,
470        _ => 0,
471    }
472}
473
474/// Returns the sell value of an item in gold.
475pub fn get_item_sell_value(rarity: &ItemRarity) -> u32 {
476    match rarity {
477        ItemRarity::Common => 25,
478        ItemRarity::Uncommon => 50,
479        ItemRarity::Rare => 75,
480        ItemRarity::Epic => 100,
481        ItemRarity::Legendary => 125,
482        ItemRarity::Mythical => 150,
483        _ => 0,
484    }
485}
486
487pub fn create_starter_weapon(character_class: &CharacterClass) -> WeaponItem {
488    let item_info = match character_class {
489        CharacterClass::Mage => ITEM_STAFF,
490        CharacterClass::Cleric => ITEM_HALBERD,
491        CharacterClass::Assassin => ITEM_DAGGER,
492        CharacterClass::Warrior => ITEM_AXE,
493        CharacterClass::Knight => ITEM_SWORD,
494    };
495    WeaponItem::new(
496        item_info,
497        1,
498        ItemRarity::Common,
499        WeaponItemStats {
500            damage: WEAPON_BASE_VALUES.min_damage,
501            crit_hit_rate: WEAPON_BASE_VALUES.min_crit_hit_rate,
502        },
503        Vec::new(),
504    )
505}
506
507pub fn random_equipment_item() -> ItemCategory {
508    let mut rng = rand::thread_rng();
509    let rand_num = rng.gen_range(0..=2);
510    match rand_num {
511        0 => ItemCategory::Weapon,
512        1 => ItemCategory::Armor,
513        2 => ItemCategory::Ring,
514        _ => ItemCategory::Unknown,
515    }
516}
517
518pub fn random_item_rarity(drop_rates: &ItemRarityDropRates) -> ItemRarity {
519    let mut rng = rand::thread_rng();
520    let rand_num = rng.gen_range(0.0..1.0);
521    let mut drop_rate = 0.0;
522
523    drop_rate += drop_rates.common;
524    if rand_num < drop_rate {
525        return ItemRarity::Common;
526    }
527
528    drop_rate += drop_rates.uncommon;
529    if rand_num < drop_rate {
530        return ItemRarity::Uncommon;
531    }
532
533    drop_rate += drop_rates.rare;
534    if rand_num < drop_rate {
535        return ItemRarity::Rare;
536    }
537
538    drop_rate += drop_rates.epic;
539    if rand_num < drop_rate {
540        return ItemRarity::Epic;
541    }
542
543    drop_rate += drop_rates.legendary;
544    if rand_num < drop_rate {
545        return ItemRarity::Legendary;
546    }
547
548    ItemRarity::Unknown
549}
550
551pub fn num_enchantments(rarity: &ItemRarity) -> u8 {
552    match rarity {
553        ItemRarity::Common => 0,
554        ItemRarity::Uncommon => 1,
555        ItemRarity::Rare => 2,
556        ItemRarity::Epic => 3,
557        ItemRarity::Legendary => 4,
558        ItemRarity::Mythical => 5,
559        _ => 0,
560    }
561}
562
563pub fn generate_item_enchantments(
564    num: u8,
565    category: ItemCategory,
566    base_values: &EnchantmentBaseValues,
567    dungeon_floor: u32,
568) -> Vec<Enchantment> {
569    let mut enchantments: Vec<Enchantment> = Vec::new();
570    for _ in 0..num {
571        match category {
572            ItemCategory::Weapon => {
573                enchantments.push(random_weapon_enchantment(base_values, dungeon_floor))
574            }
575            ItemCategory::Armor => {
576                enchantments.push(random_armor_enchantment(base_values, dungeon_floor))
577            }
578            ItemCategory::Ring => {
579                enchantments.push(random_ring_enchantment(base_values, dungeon_floor))
580            }
581            _ => {}
582        }
583    }
584    enchantments
585}
586
587pub fn random_weapon_enchantment(
588    base_values: &EnchantmentBaseValues,
589    dungeon_floor: u32,
590) -> Enchantment {
591    let mut rng = thread_rng();
592    let rand_num = rng.gen_range(0..=1);
593    match rand_num {
594        0 => {
595            let damage = rng.gen_range(base_values.min_damage..=base_values.max_damage)
596                + (2 * dungeon_floor);
597            return Enchantment::Damage(damage);
598        }
599        1 => {
600            let crit_hit_rate =
601                rng.gen_range(base_values.min_crit_hit_rate..=base_values.max_crit_hit_rate);
602            return Enchantment::CritHitRate(crit_hit_rate);
603        }
604        _ => Enchantment::Unknown,
605    }
606}
607
608pub fn random_armor_enchantment(
609    base_values: &EnchantmentBaseValues,
610    dungeon_floor: u32,
611) -> Enchantment {
612    let mut rng = thread_rng();
613    let rand_num = rng.gen_range(0..=1);
614    match rand_num {
615        0 => {
616            let health = rng.gen_range(base_values.min_health..=base_values.max_health)
617                + (4 * dungeon_floor);
618            return Enchantment::Health(health);
619        }
620        1 => {
621            let defense = rng.gen_range(base_values.min_defense..=base_values.max_defense)
622                + (1 * dungeon_floor);
623            return Enchantment::Defense(defense);
624        }
625        _ => Enchantment::Unknown,
626    }
627}
628
629pub fn random_ring_enchantment(
630    base_values: &EnchantmentBaseValues,
631    dungeon_floor: u32,
632) -> Enchantment {
633    let mut rng = thread_rng();
634    let rand_num = rng.gen_range(0..=3);
635    match rand_num {
636        0 => {
637            let mana = rng.gen_range(base_values.min_mana..=base_values.max_mana);
638            return Enchantment::Mana(mana);
639        }
640        1 => {
641            let damage = rng.gen_range(base_values.min_damage..=base_values.max_damage)
642                + (2 * dungeon_floor);
643            return Enchantment::Damage(damage);
644        }
645        2 => {
646            let health = rng.gen_range(base_values.min_health..=base_values.max_health)
647                + (3 * dungeon_floor);
648            return Enchantment::Health(health);
649        }
650        3 => {
651            let crit_hit_rate =
652                rng.gen_range(base_values.min_crit_hit_rate..=base_values.max_crit_hit_rate);
653            return Enchantment::CritHitRate(crit_hit_rate);
654        }
655        _ => Enchantment::Unknown,
656    }
657}
658
659pub fn generate_random_weapon(
660    rarity: ItemRarity,
661    base_values: WeaponBaseValues,
662    dungeon_floor: u32,
663    character_class: &CharacterClass,
664) -> WeaponItem {
665    let mut rng = thread_rng();
666    let damage =
667        rng.gen_range(base_values.min_damage..=base_values.max_damage) + (3 * dungeon_floor);
668    let crit_hit_rate =
669        rng.gen_range(base_values.min_crit_hit_rate..=base_values.max_crit_hit_rate);
670    let enchantments = generate_item_enchantments(
671        num_enchantments(&rarity),
672        ItemCategory::Weapon,
673        &ENCHANTMENT_BASE_VALUES,
674        dungeon_floor,
675    );
676    let item_info = match character_class {
677        CharacterClass::Mage => ITEM_STAFF,
678        CharacterClass::Cleric => ITEM_HALBERD,
679        CharacterClass::Assassin => ITEM_DAGGER,
680        CharacterClass::Warrior => ITEM_AXE,
681        CharacterClass::Knight => ITEM_SWORD,
682    };
683
684    WeaponItem::new(
685        item_info,
686        dungeon_floor,
687        rarity,
688        WeaponItemStats {
689            damage,
690            crit_hit_rate,
691        },
692        enchantments,
693    )
694}
695
696pub fn generate_random_armor(
697    rarity: ItemRarity,
698    base_values: ArmorBaseValues,
699    dungeon_floor: u32,
700) -> ArmorItem {
701    let mut rng = thread_rng();
702    let health =
703        rng.gen_range(base_values.min_health..=base_values.max_health) + (8 * dungeon_floor);
704    let defense =
705        rng.gen_range(base_values.min_defense..=base_values.max_defense) + (2 * dungeon_floor);
706    let enchantments = generate_item_enchantments(
707        num_enchantments(&rarity),
708        ItemCategory::Armor,
709        &ENCHANTMENT_BASE_VALUES,
710        dungeon_floor,
711    );
712
713    ArmorItem::new(
714        ITEM_ARMOR,
715        dungeon_floor,
716        rarity,
717        ArmorItemStats { health, defense },
718        enchantments,
719    )
720}
721
722pub fn generate_random_ring(
723    rarity: ItemRarity,
724    base_values: RingBaseValues,
725    dungeon_floor: u32,
726) -> RingItem {
727    let mut rng = thread_rng();
728    let mana = rng.gen_range(base_values.min_mana..=base_values.max_mana);
729    let enchantments = generate_item_enchantments(
730        num_enchantments(&rarity),
731        ItemCategory::Ring,
732        &ENCHANTMENT_BASE_VALUES,
733        dungeon_floor,
734    );
735
736    RingItem::new(
737        ITEM_RING,
738        dungeon_floor,
739        rarity,
740        RingItemStats { mana },
741        enchantments,
742    )
743}
744
745pub fn generate_random_consumable() -> ConsumableItem {
746    let mut rng = thread_rng();
747    let num = rng.gen_range(0..2);
748    let rarity = random_item_rarity(&ITEM_RARITY_DROP_RATES);
749
750    match num {
751        0 => ConsumableItem::new_health_potion(rarity),
752        1 => ConsumableItem::new_mana_potion(rarity),
753        _ => ConsumableItem::new_health_potion(rarity),
754    }
755}