Source code for tibiawikisql.models.creature

#  Copyright 2018 Allan Galarza
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

import re

from tibiawikisql import schema
from tibiawikisql.models import abc
from tibiawikisql.utils import clean_links, int_pattern, parse_boolean, parse_integer, parse_min_max

creature_loot_pattern = re.compile(r"\|{{Loot Item\|(?:([\d?+-]+)\|)?([^}|]+)")


def parse_maximum_integer(value):
    """
    From a string, finds the highest integer found.

    Parameters
    ----------
    value: :class:`str`
        The string containing integers.

    Returns
    -------
    :class:`int`, optional:
        The highest number found, or None if no number is found.
    """
    if value is None:
        return None
    matches = int_pattern.findall(value)
    try:
        return max(list(map(int, matches)))
    except ValueError:
        return None


def parse_loot(value):
    """
    Gets every item drop entry of a creature's drops.

    Parameters
    ----------
    value: :class:`str`
        A string containing item drops.

    Returns
    -------
    tuple:
        A tuple containing the amounts and the item name.
    """
    return creature_loot_pattern.findall(value)


def parse_monster_walks(value):
    """
    Matches the values against a regex to filter typos or bad data on the wiki.
    Element names followed by any character that is not a comma will be considered unknown and will not be returned.

    Examples\:
        - ``Poison?, fire`` will return ``fire``.
        - ``Poison?, fire.`` will return neither.
        - ``Poison, earth, fire?, [[ice]]`` will return ``poison,earth``.
        - ``No``, ``--``, ``>``, or ``None`` will return ``None``.

    Parameters
    ----------
    value: :class:`str`
        The string containing possible field types.

    Returns
    -------
    :class:`str`, optional
        A list of field types, separated by commas.
    """
    regex = re.compile(r"(physical)(,|$)|(holy)(,|$)|(death)(,|$)|(fire)(,|$)|(ice)(,|$)|(energy)(,|$)|(earth)(,|$)|"
                       r"(poison)(,|$)")
    content = ""
    for match in re.finditer(regex, value.lower().strip()):
        content += match.group()
    if not content:
        return None
    return content


