Beispiel #1
0
async def run_attack(ctx, embed, args, caster, attack, targets, combat):
    """Runs an attack: adds title, handles -f and -thumb args, commits combat, runs automation, edits embed."""
    if not args.last('h', type_=bool):
        name = caster.get_title_name()
    else:
        name = "An unknown creature"

    if not attack.proper:
        attack_name = a_or_an(attack.name)
    else:
        attack_name = attack.name

    verb = attack.verb or "attacks with"

    if args.last('title') is not None:
        embed.title = args.last('title') \
            .replace('[name]', name) \
            .replace('[aname]', attack_name)
    else:
        embed.title = f'{name} {verb} {attack_name}!'

    await attack.automation.run(ctx,
                                embed,
                                caster,
                                targets,
                                args,
                                combat=combat,
                                title=embed.title)
    if combat:
        await combat.final()

    embeds.add_fields_from_args(embed, args.get('f'))
    if 'thumb' in args:
        embed.set_thumbnail(url=args.last('thumb'))
Beispiel #2
0
    async def monster_atk(self, ctx, monster_name, atk_name=None, *, args=''):
        """Rolls a monster's attack.
        __Valid Arguments__
        -t "<target>" - Sets targets for the attack. You can pass as many as needed. Will target combatants if channel is in initiative.
        -t "<target>|<args>" - Sets a target, and also allows for specific args to apply to them. (e.g, -t "OR1|hit" to force the attack against OR1 to hit)

        adv/dis
        -ac [target ac]
        -b [to hit bonus]
        -d [damage bonus]
        -d# [applies damage to the first # hits]
        -rr [times to reroll]
        -t [target]
        -phrase [flavor text]
        crit (automatically crit)
        -h (hides monster name, image, and rolled values)
        """
        if atk_name is None or atk_name == 'list':
            return await ctx.invoke(self.monster_atk_list, monster_name)

        await try_delete(ctx.message)

        monster = await select_monster_full(ctx, monster_name)
        attacks = monster.attacks
        monster_name = monster.get_title_name()

        attack = await search_and_select(ctx, attacks, atk_name, lambda a: a.name)
        args = await helpers.parse_snippets(args, ctx)
        args = argparse(args)
        if not args.last('h', type_=bool):
            name = monster_name
            image = args.last('thumb') or monster.get_image_url()
        else:
            name = "An unknown creature"
            image = None

        embed = discord.Embed()
        if args.last('title') is not None:
            embed.title = args.last('title') \
                .replace('[name]', name) \
                .replace('[aname]', attack.name)
        else:
            embed.title = '{} attacks with {}!'.format(name, a_or_an(attack.name))

        if image:
            embed.set_thumbnail(url=image)

        caster, targets, combat = await targetutils.maybe_combat(ctx, monster, args)
        await attack.automation.run(ctx, embed, caster, targets, args, combat=combat, title=embed.title)
        if combat:
            await combat.final()

        _fields = args.get('f')
        embeds.add_fields_from_args(embed, _fields)
        embed.colour = random.randint(0, 0xffffff)

        if monster.source == 'homebrew':
            embeds.add_homebrew_footer(embed)

        await ctx.send(embed=embed)
Beispiel #3
0
 async def dice_roll_save(self, gctx, roll_request):
     """Save: Display like ``!s``."""
     save_name = roll_request.action
     if save_name in constants.STAT_ABBREVIATIONS:
         save_name = verbose_stat(save_name)
     await self._dice_roll_embed_common(gctx,
                                        roll_request,
                                        "{name} makes {save} Save!",
                                        save=a_or_an(save_name.title()))
Beispiel #4
0
 async def dice_roll_check(self, gctx, roll_request):
     """Check: Display like ``!c``. Requires character - if not imported falls back to default roll."""
     check_name = roll_request.action
     if check_name in constants.STAT_ABBREVIATIONS:
         check_name = verbose_stat(check_name)
     await self._dice_roll_embed_common(gctx,
                                        roll_request,
                                        "{name} makes {check} check!",
                                        check=a_or_an(check_name.title()))
Beispiel #5
0
async def run_attack(ctx, embed, args, caster, attack, targets, combat):
    """
    Runs an attack: adds title, handles -f and -thumb args, commits combat, runs automation, edits embed.

    :type ctx: discord.ext.commands.Context
    :type embed: discord.Embed
    :type args: utils.argparser.ParsedArguments
    :type caster: cogs5e.models.sheet.statblock.StatBlock
    :type attack: cogs5e.models.sheet.attack.Attack
    :type targets: list of str or list of cogs5e.models.sheet.statblock.StatBlock
    :type combat: None or cogs5e.models.initiative.Combat
    :rtype: cogs5e.models.automation.AutomationResult
    """
    if not args.last('h', type_=bool):
        name = caster.get_title_name()
    else:
        name = "An unknown creature"

    if not attack.proper:
        attack_name = a_or_an(attack.name)
    else:
        attack_name = attack.name

    verb = attack.verb or "attacks with"

    if args.last('title') is not None:
        embed.title = args.last('title') \
            .replace('[name]', name) \
            .replace('[aname]', attack_name)
    else:
        embed.title = f'{name} {verb} {attack_name}!'

    # arg overrides (#1163)
    arg_defaults = {
        'criton': attack.criton,
        'phrase': attack.phrase,
        'thumb': attack.thumb,
        'c': attack.extra_crit_damage
    }
    args.update_nx(arg_defaults)

    result = await attack.automation.run(ctx,
                                         embed,
                                         caster,
                                         targets,
                                         args,
                                         combat=combat,
                                         title=embed.title)
    if combat:
        await combat.final()

    embeds.add_fields_from_args(embed, args.get('f'))
    if 'thumb' in args:
        embed.set_thumbnail(url=args.last('thumb'))

    return result
    async def save(self, ctx, skill, *, args: str = ''):
        """Rolls a save for your current active character.
        __Valid Arguments__
        adv/dis
        -b [conditional bonus]
        -phrase [flavor text]
        -title [title] *note: [charname] and [sname] will be replaced automatically*
        -image [image URL]
        -dc [dc] (does not apply to Death Saves)
        -rr [iterations] (does not apply to Death Saves)"""
        if skill == 'death':
            ds_cmd = self.bot.get_command('game deathsave')
            if ds_cmd is None:
                return await ctx.send("Error: GameTrack cog not loaded.")
            return await ctx.invoke(ds_cmd, *shlex.split(args))

        char = await Character.from_ctx(ctx)
        saves = char.get_saves()
        if not saves:
            return await ctx.send('You must update your character sheet first.')
        try:
            save = next(a for a in saves.keys() if skill.lower() == a.lower())
        except StopIteration:
            try:
                save = next(a for a in saves.keys() if skill.lower() in a.lower())
            except StopIteration:
                return await ctx.send('That\'s not a valid save.')

        embed = EmbedWithCharacter(char, name=False)

        skill_effects = char.get_skill_effects()
        args += ' ' + skill_effects.get(save, '')  # dicecloud v11 - autoadv

        args = await self.new_arg_stuff(args, ctx, char)
        adv = args.adv()
        b = args.join('b', '+')
        phrase = args.join('phrase', '\n')
        iterations = min(args.last('rr', 1, int), 25)
        dc = args.last('dc', type_=int)
        num_successes = 0

        formatted_d20 = format_d20(adv, char.get_setting('reroll'))

        if b is not None:
            roll_str = formatted_d20 + '{:+}'.format(saves[save]) + '+' + b
        else:
            roll_str = formatted_d20 + '{:+}'.format(saves[save])

        embed.title = args.last('title', '') \
                          .replace('[charname]', char.get_name()) \
                          .replace('[sname]', camel_to_title(save)) \
                      or '{} makes {}!'.format(char.get_name(), a_or_an(camel_to_title(save)))

        if iterations > 1:
            embed.description = (f"**DC {dc}**\n" if dc else '') + ('*' + phrase + '*' if phrase is not None else '')
            for i in range(iterations):
                result = roll(roll_str, adv=adv, inline=True)
                if dc and result.total >= dc:
                    num_successes += 1
                embed.add_field(name=f"Save {i+1}", value=result.skeleton)
            if dc:
                embed.set_footer(text=f"{num_successes} Successes | {iterations - num_successes} Failues")
        else:
            result = roll(roll_str, adv=adv, inline=True)
            if dc:
                embed.set_footer(text="Success!" if result.total >= dc else "Failure!")
            embed.description = (f"**DC {dc}**\n" if dc else '') + result.skeleton + (
                '\n*' + phrase + '*' if phrase is not None else '')

        embeds.add_fields_from_args(embed, args.get('f'))

        if args.last('image') is not None:
            embed.set_thumbnail(url=args.last('image'))

        await ctx.send(embed=embed)
        try:
            await ctx.message.delete()
        except:
            pass
