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 cast(self, ctx, spell_name, *, args=''):
        """Casts a spell.
        __Valid Arguments__
        -i - Ignores Spellbook restrictions, for demonstrations or rituals.
        -l <level> - Specifies the level to cast the spell at.
        noconc - Ignores concentration requirements.
        -h - Hides rolled values.
        **__Save Spells__**
        -dc <Save DC> - Overrides the spell save DC.
        -save <Save type> - Overrides the spell save type.
        -d <damage> - Adds additional damage.
        pass - Target automatically succeeds save.
        fail - Target automatically fails save.
        adv/dis - Target makes save at advantage/disadvantage.
        **__Attack Spells__**
        See `!a`.
        **__All Spells__**
        -phrase <phrase> - adds flavor text.
        -title <title> - changes the title of the cast. Replaces [sname] with spell name.
        -thumb <url> - adds an image to the cast.
        -dur <duration> - changes the duration of any effect applied by the spell.
        -mod <spellcasting mod> - sets the value of the spellcasting ability modifier.
        int/wis/cha - different skill base for DC/AB (will not account for extra bonuses)
        """
        try:
            await ctx.message.delete()
        except:
            pass

        char: Character = await Character.from_ctx(ctx)

        args = await helpers.parse_snippets(args, ctx)
        args = await char.parse_cvars(args, ctx)
        args = argparse(args)

        if not args.last('i', type_=bool):
            spell = await select_spell_full(
                ctx,
                spell_name,
                list_filter=lambda s: s.name in char.spellbook)
        else:
            spell = await select_spell_full(ctx, spell_name)

        caster, targets, combat = await targetutils.maybe_combat(
            ctx, char, args)
        result = await spell.cast(ctx, caster, targets, args, combat=combat)

        embed = result['embed']
        embed.colour = char.get_color()
        embed.set_thumbnail(url=char.image)

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

        # save changes: combat state, spell slot usage
        await char.commit(ctx)
        if combat:
            await combat.final()
        await ctx.send(embed=embed)
Beispiel #3
0
async def _run_common(ctx, embed, args, caster, action, targets, combat):
    """
    Common automation runner for attacks/actions

    :type ctx: discord.ext.commands.Context
    :type embed: discord.Embed
    :type args: utils.argparser.ParsedArguments
    :type caster: cogs5e.models.sheet.statblock.StatBlock
    :type action: cogs5e.models.sheet.attack.Attack or cogs5e.models.sheet.action.Action
    :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
    """
    result = await action.automation.run(ctx,
                                         embed,
                                         caster,
                                         targets,
                                         args,
                                         combat=combat,
                                         title=embed.title)
    if combat:
        await combat.final()
    # commit character only if we have not already committed it via combat final
    if result.caster_needs_commit and hasattr(
            caster,
            'commit') and not (combat and caster in combat.get_combatants()):
        await caster.commit(ctx)

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

    return result
Beispiel #4
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 #5
0
    async def _cast(self, ctx, combatant_name, spell_name, args):
        args = await scripting.parse_snippets(args, ctx)
        combat = await Combat.from_ctx(ctx)

        if combatant_name is None:
            combatant = combat.current_combatant
            if combatant is None:
                return await ctx.send(
                    f"You must start combat with `{ctx.prefix}init next` first."
                )
        else:
            try:
                combatant = await combat.select_combatant(
                    combatant_name, "Select the caster.")
                if combatant is None:
                    return await ctx.send("Combatant not found.")
            except SelectionException:
                return await ctx.send("Combatant not found.")

        if isinstance(combatant, CombatantGroup):
            return await ctx.send("Groups cannot cast spells.")

        is_character = isinstance(combatant, PlayerCombatant)

        if is_character and combatant.character_owner == str(ctx.author.id):
            args = await combatant.character.parse_cvars(args, ctx)
        args = shlex.split(args)
        args = argparse(args)

        if not args.last('i', type_=bool):
            spell = await select_spell_full(
                ctx,
                spell_name,
                list_filter=lambda s: s.name.lower(
                ) in combatant.spellcasting.lower_spells)
        else:
            spell = await select_spell_full(ctx, spell_name)

        targets = []
        for i, t in enumerate(args.get('t')):
            target = await combat.select_combatant(t,
                                                   f"Select target #{i + 1}.",
                                                   select_group=True)
            if isinstance(target, CombatantGroup):
                targets.extend(target.get_combatants())
            else:
                targets.append(target)

        result = await spell.cast(ctx, combatant, targets, args, combat=combat)

        embed = result['embed']
        embed.colour = random.randint(
            0,
            0xffffff) if not is_character else combatant.character.get_color()
        add_fields_from_args(embed, args.get('f'))
        await ctx.send(embed=embed)
        await combat.final()
