Exemplo n.º 1
0
class BuildMonster:
    def __init__(self, **kwargs):
        # ID number to process
        self.monster_id = kwargs["monster_id"]
        # Raw cache data for all monsters
        self.all_monster_cache_data = kwargs["all_monster_cache_data"]
        # Processed wikitext for all monsters
        self.all_wikitext_processed = kwargs["all_wikitext_processed"]
        # Raw data dump from OSRS Wiki
        self.all_wikitext_raw = kwargs["all_wikitext_raw"]
        # The existing monster database contents
        self.all_db_monsters = kwargs["all_db_monsters"]
        # The existing item database contents
        self.all_db_items = kwargs["all_db_items"]
        # A list of already known (processed) monsters
        self.known_monsters = kwargs["known_monsters"]
        # The monster schema
        self.schema_data = kwargs["schema_data"]
        # If the JSON should be exported/created
        self.export = kwargs["export"]

        # For this monster instance, create dictionary for property storage
        self.monster_dict = dict()

        # For this monster instance, create an empty drops object
        self.drops = dict()

        # The page name the wikitext is from
        self.wiki_page_name = None
        # The version used on the wikitext page
        self.infobox_version_number = None
        # Used if the item is special (invalid, normalized etc.)
        self.status = None

        # All properties that are available for all monsters
        self.properties = [
            "id",
            "name",
            "incomplete"
            "members",
            "release_date",
            "combat_level",
            "hitpoints",
            "max_hit",
            "attack_type",
            "attack_speed",
            "aggressive",
            "poisonous",
            "immune_poison",
            "immune_venon",
            "weakness",
            "slayer_monster",
            "slayer_level",
            "slayer_xp",
            "slayer_masters",
            "duplicate",
            "examine",
            "wiki_name",
            "wiki_url",
            "attack_level",
            "strength_level",
            "defence_level",
            "magic_level",
            "ranged_level",
            "attack_stab",
            "attack_slash",
            "attack_crush",
            "attack_magic",
            "attack_ranged",
            "defence_stab",
            "defence_slash",
            "defence_crush",
            "defence_magic",
            "defence_ranged",
            "attack_accuracy",
            "melee_strength",
            "ranged_strength",
            "magic_damage",
            "drops"]

    def generate_monster_object(self):
        """Generate the `MonsterProperties` object from the monster_dict dictionary."""
        self.monster_properties = MonsterProperties(**self.monster_dict)

    def compare_new_vs_old_monster(self):
        """Compare the newly generated monster to the existing monster in the database."""
        self.compare_json_files(self.monster_properties)

    def export_monster_to_json(self):
        """Export monster to JSON, if requested."""
        if self.export:
            output_dir = Path(config.DOCS_PATH, "monsters-json")
            self.monster_properties.export_json(True, output_dir)
        logging.debug(self.monster_dict)

    def check_duplicate_monster(self) -> MonsterProperties:
        """Determine if this is a duplicate monster.

        :return: A MonsterProperties object.
        """
        # Start by setting the duplicate property to False
        self.monster_dict["duplicate"] = False
        # Create an MonsterProperties object
        monster_properties = MonsterProperties(**self.monster_dict)

        # Set the monster properties that we want to compare
        correlation_properties = {
            "wiki_name": False,
            "combat_level": False,
            "members": False
        }

        # Loop the list of currently (already processed) monsters
        for known_monster in self.known_monsters:
            # Do a quick name check before deeper inspection
            if monster_properties.name != known_monster.name:
                continue

            # If the cache names are equal, do further inspection
            for cprop in correlation_properties:
                if getattr(monster_properties, cprop) == getattr(known_monster, cprop):
                    correlation_properties[cprop] = True

            # Check is all values in correlation properties are True
            correlation_result = all(value is True for value in correlation_properties.values())
            if correlation_result:
                self.monster_dict["duplicate"] = True

        return monster_properties

    def preprocessing(self):
        """Preprocess an monster, and set important object variables.

        This function preprocesses every monster dumped from the OSRS cache. Various
        properties are set to help further processing. MORE."""
        # Set monster ID variables
        self.monster_id_int = int(self.monster_id)  # Monster ID number as an integer
        self.monster_id_str = str(self.monster_id)  # Monster ID number as a string

        # Load monster dictionary of cache data based on monster ID
        # This raw cache data is the baseline information about the specific monster
        # and can be considered 100% correct and available for every monster
        self.monster_cache_data = self.all_monster_cache_data[self.monster_id_str]

        # Set monster name variable (directly from the cache dump)
        self.monster_name = self.monster_cache_data["name"]

        # Log and print monster
        logging.debug(f"======================= {self.monster_id_str} {self.monster_name}")
        # print(f"======================= {self.monster_id_str} {self.monster_name}")
        logging.debug(f"preprocessing: using the following cache data:")
        logging.debug(self.monster_cache_data)

        # Set all variables to None (for invalid monsters)
        self.monster_wikitext = None
        self.wikitext_found_using = None
        self.has_infobox = False

        # Try to find the wiki data using direct ID number search
        if self.all_wikitext_processed.get(self.monster_id_str, None):
            self.monster_wikitext = self.all_wikitext_processed.get(self.monster_id_str, None)
            self.wikitext_found_using = "id"

        # Try to find the wiki data using direct name search
        elif self.all_wikitext_raw.get(self.monster_name, None):
            self.monster_wikitext = self.all_wikitext_raw.get(self.monster_name, None)
            self.wikitext_found_using = "name"

        logging.debug(f"preprocessing: self.monster_wikitext found using: {self.wikitext_found_using}")

        # If there is no wikitext, and the monster is valid, raise a critical error
        if not self.monster_wikitext:
            logging.critical("CRITICAL: Could not find monster_wikitext by id or name...")
            return False

        # Parse the infobox monster
        infobox_parser = WikitextTemplateParser(self.monster_wikitext)

        # Try extract infobox for monster
        self.has_infobox = infobox_parser.extract_infobox("infobox monster")
        if not self.has_infobox:
            logging.critical("CRITICAL: Could not find template...")
            return False

        self.is_versioned = infobox_parser.determine_infobox_versions()
        logging.debug(f"preprocessing: Is the infobox versioned: {self.is_versioned}")
        self.versioned_ids = infobox_parser.extract_infobox_ids()
        logging.debug(f"preprocessing: Versioned IDs: {self.versioned_ids}")

        # Set the infobox version number, default to empty string (no version number)
        try:
            if self.versioned_ids:
                self.infobox_version_number = self.versioned_ids[self.monster_id_int]
        except KeyError:
            if self.is_versioned:
                self.infobox_version_number = "1"
            else:
                self.infobox_version_number = ""
        logging.debug(f"preprocessing: infobox_version_number: {self.infobox_version_number}")

        # Set the template
        self.template = infobox_parser.template

        return True

    def populate_monster(self):
        """Populate an monster after preprocessing it.

        This is called for every monster in the OSRS cache dump. Start by populating
        the raw metadata from the cache. Then ... MORE."""
        # Start by populating the monster from the cache data
        self.populate_from_cache_data()
        self.populate_monster_properties_from_wiki_data()

    def populate_from_cache_data(self):
        """Populate a monster using raw cache data.

        This function takes the raw OSRS cache data for the specific monster and loads
        all available properties (that are extracted from the cache)."""
        # Log, then populate cache properties
        logging.debug("populate_from_cache: Loading item cache data data to object...")
        self.monster_dict["id"] = self.monster_cache_data["id"]
        self.monster_dict["name"] = self.monster_cache_data["name"]
        self.monster_dict["combat_level"] = self.monster_cache_data["combatLevel"]
        self.monster_dict["size"] = self.monster_cache_data["size"]

    def populate_monster_properties_from_wiki_data(self):
        """Populate item data from a OSRS Wiki Infobox Item template."""
        logging.debug("populate_monster_properties_from_wiki_data: Populating wiki data...")

        # STAGE ONE: Determine then set the wiki_name and wiki_url

        # Manually set OSRS Wiki name
        if self.wikitext_found_using not in ["id"]:
            # Monster found in wiki by ID, cache name is the best option
            wiki_page_name = self.monster_name
        else:
            # Monster found using direct cache name lookup on wiki page names,
            # So use wiki page name in the monster_wikitext array
            wiki_page_name = self.monster_wikitext[0]

        wiki_versioned_name = None
        wiki_name = None

        # Get the versioned, or non-versioned, name from the infobox
        if self.infobox_version_number is not None:
            key = "version" + str(self.infobox_version_number)
            wiki_versioned_name = self.extract_infobox_value(self.template, key)
        else:
            wiki_versioned_name = self.extract_infobox_value(self.template, "version")

        # Set the wiki_name property
        if wiki_versioned_name is not None:
            if wiki_versioned_name.startswith("("):
                wiki_name = wiki_page_name + " " + wiki_versioned_name
            else:
                wiki_name = wiki_page_name + " (" + wiki_versioned_name + ")"
        else:
            wiki_name = wiki_page_name

        self.monster_dict["wiki_name"] = wiki_name

        # Set the wiki_url property
        if wiki_versioned_name is not None:
            wiki_url = wiki_page_name + "#" + wiki_versioned_name
        else:
            wiki_url = wiki_page_name

        wiki_url = wiki_url.replace(" ", "_")
        self.monster_dict["wiki_url"] = "https://oldschool.runescape.wiki/w/" + wiki_url

        # STAGE TWO: Extract, process and set monster properties from the infobox template

        # MEMBERS: Determine the members status of a monster
        members = None
        if self.infobox_version_number is not None:
            key = "members" + str(self.infobox_version_number)
            members = self.extract_infobox_value(self.template, key)
        if members is None:
            members = self.extract_infobox_value(self.template, "members")
        if members is not None:
            self.monster_dict["members"] = infobox_cleaner.clean_boolean(members)
        else:
            self.monster_dict["members"] = False

        # RELEASE_DATE: Determine the release date of a monster
        release_date = None
        if self.infobox_version_number is not None:
            key = "release" + str(self.infobox_version_number)
            release_date = self.extract_infobox_value(self.template, key)
        if release_date is None:
            release_date = self.extract_infobox_value(self.template, "release")
        if release_date is not None:
            self.monster_dict["release_date"] = infobox_cleaner.clean_release_date(release_date)
        else:
            self.monster_dict["release_date"] = None
            self.monster_dict["incomplete"] = True

        # HITPOINTS: Determine the hitpoints of a monster
        hitpoints = None
        if self.infobox_version_number is not None:
            key = "hitpoints" + str(self.infobox_version_number)
            hitpoints = self.extract_infobox_value(self.template, key)
        if hitpoints is None:
            hitpoints = self.extract_infobox_value(self.template, "hitpoints")
        if hitpoints is not None:
            self.monster_dict["hitpoints"] = infobox_cleaner.clean_integer(hitpoints)
        else:
            self.monster_dict["hitpoints"] = 0
            self.monster_dict["incomplete"] = True

        # MAX HIT: Determine the max hit of a monster
        max_hit = None
        if self.infobox_version_number is not None:
            key = "max hit" + str(self.infobox_version_number)
            max_hit = self.extract_infobox_value(self.template, key)
        if max_hit is None:
            max_hit = self.extract_infobox_value(self.template, "max hit")
        if max_hit is not None:
            self.monster_dict["max_hit"] = infobox_cleaner.clean_integer(max_hit)
        else:
            self.monster_dict["max_hit"] = 0
            self.monster_dict["incomplete"] = True

        # ATTACK TYPE: Determine the attack type of a monster
        attack_type = None
        if self.infobox_version_number is not None:
            key = "attack style" + str(self.infobox_version_number)
            attack_type = self.extract_infobox_value(self.template, key)
        if attack_type is None:
            attack_type = self.extract_infobox_value(self.template, "attack style")
        if attack_type is not None:
            self.monster_dict["attack_type"] = infobox_cleaner.clean_attack_type(attack_type)
        else:
            self.monster_dict["attack_type"] = list()
            self.monster_dict["incomplete"] = True

        # ATTACK SPEED: Determine the attack speed of a monster
        attack_speed = None
        if self.infobox_version_number is not None:
            key = "attack speed" + str(self.infobox_version_number)
            attack_speed = self.extract_infobox_value(self.template, key)
        if attack_speed is None:
            attack_speed = self.extract_infobox_value(self.template, "attack speed")
        if attack_speed is not None:
            self.monster_dict["attack_speed"] = infobox_cleaner.clean_integer(attack_speed)
        else:
            self.monster_dict["attack_speed"] = None
            self.monster_dict["incomplete"] = True

        # AGGRESSIVE: Determine the aggressive property of a monster
        aggressive = None
        if self.infobox_version_number is not None:
            key = "aggressive" + str(self.infobox_version_number)
            aggressive = self.extract_infobox_value(self.template, key)
        if aggressive is None:
            aggressive = self.extract_infobox_value(self.template, "aggressive")
        if aggressive is not None:
            self.monster_dict["aggressive"] = infobox_cleaner.clean_boolean(aggressive)
        else:
            self.monster_dict["aggressive"] = False
            self.monster_dict["incomplete"] = True

        # POISONOUS: Determine the poisonous property of a monster
        poisonous = None
        if self.infobox_version_number is not None:
            key = "poisonous" + str(self.infobox_version_number)
            poisonous = self.extract_infobox_value(self.template, key)
        if poisonous is None:
            poisonous = self.extract_infobox_value(self.template, "poisonous")
        if poisonous is not None:
            self.monster_dict["poisonous"] = infobox_cleaner.clean_boolean(poisonous)
        else:
            self.monster_dict["poisonous"] = False
            self.monster_dict["incomplete"] = True

        # IMMUNE POISON: Determine the immunity to poison property of a monster
        immune_poison = None
        if self.infobox_version_number is not None:
            key = "immunepoison" + str(self.infobox_version_number)
            immune_poison = self.extract_infobox_value(self.template, key)
        if immune_poison is None:
            immune_poison = self.extract_infobox_value(self.template, "immunepoison")
        if immune_poison is not None:
            self.monster_dict["immune_poison"] = infobox_cleaner.clean_boolean(immune_poison)
        else:
            self.monster_dict["immune_poison"] = False
            self.monster_dict["incomplete"] = True

        # IMMUNE VENOM: Determine the immunity to venom property of a monster
        immune_venom = None
        if self.infobox_version_number is not None:
            key = "immunevenom" + str(self.infobox_version_number)
            immune_venom = self.extract_infobox_value(self.template, key)
        if immune_venom is None:
            immune_venom = self.extract_infobox_value(self.template, "immunevenom")
        if immune_venom is not None:
            self.monster_dict["immune_venom"] = infobox_cleaner.clean_boolean(immune_venom)
        else:
            self.monster_dict["immune_venom"] = False
            self.monster_dict["incomplete"] = True

        # WEAKNESS: Determine any weaknesses of the monster
        weakness = None
        if self.infobox_version_number is not None:
            key = "weakness" + str(self.infobox_version_number)
            weakness = self.extract_infobox_value(self.template, key)
        if weakness is None:
            weakness = self.extract_infobox_value(self.template, "weakness")
        if weakness is not None:
            weakness = infobox_cleaner.clean_weaknesses(weakness)
            self.monster_dict["weakness"] = weakness
        else:
            self.monster_dict["weakness"] = list()
            self.monster_dict["incomplete"] = True

        # CATEGORY: Determine category of the monster
        category = None
        if self.infobox_version_number is not None:
            key = "cat" + str(self.infobox_version_number)
            category = self.extract_infobox_value(self.template, key)
        if category is None:
            category = self.extract_infobox_value(self.template, "cat")
        if category is not None:
            category = infobox_cleaner.clean_category(category)
            self.monster_dict["category"] = category
        else:
            self.monster_dict["category"] = list()
            self.monster_dict["incomplete"] = True

        # SLAYER LEVEL: Determine the slayer level required
        slayer_level = None
        if self.infobox_version_number is not None:
            key = "slaylvl" + str(self.infobox_version_number)
            slayer_level = self.extract_infobox_value(self.template, key)
        if slayer_level is None:
            slayer_level = self.extract_infobox_value(self.template, "slaylvl")
        if slayer_level is not None:
            self.monster_dict["slayer_level"] = infobox_cleaner.clean_integer(slayer_level)
        else:
            self.monster_dict["slayer_level"] = None
            self.monster_dict["incomplete"] = True

        # SLAYER XP: Determine XP given from slayer monster kill
        slayer_xp = None
        if self.infobox_version_number is not None:
            key = "slayxp" + str(self.infobox_version_number)
            slayer_xp = self.extract_infobox_value(self.template, key)
        if slayer_xp is None:
            slayer_xp = self.extract_infobox_value(self.template, "slayxp")
        if slayer_xp is not None:
            self.monster_dict["slayer_xp"] = infobox_cleaner.clean_float(slayer_xp)
        else:
            self.monster_dict["slayer_xp"] = None
            self.monster_dict["incomplete"] = True

        # Quick check for slayer XP versus slayer level
        # Fix None for slayer level, when a level is not required
        if self.monster_dict["slayer_xp"] is not None:
            # print("Here", self.monster_dict["slayer_xp"])
            if self.monster_dict["slayer_level"] is None:
                self.monster_dict["slayer_level"] = 1

        # SLAYER MONSTER: Determine if the monster can be a slayer task
        if self.monster_dict["slayer_level"] or self.monster_dict["slayer_xp"]:
            self.monster_dict["slayer_monster"] = True
        else:
            self.monster_dict["slayer_monster"] = False

        # SLAYER MASTERS: Determine the slayer masters
        if self.monster_dict["slayer_monster"]:
            slayer_masters = None
            if self.infobox_version_number is not None:
                key = "assignedby" + str(self.infobox_version_number)
                slayer_masters = self.extract_infobox_value(self.template, key)
            if slayer_masters is None:
                slayer_masters = self.extract_infobox_value(self.template, "assignedby")
            if slayer_masters is not None:
                slayer_masters = slayer_masters.strip()
                slayer_masters = slayer_masters.split(",")
                slayer_masters = [x.strip() for x in slayer_masters]
                if "steve" in slayer_masters:
                    slayer_masters.remove("steve")
                    if "nieve" not in slayer_masters:
                        slayer_masters.append("nieve")
                self.monster_dict["slayer_masters"] = slayer_masters
                if self.monster_dict["slayer_masters"][0] == "No":
                    self.monster_dict["slayer_masters"] = None
            else:
                self.monster_dict["slayer_masters"] = None
                self.monster_dict["incomplete"] = True

        # Set the slayer_masters property to an empty list if not populated
        try:
            if not self.monster_dict["slayer_masters"]:
                self.monster_dict["slayer_masters"] = list()
        except KeyError:
            self.monster_dict["slayer_masters"] = list()

        # EXAMINE: Determine the monster examine text
        examine = None
        if self.infobox_version_number is not None:
            key = "examine" + str(self.infobox_version_number)
            examine = self.extract_infobox_value(self.template, key)
        if examine is None:
            examine = self.extract_infobox_value(self.template, "examine")
        if examine is not None:
            self.monster_dict["examine"] = infobox_cleaner.clean_monster_examine(examine)
        else:
            self.monster_dict["examine"] = None
            self.monster_dict["incomplete"] = True

        # MONSTER COMBAT BONUSES: Determine stats of the monster

        # Initialize a dictionary that maps database_name -> infobox_name
        # The database_name is used in this project
        # The infobox_name is used by the OSRS Wiki
        combat_bonuses = {"attack_level": "att",
                          "strength_level": "str",
                          "defence_level": "def",
                          "magic_level": "mage",
                          "ranged_level": "range",
                          "attack_stab": "astab",
                          "attack_slash": "aslash",
                          "attack_crush": "acrush",
                          "attack_magic": "amagic",
                          "attack_ranged": "arange",
                          "defence_stab": "dstab",
                          "defence_slash": "dslash",
                          "defence_crush": "dcrush",
                          "defence_magic": "dmagic",
                          "defence_ranged": "drange",
                          "attack_accuracy": "attbns",
                          "melee_strength": "strbns",
                          "ranged_strength": "rngbns",
                          "magic_damage": "mbns"
                          }

        # Loop each of the combat bonuses and populate
        for database_name, property_name in combat_bonuses.items():
            value = None
            if self.infobox_version_number is not None:
                key = property_name + str(self.infobox_version_number)
                value = self.extract_infobox_value(self.template, key)
            if value is None:
                value = self.extract_infobox_value(self.template, property_name)
            if value is not None:
                self.monster_dict[database_name] = infobox_cleaner.clean_stats_value(value)
            else:
                self.monster_dict[database_name] = 0
                self.monster_dict["incomplete"] = True

        # We finished processing, set incomplete to false if not true
        if not self.monster_dict.get("incomplete"):
            self.monster_dict["incomplete"] = False

    def extract_dropsline_header(self, table_head_type: str) -> float:
        """Parse the dropstablehead template for variable drop rarity values.

        :param table_head_type: Specify a seed, or herb table head search.
        :return: A float of the drop rarity multiplier.
        """
        # Extract "dropstablehead" templates
        # This is used for extracting "herbbase" and "seedbase" values
        self.drops_templates = wikitext_parser.extract_wikitext_template(self.monster_wikitext, "dropstablehead")

        table_head_value = None

        # Loop the found "dropstablehead" templates
        for template in self.drops_templates:
            # Parse the template
            template_parser = WikitextTemplateParser(self.monster_wikitext)
            template_parser.template = template
            if "vardefine:" + table_head_type in template:
                # Example:
                # {{DropsTableHead{{#vardefine:herbbase|{{#expr:9/123/128}}}}}}
                table_head_value = template.split("#expr:")[1]
                table_head_value = table_head_value.replace("}", "")
                table_head_value = eval(table_head_value)
                return table_head_value

    def parse_monster_drops(self):
        """Extract monster drop information.

        This function parses the monsters wiki page (raw wikitext), and extracts
        any MediaWiki template with the name "dropsline". Each template is processed
        to determine the ID, name, quantity, rarity and and requirements of the
        specific drop.
        """
        # Extract "dropsline" templates
        self.drops_templates = wikitext_parser.extract_wikitext_template(self.monster_wikitext, "dropsline")

        drops_dict_all = dict()
        drops_list_ids = list()
        self.monster_dict["drops"] = list()

        # Loop the found "dropsline" templates
        for template in self.drops_templates:
            # Parse the template
            template_parser = WikitextTemplateParser(self.monster_wikitext)
            template_parser.template = template

            # Initialize a null value dictionary for each item drop
            drop_dict = {
                "id": None,
                "name": None,
                "members": None,
                "quantity": None,
                "rarity": None,
                "drop_requirements": None
            }

            # Extract the drop information...

            # Extract the item drop name
            name = template_parser.extract_infobox_value("Name")

            # Skip any drop line with classified drop table
            if name.lower() == "drop table":
                continue

            # Determine the drop item ID
            item_id = None
            found = False
            for item in self.all_db_items:
                if item.name == name or item.wiki_name == name:
                    found = True
                    item_id = item.id
                    break
            if not found:
                item_id = None

            # Extract the item drop quantity and if the drop is noted
            quantity = template_parser.extract_infobox_value("Quantity")
            noted = False
            if quantity:
                if "noted" in quantity.lower():
                    noted = True
            quantity = infobox_cleaner.clean_drop_quantity(quantity)

            # Extract, or determine, if the item is members-only
            members = False
            name_notes = template_parser.extract_infobox_value("Namenotes")
            if self.monster_dict["members"]:
                members = True
            elif item_id:
                if self.all_db_items[item_id].members:
                    members = True
            elif name_notes:
                if "{{m}}" in name_notes:
                    members = True

            # Extract the item drop rarity
            rarity = template_parser.extract_infobox_value("Rarity")
            base_value = None
            # If the item drop has a drop variable, fetch it
            if rarity:
                if "var:herbbase" in rarity:
                    base_value = self.extract_dropsline_header("herbbase")
                elif "var:seedbase" in rarity:
                    base_value = self.extract_dropsline_header("seedbase")
                elif "var:uht" in rarity:
                    base_value = self.extract_dropsline_header("uht")
                    if not base_value:
                        base_value = "(22.5/250)/16"  # Temp fix for Lizardman shaman
                elif "var:bolttipbase" in rarity:
                    base_value = self.extract_dropsline_header("uht")
                    if not base_value:
                        base_value = "(2/128)/40"  # Temp fix for Hydra
            rarity = infobox_cleaner.clean_drop_rarity(rarity, base_value)

            # Extract the rarity notes
            drop_requirements = template_parser.extract_infobox_value("Raritynotes")
            drop_requirements = infobox_cleaner.clean_drop_requirements(drop_requirements)

            # Populate the dictionary
            drop_dict = {
                "id": item_id,
                "name": name,
                "members": members,
                "quantity": quantity,
                "noted": noted,
                "rarity": rarity,
                "drop_requirements": drop_requirements
            }

            # Attach drops dict to the drops object for this monster
            if item_id:
                # self.drops[item_id] = drop_dict
                drops_list_ids.append(str(item_id))
                drops_dict_all[str(item_id)] = drop_dict

        # Handle any embedded drop tables
        if "talismandroptable2" in self.monster_wikitext[2].lower():
            items = drop_tables.talisman(self.monster_wikitext[2])
            for item, item_dict in items.items():
                drops_list_ids.append(str(item))
                drops_dict_all[str(item)] = item_dict
        if "catacombsdroptable" in self.monster_wikitext[2].lower():
            items = drop_tables.catacombs(self.monster_dict["name"],
                                          self.monster_dict["hitpoints"],
                                          self.monster_wikitext[2])
            for item, item_dict in items.items():
                drops_list_ids.append(str(item))
                drops_dict_all[str(item)] = item_dict
        if "herbdroptable2" in self.monster_wikitext[2].lower():
            items = drop_tables.herb(self.monster_dict["members"],
                                     self.monster_wikitext[2])
            for item, item_dict in items.items():
                drops_list_ids.append(str(item))
                drops_dict_all[str(item)] = item_dict
        if "usefulherbdroptable2" in self.monster_wikitext[2].lower():
            items = drop_tables.usefulherb(self.monster_wikitext[2])
            for item, item_dict in items.items():
                drops_list_ids.append(str(item))
                drops_dict_all[str(item)] = item_dict
        if "fixedallotmentseeddroptable2" in self.monster_wikitext[2].lower():
            items = drop_tables.fixedallotmentseed(self.monster_wikitext[2])
            for item, item_dict in items.items():
                drops_list_ids.append(str(item))
                drops_dict_all[str(item)] = item_dict
        if "dropsallotmenttable" in self.monster_wikitext[2].lower():
            items = drop_tables.fixedallotmentseed(self.monster_wikitext[2])
            for item, item_dict in items.items():
                drops_list_ids.append(str(item))
                drops_dict_all[str(item)] = item_dict
        if "treeherbseeddroptable2" in self.monster_wikitext[2].lower():
            items = drop_tables.treeseed(self.monster_wikitext[2])
            for item, item_dict in items.items():
                drops_list_ids.append(str(item))
                drops_dict_all[str(item)] = item_dict
        if "rareseeddroptable2" in self.monster_wikitext[2].lower():
            items = drop_tables.rareseed(self.monster_wikitext[2])
            for item, item_dict in items.items():
                drops_list_ids.append(str(item))
                drops_dict_all[str(item)] = item_dict
        if "variableallotmentseeddroptale2" in self.monster_wikitext[2].lower():
            items = drop_tables.variableallotmentseed(self.monster_wikitext[2])
            for item, item_dict in items.items():
                drops_list_ids.append(str(item))
                drops_dict_all[str(item)] = item_dict
        if "manyseeddroptable2" in self.monster_wikitext[2].lower():
            items = drop_tables.commonseed(self.monster_wikitext[2])
            for item, item_dict in items.items():
                drops_list_ids.append(str(item))
                drops_dict_all[str(item)] = item_dict
        if "hopsdroptable2" in self.monster_wikitext[2].lower():
            items = drop_tables.hopsseed(self.monster_wikitext[2])
            for item, item_dict in items.items():
                drops_list_ids.append(str(item))
                drops_dict_all[str(item)] = item_dict
        if "superiordroptable" in self.monster_wikitext[2].lower():
            items = drop_tables.superior(self.monster_dict["slayer_level"])
            for item, item_dict in items.items():
                drops_list_ids.append(str(item))
                drops_dict_all[str(item)] = item_dict
        if ("wildernessslayerdroptable" in self.monster_wikitext[2].lower() and
                "krystilia" in self.monster_dict["slayer_masters"]):
            items = drop_tables.wildernessslayer(self.monster_dict["name"],
                                                 self.monster_dict["combat_level"], self.monster_dict["hitpoints"],
                                                 self.monster_dict["slayer_level"])
            for item, item_dict in items.items():
                drops_list_ids.append(str(item))
                drops_dict_all[str(item)] = item_dict

        # if "mainraredroptable" in self.monster_wikitext[2].lower():
        #     items = drop_tables.raredroptable(self.monster_wikitext[2])
        #     for item, item_dict in items.items():
        #         drops_list_ids.append(str(item))
        #         drops_dict_all[str(item)] = item_dict
        # if "raredroptable" in self.monster_wikitext[2].lower():
        #     items = drop_tables.raredroptable(self.monster_wikitext[2])
        #     for item, item_dict in items.items():
        #         drops_list_ids.append(str(item))
        #         drops_dict_all[str(item)] = item_dict

        # Append all parsed drops to the drops array
        for item_id in drops_list_ids:
            self.monster_dict["drops"].append(drops_dict_all[item_id])

    def extract_infobox_value(self, template: mwparserfromhell.nodes.template.Template, key: str) -> str:
        """Helper method to extract a value from a template using a specified key.

        This helper method is a simple solution to repeatedly try to fetch a specific
        entry from a wiki text template (a mwparserfromhell template object).

        :param template: A mediawiki wiki text template.
        :param key: The key to query in the template.
        :return value: The extracted template value based on supplied key.
        """
        value = None
        try:
            value = template.get(key).value
            value = value.strip()
            return value
        except ValueError:
            return value

    def compare_json_files(self, monster_properties: MonsterProperties) -> bool:
        """Print the difference between this monster object, and the monster that exists in the database."""
        # Create JSON out object to compare
        current_json = monster_properties.construct_json()
        current_drops = current_json["drops"]
        del current_json["drops"]

        # Try get existing entry (KeyError means it doesn't exist - aka a new monster)
        try:
            existing_json = self.all_db_monsters[self.monster_id]
            existing_drops = existing_json["drops"]
            del existing_json["drops"]
        except KeyError:
            print(f">>> compare_json_files: NEW MONSTER: {monster_properties.id}")
            print(current_json)
            return

        # Quick check of eqaulity, return if properties and drops are the same
        if current_json == existing_json:
            if current_drops == existing_drops:
                return

        # Print a header for the changed monster
        logging.debug(f">>> compare_json_files: CHANGED MONSTER: {monster_properties.id}: {monster_properties.name}, {monster_properties.wiki_name}")
        print(f">>> compare_json_files: CHANGED MONSTER: {monster_properties.id}: {monster_properties.name}")

        # First check the base properties
        ddiff_props = DeepDiff(existing_json, current_json, ignore_order=True)
        if ddiff_props:
            print("  > CHANGED BASE PROPERTIES...")
            print(ddiff_props)

        ddiff_drops = DeepDiff(existing_drops, current_drops, ignore_order=True)
        if ddiff_drops:
            print("  > CHANGED DROP PROPERTIES...")
            print(ddiff_drops)

        return

    def validate_monster(self):
        """Use the schema-monsters.json file to validate the populated monster."""
        # Create JSON out object to validate
        current_json = self.monster_properties.construct_json()

        # Validate object with schema attached
        v = config.MyValidator(self.schema_data)
        v.validate(current_json)

        if not v:
            print(current_json["id"], "ERROR...")
            print(v.errors)
            quit()