Beispiel #7
0
 def get_title_name(self):
     """Returns a monster's name for use in embed titles."""
     return a_or_an(self.name, upper=True) if not self.proper else self.name
Beispiel #8
0
    async def attack(self, ctx, atk_name=None, *, args: str = ''):
        """Rolls an attack for the current active character.
        __Valid Arguments__
        adv/dis
        adv#/dis# (applies adv to the first # attacks)
        ea (Elven Accuracy double advantage)
        
        -ac [target ac]
        -t [target]
        
        -b [to hit bonus]
        -criton [a number to crit on if rolled on or above]
        -d [damage bonus]
        -d# [applies damage to the first # hits]
        -c [damage bonus on crit]
        -rr [times to reroll]
        -mi [minimum weapon dice roll]
        
        -resist [damage resistance]
        -immune [damage immunity]
        -vuln [damage vulnerability]
        -neutral [damage non-resistance]
        
        hit (automatically hits)
        miss (automatically misses)
        crit (automatically crit)
        max (deals max damage)
        
        -phrase [flavor text]
        -title [title] *note: [name] and [aname] will be replaced automatically*
        -f "Field Title|Field Text" (see !embed)
        [user snippet]"""
        if atk_name is None:
            return await ctx.invoke(self.attack_list)

        char: Character = await Character.from_ctx(ctx)

        attack = await search_and_select(ctx, char.attacks, atk_name,
                                         lambda a: a.name)

        args = await self.new_arg_stuff(args, ctx, char)

        embed = EmbedWithCharacter(char, name=False)
        if args.last('title') is not None:
            embed.title = args.last('title') \
                .replace('[name]', char.name) \
                .replace('[aname]', attack.name)
        else:
            embed.title = '{} attacks with {}!'.format(char.name,
                                                       a_or_an(attack.name))

        caster, targets, combat = await targetutils.maybe_combat(
            ctx, char, args.get('t'))
        await Automation.from_attack(attack).run(ctx,
                                                 embed,
                                                 caster,
                                                 targets,
                                                 args,
                                                 combat=combat,
                                                 title=embed.title)
        if combat:
            await combat.final()

        _fields = args.get('f')
        embeds.add_fields_from_args(embed, _fields)

        await ctx.send(embed=embed)
        try:
            await ctx.message.delete()
        except:
            pass
Beispiel #9
0
    async def monster_check(self, ctx, monster_name, check, *args):
        """Rolls a check for a monster.
        __Valid Arguments__
        adv/dis
        -b [conditional bonus]
        -phrase [flavor text]
        -title [title] *note: [mname] and [cname] will be replaced automatically*
        -dc [dc]
        -rr [iterations]
        str/dex/con/int/wis/cha (different skill base; e.g. Strength (Intimidation))
        -h (hides name and image of monster)"""

        monster: Monster = await select_monster_full(ctx, monster_name)
        self.bot.rdb.incr('monsters_looked_up_life')

        monster_name = monster.get_title_name()
        skill_key = await search_and_select(ctx, SKILL_NAMES, check,
                                            lambda s: s)
        skill_name = camel_to_title(skill_key)

        embed = discord.Embed()
        embed.colour = random.randint(0, 0xffffff)

        args = await scripting.parse_snippets(args, ctx)
        args = argparse(args)

        adv = args.adv(boolwise=True)
        b = args.join('b', '+')
        phrase = args.join('phrase', '\n')
        iterations = min(args.last('rr', 1, int), 25)
        dc = args.last('dc', type_=int)
        num_successes = 0

        skill = monster.skills[skill_key]
        mod = skill.value
        formatted_d20 = skill.d20(base_adv=adv, base_only=True)

        if any(
                args.last(s, type_=bool)
                for s in ("str", "dex", "con", "int", "wis", "cha")):
            base = next(s for s in ("str", "dex", "con", "int", "wis", "cha")
                        if args.last(s, type_=bool))
            mod = mod - monster.get_mod(
                SKILL_MAP[skill_key]) + monster.get_mod(base)
            skill_name = f"{verbose_stat(base)} ({skill_name})"

        skill_name = skill_name.title()
        if not args.last('h', type_=bool):
            default_title = '{} makes {} check!'.format(
                monster_name, a_or_an(skill_name))
        else:
            default_title = f"An unknown creature makes {a_or_an(skill_name)} check!"

        if b is not None:
            roll_str = formatted_d20 + '{:+}'.format(mod) + '+' + b
        else:
            roll_str = formatted_d20 + '{:+}'.format(mod)

        embed.title = args.last('title', '') \
                          .replace('[mname]', monster_name) \
                          .replace('[cname]', skill_name) \
                      or default_title

        if iterations > 1:
            embed.description = (f"**DC {dc}**\n" if dc else '') + (
                '*' + phrase + '*' if phrase is not None else '')
            for i in range(iterations):
                result = roll(roll_str, inline=True)
                if dc and result.total >= dc:
                    num_successes += 1
                embed.add_field(name=f"Check {i + 1}", value=result.skeleton)
            if dc:
                embed.set_footer(
                    text=
                    f"{num_successes} Successes | {iterations - num_successes} Failues"
                )
        else:
            result = roll(roll_str, inline=True)
            if dc:
                embed.set_footer(
                    text="Success!" if result.total >= dc else "Failure!")
            embed.description = (
                f"**DC {dc}**\n" if dc else '') + result.skeleton + (
                    '\n*' + phrase + '*' if phrase is not None else '')

        embeds.add_fields_from_args(embed, args.get('f'))

        if args.last('image') is not None:
            embed.set_thumbnail(url=args.last('image'))
        elif not args.last('h', type_=bool):
            embed.set_thumbnail(url=monster.get_image_url())

        if monster.source == 'homebrew':
            embed.set_footer(
                text="Homebrew content.",
                icon_url="https://avrae.io/assets/img/homebrew.png")

        await ctx.send(embed=embed)
        try:
            await ctx.message.delete()
        except:
            pass