Beispiel #6
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
Beispiel #7
0
    async def monster_atk(self,
                          ctx,
                          monster_name,
                          atk_name='list',
                          *,
                          args=''):
        """Rolls a monster's attack.
        Attack name can be "list" for a list of all of the monster's attacks.
        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)"""

        try:
            await self.bot.delete_message(ctx.message)
        except:
            pass

        monster = await select_monster_full(ctx, monster_name)
        self.bot.rdb.incr('monsters_looked_up_life')
        attacks = monster.attacks
        monster_name = monster.get_title_name()
        if atk_name == 'list':
            attacks_string = '\n'.join(
                "**{0}:** +{1} To Hit, {2} damage.".format(
                    a['name'], a['attackBonus'], a['damage'] or 'no')
                for a in attacks)
            return await self.bot.say("{}'s attacks:\n{}".format(
                monster_name, attacks_string))
        attack = fuzzy_search(attacks, 'name', atk_name)
        if attack is None:
            return await self.bot.say("No attack with that name found.",
                                      delete_after=15)

        args = shlex.split(args)
        args = argparse(args)
        args['name'] = [monster_name]
        args['image'] = args.get('image') or [monster.get_image_url()]
        attack['details'] = attack.get('desc') or attack.get('details')

        result = sheet_attack(attack, args)
        embed = result['embed']
        embed.colour = random.randint(0, 0xffffff)
        embeds.add_fields_from_args(embed, args.get('f'))

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

        await self.bot.say(embed=embed)
Beispiel #8
0
    async def monster_atk(self, ctx, monster_name, atk_name='list', *, args=''):
        """Rolls a monster's attack.
        Attack name can be "list" for a list of all of the monster's attacks.
        __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)"""

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

        monster = await select_monster_full(ctx, monster_name)
        self.bot.rdb.incr('monsters_looked_up_life')
        attacks = monster.attacks
        monster_name = monster.get_title_name()
        if atk_name == 'list':
            attacks_string = '\n'.join("**{0}:** +{1} To Hit, {2} damage.".format(a['name'],
                                                                                  a['attackBonus'],
                                                                                  a['damage'] or 'no') for a in attacks)
            return await ctx.send("{}'s attacks:\n{}".format(monster_name, attacks_string))
        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):
            args['name'] = monster_name
            args['image'] = args.get('image') or monster.get_image_url()
        else:
            args['name'] = "An unknown creature"
        attack['details'] = attack.get('desc') or attack.get('details')

        result = sheet_attack(attack, args)
        embed = result['embed']
        embed.colour = random.randint(0, 0xffffff)
        embeds.add_fields_from_args(embed, args.get('f'))

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

        if args.last('h', type_=bool):
            try:
                await ctx.author.send(embed=result['full_embed'])
            except:
                pass

        await ctx.send(embed=embed)
Beispiel #9
0
    async def cast(self, ctx, spell_name, *, args=''):
        """Casts a spell.
        __Valid Arguments:__
        -i - Ignores Spellbook restrictions, for demonstrations or rituals.
        -l [level] - Specifies the level to cast the spell at.
        noconc - Ignores concentration requirements.
        **__Save Spells__**
        -dc [Save DC] - Default: Pulls a cvar called `dc`.
        -save [Save type] - Default: The spell's default save.
        -d [damage] - adds additional damage.
        **__Attack Spells__**
        See `!a`.
        **__All Spells__**
        -phrase [phrase] - adds flavor text.
        -title [title] - changes the title of the cast. Replaces [sname] with spell name.
        -dur [duration] - changes the duration of any effect applied by the spell.
        -mod [spellcasting mod] - sets the value of the spellcasting ability modifier.
        int/wis/cha - different skill base for DC/AB (will not account for extra bonuses)
        """
        try:
            await ctx.message.delete()
        except:
            pass

        char: Character = await Character.from_ctx(ctx)

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

        if not args.last('i', type_=bool):
            spell = await select_spell_full(ctx, spell_name, list_filter=lambda s: s.name in char.spellbook)
        else:
            spell = await select_spell_full(ctx, spell_name)

        caster, targets, combat = await targetutils.maybe_combat(ctx, char, args.get('t'))
        result = await spell.cast(ctx, caster, targets, args, combat=combat)

        embed = result['embed']
        embed.colour = char.get_color()
        embed.set_thumbnail(url=char.image)

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

        # save changes: spell slot usage
        if combat:
            await combat.final()
        else:
            await char.commit(ctx)
        await ctx.send(embed=embed)
Beispiel #10
0
    async def cast(self, ctx, spell_name, *, args=''):
        """Casts a spell.
        __Valid Arguments:__
        -i - Ignores Spellbook restrictions, for demonstrations or rituals.
        -l [level] - Specifies the level to cast the spell at.
        **__Save Spells__**
        -dc [Save DC] - Default: Pulls a cvar called `dc`.
        -save [Save type] - Default: The spell's default save.
        -d [damage] - adds additional damage.
        **__Attack Spells__**
        See `!a`.
        **__All Spells__**
        -phrase [phrase] - adds flavor text.
        -title [title] - changes the title of the cast. Replaces [sname] with spell name.
        -dur [duration] - changes the duration of any effect applied by the spell.
        int/wis/cha - different skill base for DC/AB (will not account for extra bonuses)"""
        try:
            await ctx.message.delete()
        except:
            pass

        char = await Character.from_ctx(ctx)

        if not '-i' in args:
            spell = await select_spell_full(
                ctx,
                spell_name,
                list_filter=lambda s: s.name in char.get_spell_list())
        else:
            spell = await select_spell_full(ctx, spell_name)

        args = await scripting.parse_snippets(args, ctx)
        args = await char.parse_cvars(args, ctx)
        args = shlex.split(args)
        args = argparse(args)

        result = await spell.cast(ctx, char, None, args)
        embed = result['embed']

        embed.colour = char.get_color()
        embed.set_thumbnail(url=char.get_image())

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

        await char.commit(ctx)  # make sure we save changes
        await ctx.send(embed=embed)