[docs]class Creature(abc.Row, abc.Parseable, table=schema.Creature): """Represents a creature. Attributes ---------- article_id: :class:`int` The id of the containing article. title: :class:`str` The title of the containing article. timestamp: :class:`int` The last time the containing article was edited. article: :class:`str` The article that goes before the name when looking at the creature. name: :class:`str` The name of the creature, as displayed in-game. class: :class:`str` The creature's classification. type: :class:`str` The creature's type. bestiary_class: :class:`str` The creature's bestiary class, if applicable. bestiary_level: :class:`str` The creature's bestiary level, from 'Trivial' to 'Hard' bestiary_occurrence: :class:`str` The creature's bestiary occurrence, from 'Common' to 'Very Rare'. hitpoints: :class:`int` The creature's hitpoints, may be `None` if unknown. experience: :class:`int` Experience points yielded by the creature. Might be `None` if unknown. armor: :class:`int` The creature's armor value. speed: :class:`int` The creature's speed value. max_damage: :class:`int` The maximum amount of damage the creature can do in a single turn. summon_cost: :class:`int` The mana needed to summon this creature. 0 if not summonable. convince_cost: :class:`int` The mana needed to convince this creature. 0 if not convincible. illusionable: :class:`bool` Whether the creature can be illusioned into using `Creature Illusion`. pushable: :class:`bool` Whether the creature can be pushed or not. sees_invisible: :class:`bool` Whether the creature can see invisible players or not. paralysable: :class:`bool` Whether the creature can be paralyzed or not. boss: :class:`bool` Whether the creature is a boss or not. modifier_physical: :class:`int` The percentage of damage received of physical damage. ``None`` if unknown. modifier_earth: :class:`int` The percentage of damage received of earth damage. ``None`` if unknown. modifier_fire: :class:`int` The percentage of damage received of fire damage. ``None`` if unknown. modifier_energy: :class:`int` The percentage of damage received of energy damage. ``None`` if unknown. modifier_ice: :class:`int` The percentage of damage received of ice damage. ``None`` if unknown. modifier_death: :class:`int` The percentage of damage received of death damage. ``None`` if unknown. modifier_holy: :class:`int` The percentage of damage received of holy damage. ``None`` if unknown. modifier_drown: :class:`int` The percentage of damage received of drown damage. ``None`` if unknown. modifier_lifedrain: :class:`int` The percentage of damage received of life drain damage. ``None`` if unknown. abilities: :class:`str` A brief description of the creature's abilities. walks_through: :class:`str` The field types the creature will walk through, separated by commas. walks_around: :class:`str` The field types the creature will walk around, separated by commas. version: :class:`str` The client version where this creature was first implemented. image: :class:`bytes` The creature's image in bytes. loot: list of :class:`CreatureDrop` The items dropped by this creature. """ _map = { "article": ("article", lambda x: x), "name": ("name", lambda x: x), "actualname": ("name", lambda x: x), "creatureclass": ("class", lambda x: x), "bestiaryclass": ("bestiary_class", lambda x: x), "bestiarylevel": ("bestiary_level", lambda x: x), "occurrence": ("bestiary_occurrence", lambda x: x), "primarytype": ("type", lambda x: x), "hp": ("hitpoints", lambda x: parse_integer(x, None)), "exp": ("experience", lambda x: parse_integer(x, None)), "armor": ("armor", lambda x: parse_integer(x, None)), "speed": ("speed", lambda x: parse_integer(x, None)), "maxdmg": ("max_damage", parse_maximum_integer), "summon": ("summon_cost", parse_integer), "convince": ("convince_cost", parse_integer), "illusionable": ("illusionable", lambda x: parse_boolean(x, None)), "pushable": ("pushable", lambda x: parse_boolean(x, None)), "senseinvis": ("sees_invisible", lambda x: parse_boolean(x, None)), "paraimmune": ("paralysable", lambda x: parse_boolean(x, None, True)), "isboss": ("boss", parse_boolean), "physicalDmgMod": ("modifier_physical", parse_integer), "earthDmgMod": ("modifier_earth", parse_integer), "fireDmgMod": ("modifier_fire", parse_integer), "iceDmgMod": ("modifier_ice", parse_integer), "energyDmgMod": ("modifier_energy", parse_integer), "deathDmgMod": ("modifier_death", parse_integer), "holyDmgMod": ("modifier_holy", parse_integer), "drownDmgMod": ("modifier_drown", parse_integer), "hpDrainDmgMod": ("modifier_hpdrain", parse_integer), "abilities": ("abilities", clean_links), "walksthrough": ("walks_through", parse_monster_walks), "walksaround": ("walks_around", parse_monster_walks), "implemented": ("version", lambda x: x) } _pattern = re.compile(r"Infobox[\s_]Creature") __slots__ = ("article_id", "title", "timestamp", "raw_attribute", "article", "name", "class", "type", "bestiary_level", "bestiary_class", "bestiary_occurrence", "hitpoints", "experience", "armor", "speed", "max_damage", "summon_cost", "convince_cost", "illusionable", "pushable", "sees_invisible", "paralysable", "boss", "modifier_physical", "modifier_earth", "modifier_fire", "modifier_energy", "modifier_ice", "modifier_death", "modifier_holy", "modifier_lifedrain", "modifier_drown", "abilities", "walks_through", "walks_around", "version", "image", "loot") def __init__(self, **kwargs): super().__init__(**kwargs)
[docs] @classmethod def from_article(cls, article): """ Parses an article into a TibiaWiki model. This method is overridden to parse extra attributes like loot. Parameters ---------- article: :class:`api.Article` The article from where the model is parsed. Returns ------- :class:`Creature` The creature represented by the current article. """ creature = super().from_article(article) if creature is None: return None if "loot" in creature._raw_attributes: loot = parse_loot(creature._raw_attributes["loot"]) loot_items = [] for amounts, item in loot: if not amounts: _min, _max = 0, 1 else: _min, _max = parse_min_max(amounts) loot_items.append(CreatureDrop(creature_id=creature.article_id, item_title=item, min=_min, max=_max)) creature.loot = loot_items return creature
[docs] def insert(self, c): """ Inserts the current model into its respective database. This method is overridden to insert elements of child rows. Parameters ---------- c: :class:`sqlite3.Cursor`, :class:`sqlite3.Connection` A cursor or connection of the database. """ super().insert(c) for attribute in getattr(self, "loot", []): attribute.insert(c)
[docs] @classmethod def get_by_field(cls, c, field, value, use_like=False): creature = super().get_by_field(c, field, value, use_like) if creature is None: return None creature.loot = CreatureDrop.search(c, "creature_id", creature.article_id, sort_by="chance", ascending=False) return creature
[docs]class CreatureDrop(abc.Row, table=schema.CreatureDrop): """ Represents an item dropped by a creature. Attributes ---------- creature_id: :class:`int` The article id of the creature the drop belongs to. creature_title: :class:`str` The title of the creature that drops the item. item_id: :class:`int` The article id of the item. item_title: :class:`str` The title of the dropped item. min: :class:`int` The minimum possible amount of the dropped item. max: :class:`int` The maximum possible amount of the dropped item. chance: :class:`float` The chance percentage of getting this item dropped by this creature. """ __slots__ = ("creature_id", "creature_title", "item_id", "item_title", "min", "max", "chance") def __init__(self, **kwargs): super().__init__(**kwargs) self.item_title = kwargs.get("item_title") self.creature_title = kwargs.get("creature_title") def __repr__(self): attributes = [] for attr in self.__slots__: try: v = getattr(self, attr) if v is None: continue attributes.append("%s=%r" % (attr, v)) except AttributeError: pass return "{0.__class__.__name__}({1})".format(self, ",".join(attributes))
[docs] def insert(self, c): """Inserts the current model into its respective database. Overridden to insert using a subquery to get the item's id from the name. Parameters ---------- c: :class:`sqlite3.Cursor`, :class:`sqlite3.Connection` A cursor or connection of the database. """ if getattr(self, "item_id", None): super().insert(c) else: query = f"""INSERT INTO {self.table.__tablename__}(creature_id, item_id, min, max) VALUES(?, (SELECT article_id from item WHERE title = ?), ?, ?)""" c.execute(query, (self.creature_id, self.item_title, self.min, self.max))
@classmethod def _is_column(cls, name): return name in cls.__slots__ @classmethod def _get_base_query(cls): return """SELECT %s.*, item.title as item_title, creature.title as creature_title FROM %s LEFT JOIN creature ON creature.article_id = creature_id LEFT JOIN item ON item.article_id = item_id""" % (cls.table.__tablename__, cls.table.__tablename__)