Beispiel #10
0
    async def monster_check(self, ctx, monster_name, check, *args):
        """Rolls a check for a monster.
        Args: adv/dis
              -b [conditional bonus]
              -phrase [flavor text]
              -title [title] *note: [mname] and [cname] will be replaced automatically*
              -dc [dc]
              -rr [iterations]
              str/dex/con/int/wis/cha (different skill base; e.g. Strength (Intimidation))"""

        monster: Monster = await select_monster_full(ctx, monster_name)
        self.bot.rdb.incr('monsters_looked_up_life')

        monster_name = monster.get_title_name()
        skills = monster.skills

        try:
            skill = next(a for a in skills.keys()
                         if check.lower() == a.lower())
        except StopIteration:
            try:
                skill = next(a for a in skills.keys()
                             if check.lower() in a.lower())
            except StopIteration:
                return await self.bot.say('That\'s not a valid check.')

        embed = discord.Embed()
        embed.colour = random.randint(0, 0xffffff)

        args = argparse(args)
        adv = args.adv()
        b = args.join('b', '+')
        phrase = args.join('phrase', '\n')
        formatted_d20 = '1d20' if adv == 0 else '2d20' + (
            'kh1' if adv == 1 else 'kl1')
        iterations = min(args.last('rr', 1, int), 25)
        dc = args.last('dc', type_=int)
        num_successes = 0

        mod = skills[skill]
        skill_name = skill
        if any(
                args.last(s, type_=bool)
                for s in ("str", "dex", "con", "int", "wis", "cha")):
            base = next(s for s in ("str", "dex", "con", "int", "wis", "cha")
                        if args.last(s, type_=bool))
            mod = mod - monster.get_mod(
                SKILL_MAP[skill]) + monster.get_mod(base)
            skill_name = f"{verbose_stat(base)} ({skill})"

        skill_name = skill_name.title()
        default_title = '{} makes {} check!'.format(monster_name,
                                                    a_or_an(skill_name))

        if b is not None:
            roll_str = formatted_d20 + '{:+}'.format(mod) + '+' + b
        else:
            roll_str = formatted_d20 + '{:+}'.format(mod)

        embed.title = args.last('title', '') \
                          .replace('[mname]', monster_name) \
                          .replace('[cname]', skill_name) \
                      or default_title

        if iterations > 1:
            embed.description = (f"**DC {dc}**\n" if dc else '') + (
                '*' + phrase + '*' if phrase is not None else '')
            for i in range(iterations):
                result = roll(roll_str, adv=adv, inline=True)
                if dc and result.total >= dc:
                    num_successes += 1
                embed.add_field(name=f"Check {i+1}", value=result.skeleton)
            if dc:
                embed.set_footer(
                    text=
                    f"{num_successes} Successes | {iterations - num_successes} Failues"
                )
        else:
            result = roll(roll_str, adv=adv, inline=True)
            if dc:
                embed.set_footer(
                    text="Success!" if result.total >= dc else "Failure!")
            embed.description = (
                f"**DC {dc}**\n" if dc else '') + result.skeleton + (
                    '\n*' + phrase + '*' if phrase is not None else '')

        embeds.add_fields_from_args(embed, args.get('f'))

        if args.last('image') is not None:
            embed.set_thumbnail(url=args.last('image'))
        else:
            embed.set_thumbnail(url=monster.get_image_url())

        if monster.source == 'homebrew':
            embed.set_footer(text="Homebrew content.",
                             icon_url="https://avrae.io/static/homebrew.png")

        await self.bot.say(embed=embed)
        try:
            await self.bot.delete_message(ctx.message)
        except:
            pass