Beispiel #11
0
    async def embed(self, ctx, *, args):
        """Creates and prints an Embed.
        __Valid Arguments__
        -title <title>
        -desc <description text>
        -thumb <image url>
        -image <image url>
        -footer <footer text>
        -f "<Field Title>|<Field Text>[|inline]"
            (e.g. "Donuts|I have 15 donuts|inline" for an inline field, or "Donuts|I have 15 donuts" for one with its own line.)
        -color <hex color>
        -t <timeout (0..600)>
        """
        try:
            await ctx.message.delete()
        except:
            pass

        embed = embeds.EmbedWithAuthor(ctx)
        args = argparse(args)
        embed.title = args.last('title')
        embed.description = args.last('desc')
        embed.set_thumbnail(url=args.last('thumb', '') if 'http' in
                            str(args.last('thumb')) else '')
        embed.set_image(url=args.last('image', '') if 'http' in
                        str(args.last('image')) else '')
        embed.set_footer(text=args.last('footer', ''))
        try:
            embed.colour = int(args.last('color', "0").strip('#'), base=16)
        except:
            pass

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

        timeout = 0
        if 't' in args:
            try:
                timeout = min(max(args.last('t', type_=int), 0), 600)
            except:
                pass

        if timeout:
            await ctx.send(embed=embed, delete_after=timeout)
        else:
            await ctx.send(embed=embed)
Beispiel #12
0
def _run_common(skill, args, embed, mod_override=None, rr_format="Check {}"):
    """
    Runs a roll for a given Skill.

    :rtype: SkillRollResult
    """
    # ephemeral support: adv, b
    # phrase
    phrase = args.join('phrase', '\n')
    # num rolls
    iterations = min(args.last('rr', 1, int), 25)
    # dc
    dc = args.last('dc', type_=int)
    # ro
    ro = args.last('ro', type_=int)
    # mc
    mc = args.last('mc', type_=int)

    desc_out = []
    num_successes = 0
    results = []

    # add DC text
    if dc:
        desc_out.append(f"**DC {dc}**")

    for i in range(iterations):
        # advantage
        adv = args.adv(boolwise=True, ephem=True)
        # roll bonus
        b = args.join('b', '+', ephem=True)

        # set up dice
        roll_str = skill.d20(base_adv=adv,
                             reroll=ro,
                             min_val=mc,
                             mod_override=mod_override)
        if b is not None:
            roll_str = f"{roll_str}+{b}"

        # roll
        result = roll(roll_str)
        if dc and result.total >= dc:
            num_successes += 1

        results.append(result)

        # output
        if iterations > 1:
            embed.add_field(name=rr_format.format(str(i + 1)),
                            value=result.result)
        else:
            desc_out.append(result.result)

    # phrase
    if phrase:
        desc_out.append(f"*{phrase}*")

    # DC footer
    if iterations > 1 and dc:
        embed.set_footer(
            text=
            f"{num_successes} Successes | {iterations - num_successes} Failures"
        )
    elif dc:
        embed.set_footer(text="Success!" if num_successes else "Failure!")

    # build embed
    embed.description = '\n'.join(desc_out)
    embeds.add_fields_from_args(embed, args.get('f'))
    if 'thumb' in args:
        embed.set_thumbnail(url=maybe_http_url(args.last('thumb', '')))

    return SkillRollResult(rolls=results,
                           iterations=iterations,
                           dc=dc,
                           successes=num_successes)
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
    async def monster_save(self, ctx, monster_name, save_stat, *args):
        """Rolls a save 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]
        -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()

        try:
            save = monster.saves.get(save_stat)
            save_name = f"{verbose_stat(save_stat[:3]).title()} Save"
        except ValueError:
            return await ctx.send('That\'s not a valid save.')

        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

        formatted_d20 = save.d20(base_adv=adv)

        if b:
            roll_str = f"{formatted_d20}+{b}"
        else:
            roll_str = formatted_d20

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

        embed.title = args.last('title', '') \
                          .replace('[mname]', monster_name) \
                          .replace('[sname]', save_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 #15
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 #16
0
    async def check(self, ctx, check, *args):
        """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: Character = await Character.from_ctx(ctx)
        skill_key = await search_and_select(ctx, SKILL_NAMES, check,
                                            lambda s: s)
        skill_name = camel_to_title(skill_key)

        embed = EmbedWithCharacter(char, False)
        skill = char.skills[skill_key]

        args = await self.new_arg_stuff(args, ctx, char)
        # advantage
        adv = args.adv(boolwise=True)
        # roll bonus
        b = args.join('b', '+')
        # phrase
        phrase = args.join('phrase', '\n')
        # num rolls
        iterations = min(args.last('rr', 1, int), 25)
        # dc
        dc = args.last('dc', type_=int)
        # reliable talent (#654)
        rt = char.get_setting('talent', 0) and skill.prof >= 1
        mc = args.last('mc') or 10 * rt
        # halfling luck
        ro = char.get_setting('reroll')

        num_successes = 0
        mod = skill.value
        formatted_d20 = skill.d20(base_adv=adv,
                                  reroll=ro,
                                  min_val=mc,
                                  base_only=True)

        if any(args.last(s, type_=bool) for s in STAT_ABBREVIATIONS):
            base = next(s for s in STAT_ABBREVIATIONS
                        if args.last(s, type_=bool))
            mod = mod - char.get_mod(SKILL_MAP[skill_key]) + char.get_mod(base)
            skill_name = f"{verbose_stat(base)} ({skill_name})"

        if b is not None:
            roll_str = f"{formatted_d20}{mod:+}+{b}"
        else:
            roll_str = f"{formatted_d20}{mod:+}"

        if args.last('title'):
            embed.title = args.last('title', '') \
                .replace('[charname]', char.name) \
                .replace('[cname]', skill_name)
        else:
            embed.title = f'{char.name} makes {a_or_an(skill_name)} check!'

        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} Failures"
                )
        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'))
        await ctx.send(embed=embed)
        try:
            await ctx.message.delete()
        except:
            pass
