import asyncio import enum import logging import re import discord from d4dj_utils.master.card_master import CardMaster from d4dj_utils.master.event_specific_bonus_master import EventSpecificBonusMaster from d4dj_utils.master.skill_master import SkillMaster from discord.ext import commands from miyu_bot.bot.bot import D4DJBot from miyu_bot.commands.common.argument_parsing import ParsedArguments, parse_arguments, ArgumentError, list_operator_for from miyu_bot.commands.common.asset_paths import get_card_icon_path, get_card_art_path from miyu_bot.commands.common.emoji import rarity_emoji_ids, attribute_emoji_ids_by_attribute_id, \ unit_emoji_ids_by_unit_id, parameter_bonus_emoji_ids_by_parameter_id from miyu_bot.commands.common.formatting import format_info from miyu_bot.commands.common.reaction_message import run_tabbed_message, run_reaction_message, run_paged_message class Card(commands.Cog): bot: D4DJBot def __init__(self, bot): self.bot = bot self.logger = logging.getLogger(__name__) @property def rarity_emoji(self): return [self.bot.get_emoji(eid) for eid in rarity_emoji_ids.values()] @commands.command(name='card', aliases=[], description='Finds the card with the given name.', help='!card secretcage') async def card(self, ctx: commands.Context, *, arg: commands.clean_content): self.logger.info(f'Searching for card "{arg}".') try: arguments = parse_arguments(arg) cards = self.get_cards(ctx, arguments) except ArgumentError as e: await ctx.send(str(e)) return if not cards: await ctx.send(f'No results for card "{arg}"') return if len(cards) == 1 or arguments.text(): embeds = self.get_card_embeds(cards[0]) asyncio.ensure_future(run_tabbed_message(ctx, self.rarity_emoji, embeds, starting_index=1)) else: message = await ctx.send(embed=self.get_card_embeds(cards[0])[1]) emojis = self.rarity_emoji + ['◀', '▶'] index = 0 limit_break = 1 async def callback(emoji): nonlocal index nonlocal limit_break try: emoji_index = emojis.index(emoji) if emoji_index == 0: limit_break = 0 elif emoji_index == 1: limit_break = 1 elif emoji_index == 2: index -= 1 else: index += 1 index = min(len(cards) - 1, max(0, index)) await message.edit(embed=self.get_card_embeds(cards[index])[limit_break]) except ValueError: pass asyncio.ensure_future(run_reaction_message(ctx, message, emojis, callback)) def get_card_embeds(self, card): if card.rarity_id >= 3: return [self.get_card_embed(card, 0), self.get_card_embed(card, 1)] else: return [self.get_card_embed(card, 0)] * 2 # no actual awakened art for 1/2* cards @commands.command(name='cards', aliases=[], description='Lists cards matching the given search terms.', help='!cards') async def cards(self, ctx: commands.Context, *, arg: commands.clean_content = ''): self.logger.info(f'Searching for cards "{arg}".') try: arguments = parse_arguments(arg) cards = self.get_cards(ctx, arguments) sort, sort_op = arguments.single('sort', None, allowed_operators=['<', '>', '='], converter=card_attribute_aliases) display, _ = arguments.single(['display', 'disp'], sort or CardAttribute.Power, allowed_operators=['='], converter=card_attribute_aliases) except ArgumentError as e: await ctx.send(str(e)) return listing = [] for card in cards: display_prefix = display.get_formatted_from_card(card) if display_prefix: listing.append( f'{display_prefix} {self.format_card_name_for_list(card)}') else: listing.append(self.format_card_name_for_list(card)) embed = discord.Embed(title=f'Card Search "{arg}"' if arg else 'Cards') asyncio.ensure_future(run_paged_message(ctx, embed, listing)) @commands.command(name='cardexp', aliases=['card_exp', 'cdexp'], description='Displays card exp totals or the difference between levels.', help='!cardexp 1-80') async def cardexp(self, ctx: commands.Context, *, arg: commands.clean_content = ''): assert isinstance(arg, str) exp = self.bot.assets.card_exp_master def comma_number(n): return '{:,}'.format(n) def format_exp(e): return comma_number(e.total_exp).rjust(9) if not arg: embed = discord.Embed(title='Card Exp', description='```' + '\n'.join(f'Lvl {n}: {format_exp(exp[n])}' for n in range(10, 90, 10)) + '```') await ctx.send(embed=embed) else: try: if arg.isnumeric(): level = int(arg) level_total = exp[level].total_exp desc = (f'```\n' f'Total: {comma_number(level_total)}\n' f'Change: {comma_number(level_total - exp[level - 1].total_exp) if level > 1 else "N/A"}\n' f'```') await ctx.send(embed=discord.Embed(title=f'Card Exp Lvl {level}', description=desc)) else: start, end = arg.split('-') start = int(start) end = int(end) if start > end: await ctx.send('End exp is greater than start exp.') return start_exp = exp[start] end_exp = exp[end] change_amount = end_exp.total_exp - start_exp.total_exp embed = discord.Embed(title=f'Card Exp Lvl {start}-{end}', description=f'```\n' f'Lvl {str(start).rjust(2)}: {format_exp(start_exp)}\n' f'Lvl {str(end).rjust(2)}: {format_exp(end_exp)}\n' f'Change: {comma_number(change_amount).rjust(9)}\n' f'```') await ctx.send(embed=embed) except Exception: await ctx.send(f'Invalid card exp {arg}') def get_cards(self, ctx, arguments: ParsedArguments): sort, sort_op = arguments.single('sort', None, allowed_operators=['<', '>', '='], converter=card_attribute_aliases) reverse_sort = sort_op == '<' or arguments.tag('reverse') # Not used, but here because it's a valid argument before running require_all_arguments_used. display, _ = arguments.single(['display', 'disp'], sort, allowed_operators=['='], converter=card_attribute_aliases) characters = {self.bot.aliases.characters_by_name[c].id for c in arguments.words(self.bot.aliases.characters_by_name.keys()) | arguments.tags(self.bot.aliases.characters_by_name.keys())} units = {self.bot.aliases.units_by_name[unit].id for unit in arguments.tags(names=self.bot.aliases.units_by_name.keys(), aliases=self.bot.aliases.unit_aliases)} rarity_names = ['4*', '3*', '2*', '1*', r'4\*', r'3\*', r'2\*', r'1\*'] rarities = {int(r[0]) for r in arguments.words(rarity_names) | arguments.tags(rarity_names)} attributes = {self.bot.aliases.attributes_by_name[a].id for a in arguments.tags(self.bot.aliases.attributes_by_name.keys())} birthday = arguments.tag('birthday') | arguments.word('birthday') score_up_filters = arguments.repeatable(['skill', 'score_up', 'score'], is_list=True, numeric=True) heal_filters = arguments.repeatable(['heal', 'recovery'], is_list=True, numeric=True) event_bonus = bool(arguments.tags(['event', 'eventbonus', 'event_bonus'])) if event_bonus: latest_event = self.bot.asset_filters.events.get_latest_event(ctx) bonus: EventSpecificBonusMaster = latest_event.bonus if not characters: characters.update(bonus.character_ids) elif bonus.character_ids: characters = {char for char in characters if char in bonus.character_ids} if bonus.attribute_id: attributes = {bonus.attribute_id} if not arguments.has_named('sort'): sort = CardAttribute.Date arguments.require_all_arguments_used() cards = self.bot.asset_filters.cards.get_sorted(arguments.text(), ctx) if not (arguments.text() and sort is None): sort = sort or CardAttribute.Power cards = sorted(cards, key=lambda c: (sort.get_sort_key_from_card(c), c.max_power_with_limit_break)) if sort in [CardAttribute.Power, CardAttribute.Date, CardAttribute.ScoreUp, CardAttribute.Heal]: cards = cards[::-1] if reverse_sort: cards = cards[::-1] if characters: cards = [card for card in cards if card.character.id in characters] if units: cards = [card for card in cards if card.character.unit.id in units] if rarities: cards = [card for card in cards if card.rarity_id in rarities] if attributes: cards = [card for card in cards if card.attribute.id in attributes] if birthday: cards = [card for card in cards if card.name == 'Birthday'] for value, operation in score_up_filters: operator = list_operator_for(operation) cards = [card for card in cards if operator(card.skill.score_up_rate, value)] for value, operation in heal_filters: operator = list_operator_for(operation) cards = [card for card in cards if operator(card.skill.max_recovery_value, value)] return cards def get_card_embed(self, card: CardMaster, limit_break): embed = discord.Embed(title=self.format_card_name(card)) thumb_url = self.bot.asset_url + get_card_icon_path(card, limit_break) art_url = self.bot.asset_url + get_card_art_path(card, limit_break) embed.set_thumbnail(url=thumb_url) embed.set_image(url=art_url) embed.add_field(name='Info', value=format_info({ 'Rarity': f'{card.rarity_id}★', 'Character': f'{card.character.full_name_english}', 'Attribute': f'{self.bot.get_emoji(attribute_emoji_ids_by_attribute_id[card.attribute_id])} {card.attribute.en_name.capitalize()}', 'Unit': f'{self.bot.get_emoji(unit_emoji_ids_by_unit_id[card.character.unit_id])} {card.character.unit.name}', 'Release Date': f'{card.start_datetime}', }), inline=False) embed.add_field(name='Parameters', value=format_info({ f'Total': f'{"{:,}".format(card.max_power_with_limit_break)}', f'{self.bot.get_emoji(parameter_bonus_emoji_ids_by_parameter_id[1])} Heart': f'{"{:,}".format(card.max_parameters_with_limit_break[0])}', f'{self.bot.get_emoji(parameter_bonus_emoji_ids_by_parameter_id[2])} Technique': f'{"{:,}".format(card.max_parameters_with_limit_break[1])}', f'{self.bot.get_emoji(parameter_bonus_emoji_ids_by_parameter_id[3])} Physical': f'{"{:,}".format(card.max_parameters_with_limit_break[2])}', }), inline=True) skill: SkillMaster = card.skill embed.add_field(name='Skill', value=format_info({ 'Name': card.skill_name, 'Duration': f'{skill.min_seconds}-{skill.max_seconds}s', 'Score Up': f'{skill.score_up_rate}%', 'Heal': (f'{skill.min_recovery_value}-{skill.max_recovery_value}' if skill.min_recovery_value != skill.max_recovery_value else str(skill.min_recovery_value)) }), inline=True) return embed def format_card_name(self, card): return f'{card.rarity_id}★ {card.name} {card.character.full_name_english}' def format_card_name_for_list(self, card): unit_emoji = self.bot.get_emoji(unit_emoji_ids_by_unit_id[card.character.unit_id]) attribute_emoji = self.bot.get_emoji(attribute_emoji_ids_by_attribute_id[card.attribute_id]) return f'`{unit_emoji}`+`{attribute_emoji}` {card.rarity_id}★ {card.name} {card.character.first_name_english}' class CardAttribute(enum.Enum): Name = enum.auto() Character = enum.auto() Id = enum.auto() Power = enum.auto() Date = enum.auto() ScoreUp = enum.auto() Heal = enum.auto() def get_sort_key_from_card(self, card: CardMaster): return { self.Name: None, self.Character: card.character_id, self.Id: card.id, self.Power: card.max_power_with_limit_break, self.Date: card.start_datetime, self.ScoreUp: card.skill.score_up_rate, self.Heal: card.skill.max_recovery_value, }[self] def get_formatted_from_card(self, card: CardMaster): return { self.Name: None, self.Character: None, self.Id: str(card.id).zfill(9), self.Power: str(card.max_power_with_limit_break).rjust(5), self.Date: str(card.start_datetime.date()), self.ScoreUp: self.format_skill(card.skill), self.Heal: self.format_skill(card.skill), }[self] @staticmethod def format_skill(skill): return f'{str(skill.score_up_rate).rjust(2)}%,{str(skill.max_recovery_value).rjust(3)}hp' card_attribute_aliases = { 'name': CardAttribute.Name, 'character': CardAttribute.Character, 'char': CardAttribute.Character, 'id': CardAttribute.Id, 'power': CardAttribute.Power, 'stats': CardAttribute.Power, 'date': CardAttribute.Date, 'skill': CardAttribute.ScoreUp, 'score_up': CardAttribute.ScoreUp, 'scoreup': CardAttribute.ScoreUp, 'heal': CardAttribute.Heal, 'recovery': CardAttribute.Heal, } def setup(bot): bot.add_cog(Card(bot))