Beispiel #11
0
def sheet_attack(attack, args, embed=None):
    """
    :param attack: (dict) The attack to roll
    :param args: (dict) Metadata arguments
    :param embed: (discord.Embed) if supplied, will use as base. If None, will create one.
    :returns: a dict with structure {"embed": discord.Embed(), "result": {metadata}}"""
    # print(args)
    if embed is None:
        embed = discord.Embed()
    total_damage = 0
    dnum_keys = [k for k in args.keys()
                 if re.match(r'd\d+', k)]  # ['d1', 'd2'...]
    dnum = {}
    for k in dnum_keys:  # parse d# args
        for dmg in args[k].split('|'):
            try:
                dnum[dmg] = int(k.split('d')[-1])
            except ValueError:
                embed.title = "Error"
                embed.colour = 0xff0000
                embed.description = "Malformed tag: {}".format(k)
                return {"embed": embed, "total_damage": 0}

    advnum_keys = [k for k in args.keys() if re.match(r'(adv|dis)\d+', k)]
    advnum = {}
    for k in advnum_keys:  # parse adv# args
        m = re.match(r'(adv|dis)(\d+)', k)
        _adv = m.group(1)
        num = int(m.group(2))
        advnum[_adv] = num

    if args.get('phrase') is not None:  # parse phrase
        embed.description = '*' + args.get('phrase') + '*'
    else:
        embed.description = '~~' + ' ' * 500 + '~~'

    if args.get('title') is not None:
        embed.title = args.get('title').replace(
            '[charname]', args.get('name')).replace(
                '[aname]', attack.get('name')).replace('[target]',
                                                       args.get('t', ''))
    elif args.get('t') is not None:  # parse target
        embed.title = '{} attacks with {} at {}!'.format(
            args.get('name'), a_or_an(attack.get('name')), args.get('t'))
    else:
        embed.title = '{} attacks with {}!'.format(args.get('name'),
                                                   a_or_an(attack.get('name')))

    for arg in ('rr', 'ac'):  # parse reroll/ac
        try:
            args[arg] = int(args.get(arg, None))
        except (ValueError, TypeError):
            args[arg] = None
    args['adv'] = 0 if args.get(
        'adv', False) and args.get('dis', False) else 1 if args.get(
            'adv', False) else -1 if args.get('dis', False) else 0
    args['adv'] = 2 if args.get(
        'ea', False) and not args.get('dis', False) else args['adv']
    args['crit'] = 1 if args.get('crit', False) else None
    args['hit'] = 1 if args.get('hit', False) else None
    for r in range(args.get('rr', 1) or 1):  # start rolling attacks
        out = ''
        itercrit = 0
        if attack.get('attackBonus') is None and args.get('b') is not None:
            attack['attackBonus'] = '0'
        if attack.get('attackBonus') is not None and not args.get('hit'):
            adv = args.get('adv')
            for _adv, numHits in advnum.items():
                if numHits > 0:
                    adv = 1 if _adv == 'adv' else -1
                    advnum[_adv] -= 1
            formatted_d20 = ('1d20' if adv == 0 else '2d20' + (
                'kh1' if adv == 1 else 'kl1') if not adv == 2 else '3d20kh1') \
                            + ('ro{}'.format(args.get('reroll', 0))
                               if int(args.get('reroll', 0)) else '')
            if args.get('b') is not None:
                toHit = roll(f'{formatted_d20}+' + attack.get('attackBonus') +
                             '+' + args.get('b'),
                             rollFor='To Hit',
                             inline=True,
                             show_blurbs=False)
            else:
                toHit = roll(f'{formatted_d20}+' + attack.get('attackBonus'),
                             rollFor='To Hit',
                             inline=True,
                             show_blurbs=False)

            try:
                parts = len(toHit.raw_dice.parts)
            except:
                parts = 0

            if parts > 0:
                out += toHit.result + '\n'
                try:
                    raw = next(p for p in toHit.raw_dice.parts
                               if isinstance(p, SingleDiceGroup)
                               and p.max_value == 20).get_total()
                except StopIteration:
                    raw = 0
                if raw >= (int(args.get('criton', 20)) or 20):
                    itercrit = 1
                else:
                    itercrit = toHit.crit
                if args.get('ac') is not None:
                    if toHit.total < args.get('ac') and itercrit == 0:
                        itercrit = 2  # miss!
                if args.get('crit') and itercrit < 2:
                    itercrit = args.get('crit', 0)
            else:  # output wherever was there if error
                out += "**To Hit**: " + attack.get('attackBonus') + '\n'
        else:
            if args.get('hit'):
                out += "**To Hit**: Automatic hit!\n"
            if args.get('crit'):
                itercrit = args.get('crit', 0)
            else:
                itercrit = 0

        if attack.get('damage') is None and args.get('d') is not None:
            attack['damage'] = '0'
        if attack.get('damage') is not None:

            def parsecrit(damage_str, wep=False):
                if itercrit == 1:
                    if args.get('crittype') == '2x':
                        critDice = f"({damage_str})*2"
                        if args.get('c') is not None:
                            critDice += '+' + args.get('c', '')
                    else:

                        def critSub(matchobj):
                            hocrit = 1 if args.get('hocrit') and wep else 0
                            return str(int(matchobj.group(1)) * 2 +
                                       hocrit) + 'd' + matchobj.group(2)

                        critDice = re.sub(r'(\d+)d(\d+)', critSub, damage_str)
                else:
                    critDice = damage_str
                return critDice

            # -d, -d# parsing
            if args.get('d') is not None:
                damage = parsecrit(attack.get('damage'),
                                   wep=True) + '+' + parsecrit(args.get('d'))
            else:
                damage = parsecrit(attack.get('damage'), wep=True)

            for dice, numHits in dnum.items():
                if not itercrit == 2 and numHits > 0:
                    damage += '+' + parsecrit(dice)
                    dnum[dice] -= 1

            # crit parsing
            rollFor = "Damage"
            if itercrit == 1:
                if args.get('c') is not None:
                    damage += '+' + args.get('c', '')
                rollFor = "Damage (CRIT!)"
            elif itercrit == 2:
                rollFor = "Damage (Miss!)"

            # resist parsing
            if 'resist' in args or 'immune' in args or 'vuln' in args:
                resistances = args.get('resist', '').split('|')
                immunities = args.get('immune', '').split('|')
                vulnerabilities = args.get('vuln', '').split('|')
                damage = parse_resistances(damage, resistances, immunities,
                                           vulnerabilities)

            # actual roll
            if itercrit == 2 and not args.get('showmiss', False):
                out += '**Miss!**\n'
            else:
                dmgroll = roll(damage,
                               rollFor=rollFor,
                               inline=True,
                               show_blurbs=False)
                out += dmgroll.result + '\n'
                if not itercrit == 2:  # if we actually hit
                    total_damage += dmgroll.total

        if out is not '':
            if (args.get('rr', 1) or 1) > 1:
                embed.add_field(name='Attack {}'.format(r + 1),
                                value=out,
                                inline=False)
            else:
                embed.add_field(name='Attack', value=out, inline=False)

    if (args.get('rr', 1) or 1) > 1 and attack.get('damage') is not None:
        embed.add_field(name='Total Damage', value=str(total_damage))

    if attack.get('details') is not None:
        embed.add_field(name='Effect', value=(attack.get('details', '')))

    if args.get('image') is not None:
        embed.set_thumbnail(url=args.get('image'))

    return {'embed': embed, 'total_damage': total_damage}
    async def check(self, ctx, check, *, args: str = ''):
        """Rolls a check for your current active character.
        __Valid Arguments__
        adv/dis
        -b [conditional bonus]
        -mc [minimum roll]
        -phrase [flavor text]
        -title [title] *note: [charname] and [cname] will be replaced automatically*
        -dc [dc]
        -rr [iterations]
        str/dex/con/int/wis/cha (different skill base; e.g. Strength (Intimidation))
        """
        char = await Character.from_ctx(ctx)
        skills = char.get_skills()
        if not skills:
            return await ctx.send('You must update your character sheet first.')
        try:
            skill = next(a for a in skills.keys() if check.lower() == a.lower())#this checks for the skill exactly
        except StopIteration:
            try:
                skill = next(a for a in skills.keys() if check.lower() in a.lower())#this checks for the partial name of the skill
            except StopIteration:
                try:
                    # Probably will be fairly slow, but whatever
                    skill = next(SKILL_ALIASES[alias] for alias in SKILL_ALIASES.keys() if check.lower() == alias.lower())#go through our alias names
                except StopIteration:
                    return await ctx.send('That\'s not a valid check.')

        embed = EmbedWithCharacter(char, False)

        skill_effects = char.get_skill_effects()
        args += ' ' + skill_effects.get(skill, '')  # dicecloud v7 - autoadv

        args = await self.new_arg_stuff(args, ctx, char)
        adv = args.adv()
        b = args.join('b', '+')
        phrase = args.join('phrase', '\n')
        iterations = min(args.last('rr', 1, int), 25)
        dc = args.last('dc', type_=int)
        num_successes = 0

        formatted_d20 = format_d20(adv, char.get_setting('reroll'))

        mc = args.last('mc', None)
        if mc:
            formatted_d20 = f"{formatted_d20}mi{mc}"

        mod = skills[skill]
        skill_name = skill
        if any(args.last(s, type_=bool) for s in ("str", "dex", "con", "int", "wis", "cha")):
            base = next(s for s in ("str", "dex", "con", "int", "wis", "cha") if args.last(s, type_=bool))
            mod = mod - char.get_mod(SKILL_MAP[skill]) + char.get_mod(base)
            skill_name = f"{verbose_stat(base)} ({skill})"

        skill_name = camel_to_title(skill_name)
        default_title = '{} makes {} check!'.format(char.get_name(), a_or_an(skill_name))

        if b is not None:
            roll_str = formatted_d20 + '{:+}'.format(mod) + '+' + b
        else:
            roll_str = formatted_d20 + '{:+}'.format(mod)

        embed.title = args.last('title', '') \
                          .replace('[charname]', char.get_name()) \
                          .replace('[cname]', skill_name) \
                      or default_title

        if iterations > 1:
            embed.description = (f"**DC {dc}**\n" if dc else '') + ('*' + phrase + '*' if phrase is not None else '')
            for i in range(iterations):
                result = roll(roll_str, adv=adv, inline=True)
                if dc and result.total >= dc:
                    num_successes += 1
                embed.add_field(name=f"Check {i+1}", value=result.skeleton)
            if dc:
                embed.set_footer(text=f"{num_successes} Successes | {iterations - num_successes} Failues")
        else:
            result = roll(roll_str, adv=adv, inline=True)
            if dc:
                embed.set_footer(text="Success!" if result.total >= dc else "Failure!")
            embed.description = (f"**DC {dc}**\n" if dc else '') + result.skeleton + (
                '\n*' + phrase + '*' if phrase is not None else '')

        embeds.add_fields_from_args(embed, args.get('f'))

        if args.last('image') is not None:
            embed.set_thumbnail(url=args.last('image'))
        await ctx.send(embed=embed)
        try:
            await ctx.message.delete()
        except:
            pass