class BuildMonster:
    def __init__(self, **kwargs):
        # ID number to process
        self.monster_id = kwargs["monster_id"]
        # Raw cache data for all monsters
        self.all_monster_cache_data = kwargs["all_monster_cache_data"]
        # The existing monster database contents
        self.all_db_monsters = kwargs["all_db_monsters"]
        # Raw data dump from OSRS Wiki
        self.all_wikitext_raw = kwargs["all_wikitext_raw"]
        # Processed wikitext for all monsters
        self.all_wikitext_processed = kwargs["all_wikitext_processed"]
        # Processed monster drops
        self.monsters_drops = kwargs["monsters_drops"]
        # The monster schema
        self.schema_data = kwargs["schema_data"]
        # A list of already known (processed) monsters
        self.known_monsters = kwargs["known_monsters"]
        # Specify verbosity
        self.verbose = kwargs["verbose"]

        # For this monster instance, create dictionary for property storage
        self.monster_dict = dict()
        # The page name the wikitext is from
        self.wiki_page_name = None
        # The version used on the wikitext page
        self.infobox_version_number = None
        # Used if the item is special (invalid, normalized etc.)
        self.status = None

    def preprocessing(self):
        """Preprocess an monster, and set important object variables.

        This function preprocesses every monster dumped from the OSRS cache. Various
        properties are set to help further processing. MORE."""
        # Set monster ID variables
        self.monster_id_int = int(
            self.monster_id)  # Monster ID number as an integer
        self.monster_id_str = str(
            self.monster_id)  # Monster ID number as a string

        # Load monster dictionary of cache data based on monster ID
        # This raw cache data is the baseline information about the specific monster
        # and can be considered 100% correct and available for every monster
        self.monster_cache_data = self.all_monster_cache_data[
            self.monster_id_str]

        # Set monster name variable (directly from the cache dump)
        self.monster_name = self.monster_cache_data["name"]

        # Log and print monster
        if self.verbose:
            print(
                f"======================= {self.monster_id_str} {self.monster_name}"
            )

        # Set all variables to None (for invalid monsters)
        self.monster_wikitext = None
        self.wikitext_found_using = None
        self.has_infobox = False

        # Try to find the wiki data using direct ID number search
        if self.all_wikitext_processed.get(self.monster_id_str, None):
            self.monster_wikitext = self.all_wikitext_processed.get(
                self.monster_id_str, None)
            self.wikitext_found_using = "id"

        # Try to find the wiki data using direct name search
        elif self.all_wikitext_raw.get(self.monster_name, None):
            self.monster_wikitext = self.all_wikitext_raw.get(
                self.monster_name, None)
            self.wikitext_found_using = "name"

        # If there is no wikitext, and the monster is valid, raise a critical error
        if not self.monster_wikitext:
            return False

        # Parse the infobox monster
        infobox_parser = WikitextTemplateParser(self.monster_wikitext)

        # Try extract infobox for monster
        self.has_infobox = infobox_parser.extract_infobox("infobox monster")
        if not self.has_infobox:
            return False

        self.is_versioned = infobox_parser.determine_infobox_versions()
        self.versioned_ids = infobox_parser.extract_infobox_ids()

        # Set the infobox version number, default to empty string (no version number)
        try:
            if self.versioned_ids:
                self.infobox_version_number = self.versioned_ids[
                    self.monster_id_int]
        except KeyError:
            if self.is_versioned:
                self.infobox_version_number = "1"
            else:
                self.infobox_version_number = ""

        # Set the template
        self.template = infobox_parser.template

        return True

    def populate_monster(self):
        """Populate a monster after preprocessing it.

        This is called for every monster in the OSRS cache dump that has a wiki page.
        Start by populating the raw metadata from the cache. Then use the wiki data
        to populate more properties.
        """
        self.populate_from_cache_data()
        self.populate_monster_properties_from_wiki_data()

    def populate_from_cache_data(self):
        """Populate a monster using raw cache data.

        This function takes the raw OSRS cache data for the specific monster and loads
        all available properties (that are extracted from the cache)."""
        # Log, then populate cache properties
        self.monster_dict["id"] = self.monster_cache_data["id"]
        self.monster_dict["name"] = self.monster_cache_data["name"]
        self.monster_dict["combat_level"] = self.monster_cache_data[
            "combatLevel"]
        self.monster_dict["size"] = self.monster_cache_data["size"]

    def populate_monster_properties_from_wiki_data(self):
        """Populate item data from a OSRS Wiki Infobox Item template."""
        # STAGE ONE: Determine then set the wiki_name and wiki_url

        # Manually set OSRS Wiki name
        if self.wikitext_found_using not in ["id"]:
            # Monster found in wiki by ID, cache name is the best option
            wiki_page_name = self.monster_name
        else:
            # Monster found using direct cache name lookup on wiki page names,
            # So use wiki page name in the monster_wikitext array
            wiki_page_name = self.monster_wikitext[0]

        wiki_versioned_name = None
        wiki_name = None

        # Get the versioned, or non-versioned, name from the infobox
        if self.infobox_version_number is not None:
            key = "version" + str(self.infobox_version_number)
            wiki_versioned_name = self.extract_infobox_value(
                self.template, key)
        else:
            wiki_versioned_name = self.extract_infobox_value(
                self.template, "version")

        # Set the wiki_name property
        if wiki_versioned_name is not None:
            if wiki_versioned_name.startswith("("):
                wiki_name = wiki_page_name + " " + wiki_versioned_name
            else:
                wiki_name = wiki_page_name + " (" + wiki_versioned_name + ")"
        else:
            wiki_name = wiki_page_name

        self.monster_dict["wiki_name"] = wiki_name

        # Set the wiki_url property
        if wiki_versioned_name is not None:
            wiki_url = wiki_page_name + "#" + wiki_versioned_name
        else:
            wiki_url = wiki_page_name

        wiki_url = wiki_url.replace(" ", "_")
        self.monster_dict[
            "wiki_url"] = "https://oldschool.runescape.wiki/w/" + wiki_url

        # STAGE TWO: Extract, process and set monster properties from the infobox template

        # Initialize a dictionary that maps proj_name -> prop_name
        # proj_name is used in this project
        # prop_name is used by the OSRS Wiki
        monster_properties = {
            "members": "members",
            "release_date": "release",
            "hitpoints": "hitpoints",
            "max_hit": "max hit",
            "attack_type": "attack style",
            "attack_speed": "attack speed",
            "aggressive": "aggressive",
            "poisonous": "poisonous",
            "venomous": "poisonous",
            "immune_poison": "immunepoison",
            "immune_venom": "immunevenom",
            "attributes": "attributes",
            "category": "cat",
            "slayer_level": "slaylvl",
            "slayer_xp": "slayxp",
            "examine": "examine"
        }

        # Loop each of the combat bonuses and populate
        for proj_name, prop_name in monster_properties.items():
            value = None
            if self.infobox_version_number is not None:
                key = prop_name + str(self.infobox_version_number)
                value = self.extract_infobox_value(self.template, key)

            if value is None:
                value = self.extract_infobox_value(self.template, prop_name)

            self.monster_dict[proj_name] = infobox_cleaner.caller(
                value, proj_name)

            if value is None:
                self.monster_dict["incomplete"] = True

        # Set slayer level to one, if slayer xp is given and
        # slayer level is None
        if self.monster_dict["slayer_xp"]:
            if self.monster_dict["slayer_level"] is None:
                self.monster_dict["slayer_level"] = 1

        # SLAYER MONSTER: Determine if the monster can be a slayer task
        if self.monster_dict["slayer_xp"]:
            self.monster_dict["slayer_monster"] = True
        else:
            self.monster_dict["slayer_monster"] = False

        # SLAYER MASTERS: Determine the slayer masters
        if self.monster_dict["slayer_monster"]:
            slayer_masters = None
            if self.infobox_version_number is not None:
                key = "assignedby" + str(self.infobox_version_number)
                slayer_masters = self.extract_infobox_value(self.template, key)
            if slayer_masters is None:
                slayer_masters = self.extract_infobox_value(
                    self.template, "assignedby")
            if slayer_masters is not None:
                self.monster_dict[
                    "slayer_masters"] = infobox_cleaner.slayer_masters(
                        slayer_masters)
            else:
                self.monster_dict["slayer_masters"] = list()
                self.monster_dict["incomplete"] = True
        else:
            self.monster_dict["slayer_masters"] = list()

        # MONSTER COMBAT BONUSES: Determine stats of the monster

        # Initialize a dictionary that maps database_name -> property_name
        # The database_name is used in this project
        # The property_name is used by the OSRS Wiki
        combat_bonuses = {
            "attack_level": "att",
            "strength_level": "str",
            "defence_level": "def",
            "magic_level": "mage",
            "ranged_level": "range",
            "attack_bonus": "attbns",
            "strength_bonus": "strbns",
            "attack_magic": "amagic",
            "magic_bonus": "mbns",
            "attack_ranged": "arange",
            "ranged_bonus": "rngbns",
            "defence_stab": "dstab",
            "defence_slash": "dslash",
            "defence_crush": "dcrush",
            "defence_magic": "dmagic",
            "defence_ranged": "drange",
        }

        # Loop each of the combat bonuses and populate
        for database_name, property_name in combat_bonuses.items():
            value = None
            if self.infobox_version_number is not None:
                key = property_name + str(self.infobox_version_number)
                value = self.extract_infobox_value(self.template, key)
            if value is None:
                value = self.extract_infobox_value(self.template,
                                                   property_name)
            if value is not None:
                self.monster_dict[database_name] = infobox_cleaner.stats(value)
            else:
                self.monster_dict[database_name] = 0
                self.monster_dict["incomplete"] = True

        # We finished processing, set incomplete to false if not true
        if not self.monster_dict.get("incomplete"):
            self.monster_dict["incomplete"] = False

    def extract_infobox_value(
            self, template: mwparserfromhell.nodes.template.Template,
            key: str) -> str:
        """Helper method to extract a value from a template using a specified key.

        This helper method is a simple solution to repeatedly try to fetch a specific
        entry from a wiki text template (a mwparserfromhell template object).

        :param template: A mediawiki wiki text template.
        :param key: The key to query in the template.
        :return value: The extracted template value based on supplied key.
        """
        value = None
        try:
            value = template.get(key).value
            value = value.strip()
            return value
        except ValueError:
            return value

    def check_duplicate_monster(self) -> MonsterProperties:
        """Determine if this is a duplicate monster.

        :return: A MonsterProperties object.
        """
        # Start by setting the duplicate property to False
        self.monster_dict["duplicate"] = False

        # Check/set last update
        last_update = self.all_db_monsters.get(self.monster_id, None)
        if last_update:
            self.monster_dict["last_updated"] = self.all_db_monsters[
                self.monster_id]["last_updated"]
        else:
            self.monster_dict["last_updated"] = datetime.now(
                timezone.utc).strftime("%Y-%m-%d")

        # Create an MonsterProperties object
        monster_properties = MonsterProperties(**self.monster_dict)

        # Set the monster properties that we want to compare
        correlation_properties = {
            "wiki_name": False,
            "combat_level": False,
            "members": False
        }

        # Loop the list of currently (already processed) monsters
        for known_monster in self.known_monsters:
            # Do a quick name check before deeper inspection
            if monster_properties.name != known_monster.name:
                continue

            # If the cache names are equal, do further inspection
            for cprop in correlation_properties:
                if getattr(monster_properties,
                           cprop) == getattr(known_monster, cprop):
                    correlation_properties[cprop] = True

            # Check is all values in correlation properties are True
            correlation_result = all(
                value is True for value in correlation_properties.values())
            if correlation_result:
                self.monster_dict["duplicate"] = True

        return monster_properties

    def populate_monster_drops(self):
        """Set the monster drops from preprocessed data."""
        try:
            self.monster_dict["drops"] = self.monsters_drops[self.monster_id]
        except KeyError:
            self.monster_dict["drops"] = []

    def compare_new_vs_old_monster(self):
        """Diff this monster and the monster that exists in the database."""
        # Create JSON out object to compare
        self.monster_properties = MonsterProperties(**self.monster_dict)
        current_json = self.monster_properties.construct_json()

        # Try get existing entry (KeyError means it doesn't exist - aka a new monster)
        try:
            existing_json = self.all_db_monsters[self.monster_id]
        except KeyError:
            print(
                f">>> compare_json_files: NEW MONSTER: {self.monster_properties.id}"
            )
            print(current_json)
            self.monster_dict["last_updated"] = datetime.now(
                timezone.utc).strftime("%Y-%m-%d")
            return

        # Quick check of eqaulity, return if properties and drops are the same
        if current_json == existing_json:
            self.monster_dict["last_updated"] = self.all_db_monsters[
                self.monster_id]["last_updated"]
            return

        # Print a header for the changed monster
        print(
            f">>> compare_json_files: CHANGED MONSTER: {self.monster_properties.id}: {self.monster_properties.name}"
        )

        # First check the base properties
        ddiff_props = DeepDiff(existing_json, current_json, ignore_order=True)
        if ddiff_props:
            print(ddiff_props)

        self.monster_dict["last_updated"] = datetime.now(
            timezone.utc).strftime("%Y-%m-%d")

    def export_monster_to_json(self):
        """Export monster to JSON, if requested."""
        self.monster_properties = MonsterProperties(**self.monster_dict)
        output_dir = Path(config.DOCS_PATH, "monsters-json")
        self.monster_properties.export_json(True, output_dir)

    def validate_monster(self):
        """Use the schema-monsters.json file to validate the populated monster."""
        # Create JSON out object to validate
        self.monster_properties = MonsterProperties(**self.monster_dict)
        current_json = self.monster_properties.construct_json()

        # Validate object with schema attached
        v = validator.MyValidator(self.schema_data)
        v.validate(current_json)

        # Print any validation errors
        if v.errors:
            print(v.errors)
            exit(1)

        assert v.validate(current_json)