Beispiel #17
0
    async def cast(self, ctx, caster, targets, args, combat=None):
        """
        Casts this spell.

        :param ctx: The context of the casting.
        :param caster: The caster of this spell.
        :type caster: :class:`~cogs5e.models.sheet.statblock.StatBlock`
        :param targets: A list of targets
        :type targets: list of :class:`~cogs5e.models.sheet.statblock.StatBlock`
        :param args: Args
        :type args: :class:`~utils.argparser.ParsedArguments`
        :param combat: The combat the spell was cast in, if applicable.
        :rtype: CastResult
        """

        # generic args
        l = args.last('l', self.level, int)
        i = args.last('i', type_=bool)
        title = args.last('title')
        nopact = args.last('nopact', type_=bool)

        # meta checks
        if not self.level <= l <= 9:
            raise SpellException("Invalid spell level.")

        # caster spell-specific overrides
        dc_override = None
        ab_override = None
        spell_override = None
        is_prepared = True
        spellbook_spell = caster.spellbook.get_spell(self)
        if spellbook_spell is not None:
            dc_override = spellbook_spell.dc
            ab_override = spellbook_spell.sab
            spell_override = spellbook_spell.mod
            is_prepared = spellbook_spell.prepared

        if not i:
            # if I'm a warlock, and I didn't have any slots of this level anyway (#655)
            # automatically scale up to our pact slot level (or the next available level s.t. max > 0)
            if l > 0 \
                    and l == self.level \
                    and not caster.spellbook.get_max_slots(l) \
                    and not caster.spellbook.can_cast(self, l):
                if caster.spellbook.pact_slot_level is not None:
                    l = caster.spellbook.pact_slot_level
                else:
                    l = next((sl for sl in range(l, 6)
                              if caster.spellbook.get_max_slots(sl)),
                             l)  # only scale up to l5
                args['l'] = l

            # can I cast this spell?
            if not caster.spellbook.can_cast(self, l):
                embed = EmbedWithAuthor(ctx)
                embed.title = "Cannot cast spell!"
                if not caster.spellbook.get_slots(l):
                    # out of spell slots
                    err = (
                        f"You don't have enough level {l} slots left! Use `-l <level>` to cast at a different "
                        f"level, `{ctx.prefix}g lr` to take a long rest, or `-i` to ignore spell slots!"
                    )
                elif self.name not in caster.spellbook:
                    # don't know spell
                    err = (
                        f"You don't know this spell! Use `{ctx.prefix}sb add {self.name}` to add it to your "
                        f"spellbook, or pass `-i` to ignore restrictions.")
                else:
                    # ?
                    err = (
                        "Not enough spell slots remaining, or spell not in known spell list!\n"
                        f"Use `{ctx.prefix}game longrest` to restore all spell slots if this is a character, "
                        f"or pass `-i` to ignore restrictions.")
                embed.description = err
                if l > 0:
                    embed.add_field(name="Spell Slots",
                                    value=caster.spellbook.remaining_casts_of(
                                        self, l))
                return CastResult(embed=embed,
                                  success=False,
                                  automation_result=None)

            # #1000: is this spell prepared (soft check)?
            if not is_prepared:
                skip_prep_conf = await confirm(
                    ctx,
                    f"{self.name} is not prepared. Do you want to cast it anyway? (Reply with yes/no)",
                    delete_msgs=True)
                if not skip_prep_conf:
                    embed = EmbedWithAuthor(
                        ctx,
                        title=f"Cannot cast spell!",
                        description=
                        f"{self.name} is not prepared! Prepare it on your character sheet and use "
                        f"`{ctx.prefix}update` to mark it as prepared, or use `-i` to ignore restrictions."
                    )
                    return CastResult(embed=embed,
                                      success=False,
                                      automation_result=None)

            # use resource
            caster.spellbook.cast(self, l, pact=not nopact)

        # base stat stuff
        mod_arg = args.last("mod", type_=int)
        with_arg = args.last("with")
        stat_override = ''
        if mod_arg is not None:
            mod = mod_arg
            prof_bonus = caster.stats.prof_bonus
            dc_override = 8 + mod + prof_bonus
            ab_override = mod + prof_bonus
            spell_override = mod
        elif with_arg is not None:
            if with_arg not in STAT_ABBREVIATIONS:
                raise InvalidArgument(
                    f"{with_arg} is not a valid stat to cast with.")
            mod = caster.stats.get_mod(with_arg)
            dc_override = 8 + mod + caster.stats.prof_bonus
            ab_override = mod + caster.stats.prof_bonus
            spell_override = mod
            stat_override = f" with {verbose_stat(with_arg)}"

        # begin setup
        embed = discord.Embed()
        if title:
            embed.title = title.replace('[name]', caster.name) \
                .replace('[aname]', self.name) \
                .replace('[sname]', self.name) \
                .replace('[verb]', 'casts')  # #1514, [aname] is action name now, #1587, add verb to action/cast
        else:
            embed.title = f"{caster.get_title_name()} casts {self.name}{stat_override}!"
        if targets is None:
            targets = [None]

        # concentration
        noconc = args.last("noconc", type_=bool)
        conc_conflict = None
        conc_effect = None
        if all((self.concentration, isinstance(caster, BaseCombatant), combat,
                not noconc)):
            duration = args.last('dur', self.get_combat_duration(), int)
            conc_effect = Effect.new(combat, caster, self.name, duration, "",
                                     True)
            effect_result = caster.add_effect(conc_effect)
            conc_conflict = effect_result['conc_conflict']

        # run
        automation_result = None
        if self.automation and self.automation.effects:
            title = f"{caster.name} cast {self.name}!"
            automation_result = await self.automation.run(
                ctx,
                embed,
                caster,
                targets,
                args,
                combat,
                self,
                conc_effect=conc_effect,
                ab_override=ab_override,
                dc_override=dc_override,
                spell_override=spell_override,
                title=title)
        else:  # no automation, display spell description
            phrase = args.join('phrase', '\n')
            if phrase:
                embed.description = f"*{phrase}*"
            embed.add_field(name="Description",
                            value=smart_trim(self.description),
                            inline=False)
            embed.set_footer(text="No spell automation found.")

        if l != self.level and self.higherlevels:
            embed.add_field(name="At Higher Levels",
                            value=smart_trim(self.higherlevels),
                            inline=False)

        if l > 0 and not i:
            embed.add_field(name="Spell Slots",
                            value=caster.spellbook.remaining_casts_of(self, l))

        if conc_conflict:
            conflicts = ', '.join(e.name for e in conc_conflict)
            embed.add_field(name="Concentration",
                            value=f"Dropped {conflicts} due to concentration.")

        if 'thumb' in args:
            embed.set_thumbnail(url=maybe_http_url(args.last('thumb', '')))
        elif self.image:
            embed.set_thumbnail(url=self.image)

        add_fields_from_args(embed, args.get('f'))
        gamedata.lookuputils.handle_source_footer(embed,
                                                  self,
                                                  add_source_str=False)

        return CastResult(embed=embed,
                          success=True,
                          automation_result=automation_result)
    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]
        -rd [extra dice rolled for damage]
        -rh [extra dice rolled for to hit]
        
        -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: [charname], [aname], and [target] will be replaced automatically*
        -f "Field Title|Field Text" (see !embed)
        -h (hides attack details)
        [user snippet]"""
        if atk_name is None:
            return await ctx.invoke(self.attack_list)

        char = await Character.from_ctx(ctx)
        attacks = char.get_attacks()

        try:  # fuzzy search for atk_name
            attack = next(a for a in attacks if atk_name.lower() == a.get('name').lower())
        except StopIteration:
            try:
                attack = next(a for a in attacks if atk_name.lower() in a.get('name').lower())
            except StopIteration:
                return await ctx.send('No attack with that name found.')

        args = await self.new_arg_stuff(args, ctx, char)
        args['name'] = char.get_name()
        args['criton'] = args.last('criton') or char.get_setting('criton', 20)
        args['reroll'] = char.get_setting('reroll', 0)
        args['critdice'] = int(char.get_setting('hocrit', False)) + char.get_setting('critdice', 0)
        args['crittype'] = char.get_setting('crittype', 'default')
        if attack.get('details') is not None:
            try:
                attack['details'] = await char.parse_cvars(attack['details'], ctx)
            except AvraeException:
                pass  # failed to eval, probably DDB nonsense

        if args.last('rd') is not None:
            attack["damage"] = (attack.get("damage") + "+" + args.last('rd'))

        result = sheet_attack(attack, args, EmbedWithCharacter(char, name=False))
        embed = result['embed']
        if args.last('h', type_=bool):
            try:
                await ctx.author.send(embed=result['full_embed'])
            except:
                pass

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

        await ctx.send(embed=embed)
        try:
            await ctx.message.delete()
        except:
            pass
    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
    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 #21
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: [charname], [aname], and [target] will be replaced automatically*
        -f "Field Title|Field Text" (see !embed)
        -h (hides attack details)
        [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)
        args['name'] = char.name
        args['criton'] = args.last('criton') or char.get_setting('criton', 20)
        args['reroll'] = char.get_setting('reroll', 0)
        args['critdice'] = char.get_setting('critdice', 0)
        args['crittype'] = char.get_setting('crittype', 'default')

        result = sheet_attack(attack.to_old(), args,
                              EmbedWithCharacter(char, name=False))
        embed = result['embed']
        if args.last('h', type_=bool):
            try:
                await ctx.author.send(embed=result['full_embed'])
            except:
                pass

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

        await ctx.send(embed=embed)
        try:
            await ctx.message.delete()
        except:
            pass
Beispiel #22
0
    async def monster_cast(self, ctx, monster_name, spell_name, *args):
        """
        Casts a spell as a monster.
        __Valid Arguments__
        -i - Ignores Spellbook restrictions, for demonstrations or rituals.
        -l <level> - Specifies the level to cast the spell at.
        noconc - Ignores concentration requirements.
        -h - Hides rolled values.
        **__Save Spells__**
        -dc <Save DC> - Overrides the spell save DC.
        -save <Save type> - Overrides the spell save type.
        -d <damage> - Adds additional damage.
        pass - Target automatically succeeds save.
        fail - Target automatically fails save.
        adv/dis - Target makes save at advantage/disadvantage.
        **__Attack Spells__**
        See `!a`.
        **__All Spells__**
        -phrase <phrase> - adds flavor text.
        -title <title> - changes the title of the cast. Replaces [sname] with spell name.
        -thumb <url> - adds an image to the cast.
        -dur <duration> - changes the duration of any effect applied by the spell.
        -mod <spellcasting mod> - sets the value of the spellcasting ability modifier.
        int/wis/cha - different skill base for DC/AB (will not account for extra bonuses)
        """
        await try_delete(ctx.message)
        monster: Monster = await select_monster_full(ctx, monster_name)

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

        if not args.last('i', type_=bool):
            spell = await select_spell_full(
                ctx,
                spell_name,
                list_filter=lambda s: s.name in monster.spellbook)
        else:
            spell = await select_spell_full(ctx, spell_name)

        caster, targets, combat = await targetutils.maybe_combat(
            ctx, monster, args)
        result = await spell.cast(ctx, caster, targets, args, combat=combat)

        # embed display
        embed = result['embed']
        embed.colour = random.randint(0, 0xffffff)

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

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

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

        # save changes: combat state
        if combat:
            await combat.final()
        await ctx.send(embed=embed)
Beispiel #23
0
    async def _attack(self, ctx, combatant_name, target_name, atk_name, args):
        args = await scripting.parse_snippets(args, ctx)
        combat = await Combat.from_ctx(ctx)

        try:
            target = await combat.select_combatant(target_name, "Select the target.")
            if target is None:
                return await ctx.send("Target not found.")
        except SelectionException:
            return await ctx.send("Target not found.")

        if combatant_name is None:
            combatant = combat.current_combatant
            if combatant is None:
                return await ctx.send(f"You must start combat with `{ctx.prefix}init next` first.")
        else:
            try:
                combatant = await combat.select_combatant(combatant_name, "Select the attacker.")
                if combatant is None:
                    return await ctx.send("Combatant not found.")
            except SelectionException:
                return await ctx.send("Combatant not found.")

        attacks = combatant.attacks
        if '-custom' in args:
            attack = {'attackBonus': None, 'damage': None, 'name': atk_name}
        else:
            try:
                attack = await get_selection(ctx,
                                             [(a['name'], a) for a in attacks if atk_name.lower() in a['name'].lower()],
                                             message="Select your attack.")
            except SelectionException:
                return await ctx.send("Attack not found.")

        is_player = isinstance(combatant, PlayerCombatant)

        if is_player and combatant.character_owner == str(ctx.author.id):
            args = await combatant.character.parse_cvars(args, ctx)

        args = argparse(shlex.split(args))  # set up all the arguments
        args['name'] = combatant.name
        if target.ac is not None: args['ac'] = target.ac
        args['t'] = target.name
        args['resist'] = args.get('resist') or target.resists['resist']
        args['immune'] = args.get('immune') or target.resists['immune']
        args['vuln'] = args.get('vuln') or target.resists['vuln']
        args['neutral'] = args.get('neutral') or target.resists['neutral']
        if is_player:
            args['c'] = combatant.character.get_setting('critdmg') or args.get('c')
            args['reroll'] = combatant.character.get_setting('reroll') or 0
            args['crittype'] = combatant.character.get_setting('crittype') or 'default'
            args['critdice'] = (combatant.character.get_setting('critdice') or 0) + int(
                combatant.character.get_setting('hocrit', False))
            args['criton'] = combatant.character.get_setting('criton') or args.get('criton')

        result = sheet_attack(attack, args)
        embed = result['embed']

        if args.last('h', type_=bool):
            try:
                controller = ctx.guild.get_member(int(combatant.controller))
                await controller.send(embed=result['full_embed'])
            except:
                pass

        if is_player:
            embed.colour = combatant.character.get_color()
        else:
            embed.colour = random.randint(0, 0xffffff)
        if target.ac is not None and target.hp is not None:
            target.mod_hp(-result['total_damage'], overheal=False)

        if target.ac is not None:
            if target.hp is not None:
                embed.set_footer(text="{}: {}".format(target.name, target.get_hp_str()))
                if target.isPrivate:
                    try:
                        controller = ctx.guild.get_member(int(target.controller))
                        await controller.send(
                            f"{combatant.name} attacked with a {attack['name']}!"
                            f"\n{target.name}'s HP: {target.get_hp_str(True)}")
                    except:
                        pass
            else:
                embed.set_footer(text="Dealt {} damage to {}!".format(result['total_damage'], target.name))
            if target.is_concentrating() and result['total_damage'] > 0:
                dcs = []
                for atk in result['raw_attacks']:
                    if atk['crit'] == 2:
                        continue
                    dcs.append(int(max(atk['damage'] / 2, 10)))
                if len(dcs) > 1:
                    dcs = ', '.join(map(str, dcs))
                    embed.add_field(name="Concentration",
                                    value=f"Check your concentration (DCs {dcs})!")
                else:
                    embed.add_field(name="Concentration",
                                    value=f"Check your concentration (DC {dcs[0]})!")
        else:
            embed.set_footer(text="Target AC not set.")

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

        await ctx.send(embed=embed)
        await combat.final()
Beispiel #24
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 #25
0
    async def save(self, ctx, skill, *args):
        """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, *args)

        char: Character = await Character.from_ctx(ctx)
        try:
            save = char.saves.get(skill)
        except ValueError:
            return await ctx.send('That\'s not a valid save.')

        embed = EmbedWithCharacter(char, name=False)

        args = await self.new_arg_stuff(args, ctx, char)
        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

        formatted_d20 = save.d20(base_adv=adv,
                                 reroll=char.get_setting('reroll'))

        if b:
            roll_str = f"{formatted_d20}+{b}"
        else:
            roll_str = formatted_d20

        save_name = f"{verbose_stat(skill[:3]).title()} Save"
        if args.last('title'):
            embed.title = args.last('title', '') \
                .replace('[charname]', char.name) \
                .replace('[sname]', save_name)
        else:
            embed.title = f'{char.name} makes {a_or_an(save_name)}!'

        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"Save {i + 1}", value=result.skeleton)
            if dc:
                embed.set_footer(
                    text=
                    f"{num_successes} Successes | {iterations - num_successes} Failures"
                )
        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'))

        await ctx.send(embed=embed)
        try:
            await ctx.message.delete()
        except:
            pass