Beispiel #13
0
    async def monster_save(self, ctx, monster_name, save, *args):
        """Rolls a save for a monster.
        Args: adv/dis
              -b [conditional bonus]
              -phrase [flavor text]
              -title [title] *note: [mname] and [cname] will be replaced automatically*
              -dc [dc]
              -rr [iterations]"""

        monster: Monster = await select_monster_full(ctx, monster_name)
        self.bot.db.incr('monsters_looked_up_life')
        monster_name = monster.get_title_name()

        saves = monster.saves

        try:
            save = next(a for a in saves.keys() if save.lower() == a.lower())
        except StopIteration:
            try:
                save = next(a for a in saves.keys()
                            if save.lower() in a.lower())
            except StopIteration:
                return await self.bot.say('That\'s not a valid save.')

        embed = discord.Embed()
        embed.colour = random.randint(0, 0xffffff)

        args = parse_args_3(args)
        adv = 0 if args.get(
            'adv', []) and args.get('dis', []) else 1 if args.get(
                'adv', False) else -1 if args.get('dis', False) else 0
        b = "+".join(args.get('b', [])) or None
        phrase = '\n'.join(args.get('phrase', [])) or None
        iterations = min(int(args.get('rr', [1])[-1]), 25)
        try:
            dc = int(args.get('dc', [None])[-1])
        except (ValueError, TypeError):
            dc = None
        num_successes = 0

        if b is not None:
            roll_str = '1d20{:+}'.format(saves[save]) + '+' + b
        else:
            roll_str = '1d20{:+}'.format(saves[save])

        embed.title = args.get('title', [''])[-1].replace('[mname]', monster_name).replace('[sname]', re.sub(
            r'((?<=[a-z])[A-Z]|(?<!\A)[A-Z](?=[a-z]))', r' \1', save).title()) or \
                      '{} makes {}!'.format(monster_name,
                                            a_or_an(re.sub(
                                                r'((?<=[a-z])[A-Z]|(?<!\A)[A-Z](?=[a-z]))',
                                                r' \1',
                                                save).title()))

        if iterations > 1:
            embed.description = (f"**DC {dc}**\n" if dc else '') + (
                '*' + phrase + '*' if phrase is not None else '')
            for i in range(iterations):
                result = roll(roll_str, adv=adv, inline=True)
                if dc and result.total >= dc:
                    num_successes += 1
                embed.add_field(name=f"Check {i+1}", value=result.skeleton)
            if dc:
                embed.set_footer(
                    text=
                    f"{num_successes} Successes | {iterations - num_successes} Failues"
                )
        else:
            result = roll(roll_str, adv=adv, inline=True)
            if dc:
                embed.set_footer(
                    text="Success!" if result.total >= dc else "Failure!")
            embed.description = (
                f"**DC {dc}**\n" if dc else '') + result.skeleton + (
                    '\n*' + phrase + '*' if phrase is not None else '')

        embeds.add_fields_from_args(embed, args.get('f', []))

        if args.get('image') is not None:
            embed.set_thumbnail(url=args.get('image'))
        else:
            embed.set_thumbnail(url=monster.get_image_url())

        if monster.source == 'homebrew':
            embed.set_footer(text="Homebrew content.",
                             icon_url="https://avrae.io/static/homebrew.png")

        await self.bot.say(embed=embed)
        try:
            await self.bot.delete_message(ctx.message)
        except:
            pass
