add cards command and update card search arguments

pull/1/head
qwewqa 4 years ago
parent 3cc08a0be1
commit 84ff7e74b6
  1. 183
      miyu_bot/commands/cogs/card.py
  2. 22
      miyu_bot/commands/cogs/event.py
  3. 14
      miyu_bot/commands/cogs/music.py
  4. 3
      miyu_bot/commands/common/argument_parsing.py
  5. 19
      miyu_bot/commands/common/event.py
  6. 2
      miyu_bot/commands/common/master_asset_manager.py
  7. 11
      miyu_bot/commands/common/name_aliases.py

@ -1,16 +1,23 @@
import asyncio import asyncio
import enum
import logging import logging
import discord import discord
from d4dj_utils.master.card_master import CardMaster from d4dj_utils.master.card_master import CardMaster
from d4dj_utils.master.event_master import EventMaster
from d4dj_utils.master.event_specific_bonus_master import EventSpecificBonusMaster
from d4dj_utils.master.skill_master import SkillMaster
from discord.ext import commands from discord.ext import commands
from main import masters from main import masters, asset_manager
from miyu_bot.commands.common.argument_parsing import ParsedArguments, parse_arguments, ArgumentError
from miyu_bot.commands.common.emoji import rarity_emoji_ids, attribute_emoji_ids_by_attribute_id, \ 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 unit_emoji_ids_by_unit_id, parameter_bonus_emoji_ids_by_parameter_id
from miyu_bot.commands.common.event import get_latest_event
from miyu_bot.commands.common.formatting import format_info from miyu_bot.commands.common.formatting import format_info
from miyu_bot.commands.common.master_asset_manager import hash_master from miyu_bot.commands.common.master_asset_manager import hash_master
from miyu_bot.commands.common.reaction_message import run_tabbed_message from miyu_bot.commands.common.name_aliases import characters_by_name, attributes_by_name, units_by_name
from miyu_bot.commands.common.reaction_message import run_tabbed_message, run_reaction_message, run_paged_message
class Card(commands.Cog): class Card(commands.Cog):
@ -28,14 +35,129 @@ class Card(commands.Cog):
help='!card secretcage') help='!card secretcage')
async def card(self, ctx: commands.Context, *, arg: commands.clean_content): async def card(self, ctx: commands.Context, *, arg: commands.clean_content):
self.logger.info(f'Searching for card "{arg}".') self.logger.info(f'Searching for card "{arg}".')
card = masters.cards.get(arg, ctx)
if card.rarity_id >= 3: try:
embeds = [self.get_card_embed(card, 0), self.get_card_embed(card, 1)] 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)) asyncio.ensure_future(run_tabbed_message(ctx, self.rarity_emoji, embeds, starting_index=1))
else: else:
embeds = [self.get_card_embed(card, 0)] message = await ctx.send(embed=self.get_card_embeds(cards[0])[1])
asyncio.ensure_future(run_tabbed_message(ctx, self.rarity_emoji[:1], embeds, starting_index=0))
emojis = self.rarity_emoji + ['', '']
index = 0
limit_break = 1
async def callback(emoji, _ctx, _message):
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(card)}')
else:
listing.append(self.format_card_name(card))
embed = discord.Embed(title=f'Card Search "{arg}"' if arg else 'Cards')
asyncio.ensure_future(run_paged_message(ctx, embed, listing))
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)
character = {characters_by_name[c].id for c in arguments.words(characters_by_name.keys())}
unit = {units_by_name[a].id for a in arguments.words(units_by_name.keys())}
rarity = {int(r[0]) for r in arguments.words(['4*', '3*', '2*', '1*', r'4\*', r'3\*', r'2\*', r'1\*'])}
attribute = {attributes_by_name[a].id for a in arguments.words(attributes_by_name.keys())}
event_bonus = bool(arguments.tags(['event', 'eventbonus', 'event_bonus']))
if event_bonus:
latest_event = get_latest_event(ctx)
bonus: EventSpecificBonusMaster = latest_event.bonus
character.update(bonus.character_ids)
attribute.add(bonus.attribute_id)
if not arguments.has_named('sort'):
sort = CardAttribute.Date
arguments.require_all_arguments_used()
cards = masters.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]:
cards = cards[::-1]
if reverse_sort:
cards = cards[::-1]
if character:
cards = [card for card in cards if card.character.id in character]
if unit:
cards = [card for card in cards if card.character.unit.id in unit]
if rarity:
cards = [card for card in cards if card.rarity_id in rarity]
if attribute:
cards = [card for card in cards if card.attribute.id in attribute]
return cards
def get_card_embed(self, card: CardMaster, limit_break): def get_card_embed(self, card: CardMaster, limit_break):
embed = discord.Embed(title=self.format_card_name(card)) embed = discord.Embed(title=self.format_card_name(card))
@ -53,7 +175,7 @@ class Card(commands.Cog):
value=format_info({ value=format_info({
'Rarity': f'{card.rarity_id}', 'Rarity': f'{card.rarity_id}',
'Character': f'{card.character.full_name_english}', '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}', '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}', '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}', 'Release Date': f'{card.start_datetime}',
}), }),
@ -65,7 +187,18 @@ class Card(commands.Cog):
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[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])}', f'{self.bot.get_emoji(parameter_bonus_emoji_ids_by_parameter_id[3])} Physical': f'{"{:,}".format(card.max_parameters_with_limit_break[2])}',
}), }),
inline=False) 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 return embed
@ -73,5 +206,37 @@ class Card(commands.Cog):
return f'{card.rarity_id}{card.name} {card.character.full_name_english}' return f'{card.rarity_id}{card.name} {card.character.full_name_english}'
class CardAttribute(enum.Enum):
Name = enum.auto()
Id = enum.auto()
Power = enum.auto()
Date = enum.auto()
def get_sort_key_from_card(self, card: CardMaster):
return {
self.Name: card.name,
self.Id: card.id,
self.Power: card.max_power_with_limit_break,
self.Date: card.start_datetime,
}[self]
def get_formatted_from_card(self, card: CardMaster):
return {
self.Name: 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]
card_attribute_aliases = {
'name': CardAttribute.Name,
'id': CardAttribute.Id,
'power': CardAttribute.Power,
'stats': CardAttribute.Power,
'date': CardAttribute.Date,
}
def setup(bot): def setup(bot):
bot.add_cog(Card(bot)) bot.add_cog(Card(bot))

