underworld_core 0.6.3

Small library to either play a game or assist in generating rooms
Documentation
use rand::{prelude::ThreadRng, Rng};
use std::ops::RangeInclusive;
use uuid::Uuid;

use crate::components::{
    damage::AttackEffect,
    items::{Descriptor, Item, ItemType},
    Material, Tagged, {Attack, Defense},
};

use super::generator::Generator;

pub fn item_generator_for_level(
    item_type: &ItemType,
    is_equipped: bool,
    level: u32,
) -> impl Generator<Item> {
    ItemPrototype {
        item_type: *item_type,
        num_descriptors: 1..=2,
        materials: super::utils::materials::possible_materials(item_type),
        is_equipped,
        item_level: Some(level),
    }
}

pub struct ItemPrototype {
    pub item_type: ItemType,
    pub num_descriptors: RangeInclusive<usize>,
    pub materials: Vec<Material>,
    pub is_equipped: bool,
    pub item_level: Option<u32>,
}

impl Generator<Item> for ItemPrototype {
    fn generate(&self) -> Item {
        let mut rng = rand::thread_rng();
        let material = self.material();
        let descriptors = self.descriptors(&material);
        let attack = self.attack(&mut rng);
        let defense = self.defense(&mut rng);
        let tags = self.item_type.tags();

        Item {
            id: Uuid::new_v4(),
            name: None,
            item_type: self.item_type,
            tags,
            descriptors,
            material,
            attack,
            defense,
            consumable: None,
            throwable: None,
        }
    }
}

impl ItemPrototype {
    fn material(&self) -> Option<Material> {
        if self.materials.is_empty() {
            None
        } else {
            let mut rng = rand::thread_rng();
            let index = rng.gen_range(0..self.materials.len());
            self.materials.get(index).cloned()
        }
    }

    fn descriptors(&self, material: &Option<Material>) -> Vec<Descriptor> {
        let mut rng = rand::thread_rng();
        let num_descriptors: usize = rng.gen_range(self.num_descriptors.clone());

        let num_descriptor_range = 0..num_descriptors;

        if num_descriptor_range.is_empty() {
            return Vec::new();
        }

        let mut possible_descriptors: Vec<Descriptor> =
            super::utils::item_descriptors::possible_descriptors(
                &self.item_type,
                material,
                self.level(),
            );
        let descriptors = num_descriptor_range.filter_map(|_| {
            if possible_descriptors.is_empty() {
                None
            } else {
                let index = rng.gen_range(0..possible_descriptors.len());
                Some(possible_descriptors.remove(index))
            }
        });

        descriptors.into_iter().collect()
    }