Beispiel #14
0
def sheet_attack(attack, args, embed=None):
    """
    :param attack: (dict) The attack to roll
    :param args: (dict) Metadata arguments
    :param embed: (discord.Embed) if supplied, will use as base. If None, will create one.
    :returns: a dict with structure {"embed": discord.Embed(), "result": {metadata}}"""
    # print(args)
    if embed is None:
        embed = discord.Embed()

    total_damage = 0

    advnum_keys = [k for k in args if re.match(r'(adv|dis)\d+', k) and args.last(k, type_=bool)]
    advnum = {}
    for k in advnum_keys:  # parse adv# args
        m = re.match(r'(adv|dis)(\d+)', k)
        _adv = m.group(1)
        num = int(m.group(2))
        advnum[_adv] = num

    dnum_keys = [k for k in args if re.match(r'd\d+', k)]  # ['d1', 'd2'...]
    dnum = {}
    for k in dnum_keys:  # parse d# args
        for dmg in args.get(k):
            try:
                dnum[dmg] = int(k.split('d')[-1])
            except ValueError:
                embed.title = "Error"
                embed.colour = 0xff0000
                embed.description = "Malformed tag: {}".format(k)
                return {"embed": embed, "total_damage": 0}

    if args.get('phrase'):  # parse phrase
        embed.description = '*' + args.join('phrase', '\n') + '*'
    else:
        embed.description = '~~' + ' ' * 500 + '~~'

    if args.last('title') is not None:
        embed.title = args.last('title') \
            .replace('[charname]', args.last('name')) \
            .replace('[aname]', attack.get('name')) \
            .replace('[target]', args.last('t', ''))
    elif args.last('t') is not None:  # parse target
        embed.title = '{} attacks with {} at {}!'.format(args.last('name'), a_or_an(attack.get('name')), args.last('t'))
    else:
        embed.title = '{} attacks with {}!'.format(args.last('name'), a_or_an(attack.get('name')))

    if args.last('image') is not None:
        embed.set_thumbnail(url=args.last('image'))

    adv = args.adv(True)
    crit = args.last('crit', None, bool) and 1
    hit = args.last('hit', None, bool) and 1
    miss = (args.last('miss', None, bool) and not hit) and 1
    ac = args.last('ac', type_=int)
    criton = args.last('criton', 20, int)
    rr = min(args.last('rr', 1, int), 25)
    reroll = args.last('reroll', 0, int)
    b = args.join('b', '+')
    h = args.last('h', None, bool)

    if h:
        hidden_embed = copy.copy(embed)
    else:
        hidden_embed = discord.Embed()  # less memory? idek we don't use it anyway

    raw_attacks = []

    for r in range(rr):  # start rolling attacks
        out = ''
        hidden_out = ''
        itercrit = 0
        if attack.get('attackBonus') is None and b:
            attack['attackBonus'] = '0'
        if attack.get('attackBonus') is not None and not (hit or miss):
            iteradv = adv
            for _adv, numHits in advnum.items():
                if numHits > 0:
                    iteradv = 1 if _adv == 'adv' else -1
                    advnum[_adv] -= 1

            formatted_d20 = format_d20(iteradv, reroll)

            if b:
                toHit = roll(f"{formatted_d20}+{attack.get('attackBonus')}+{b}",
                             rollFor='To Hit', inline=True, show_blurbs=False)
            else:
                toHit = roll(f"{formatted_d20}+{attack.get('attackBonus')}", rollFor='To Hit', inline=True,
                             show_blurbs=False)

            try:
                parts = len(toHit.raw_dice.parts)
            except:
                parts = 0

            if parts > 0:
                out += toHit.result + '\n'
                try:
                    raw = next(p for p in toHit.raw_dice.parts if
                               isinstance(p, SingleDiceGroup) and p.max_value == 20).get_total()
                except StopIteration:
                    raw = 0
                if raw >= criton:
                    itercrit = 1
                else:
                    itercrit = toHit.crit
                if ac is not None:
                    if toHit.total < ac and itercrit == 0:
                        itercrit = 2  # miss!
                if crit and itercrit < 2:
                    itercrit = crit
                if ac:
                    hidden_out += f"**To Hit**: {formatted_d20}... = `{HIT_DICT[itercrit]}`\n"
                else:
                    hidden_out += f"**To Hit**: {formatted_d20}... = `{toHit.total}`\n"
            else:  # output wherever was there if error
                out += "**To Hit**: " + attack.get('attackBonus') + '\n'
                hidden_out += "**To Hit**: Unknown"
        else:
            if hit:
                out += "**To Hit**: Automatic hit!\n"
            elif miss:
                out += "**To Hit**: Automatic miss!\n"
            if crit:
                itercrit = crit
            else:
                if miss:
                    itercrit = 2
                else:
                    itercrit = 0

        res = sheet_damage(attack.get('damage'), args, itercrit, dnum)
        out += res['damage']
        if res['roll']:
            hidden_out += f"**Damage**: {res['roll'].consolidated()} = `{res['roll'].total}`"
        else:
            hidden_out += res['damage']
        total_damage += res['total']

        raw_attacks.append({'damage': res['total'], 'crit': itercrit})

        if out is not '':
            if rr > 1:
                embed.add_field(name='Attack {}'.format(r + 1), value=out, inline=False)
                hidden_embed.add_field(name='Attack {}'.format(r + 1), value=hidden_out, inline=False)
            else:
                embed.add_field(name='Attack', value=out, inline=False)
                hidden_embed.add_field(name='Attack', value=hidden_out, inline=False)

    if rr > 1 and attack.get('damage') is not None:
        embed.add_field(name='Total Damage', value=str(total_damage))
        hidden_embed.add_field(name='Total Damage', value=str(total_damage))

    if attack.get('details'):
        embed.add_field(name='Effect',
                        value=attack['details'] if len(attack['details']) < 1020 else f"{attack['details'][:1020]}...")

    out = {'embed': embed, 'total_damage': total_damage, 'full_embed': embed, 'raw_attacks': raw_attacks}
    if h:
        out['embed'] = hidden_embed
    return out
