cogs/tibiawiki.py
# Copyright 2019 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 datetime as dt
import io
import logging
import random
import re
import sqlite3
from collections import defaultdict
from contextlib import closing
from typing import Dict, List
import discord
import tibiawikisql
from discord.ext import commands
from tibiawikisql import models
from cogs import utils
from cogs.utils.converter import TibiaNumber
from nabbot import NabBot
from .utils import FIELD_VALUE_LIMIT, average_color, checks, config, join_list, split_params
from .utils.context import NabCtx
from .utils.database import wiki_db
from .utils.errors import CannotPaginate
from .utils.messages import split_message
from .utils.pages import Pages
from .utils.tibia import get_map_area, get_tibia_weekday
log = logging.getLogger("nabbot")
WIKI_CHARMS_ARTICLE = "Cyclopedia#List_of_Charms"
WIKI_ICON = "https://vignette.wikia.nocookie.net/tibia/images/b/bc/Wiki.png/revision/latest?path-prefix=en"
DIFFICULTIES = {
"Harmless": config.difficulty_off_emoji * 4,
"Trivial": config.difficulty_on_emoji + config.difficulty_off_emoji * 3,
"Easy": config.difficulty_on_emoji * 2 + config.difficulty_off_emoji * 2,
"Medium": config.difficulty_on_emoji * 3 + config.difficulty_off_emoji,
"Hard": config.difficulty_on_emoji * 4
}
OCCURRENCES = {
"Common": config.occurrence_on_emoji * 1 + config.occurrence_off_emoji * 3,
"Uncommon": config.occurrence_on_emoji * 2 + config.occurrence_off_emoji * 2,
"Rare": config.occurrence_on_emoji * 3 + config.occurrence_off_emoji * 1,
"Very Rare": config.occurrence_on_emoji * 4,
}
class TibiaWiki(commands.Cog, utils.CogUtils):
"""Commands that show information about Tibia, provided by TibiaWiki.
The information is read generated using [tibiawiki-sql](https://github.com/Galarzaa90/tibiawiki-sql)."""
def __init__(self, bot: NabBot):
self.bot = bot
def cog_unload(self):
log.info(f"{self.tag} Unloading cog")
# region Commands
@checks.can_embed()
@commands.command(aliases=["achiev"])
async def achievement(self, ctx: NabCtx, *, name: str):
"""Displays an achievement's information.
Shows the achievement's grade, points, description, and instructions on how to unlock."""
entries = self.search_entry("achievement", name)
if not entries:
await ctx.send("I couldn't find an achievement with that name.")
return
if len(entries) > 1:
title = await ctx.choose([e["title"] for e in entries])
if title is None:
return
else:
title = entries[0]["title"]
achievement: models.Achievement = self.get_entry(title, models.Achievement)
embed = TibiaWiki.get_base_embed(achievement)
embed.description = achievement.description
embed.add_field(name="Grade", value="ā" * int(achievement.grade))
embed.add_field(name="Points", value=achievement.points)
embed.add_field(name="Spoiler", value=f"||{achievement.spoiler}||", inline=True)
await ctx.send(embed=embed)
@checks.can_embed()
@commands.command(usage="[class]")
async def bestiary(self, ctx: NabCtx, *, _class: str = None):
"""Displays a category's creatures or all the categories.
If a category is specified, it will list all the creatures that belong to the category and their level.
If no category is specified, it will list all the bestiary categories."""
if _class is None:
categories = self.get_bestiary_classes()
entries = [f"**{name}** - {count} creatures" for name, count in categories.items()]
description = ""
title = "Bestiary Classes"
else:
creatures = self.get_bestiary_creatures(_class)
if not creatures:
await ctx.error("There's no class with that name.")
return
entries = [f"**{name}** - {level}" for name, level in creatures.items()]
description = f"Use `{ctx.clean_prefix} monster <name>` to see more info"
title = f"Creatures in the {_class.title()} class"
pages = Pages(ctx, entries=entries, per_page=20 if await ctx.is_long() else 10, header=description)
pages.embed.title = title
pages.embed.set_author(name="TibiaWiki", icon_url=WIKI_ICON, url=tibiawikisql.api.BASE_URL)
pages.embed.url = "https://tibia.fandom.com/wiki/Bestiary_Creature_Classes"
try:
await pages.paginate()
except CannotPaginate as e:
await ctx.send(e)
@checks.can_embed()
@commands.command(aliases=["charms"])
async def charm(self, ctx: NabCtx, name: str = None):
"""Displays information about a charm.
If no name is specified, displays a list of all charms for the user to choose from."""
if name is None:
embed = self.get_charms_embed(ctx)
return await ctx.send(embed=embed)
charm = models.Charm.get_by_field(wiki_db, "name", name, True)
if charm is None:
embed = self.get_charms_embed(ctx)
return await ctx.error("There's no charm with that name, try one of these:", embed=embed)
embed = await self.get_charm_embed(charm)
await self.send_embed_with_image(charm, ctx, embed, True, extension="png")
@checks.can_embed()
@commands.command(aliases=["imbue"], usage="<name>[,price1[,price2[,price3]]][,tokenprice]")
async def imbuement(self, ctx: NabCtx, *, params: str):
"""Displays information about an imbuement.
You can optionally provide prices for the materials, in the order of the tier they belong to.
Additionally, for Vampirism, Void and Strike imbuements, you can provide the price for gold tokens.
The total cost will be calculated, as well as the hourly cost.
If applicable, it will show the cheapest way to get it using gold tokens.
It can also accept prices using the 'k' suffix, e.g. 1.5k
"""
params = split_params(params)
if len(params) > 5:
await ctx.send(f"{ctx.tick(False)} Invalid syntax. The correct syntax is: `{ctx.usage}`.")
return
try:
prices = [TibiaNumber(p) for p in params[1:]]
except commands.BadArgument:
await ctx.send(f"{ctx.tick(False)} Invalid syntax. The correct syntax is: `{ctx.usage}`.")
return
name = params[0]
entries = self.search_entry("imbuement", name)
if not entries:
await ctx.send("I couldn't find an imbuement with that name.")
return
if len(entries) > 1:
title = await ctx.choose([e["title"] for e in entries])
if title is None:
return
else:
title = entries[0]["title"]
imbuement: models.Imbuement = self.get_entry(title, models.Imbuement)
embed = self.get_imbuement_embed(ctx, imbuement, prices)
await self.send_embed_with_image(imbuement, ctx, embed, True)
@checks.can_embed()
@commands.command(aliases=["itemprice"])
async def item(self, ctx: NabCtx, *, name: str):
"""Displays information about an item.
Shows who buys and sells the item, what creatures drops it and many attributes.
The embed is colored if a major loot NPC buys it, so it can be noted at quick glance.
Yellow for Rashid, Blue and Green for Djinns and Purple for gems.
More information is shown if used in private messages or in the command channel."""
entries = self.search_entry("item", name)
if not entries:
await ctx.send("I couldn't find an item with that name.")
return
if len(entries) > 1:
title = await ctx.choose([e["title"] for e in entries])
if title is None:
return
else:
title = entries[0]["title"]
item: models.Item = self.get_entry(title, models.Item)
embed = await self.get_item_embed(ctx, item, await ctx.is_long())
await self.send_embed_with_image(item, ctx, embed)
@checks.can_embed()
@commands.group(invoke_without_command=True, case_insensitive=True)
async def key(self, ctx: NabCtx, number: str):
"""Displays information about a key.
Shows the key's known names, how to obtain it and its uses."""
if number is None:
await ctx.send("Tell me the number of the key you want to check.")
return
try:
number = int(number)
except ValueError:
await ctx.send("Tell me a numeric value, to search keys, try: `/key search`")
return
key: models.Key = models.Key.get_by_field(wiki_db, "number", number)
if not key:
return await ctx.send("There's no key with that number.")
embed = self.get_key_embed(key)
# Attach key's image only if the bot has permissions
if key.item_id:
item = models.Item.get_by_field(wiki_db, "article_id", key.item_id)
return await self.send_embed_with_image(item, ctx, embed, True)
await ctx.send(embed=embed)
@checks.can_embed()
@key.command(name="search")
async def key_search(self, ctx: NabCtx, *, term: str):
"""Searches for a key by keywords.
Search for matches on the key's names, location, origin or uses.
if there are multiple matches, a list is shown.
If only one matches, the key's information is shwon directly."""
keys = self.search_key(term)
if keys is None:
await ctx.send("I couldn't find any related keys.")
return
if len(keys) > 1:
embed = discord.Embed(title="Possible keys")
embed.set_author(name="TibiaWiki", url=tibiawikisql.api.BASE_URL, icon_url=WIKI_ICON)
embed.description = ""
for key in keys:
name = f" - {key['name']}" if key["name"] else ""
embed.description += f"\n**Key {key['number']}**{name}"
await ctx.send(embed=embed)
return
await ctx.invoke(self.bot.all_commands.get('key'), keys[0]["number"])
@checks.can_embed()
@commands.command(aliases=['mob', 'creature'])
async def monster(self, ctx: NabCtx, *, name: str):
"""Displays information about a monster.
Shows the monster's attributes, resistances, loot and more.
More information is displayed if used on a private message or in the command channel."""
if name is None:
await ctx.send("Tell me the name of the monster you want to search.")
return
if ctx.is_private:
bot_member = self.bot.user
else:
bot_member = self.bot.get_member(self.bot.user.id, ctx.guild)
if name.lower() == bot_member.display_name.lower():
await ctx.send(random.choice(["**" + bot_member.display_name + "** is too strong for you to hunt!",
"Sure, you kill *one* child and suddenly you're a monster!",
"I'M NOT A MONSTER",
"I'm a monster, huh? I'll remember that, human...š„",
"You misspelled *future ruler of the world*.",
"You're not a good person. You know that, right?",
"I guess we both know that isn't going to happen.",
"You can't hunt me.",
"That's funny... If only I was programmed to laugh."]))
return
entries = self.search_entry("creature", name)
if not entries:
await ctx.send("I couldn't find a monster with that name.")
return
if len(entries) > 1:
title = await ctx.choose([e["title"] for e in entries])
if title is None:
return
else:
title = entries[0]["title"]
monster = self.get_entry(title, models.Creature)
embed = await self.get_monster_embed(ctx, monster, await ctx.is_long())
await self.send_embed_with_image(monster, ctx, embed, True)
@checks.can_embed()
@commands.command()
async def npc(self, ctx: NabCtx, *, name: str):
"""Displays information about a NPC.
Shows the NPC's item offers, their location and their travel destinations.
More information is displayed if used on private messages or the command channel."""
entries = self.search_entry("npc", name)
if not entries:
await ctx.send("I couldn't find an NPC with that name.")
return
if len(entries) > 1:
title = await ctx.choose([e["title"] for e in entries])
if title is None:
return
else:
title = entries[0]["title"]
npc: models.Npc = self.get_entry(title, models.Npc)
embed = await self.get_npc_embed(ctx, npc, await ctx.is_long())
# Attach spell's image only if the bot has permissions
if ctx.bot_permissions.attach_files:
files = []
if npc.image is not None:
thumbnail = io.BytesIO(npc.image)
filename = re.sub(r"[^A-Za-z0-9]", "", npc.name) + ".gif"
embed.set_thumbnail(url=f"attachment://{filename}")
files.append(discord.File(thumbnail, filename))
if None not in [npc.x, npc.y, npc.z]:
map_filename = re.sub(r"[^A-Za-z0-9]", "", npc.name) + "-map.png"
map_image = io.BytesIO(get_map_area(npc.x, npc.y, npc.z))
embed.set_image(url=f"attachment://{map_filename}")
embed.add_field(name="Location", value=f"[Mapper link]({self.get_mapper_link(npc.x, npc.y, npc.z)})",
inline=False)
files.append(discord.File(map_image, map_filename))
await ctx.send(files=files, embed=embed)
else:
await ctx.send(embed=embed)
@checks.can_embed()
@commands.command()
async def rashid(self, ctx: NabCtx):
"""Shows where Rashid is today.
For more information, use `npc Rashid`."""
rashid = self.get_rashid_position()
npc = models.Npc.get_by_field(wiki_db, "name", "Rashid")
embed = TibiaWiki.get_base_embed(npc)
embed.colour = discord.Colour.greyple()
embed.description = f"Rashid is in **{rashid.city}** today."
embed.set_footer(text=rashid.location)
if ctx.bot_permissions.attach_files:
files = []
if npc.image is not None:
thumbnail = io.BytesIO(npc.image)
filename = re.sub(r"[^A-Za-z0-9]", "", npc.name) + ".gif"
embed.set_thumbnail(url=f"attachment://{filename}")
files.append(discord.File(thumbnail, filename))
if None not in [rashid.x, rashid.y, rashid.z]:
map_filename = re.sub(r"[^A-Za-z0-9]", "", npc.name) + "-map.png"
map_image = io.BytesIO(get_map_area(rashid.x, rashid.y, rashid.z))
embed.set_image(url=f"attachment://{map_filename}")
embed.add_field(name="Location", value=f"[Mapper link]"
f"({self.get_mapper_link(rashid.x,rashid.y,rashid.z)})",
inline=False)
files.append(discord.File(map_image, map_filename))
return await ctx.send(files=files, embed=embed)
await ctx.send(embed=embed)
@checks.can_embed()
@commands.command(usage="<name|words>")
async def spell(self, ctx: NabCtx, *, name: str):
"""Displays information about a spell.
Shows the spell's attributes, NPCs that teach it and more.
More information is displayed if used on private messages or the command channel."""
spell = models.Spell.get_by_field(wiki_db, "title", name, True)
if spell is None:
spell = models.Spell.get_by_field(wiki_db, "words", name, True)
if spell is None:
entries = self.search_entry("spell", name, additional_field="words")
if not entries:
await ctx.send("I couldn't find a spell with that name or words.")
return
if len(entries) > 1:
titles = ["{title} ({words})".format(**e) for e in entries]
title = await ctx.choose(titles)
if title is None:
return
title = entries[titles.index(title)]["title"]
else:
title = entries[0]["title"]
spell: models.Spell = self.get_entry(title, models.Spell)
embed = await self.get_spell_embed(ctx, spell, await ctx.is_long())
await self.send_embed_with_image(spell, ctx, embed)
@checks.can_embed()
@commands.command(aliases=["wikiinfo"])
async def wikistats(self, ctx: NabCtx):
"""Shows information about the TibiaWiki database."""
embed = discord.Embed(colour=discord.Colour.blurple(), title="TibiaWiki database statistics", description="")
embed.set_thumbnail(url=WIKI_ICON)
version = ""
gen_date = None
with closing(wiki_db.cursor()) as c:
info = c.execute("SELECT * FROM database_info").fetchall()
for entry in info:
if entry['key'] == "version":
version = f" v{entry['value']}"
if entry['key'] == "timestamp":
gen_date = float(entry['value'])
nb_space = '\u00a0'
embed.description += f"**ā£ Achievements:** {self.count_table('achievement'):,}"
embed.description += f"\n**ā£ Charms:** {self.count_table('charm'):,}"
embed.description += f"\n**ā£ Creatures:** {self.count_table('creature'):,}"
embed.description += f"\n**{nb_space*8}ā£ Drops:** {self.count_table('creature_drop'):,}"
embed.description += f"\n**ā£ Houses:** {self.count_table('house'):,}"
embed.description += f"\n**ā£ Imbuements:** {self.count_table('imbuement'):,}"
embed.description += f"\n**ā£ Items:** {self.count_table('item'):,}"
embed.description += f"\n**{nb_space*8}ā£ Attributes:** {self.count_table('item_attribute'):,}"
embed.description += f"\n**ā£ Keys:** {self.count_table('item_key'):,}"
embed.description += f"\n**ā£ NPCs:** {self.count_table('npc'):,}"
embed.description += f"\n**{nb_space*8}ā£ Buy offers:** {self.count_table('npc_offer_buy'):,}"
embed.description += f"\n**{nb_space*8}ā£ Sell offers:** {self.count_table('npc_offer_sell'):,}"
embed.description += f"\n**{nb_space*8}ā£ Destinations:** {self.count_table('npc_destination'):,}"
embed.description += f"\n**{nb_space*8}ā£ Spell offers:** {self.count_table('npc_spell'):,}"
embed.description += f"\n**ā£ Quests:** {self.count_table('quest'):,}"
embed.description += f"\n**ā£ Spells:** {self.count_table('spell'):,}"
embed.set_footer(text=f"Database generation date")
embed.timestamp = dt.datetime.utcfromtimestamp(gen_date)
embed.set_author(name=f"tibiawiki-sql{version}", icon_url="https://github.com/fluidicon.png",
url="https://github.com/Galarzaa90/tibiawiki-sql")
await ctx.send(embed=embed)
# endregion
# region Helper Methods
@classmethod
def count_table(cls, table):
try:
c = wiki_db.execute("SELECT COUNT(*) as count FROM %s" % table)
result = c.fetchone()
if not result:
return 0
return int(result["count"])
except sqlite3.OperationalError:
return 0
@classmethod
def get_charms_embed(cls, ctx: NabCtx):
charms = models.Charm.search(wiki_db, sort_by="type")
charms_url = f"{tibiawikisql.api.BASE_URL}/wiki/{WIKI_CHARMS_ARTICLE}"
embed = discord.Embed(title="Charms", url=charms_url)
embed.set_author(name="TibiaWiki", url=tibiawikisql.api.BASE_URL, icon_url=WIKI_ICON)
charm_fields = dict()
for charm in charms: # type: models.Charm
if not charm_fields.get(charm.type):
charm_fields[charm.type] = ""
charm_fields[charm.type] += f"\n**{charm.name}** - {charm.points:,}"
for _type, content in charm_fields.items():
embed.add_field(name=_type, value=content.strip())
embed.set_footer(text=f"Use {ctx.clean_prefix}{ctx.invoked_with} <name> to see more information.")
return embed
@classmethod
async def send_embed_with_image(cls, entity, ctx, embed, apply_color=False, extension="gif"):
if ctx.bot_permissions.attach_files and entity.image:
thumbnail = io.BytesIO(entity.image)
filename = f"thumbnail.{extension}"
embed.set_thumbnail(url=f"attachment://{filename}")
if apply_color:
main_color = await ctx.execute_async(average_color, entity.image)
embed.color = discord.Color.from_rgb(*main_color)
await ctx.send(file=discord.File(thumbnail, f"{filename}"), embed=embed)
else:
await ctx.send(embed=embed)
@classmethod
async def get_charm_embed(cls, charm: models.Charm):
charms_url = f"{tibiawikisql.api.BASE_URL}/wiki/{WIKI_CHARMS_ARTICLE}"
embed = discord.Embed(title=charm.name, url=charms_url)
embed.set_author(name="TibiaWiki", url=charms_url, icon_url=WIKI_ICON)
embed.description = f"**Type**: {charm.type} | **Cost**: {charm.points:,} points"
embed.add_field(name="Description", value=charm.description)
return embed
@classmethod
def get_base_embed(cls, article, alternate_title="") -> discord.Embed:
""" Builds the base embed for TibiaWiki articles.
:param article: The article to display, must be a subclass of Article.
:param alternate_title: Alternate title to display instead of the article's title.
:return: The embed object.
"""
embed = discord.Embed(title=alternate_title if alternate_title else article.title, url=article.url)
embed.set_author(name="TibiaWiki", icon_url=WIKI_ICON, url=tibiawikisql.api.BASE_URL)
return embed
@classmethod
def get_bestiary_classes(cls) -> Dict[str, int]:
"""Gets all the bestiary classes
:return: The classes and how many creatures it has.
"""
rows = wiki_db.execute("SELECT DISTINCT bestiary_class, count(*) as count "
"FROM creature WHERE bestiary_class not NUll "
"GROUP BY bestiary_class ORDER BY bestiary_class")
classes = {}
for r in rows:
classes[r["bestiary_class"]] = r["count"]
return classes
@classmethod
def get_bestiary_creatures(cls, _class: str) -> Dict[str, str]:
"""Gets the creatures that belong to a bestiary class
:param _class: The name of the class.
:return: The creatures in the class, with their difficulty level.
"""
rows = wiki_db.execute("""
SELECT title, bestiary_level
FROM creature
WHERE bestiary_class LIKE ?
ORDER BY
CASE bestiary_level
WHEN "Trivial" THEN 0
WHEN "Easy" THEN 1
WHEN "Medium" THEN 2
WHEN "Hard" THEN 3
END
""", (_class,))
creatures = {}
for r in rows:
creatures[r["title"]] = r["bestiary_level"]
return creatures
@classmethod
def get_key_embed(cls, key: models.Key):
if key is None:
return None
embed = cls.get_base_embed(key)
if key.name:
embed.description = f"**Also known as:** {key.name}"
if key.location:
embed.add_field(name="Location", value=key.location)
if key.origin is not None:
embed.add_field(name="Origin", value=key.origin)
if key.name is not None:
embed.add_field(name="Notes/Use", value=key.notes)
return embed
@classmethod
def get_imbuement_embed(cls, ctx: NabCtx, imbuement: models.Imbuement, prices):
"""Gets the item embed to show in /item command"""
embed = cls.get_base_embed(imbuement)
embed.add_field(name="Effect", value=imbuement.effect)
if not prices:
embed.set_footer(text=f"Provide material prices to calculate costs."
f" More info: {ctx.clean_prefix}help {ctx.invoked_with}")
elif len(prices) < len(imbuement.materials):
embed.set_footer(text="Not enough material prices provided for this tier.")
prices = []
materials = cls.get_imbuement_embed_parse_materials(imbuement, prices)
if not prices:
embed.add_field(name="Materials", value=materials)
return embed
fees = [5000, 25000, 100000] # Gold fees for each tier
fees_100 = [15000, 55000, 150000] # Gold fees for each tier with 100% chance
tiers = {"Basic": 0, "Intricate": 1, "Powerful": 2} # Tiers order
tokens = [2, 4, 6] # Token cost for materials of each tier
tier = tiers[imbuement.tier] # Current tier
token_imbuements = ["Vampirism", "Void", "Strike"] # Imbuements that can be bought with gold tokens
tier_prices = [] # The total materials cost for each tier
materials_cost = 0 # The cost of all materials for the current tier
for m, p in zip(imbuement.materials, prices):
materials_cost += m.amount * p
tier_prices.append(materials_cost)
def parse_prices(_tier: int, _materials: int):
return f"**Materials:** {_materials:,} gold.\n" \
f"**Total:** {_materials+fees[_tier]:,} gold | " \
f"{(_materials+fees[_tier])/20:,.0f} gold/hour\n" \
f"**Total (100% chance):** {_materials+fees_100[_tier]:,} gold | " \
f"{(_materials+fees_100[_tier])/20:,.0f} gold/hour"
# If no gold token price was provided or the imbuement type is not applicable, just show material cost
if len(prices)-1 <= tier or imbuement.type not in token_imbuements:
embed.add_field(name="Materials", value=materials)
embed.add_field(name="Cost", value=parse_prices(tier, materials_cost), inline=False)
if imbuement.type in token_imbuements:
embed.set_footer(text="Add gold token price at the end to find the cheapest option.")
return embed
token_price = prices[tier+1] # Gold token's price
possible_tokens = "2" if tokens[tier] == 2 else f"2-{tokens[tier]}"
embed.add_field(name="Materials", value=f"{materials}\nāāāāāā\n"
f"{possible_tokens} Gold Tokens ({token_price:,} gold each)")
token_cost = 0 # The total cost of the part that will be bought with tokens
cheapeast_tier = -1 # The tier which materials are more expensive than gold tokens.
for i in range(tier+1):
_token_cost = token_price*tokens[i]
if _token_cost < tier_prices[i]:
token_cost = _token_cost
cheapeast_tier = i
# Using gold tokens is never cheaper.
if cheapeast_tier == -1:
embed.add_field(name="Cost", value=f"Getting the materials is cheaper.\n\n"
f"{parse_prices(tier, materials_cost)}",
inline=False)
# Buying everything with gold tokens is cheaper
elif cheapeast_tier == tier:
embed.add_field(name="Cost", value=f"Getting all materials with gold tokens is cheaper.\n\n"
f"{parse_prices(tier, token_cost)}",
inline=False)
else:
total_cost = token_cost+tier_prices[cheapeast_tier+1]-tier_prices[cheapeast_tier]
embed.add_field(name="Cost", value=f"Getting the materials for **{list(tiers.keys())[cheapeast_tier]} "
f"{imbuement.type}** with gold tokens and buying the rest is "
f"cheaper.\n\n{parse_prices(tier, total_cost)}",
inline=False)
return embed
@classmethod
def get_imbuement_embed_parse_materials(cls, imbuement, prices):
content = ""
for i, material in enumerate(imbuement.materials):
price = ""
if prices:
price = f" ({prices[i]:,} gold each)"
content += "\nx{0.amount} {0.item_title}{1}".format(material, price)
return content
async def get_item_embed(self, ctx: NabCtx, item: models.Item, long):
"""Gets the item embed to show in /item command"""
short_limit = 5
long_limit = 40
embed = self.get_base_embed(item)
embed.description = item.flavor_text
await self.get_item_embed_parse_properties(embed, item)
too_long = self.get_item_embed_parse_offers(embed, item.sold_by, "Sold", long, short_limit)
too_long |= self.get_item_embed_parse_offers(embed, item.bought_by, "Bought", long, short_limit, True)
too_long |= self.get_item_embed_parse_rewards(embed, item.awarded_in, long, short_limit)
too_long |= self.get_item_embed_parse_loot(embed, item.dropped_by, long, long_limit, short_limit)
if too_long and not long:
ask_channel = await ctx.ask_channel_name()
if ask_channel:
askchannel_string = " or use #" + ask_channel
else:
askchannel_string = ""
embed.set_footer(text="To see more, PM me{0}.".format(askchannel_string))
return embed
# region Item Embed Submethods
@classmethod
async def get_item_embed_parse_properties(cls, embed, item: models.Item):
properties = f"Weight: {item.weight} oz"
for attribute in item.attributes: # type: models.ItemAttribute
value = attribute.value
if attribute.name in ["imbuements"]:
continue
if attribute.name == "vocation":
value = ", ".join(attribute.value.title().split("+"))
properties += f"\n{attribute.name.replace('_', ' ').title()}: {value}"
embed.add_field(name="Properties", value=properties)
imbuement_attribute = discord.utils.get(item.attributes, name="imbuements")
if imbuement_attribute:
embed.add_field(name="Used for", value=imbuement_attribute.value)
@classmethod
def get_item_embed_parse_loot(cls, embed, item_drops, long, long_limit, short_limit):
if not item_drops:
return False
too_long = True
name = "Dropped by"
count = 0
value = ""
for drop in item_drops: # type: models.CreatureDrop
count += 1
creature = {"name": drop.creature_title}
if drop.chance is None:
creature["chance"] = "??.??%"
elif drop.chance >= 100:
creature["chance"] = "Always"
else:
creature["chance"] = f"{drop.chance:05.2f}%"
value += "\n`{chance} {name}`".format(**creature)
if count >= short_limit and not long:
value += "\n*...And {0} others*".format(len(item_drops) - short_limit)
too_long = True
break
if long and count >= long_limit:
value += "\n*...And {0} others*".format(len(item_drops) - long_limit)
break
embed.add_field(name=name, value=value, inline=not long)
return too_long
@classmethod
def get_item_embed_parse_rewards(cls, embed, quest_rewards, long, short_limit):
if not quest_rewards:
return False
value = ""
count = 0
name = "Awarded in"
too_long = False
for quest in quest_rewards: # type: models.QuestReward
count += 1
value += "\n" + quest.quest_title
if count >= short_limit and not long:
value += "\n*...And {0} others*".format(len(quest_rewards) - short_limit)
too_long = True
break
embed.add_field(name=name, value=value)
return too_long
@classmethod
def get_item_embed_adjust_city(cls, name, city, embed):
name = name.lower()
if name == 'alesar' or name == 'yaman':
embed.colour = discord.Colour.green()
return "Green Djinn's Fortress"
elif name == "nah'bob" or name == "haroun":
embed.colour = discord.Colour.blue()
return "Blue Djinn's Fortress"
elif name == 'rashid':
embed.colour = discord.Colour(0xF0E916)
return cls.get_rashid_position().city
elif name == 'yasir':
return 'his boat'
elif name == 'briasol':
embed.colour = discord.Colour(0xA958C4)
return city
@classmethod
def get_item_embed_parse_offers(cls, embed, offers, label, long, short_limit, adjust_color=False):
too_long = False
if not offers:
return too_long
item_value = 0
currency = ""
count = 0
value = ""
for i, offer in enumerate(offers): # type: models.NpcSellOffer
if i == 0:
item_value = offer.value
currency = offer.currency_title
if offer.value != item_value:
break
city = cls.get_item_embed_adjust_city(offer.npc_title, offer.npc_city, embed if adjust_color else discord.Embed())
value += f"\n{offer.npc_title} ({city})"
count += 1
if count > short_limit and not long:
value += "\n*...And {0} others*".format(len(offers) - short_limit)
too_long = True
break
embed.add_field(name=f"{label} for {item_value:,} {currency} by", value=value)
return too_long
# endregion
@classmethod
async def get_monster_embed(cls, ctx: NabCtx, monster: models.Creature, long):
"""Gets the monster embeds to show in /mob command
The message is split in two embeds, the second contains loot only and is only shown if long is True"""
embed = cls.get_base_embed(monster)
cls.get_monster_embed_description(embed, monster)
cls.get_monster_embed_attributes(embed, monster, ctx)
cls.get_monster_embed_elemental_modifiers(embed, monster)
cls.get_monster_embed_bestiary_info(embed, monster)
cls.get_monster_embed_damage(embed, monster, long)
cls.get_monster_embed_field_walking(embed, monster)
embed.add_field(name="Abilities", value=monster.abilities, inline=False)
cls.get_monster_embed_loot(embed, monster, long)
await cls.get_monster_embed_footer(ctx, monster, embed, long)
return embed
# region Monster Embed Submethods
@classmethod
def get_monster_embed_field_walking(cls, embed, monster: models.Creature):
content = cls.get_monster_embed_parse_walking(monster, "Through: ", "walks_through")
content += "\n"+cls.get_monster_embed_parse_walking(monster, "Around: ", "walks_around")
if content.strip():
embed.add_field(name="Field Walking", value=content.strip(), inline=True)
@classmethod
async def get_monster_embed_footer(cls, ctx, monster: models.Creature, embed, long):
if monster.loot and not long:
ask_channel = await ctx.ask_channel_name()
if ask_channel:
askchannel_string = " or use #" + ask_channel
else:
askchannel_string = ""
embed.set_footer(text="To see more, PM me{0}.".format(askchannel_string))
@classmethod
def get_monster_embed_parse_walking(cls, monster: models.Creature, walk_field_name, attribute_name):
"""Adds the embed field describing which elemnts the monster walks around or through."""
attribute_value = getattr(monster, attribute_name)
field_types = ["poison", "fire", "energy"]
content = ""
if attribute_value:
content = walk_field_name
if config.use_elemental_emojis:
walks_elements = []
for element in field_types:
if element not in attribute_value.lower():
continue
walks_elements.append(element)
for element in walks_elements:
content += f"{config.elemental_emojis[element]}"
else:
content += attribute_value
return content
@classmethod
def get_monster_embed_damage(cls, embed, monster, long):
if long or not monster.loot:
embed.add_field(name="Max damage",
value=f"{monster.max_damage:,}" if monster.max_damage else "???")
@classmethod
def get_monster_embed_loot(cls, embed, monster, long):
if monster.loot and long:
split_loot = cls.get_monster_embed_parse_loot(monster.loot)
for loot in split_loot:
if loot == split_loot[0]:
name = "Loot"
else:
name = "\u200F"
embed.add_field(name=name, value="`" + loot + "`")
@classmethod
def get_monster_embed_parse_loot(cls, loot: List[models.CreatureDrop]):
loot_string = ""
for drop in loot:
item = {"name": drop.item_title}
if drop.chance is None:
item["chance"] = "??.??%"
elif drop.chance >= 100:
item["chance"] = "Always"
else:
item["chance"] = f"{drop.chance:05.2f}%"
if drop.max > 1:
item["count"] = f"({drop.min}-{drop.max})"
else:
item["count"] = ""
loot_string += "{chance} {name} {count}\n".format(**item)
return split_message(loot_string, FIELD_VALUE_LIMIT - 20)
@classmethod
def get_monster_embed_bestiary_info(cls, embed, monster: models.Creature):
if monster.bestiary_class:
bestiary_info = monster.bestiary_class
if monster.bestiary_level:
difficulty = DIFFICULTIES.get(monster.bestiary_level, f"({monster.bestiary_level})")
bestiary_info += f"\n{difficulty}"
if monster.bestiary_occurrence is not None:
bestiary_info += f"\n{OCCURRENCES.get(monster.bestiary_occurrence, monster.bestiary_occurrence)}"
bestiary_info += f"\n{monster.bestiary_kills:,} kills | {monster.charm_points}{config.charms_emoji}"
embed.add_field(name="Bestiary Class", value=bestiary_info)
@classmethod
def get_monster_embed_elemental_modifiers(cls, embed, monster: models.Creature):
# Iterate through elemental types
if monster:
content = ""
for element, value in monster.elemental_modifiers.items():
# TODO: Find icon for drown damage
try:
if value is None or value == 100:
continue
value -= 100
if config.use_elemental_emojis:
content += f"\n{config.elemental_emojis[element]} {value:+}%"
else:
content += f"\n{value:+}% {element.title()}"
except KeyError:
pass
if content:
embed.add_field(name="Elemental modifiers", value=content)
@classmethod
def get_monster_embed_attributes(cls, embed, monster, ctx):
attributes = {"summon_cost": "Summonable",
"convince_cost": "Convinceable",
"illusionable": "Illusionable",
"pushable": "Pushable",
"paralysable": "Paralysable",
"sees_invisible": "Sees Invisible"
}
attributes = "\n".join([f"{ctx.tick(getattr(monster, x))} {repl}" for x, repl in attributes.items()
if getattr(monster, x) is not None])
embed.add_field(name="Attributes", value=attributes or "Unknown")
@classmethod
def get_monster_embed_description(cls, embed, monster: models.Creature):
hp = f"{monster.hitpoints:,}" if monster.hitpoints else "?"
speed = f"{monster.speed:,}" if monster.speed else "?"
experience = f"{monster.experience:,}" if monster.experience else "?"
embed.description = f"**HP:** {hp} | **Exp:** {experience} | **Speed:** {speed}"
if monster.armor:
embed.description += f" | **Armor** {monster.armor}"
# endregion
@classmethod
async def get_npc_embed(cls, ctx: NabCtx, npc: models.Npc, long):
"""Gets the embed to show in /npc command"""
short_limit = 5
long_limit = 50
too_long = False
embed = cls.get_base_embed(npc)
cls.get_npc_embed_parse_basic_info(embed, npc)
too_long |= cls.get_npc_embed_parse_offers(embed, npc.sell_offers, long, long_limit, short_limit, "Selling")
too_long |= cls.get_npc_embed_parse_offers(embed, npc.buy_offers, long, long_limit, short_limit, "Buying")
if npc.destinations:
value = ""
for destination in npc.destinations: # type: models.NpcDestination
value += "\n{0.name} \u2192 {0.price} gold".format(destination)
embed.add_field(name="Destinations", value=value)
too_long |= await cls.get_npc_embed_parse_spells(embed, npc.teaches, long, short_limit)
if too_long:
ask_channel = await ctx.ask_channel_name()
if ask_channel:
askchannel_string = " or use #" + ask_channel
else:
askchannel_string = ""
embed.set_footer(text="To see more, PM me{0}.".format(askchannel_string))
return embed
# region NPC submethods
@classmethod
def get_npc_embed_parse_basic_info(cls, embed, npc):
embed.add_field(name="Job", value=npc.job)
if npc.name == "Rashid":
rashid = cls.get_rashid_position()
npc.city = rashid.city
npc.x = rashid.x
npc.y = rashid.y
npc.z = rashid.z
if npc.name == "Yasir":
npc.x = None
npc.y = None
npc.z = None
embed.add_field(name="City", value=npc.city)
@classmethod
def get_npc_embed_parse_offers(cls, embed, offers, long, long_limit, short_limit, label):
if not offers:
return False
too_long = False
count = 0
value = ""
for offer in offers:
count += 1
currency = offer.currency_title.replace("Gold Coin", "gold")
value += "\n{0.item_title} \u2192 {0.value:,} {1}".format(offer, currency)
if count > short_limit and not long:
value += "\n*...And {0} others*".format(len(offers) - short_limit)
too_long = True
break
if long and count > long_limit:
value += "\n*...And {0} others*".format(len(offers) - long_limit)
break
split_selling = split_message(value, FIELD_VALUE_LIMIT)
for value in split_selling:
if value == split_selling[0]:
name = label
else:
name = "\u200F"
embed.add_field(name=name, value=value)
return too_long
@classmethod
async def get_npc_embed_parse_spells(cls, embed, spells: List[models.NpcSpell], long, short_limit):
vocs = ["knight", "sorcerer", "paladin", "druid"]
too_long = False
values = defaultdict(str)
count = defaultdict(int)
skip = defaultdict(bool)
for spell in spells:
value = f"\n{spell.spell_title} \u2014 {spell.price:,} gold"
for voc in vocs:
if skip[voc] or not getattr(spell, voc):
continue
values[voc] += value
count[voc] += 1
if count[voc] >= short_limit and not long:
values[voc] += "\n*...And more*"
too_long = True
skip[voc] = True
for voc, content in values.items():
fields = split_message(content, FIELD_VALUE_LIMIT)
for i, split_field in enumerate(fields):
name = f"Teaches ({voc.title()}s)" if i == 0 else "\u200F"
embed.add_field(name=name, value=split_field, inline=not len(fields) > 1)
return too_long
# endregion
@classmethod
async def get_spell_embed(cls, ctx: NabCtx, spell: models.Spell, long):
"""Gets the embed to show in /spell command"""
short_limit = 5
words = spell.words
if "exani hur" in spell.words:
words = "exani hur up|down"
embed = cls.get_base_embed(spell, f"{spell.title} ({words})")
premium = "**premium** " if spell.premium else ""
mana = spell.mana if spell.mana else "variable"
voc_list = list()
if spell.knight:
voc_list.append("knights")
if spell.paladin:
voc_list.append("paladins")
if spell.druid:
voc_list.append("druids")
if spell.sorcerer:
voc_list.append("sorcerers")
vocs = join_list(voc_list, ", ", " and ")
description = f"A {premium}spell for level **{spell.level}** and up. " \
f"It uses **{mana}** mana. It can be used by {vocs}"
if spell.price == 0:
description += "\nIt can be obtained for free."
else:
description += f"\nIt can be bought for {spell.price:,} gold coins."
embed.description = description
too_long = await cls.get_spell_embed_parse_teachers(embed, spell.taught_by, long, short_limit, voc_list, vocs)
# Set embed color based on element:
element_color = {
"Fire": discord.Colour(0xFF9900),
"Ice": discord.Colour(0x99FFFF),
"Energy": discord.Colour(0xCC33FF),
"Earth": discord.Colour(0x00FF00),
"Holy": discord.Colour(0xFFFF00),
"Death": discord.Colour(0x990000),
"Physical": discord.Colour(0xF70000),
"Bleed": discord.Colour(0xF70000),
}
elemental_emoji = ""
if spell.element in element_color:
embed.colour = element_color[spell.element]
if spell.element == "Bleed":
spell.element = "Physical"
if config.use_elemental_emojis:
elemental_emoji = config.elemental_emojis[spell.element.lower()]
effect = f"\n\n{elemental_emoji}{spell.effect}"
embed.description += effect
if too_long:
ask_channel = await ctx.ask_channel_name()
if ask_channel:
askchannel_string = " or use #" + ask_channel
else:
askchannel_string = ""
embed.set_footer(text="To see more, PM me{0}.".format(askchannel_string))
return embed
@classmethod
async def get_spell_embed_parse_teachers(cls, embed, teachers: List[models.NpcSpell], long, short_limit, voc_list,
vocs):
if not teachers:
return False
too_long = False
for voc in voc_list:
value = ""
name = "Sold by ({0})".format(voc.title())
if len(vocs) == 1:
name = "Sold by"
count = 0
for npc in teachers:
if not getattr(npc, voc[:-1]):
continue
count += 1
value += f"\n{npc.npc_title} ({npc.npc_city})"
if count >= short_limit and not long:
value += "\n*...And more*"
too_long = True
break
if value:
embed.add_field(name=name, value=value)
return too_long
@classmethod
def search_entry(cls, table, term, *, additional_field=""):
if additional_field:
query = """SELECT article_id, title, name, %s FROM %s
WHERE title LIKE ? or %s LIKE ?
ORDER BY LENGTH(title) ASC LIMIT 15
""" % (additional_field, table, additional_field)
params = ("%%%s%%" % term, "%%%s%%" % term)
else:
query = "SELECT article_id, title, name FROM %s WHERE title LIKE ? ORDER BY LENGTH(title) ASC LIMIT 15" % \
table
params = ("%%%s%%" % term,)
c = wiki_db.execute(query, params)
results = c.fetchall()
if not results:
return []
if results[0]["title"].lower() == term.lower() or \
(additional_field and results[0][additional_field].lower() == term.lower()):
return [dict(results[0])]
return [dict(r) for r in results]
@classmethod
def search_key(cls, terms):
"""Returns a dictionary containing a NPC's info, a list of possible matches or None"""
c = wiki_db.cursor()
try:
# search query
c.execute("SELECT article_id, number, name FROM item_key "
"WHERE name LIKE ? OR notes LIKE ? or origin LIKE ? LIMIT 10 ",
("%" + terms + "%",) * 3)
result = c.fetchall()
if not result:
return []
return result
finally:
c.close()
@classmethod
def get_entry(cls, title, model):
entry = model.get_by_field(wiki_db, "title", title)
return entry
@classmethod
def get_rashid_position(cls) -> models.RashidPosition:
return models.RashidPosition.get_by_field(wiki_db, "day", get_tibia_weekday())
@classmethod
def get_mapper_link(cls, x, y, z):
def convert_pos(pos):
return f"{(pos&0xFF00)>>8}.{pos&0x00FF}"
return f"http://tibia.fandom.com/wiki/Mapper?coords={convert_pos(x)}-{convert_pos(y)}-{z}-4-1-1"
# endregion
def setup(bot):
bot.add_cog(TibiaWiki(bot))