# 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__)