Beispiel #15
0
def sheet_attack(attack, args, embed=None):
    """
    :param attack: (dict) The attack to roll
    :param args: (dict) Metadata arguments
    :param embed: (discord.Embed) if supplied, will use as base. If None, will create one.
    :returns: a dict with structure {"embed": discord.Embed(), "result": {metadata}}"""
    # print(args)
    if embed is None:
        embed = discord.Embed()

    total_damage = 0

    advnum_keys = [k for k in args.keys() if re.match(r'(adv|dis)\d+', k)]
    advnum = {}
    for k in advnum_keys:  # parse adv# args
        m = re.match(r'(adv|dis)(\d+)', k)
        _adv = m.group(1)
        num = int(m.group(2))
        advnum[_adv] = num

    dnum_keys = [k for k in args.keys() if re.match(r'd\d+', k)
                 ]  # ['d1', 'd2'...] TODO should probably move this
    dnum = {}
    for k in dnum_keys:  # parse d# args
        for dmg in args[k].split('|'):
            try:
                dnum[dmg] = int(k.split('d')[-1])
            except ValueError:
                embed.title = "Error"
                embed.colour = 0xff0000
                embed.description = "Malformed tag: {}".format(k)
                return {"embed": embed, "total_damage": 0}

    if args.get('phrase') is not None:  # parse phrase
        embed.description = '*' + args.get('phrase') + '*'
    else:
        embed.description = '~~' + ' ' * 500 + '~~'

    if args.get('title') is not None:
        embed.title = args.get('title').replace(
            '[charname]', args.get('name')).replace(
                '[aname]', attack.get('name')).replace('[target]',
                                                       args.get('t', ''))
    elif args.get('t') is not None:  # parse target
        embed.title = '{} attacks with {} at {}!'.format(
            args.get('name'), a_or_an(attack.get('name')), args.get('t'))
    else:
        embed.title = '{} attacks with {}!'.format(args.get('name'),
                                                   a_or_an(attack.get('name')))

    for arg in ('rr', 'ac'):  # parse reroll/ac
        try:
            args[arg] = int(args.get(arg, None))
        except (ValueError, TypeError):
            args[arg] = None
    args['adv'] = 0 if args.get(
        'adv', False) and args.get('dis', False) else 1 if args.get(
            'adv', False) else -1 if args.get('dis', False) else 0
    args['adv'] = 2 if args.get(
        'ea', False) and not args.get('dis', False) else args['adv']
    args['crit'] = 1 if args.get('crit', False) else None
    args['hit'] = 1 if args.get('hit', False) else None
    args['miss'] = 1 if args.get('miss',
                                 False) and not args.get('hit') else None
    for r in range(args.get('rr', 1) or 1):  # start rolling attacks
        out = ''
        itercrit = 0
        if attack.get('attackBonus') is None and args.get('b') is not None:
            attack['attackBonus'] = '0'
        if attack.get('attackBonus') is not None and not args.get(
                'hit') and not args.get('miss'):
            adv = args.get('adv')
            for _adv, numHits in advnum.items():
                if numHits > 0:
                    adv = 1 if _adv == 'adv' else -1
                    advnum[_adv] -= 1
            formatted_d20 = ('1d20' if adv == 0 else '2d20' + (
                'kh1' if adv == 1 else 'kl1') if not adv == 2 else '3d20kh1') \
                            + ('ro{}'.format(args.get('reroll', 0))
                               if int(args.get('reroll', 0)) else '')
            if args.get('b') is not None:
                toHit = roll(f'{formatted_d20}+' + attack.get('attackBonus') +
                             '+' + args.get('b'),
                             rollFor='To Hit',
                             inline=True,
                             show_blurbs=False)
            else:
                toHit = roll(f'{formatted_d20}+' + attack.get('attackBonus'),
                             rollFor='To Hit',
                             inline=True,
                             show_blurbs=False)

            try:
                parts = len(toHit.raw_dice.parts)
            except:
                parts = 0

            if parts > 0:
                out += toHit.result + '\n'
                try:
                    raw = next(p for p in toHit.raw_dice.parts
                               if isinstance(p, SingleDiceGroup)
                               and p.max_value == 20).get_total()
                except StopIteration:
                    raw = 0
                if raw >= int(args.get('criton', 20) or 20):
                    itercrit = 1
                else:
                    itercrit = toHit.crit
                if args.get('ac') is not None:
                    if toHit.total < args.get('ac') and itercrit == 0:
                        itercrit = 2  # miss!
                if args.get('crit') and itercrit < 2:
                    itercrit = args.get('crit', 0)
            else:  # output wherever was there if error
                out += "**To Hit**: " + attack.get('attackBonus') + '\n'
        else:
            if args.get('hit'):
                out += "**To Hit**: Automatic hit!\n"
            elif args.get('miss'):
                out += "**To Hit**: Automatic miss!\n"
            if args.get('crit'):
                itercrit = args.get('crit', 0)
            else:
                if args.get("miss"):
                    itercrit = 2
                else:
                    itercrit = 0

        res = sheet_damage(attack.get('damage'), args, itercrit, dnum)
        out += res['damage']
        total_damage += res['total']

        if out is not '':
            if (args.get('rr', 1) or 1) > 1:
                embed.add_field(name='Attack {}'.format(r + 1),
                                value=out,
                                inline=False)
            else:
                embed.add_field(name='Attack', value=out, inline=False)

    if (args.get('rr', 1) or 1) > 1 and attack.get('damage') is not None:
        embed.add_field(name='Total Damage', value=str(total_damage))

    if attack.get('details') is not None:
        embed.add_field(name='Effect', value=(attack.get('details', '')))

    if args.get('image') is not None:
        embed.set_thumbnail(url=args.get('image'))

    return {'embed': embed, 'total_damage': total_damage}