Beispiel #26
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.rdb.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 = argparse(args)
        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

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

        default_title = f'{monster_name} makes {a_or_an(camel_to_title(save))}!'

        embed.title = args.last('title', '') \
                          .replace('[mname]', monster_name) \
                          .replace('[sname]', camel_to_title(save)) \
                      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 #27
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 #28
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 #29
0
    async def cast(self, ctx, caster, targets, args, combat=None):
        """
        Casts this spell.

        :param ctx: The context of the casting.
        :param caster: The caster of this spell.
        :type caster: :class:`~cogs5e.models.sheet.statblock.StatBlock`
        :param targets: A list of targets
        :type targets: list of :class:`~cogs5e.models.sheet.statblock.StatBlock`
        :param args: Args
        :type args: :class:`~utils.argparser.ParsedArguments`
        :param combat: The combat the spell was cast in, if applicable.
        :return: {embed: Embed}
        """

        # generic args
        l = args.last('l', self.level, int)
        i = args.last('i', type_=bool)
        title = args.last('title')

        # meta checks
        if not self.level <= l <= 9:
            raise SpellException("Invalid spell level.")

        # caster spell-specific overrides
        dc_override = None
        ab_override = None
        spell_override = None
        spellbook_spell = caster.spellbook.get_spell(self)
        if spellbook_spell is not None:
            dc_override = spellbook_spell.dc
            ab_override = spellbook_spell.sab
            spell_override = spellbook_spell.mod

        if not i:
            # if I'm a warlock, and I didn't have any slots of this level anyway (#655)
            # automatically scale up to the next level s.t. our slots are not 0
            if l > 0 \
                    and l == self.level \
                    and not caster.spellbook.get_max_slots(l) \
                    and not caster.spellbook.can_cast(self, l):
                l = next((sl for sl in range(l, 6)
                          if caster.spellbook.get_max_slots(sl)),
                         l)  # only scale up to l5
                args['l'] = l

            # can I cast this spell?
            if not caster.spellbook.can_cast(self, l):
                embed = EmbedWithAuthor(ctx)
                embed.title = "Cannot cast spell!"
                if not caster.spellbook.get_slots(l):
                    # out of spell slots
                    err = f"You don't have enough level {l} slots left! Use `-l <level>` to cast at a different level, " \
                          f"`{ctx.prefix}g lr` to take a long rest, or `-i` to ignore spell slots!"
                elif self.name not in caster.spellbook:
                    # don't know spell
                    err = f"You don't know this spell! Use `{ctx.prefix}sb add {self.name}` to add it to your spellbook, " \
                          f"or pass `-i` to ignore restrictions."
                else:
                    # ?
                    err = "Not enough spell slots remaining, or spell not in known spell list!\n" \
                          f"Use `{ctx.prefix}game longrest` to restore all spell slots if this is a character, " \
                          f"or pass `-i` to ignore restrictions."
                embed.description = err
                if l > 0:
                    embed.add_field(name="Spell Slots",
                                    value=caster.spellbook.remaining_casts_of(
                                        self, l))
                return {"embed": embed}

            # use resource
            caster.spellbook.cast(self, l)

        # character setup
        character = None
        if isinstance(caster, PlayerCombatant):
            character = caster.character
        elif isinstance(caster, Character):
            character = caster

        # base stat stuff
        mod_arg = args.last("mod", type_=int)
        stat_override = ''
        if mod_arg is not None:
            mod = mod_arg
            if character:
                prof_bonus = character.stats.prof_bonus
            else:
                prof_bonus = 0
            dc_override = 8 + mod + prof_bonus
            ab_override = mod + prof_bonus
            spell_override = mod
        elif character and any(
                args.last(s, type_=bool) for s in STAT_ABBREVIATIONS):
            base = next(s for s in STAT_ABBREVIATIONS
                        if args.last(s, type_=bool))
            mod = character.stats.get_mod(base)
            dc_override = 8 + mod + character.stats.prof_bonus
            ab_override = mod + character.stats.prof_bonus
            spell_override = mod
            stat_override = f" with {verbose_stat(base)}"

        if spell_override is None and (caster.spellbook.sab is None
                                       or caster.spellbook.dc is None):
            raise SpellException(
                "This caster does not have the ability to cast spells.")

        # begin setup
        embed = discord.Embed()
        if title:
            embed.title = title.replace('[sname]', self.name)
        else:
            embed.title = f"{caster.get_title_name()} casts {self.name}{stat_override}!"
        if targets is None:
            targets = [None]

        # concentration
        noconc = args.last("noconc", type_=bool)
        conc_conflict = None
        conc_effect = None
        if all(
            (self.concentration, isinstance(caster,
                                            Combatant), combat, not noconc)):
            duration = args.last('dur', self.get_combat_duration(), int)
            conc_effect = initiative.Effect.new(combat, caster, self.name,
                                                duration, "", True)
            effect_result = caster.add_effect(conc_effect)
            conc_conflict = effect_result['conc_conflict']

        if self.automation and self.automation.effects:
            title = f"{caster.name} cast {self.name}!"
            await self.automation.run(ctx,
                                      embed,
                                      caster,
                                      targets,
                                      args,
                                      combat,
                                      self,
                                      conc_effect=conc_effect,
                                      ab_override=ab_override,
                                      dc_override=dc_override,
                                      spell_override=spell_override,
                                      title=title)
        else:
            phrase = args.join('phrase', '\n')
            if phrase:
                embed.description = f"*{phrase}*"

            text = self.description
            if len(text) > 1020:
                text = f"{text[:1020]}..."
            embed.add_field(name="Description", value=text, inline=False)
            if l != self.level and self.higherlevels:
                embed.add_field(name="At Higher Levels",
                                value=self.higherlevels,
                                inline=False)
            embed.set_footer(text="No spell automation found.")

        if l > 0 and not i:
            embed.add_field(name="Spell Slots",
                            value=caster.spellbook.remaining_casts_of(self, l))

        if conc_conflict:
            conflicts = ', '.join(e.name for e in conc_conflict)
            embed.add_field(name="Concentration",
                            value=f"Dropped {conflicts} due to concentration.")

        if 'thumb' in args:
            embed.set_thumbnail(url=args.last('thumb'))
        elif self.image:
            embed.set_thumbnail(url=self.image)

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

        if self.source == 'homebrew':
            add_homebrew_footer(embed)

        return {"embed": embed}
Beispiel #30
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