@ -14,6 +14,7 @@ from main import asset_manager, masters
from miyu_bot.commands.common.emoji import attribute_emoji_ids_by_attribute_id, unit_emoji_ids_by_unit_id, \ from miyu_bot.commands.common.emoji import attribute_emoji_ids_by_attribute_id, unit_emoji_ids_by_unit_id, \
parameter_bonus_emoji_ids_by_parameter_id, \ parameter_bonus_emoji_ids_by_parameter_id, \
event_point_emoji_id event_point_emoji_id
from miyu_bot.commands.common.event import get_latest_event
from miyu_bot.commands.common.formatting import format_info from miyu_bot.commands.common.formatting import format_info
from miyu_bot.commands.common.fuzzy_matching import romanize from miyu_bot.commands.common.fuzzy_matching import romanize
from miyu_bot.commands.common.master_asset_manager import MasterFilter, hash_master from miyu_bot.commands.common.master_asset_manager import MasterFilter, hash_master
@ -37,14 +38,14 @@ class Event(commands.Cog):
# Allows relative id searches like `!event +1` for next event or `!event -2` for the event before last event # Allows relative id searches like `!event +1` for next event or `!event -2` for the event before last event
if arg[0] in ['-', '+']: if arg[0] in ['-', '+']:
try: try:
latest = self.get_latest_event(ctx) latest = get_latest_event(ctx)
event = masters.events.get(str(latest.id + int(arg)), ctx) event = masters.events.get(str(latest.id + int(arg)), ctx)
except ValueError: except ValueError:
event = masters.events.get(arg, ctx) event = masters.events.get(arg, ctx)
else: else:
event = masters.events.get(arg, ctx) event = masters.events.get(arg, ctx)
else: else:
event = self.get_latest_event(ctx) event = get_latest_event(ctx)
if not event: if not event:
msg = f'Failed to find event "{arg}".' msg = f'Failed to find event "{arg}".'
@ -150,7 +151,7 @@ class Event(commands.Cog):
description='Displays the time left in the current event', description='Displays the time left in the current event',
help='!timeleft') help='!timeleft')
async def time_left(self, ctx: commands.Context): async def time_left(self, ctx: commands.Context):
latest = self.get_latest_event(ctx) latest = get_latest_event(ctx)
state = latest.state() state = latest.state()
@ -204,19 +205,6 @@ class Event(commands.Cog):
await ctx.send(files=[logo], embed=embed) await ctx.send(files=[logo], embed=embed)
def get_latest_event(self, ctx: commands.Context) -> EventMaster:
"""Returns the oldest event that has not ended or the newest event otherwise."""
try:
# NY event overlapped with previous event
return min((v for v in masters.events.values(ctx) if v.state() == EventState.Open),
key=lambda e: e.start_datetime)
except ValueError:
try:
return min((v for v in masters.events.values(ctx) if v.state() < EventState.Ended),
key=lambda e: e.start_datetime)
except ValueError:
return max(masters.events.values(ctx), key=lambda v: v.start_datetime)
@commands.command(name='t20', @commands.command(name='t20',
aliases=['top20', 'top_20'], aliases=['top20', 'top_20'],
description='Displays the top 20 in the main leaderboard', description='Displays the top 20 in the main leaderboard',
@ -226,7 +214,7 @@ class Event(commands.Cog):
async with session.get('http://www.projectdivar.com/eventdata/t20') as resp: async with session.get('http://www.projectdivar.com/eventdata/t20') as resp:
leaderboard = await resp.json(encoding='utf-8') leaderboard = await resp.json(encoding='utf-8')
latest = self.get_latest_event(ctx) latest = get_latest_event(ctx)
logo = discord.File(latest.logo_path, filename='logo.png') logo = discord.File(latest.logo_path, filename='logo.png')
embed = discord.Embed(title=f'{latest.name} t20').set_thumbnail(url=f'attachment://logo.png') embed = discord.Embed(title=f'{latest.name} t20').set_thumbnail(url=f'attachment://logo.png')
max_points_digits = len(str(leaderboard[0]['points'])) max_points_digits = len(str(leaderboard[0]['points']))

@ -56,7 +56,7 @@ class Music(commands.Cog):
song = masters.music.get(arg, ctx) song = masters.music.get(arg, ctx)
if not song: if not song:
msg = f'Failed to find song "{arg}".' msg = f'No results for song "{arg}".'
await ctx.send(msg) await ctx.send(msg)
self.logger.info(msg) self.logger.info(msg)
return return
@ -171,7 +171,6 @@ class Music(commands.Cog):
async def songs(self, ctx: commands.Context, *, arg: commands.clean_content = ''): async def songs(self, ctx: commands.Context, *, arg: commands.clean_content = ''):
self.logger.info(f'Searching for songs "{arg}".' if arg else 'Listing songs.') self.logger.info(f'Searching for songs "{arg}".' if arg else 'Listing songs.')
arguments = parse_arguments(arg) arguments = parse_arguments(arg)
songs = masters.music.get_sorted(arguments.text_argument, ctx)
try: try:
sort, sort_op = arguments.single('sort', MusicAttribute.DefaultOrder, sort, sort_op = arguments.single('sort', MusicAttribute.DefaultOrder,
@ -203,6 +202,9 @@ class Music(commands.Cog):
difficulty = arguments.repeatable(['difficulty', 'diff', 'level'], is_list=True, difficulty = arguments.repeatable(['difficulty', 'diff', 'level'], is_list=True,
converter=difficulty_converter) converter=difficulty_converter)
songs = masters.music.get_sorted(arguments.text(), ctx)
arguments.require_all_arguments_used() arguments.require_all_arguments_used()
except ArgumentError as e: except ArgumentError as e:
await ctx.send(str(e)) await ctx.send(str(e))
@ -231,11 +233,13 @@ class Music(commands.Cog):
songs = [song for song in songs if song.unit.id in allowed_unit_ids] songs = [song for song in songs if song.unit.id in allowed_unit_ids]
if not (arguments.text_argument and sort == MusicAttribute.DefaultOrder): if not (arguments.text_argument and sort == MusicAttribute.DefaultOrder):
songs = sorted(songs, key=lambda s: sort.get_from_music(s)) songs = sorted(songs, key=lambda s: sort.get_sort_key_from_music(s))
if sort == MusicAttribute.DefaultOrder and songs and songs[0].id == 1: if sort == MusicAttribute.DefaultOrder and songs and songs[0].id == 1:
songs = [*songs[1:], songs[0]] songs = [*songs[1:], songs[0]]
if sort in [MusicAttribute.Level, MusicAttribute.Date]:
songs = songs[::-1]
if reverse_sort: if reverse_sort:
songs = reversed(songs) songs = songs[::-1]
listing = [] listing = []
for song in songs: for song in songs:
@ -394,7 +398,7 @@ class MusicAttribute(enum.Enum):
Duration = enum.auto() Duration = enum.auto()
Date = enum.auto() Date = enum.auto()
def get_from_music(self, music: MusicMaster): def get_sort_key_from_music(self, music: MusicMaster):
return { return {
self.DefaultOrder: -music.default_order, self.DefaultOrder: -music.default_order,
self.Name: music.name, self.Name: music.name,

@ -107,6 +107,9 @@ class ParsedArguments:
self.used_tags.add(alias) self.used_tags.add(alias)
return results return results
def has_named(self, name: str):
return name in self.named_arguments
def single(self, names: Union[List[str], str], default: Any = None, allowed_operators: Optional[Container] = None, def single(self, names: Union[List[str], str], default: Any = None, allowed_operators: Optional[Container] = None,
is_list=False, numeric=False, converter: Union[dict, Callable] = lambda n: n): is_list=False, numeric=False, converter: Union[dict, Callable] = lambda n: n):
if allowed_operators is None: if allowed_operators is None:

@ -0,0 +1,19 @@
from d4dj_utils.master.event_master import EventMaster, EventState
from discord.ext import commands
from main import masters
def get_latest_event(ctx: commands.Context) -> EventMaster:
"""Returns the oldest event that has not ended or the newest event otherwise."""
try:
# NY event overlapped with previous event
return min((v for v in masters.events.values(ctx) if v.state() == EventState.Open),
key=lambda e: e.start_datetime)
except ValueError:
try:
return min((v for v in masters.events.values(ctx) if v.state() < EventState.Ended),
key=lambda e: e.start_datetime)
except ValueError:
return max(masters.events.values(ctx), key=lambda v: v.start_datetime)

@ -30,7 +30,7 @@ class MasterFilterManager:
) )
self.cards = MasterFilter( self.cards = MasterFilter(
self.manager.card_master, self.manager.card_master,
naming_function=lambda c: c.name, naming_function=lambda c: f'{c.name} {c.character.first_name_english}',
filter_function=lambda c: c.is_released, filter_function=lambda c: c.is_released,
) )

@ -0,0 +1,11 @@
from main import asset_manager
characters_by_name = {}
for character in asset_manager.character_master.values():
for name in character.full_name_english.split():
characters_by_name[name.lower()] = character
attributes_by_name = {attribute.en_name: attribute for attribute in asset_manager.attribute_master.values()}
units_by_name = {unit.name: unit for unit in asset_manager.unit_master.values()}
units_by_name['rondo'] = units_by_name['燐舞曲']
Loading…
Cancel
Save