Beispiel #16
0
    async def monster_atk(self, ctx, monster_name, atk_name=None, *, args=''):
        """Rolls a monster's attack.
        __Valid Arguments__
        adv/dis
        -ac [target ac]
        -b [to hit bonus]
        -d [damage bonus]
        -d# [applies damage to the first # hits]
        -rr [times to reroll]
        -t [target]
        -phrase [flavor text]
        crit (automatically crit)
        -h (hides monster name, image, and attack details)"""
        if atk_name is None or atk_name == 'list':
            return await ctx.invoke(self.monster_atk_list, monster_name)

        try:
            await ctx.message.delete()
        except:
            pass

        monster = await select_monster_full(ctx, monster_name)
        attacks = monster.attacks
        monster_name = monster.get_title_name()

        attack = await search_and_select(ctx, attacks, atk_name,
                                         lambda a: a['name'])
        args = await scripting.parse_snippets(args, ctx)
        args = argparse(args)
        if not args.last('h', type_=bool):
            name = monster_name
            image = args.get('image') or monster.get_image_url()
        else:
            name = "An unknown creature"
            image = None

        attack = Attack.from_old(attack)

        embed = discord.Embed()
        if args.last('title') is not None:
            embed.title = args.last('title') \
                .replace('[name]', name) \
                .replace('[aname]', attack.name)
        else:
            embed.title = '{} attacks with {}!'.format(name,
                                                       a_or_an(attack.name))

        if image:
            embed.set_thumbnail(url=image)

        caster, targets, combat = await targetutils.maybe_combat(
            ctx, monster, args.get('t'))
        await Automation.from_attack(attack).run(ctx,
                                                 embed,
                                                 caster,
                                                 targets,
                                                 args,
                                                 combat=combat,
                                                 title=embed.title)
        if combat:
            await combat.final()

        _fields = args.get('f')
        embeds.add_fields_from_args(embed, _fields)
        embed.colour = random.randint(0, 0xffffff)

        if monster.source == 'homebrew':
            embeds.add_homebrew_footer(embed)

        await ctx.send(embed=embed)
Beispiel #17
0
def embed_for_action(gctx,
                     action,
                     character,
                     to_hit_roll=None,
                     damage_roll=None):
    """
    Creates an embed for a character performing some action (attack or spell).

    Handles inserting the correct fields for to-hit and damage based on the action's automation and whether the rolls
    are present.

    :type gctx: GameLogEventContext
    :type action: Attack or gamedata.spell.Spell
    :type character: cogs5e.models.character.Character
    :type to_hit_roll: ddb.dice.tree.RollRequestRoll
    :type damage_roll: ddb.dice.tree.RollRequestRoll
    """
    embed = EmbedWithCharacter(character, name=False)
    automation = action.automation
    waiting_for_damage = False

    # set title
    if isinstance(action, Attack):
        attack_name = a_or_an(
            action.name) if not action.proper else action.name
        verb = action.verb or "attacks with"
        embed.title = f'{character.get_title_name()} {verb} {attack_name}!'
    else:  # spell
        embed.title = f'{character.get_title_name()} casts {action.name}!'

    # add to hit (and damage, either if it is provided or the action expects damage and it is not provided)
    meta_rolls = []
    if to_hit_roll is not None:
        meta_rolls.append(f"**To Hit**: {str(to_hit_roll.to_d20())}")
    if damage_roll is not None:
        if damage_roll.roll_kind == ddb.dice.RollKind.CRITICAL_HIT:
            meta_rolls.append(
                f"**Damage (CRIT!)**: {str(damage_roll.to_d20())}")
        else:
            meta_rolls.append(f"**Damage**: {str(damage_roll.to_d20())}")
    elif automation_has_damage(automation):
        meta_rolls.append("**Damage**: Waiting for roll...")
        waiting_for_damage = True

    # add dcs, texts
    if automation:
        for effect in automation_dfg(automation,
                                     enter_filter=action_enter_filter):
            # break if we see a damage and are waiting on a damage roll
            if effect.type == 'damage' and waiting_for_damage:
                break

            # note: while certain fields here are AnnotatedStrings, it should never be annotated directly from the sheet
            # and GameLog events cannot trigger custom attacks, so this should be fine

            # save: add the DC
            if effect.type == 'save':
                meta_rolls.append(
                    f"**DC**: {effect.dc}\n{effect.stat[:3].upper()} Save")
            # text: add the text as a field
            elif effect.type == 'text':
                embed.add_field(name="Effect", value=effect.text, inline=False)

    embed.insert_field_at(0,
                          name="Meta",
                          value='\n'.join(meta_rolls),
                          inline=False)

    # set footer
    embed.set_footer(text=f"Rolled in {gctx.campaign.campaign_name}",
                     icon_url=constants.DDB_LOGO_ICON)
    return embed
Beispiel #18
0
    async def attack(self, ctx, atk_name=None, *, args: str = ''):
        """Rolls an attack for the current active character.
        __Valid Arguments__
        -t "<target>" - Sets targets for the attack. You can pass as many as needed. Will target combatants if channel is in initiative.
        -t "<target>|<args>" - Sets a target, and also allows for specific args to apply to them. (e.g, -t "OR1|hit" to force the attack against OR1 to hit)

        *adv/dis*
        *ea* (Elven Accuracy double advantage)
        
        -ac [target ac]
        -t [target]
        
        *-b* [to hit bonus]
        -criton [a number to crit on if rolled on or above]
        *-d* [damage bonus]
        *-c* [damage bonus on crit]
        -rr [times to reroll]
        *-mi* [minimum weapon dice roll]
        
        *-resist* [damage resistance]
        *-immune* [damage immunity]
        *-vuln* [damage vulnerability]
        *-neutral* [damage non-resistance]
        
        *hit* (automatically hits)
        *miss* (automatically misses)
        *crit* (automatically crit)
        *max* (deals max damage)

        -h (hides rolled values)
        -phrase [flavor text]
        -title [title] *note: [name] and [aname] will be replaced automatically*
        -thumb [url]
        -f "Field Title|Field Text" (see !embed)
        [user snippet]

        An italicized argument means the argument supports ephemeral arguments - e.g. `-d1` applies damage to the first hit, `-b1` applies a bonus to one attack, and so on."""
        if atk_name is None:
            return await ctx.invoke(self.attack_list)

        char: Character = await Character.from_ctx(ctx)
        args = await self.new_arg_stuff(args, ctx, char)

        caster, targets, combat = await targetutils.maybe_combat(ctx, char, args)
        attack = await search_and_select(ctx, caster.attacks, atk_name, lambda a: a.name)

        embed = EmbedWithCharacter(char, name=False)
        if args.last('title') is not None:
            embed.title = args.last('title') \
                .replace('[name]', char.name) \
                .replace('[aname]', attack.name)
        else:
            embed.title = '{} attacks with {}!'.format(char.name, a_or_an(attack.name))

        await attack.automation.run(ctx, embed, caster, targets, args, combat=combat, title=embed.title)
        if combat:
            await combat.final()

        _fields = args.get('f')
        embeds.add_fields_from_args(embed, _fields)
        if 'thumb' in args:
            embed.set_thumbnail(url=args.last('thumb'))

        await ctx.send(embed=embed)
        try:
            await ctx.message.delete()
        except:
            pass