    fn attack(&self, rng: &mut ThreadRng) -> Option<Attack> {
        let base_rolls = match self.item_type {
            ItemType::Buckler
            | ItemType::Dagger
            | ItemType::Dirk
            | ItemType::Shield
            | ItemType::ShortSword => 1,
            ItemType::Club
            | ItemType::Hammer
            | ItemType::LongSword
            | ItemType::Mace
            | ItemType::Morningstar
            | ItemType::Whip
            | ItemType::Spear
            | ItemType::Pike => 2,
            ItemType::GreatSword | ItemType::Halberd => 3,
            ItemType::Breastplate
            | ItemType::Boots
            | ItemType::BowlerHat
            | ItemType::Cloak
            | ItemType::Crown
            | ItemType::Fedora
            | ItemType::Flask
            | ItemType::Gloves
            | ItemType::Helm
            | ItemType::LoinCloth
            | ItemType::Mask
            | ItemType::PlateBoots
            | ItemType::PlateGauntlets
            | ItemType::PlateHelmet
            | ItemType::Pot
            | ItemType::Scroll
            | ItemType::Shirt
            | ItemType::Shackles
            | ItemType::TopHat
            | ItemType::Trousers
            | ItemType::Vest => return None,
        };

        let level = self.level();
        let (additional_rolls, modifier) = if level == 1 {
            (0, -1)
        } else if (2..=5).contains(&level) {
            (0, rng.gen_range(0..=2))
        } else if (6..=10).contains(&level) {
            (1, 0)
        } else if (11..=15).contains(&level) {
            (1, rng.gen_range(0..=2))
        } else if (16..=20).contains(&level) {
            (2, 0)
        } else if (21..=25).contains(&level) {
            (2, rng.gen_range(0..=2))
        } else if (26..=30).contains(&level) {
            (3, 0)
        } else if (31..=35).contains(&level) {
            (3, rng.gen_range(0..=2))
        } else if (36..=40).contains(&level) {
            (4, 0)
        } else if (41..=45).contains(&level) {
            (4, rng.gen_range(0..=2))
        } else if (46..=50).contains(&level) {
            (5, 1)
        } else if (51..=55).contains(&level) {
            (5, rng.gen_range(0..=2))
        } else {
            (7, 1)
        };

        let max_rolls = base_rolls + additional_rolls;
        let roll_range = base_rolls..=max_rolls;

        let mut rng = rand::thread_rng();
        let num_rolls = rng.gen_range(roll_range);

        let (num_effects, mut possible_effects) = if (1..=30).contains(&level) {
            (0, Vec::new())
        } else if (31..=50).contains(&level) {
            (
                rng.gen_range(0..=1),
                vec![AttackEffect::Crushing, AttackEffect::Sharp],
            )
        } else if (51..=65).contains(&level) {
            (
                rng.gen_range(0..=1),
                vec![
                    AttackEffect::Crushing,
                    AttackEffect::Sharp,
                    AttackEffect::Acidic,
                    AttackEffect::Toxic,
                ],
            )
        } else {
            (
                rng.gen_range(1..=3),
                vec![
                    AttackEffect::Crushing,
                    AttackEffect::Sharp,
                    AttackEffect::Acidic,
                    AttackEffect::Toxic,
                ],
            )
        };

        let effects: Vec<AttackEffect> = if num_effects > 0 {
            (0..num_effects)
                .map(|_| {
                    let index = rng.gen_range(0..possible_effects.len());
                    possible_effects.remove(index)
                })
                .collect()
        } else {
            Vec::new()
        };

        Some(Attack {
            num_rolls,
            modifier,
            effects,
        })
    }

    fn defense(&self, rng: &mut ThreadRng) -> Option<Defense> {
        let base_resistance = match self.item_type {
            ItemType::Boots
            | ItemType::Buckler
            | ItemType::Shield
            | ItemType::Vest
            | ItemType::Helm => 2,
            ItemType::Shirt
            | ItemType::Gloves
            | ItemType::Trousers
            | ItemType::Cloak
            | ItemType::LoinCloth
            | ItemType::Shackles
            | ItemType::BowlerHat
            | ItemType::Crown
            | ItemType::Fedora
            | ItemType::Mask
            | ItemType::TopHat => 0,
            ItemType::Breastplate
            | ItemType::PlateBoots
            | ItemType::PlateGauntlets
            | ItemType::PlateHelmet => 4,
            ItemType::Club
            | ItemType::Dagger
            | ItemType::Dirk
            | ItemType::Flask
            | ItemType::GreatSword
            | ItemType::Halberd
            | ItemType::Hammer
            | ItemType::LongSword
            | ItemType::Mace
            | ItemType::Morningstar
            | ItemType::Pike
            | ItemType::Pot
            | ItemType::Scroll
            | ItemType::ShortSword
            | ItemType::Spear
            | ItemType::Whip => return None,
        };

        let level = self.level();
        let level_modifier = if level == 1 {
            0
        } else if (2..=10).contains(&level) {
            rng.gen_range(1..=3)
        } else if (11..=20).contains(&level) {
            rng.gen_range(2..=4)
        } else if (21..=30).contains(&level) {
            rng.gen_range(4..=6)
        } else if (31..=40).contains(&level) {
            rng.gen_range(7..=9)
        } else {
            rng.gen_range(10..=25)
        };

        Some(Defense {
            damage_resistance: base_resistance + level_modifier,
        })
    }

    fn level(&self) -> u32 {
        self.item_level.unwrap_or(1)
    }
}