Пример #1
0
 async def game_spellslot(self, ctx, level: int = None, value: str = None):
     """Views or sets your remaining spell slots."""
     if level is not None:
         try:
             assert 0 < level < 10
         except AssertionError:
             return await ctx.send("Invalid spell level.")
     character: Character = await Character.from_ctx(ctx)
     embed = EmbedWithCharacter(character)
     embed.set_footer(text="\u25c9 = Available / \u3007 = Used")
     if level is None and value is None:  # show remaining
         embed.description = f"__**Remaining Spell Slots**__\n{character.spellbook.slots_str()}"
     elif value is None:
         embed.description = f"__**Remaining Level {level} Spell Slots**__\n" \
                             f"{character.spellbook.slots_str(level)}"
     else:
         try:
             if value.startswith(('+', '-')):
                 value = character.spellbook.get_slots(level) + int(value)
             else:
                 value = int(value)
         except ValueError:
             return await ctx.send(f"{value} is not a valid integer.")
         try:
             assert 0 <= value <= character.spellbook.get_max_slots(level)
         except AssertionError:
             raise CounterOutOfBounds()
         character.spellbook.set_slots(level, value)
         await character.commit(ctx)
         embed.description = f"__**Remaining Level {level} Spell Slots**__\n" \
                             f"{character.spellbook.slots_str(level)}"
     await ctx.send(embed=embed)
Пример #2
0
    async def game_spellslot(self, ctx, level: int = None, value: str = None, *args):
        """
        Views or sets your remaining spell slots.
        __Valid Arguments__
        nopact - Modifies normal spell slots first instead of a Pact Magic slots, if applicable.
        """
        if level is not None:
            try:
                assert 0 < level < 10
            except AssertionError:
                return await ctx.send("Invalid spell level.")
        character: Character = await Character.from_ctx(ctx)
        embed = EmbedWithCharacter(character)

        if level is None and value is None:  # show remaining
            embed.description = f"__**Remaining Spell Slots**__\n{character.spellbook.slots_str()}"
        elif value is None:
            embed.description = f"__**Remaining Level {level} Spell Slots**__\n" \
                                f"{character.spellbook.slots_str(level)}"
        else:
            old_slots = character.spellbook.get_slots(level)
            value = maybe_mod(value, old_slots)
            character.spellbook.set_slots(level, value, pact='nopact' not in args)
            await character.commit(ctx)
            embed.description = f"__**Remaining Level {level} Spell Slots**__\n" \
                                f"{character.spellbook.slots_str(level)} ({(value - old_slots):+})"

        # footer - pact vs non pact
        if character.spellbook.max_pact_slots is not None:
            embed.set_footer(text=f"{constants.FILLED_BUBBLE} = Available / {constants.EMPTY_BUBBLE} = Used\n"
                                  f"{constants.FILLED_BUBBLE_ALT} / {constants.EMPTY_BUBBLE_ALT} = Pact Slot")
        else:
            embed.set_footer(text=f"{constants.FILLED_BUBBLE} = Available / {constants.EMPTY_BUBBLE} = Used")

        await ctx.send(embed=embed)
Пример #3
0
 async def customcounter_summary(self, ctx, page: int = 0):
     """
     Prints a summary of all custom counters.
     Use `!cc list <page>` to view pages if you have more than 25 counters.
     """
     character: Character = await Character.from_ctx(ctx)
     embed = EmbedWithCharacter(character, title="Custom Counters")
     # Check that we're not over the field limit
     total = len(character.consumables)
     if total > 25:  # Discord Field limit
         page = max(0, page - 1)  # Humans count from 1
         maxpage = total // 25
         start = min(page * 25, total - 25)
         end = max(start + 25, total)
         # Build the current page
         embed.set_footer(
             text=
             f"Page [{page + 1}/{maxpage + 1}] | {ctx.prefix}cc list <page>"
         )
         for counter in character.consumables[start:end]:
             embed.add_field(name=counter.name, value=counter.full_str())
     else:
         for counter in character.consumables:
             embed.add_field(name=counter.name, value=counter.full_str())
     await ctx.send(embed=embed)
Пример #4
0
    async def join(self, ctx, *, args: str = ''):
        """Adds the current active character to combat. A character must be loaded through the SheetManager module first.
        Args: adv/dis
              -b [conditional bonus]
              -phrase [flavor text]
              -p [init value]
              -h (same as !init add)
              --group (same as !init add)"""
        char: Character = await Character.from_ctx(ctx)

        embed = EmbedWithCharacter(char, False)
        embed.colour = char.get_color()

        args = shlex.split(args)
        args = argparse(args)
        adv = args.adv(boolwise=True)
        b = args.join('b', '+') or None
        p = args.last('p', type_=int)
        phrase = args.join('phrase', '\n') or None
        group = args.last('group')

        if p is None:
            roll_str = char.skills.initiative.d20(base_adv=adv)
            if b:
                roll_str = f"{roll_str}+{b}"
            check_roll = roll(roll_str, inline=True)

            embed.title = '{} makes an Initiative check!'.format(char.name)
            embed.description = check_roll.skeleton + ('\n*' + phrase + '*' if phrase is not None else '')
            init = check_roll.total
        else:
            init = p
            embed.title = "{} already rolled initiative!".format(char.name)
            embed.description = "Placed at initiative `{}`.".format(init)

        controller = str(ctx.author.id)
        private = args.last('h', type_=bool)
        bonus = char.skills.initiative.value

        combat = await Combat.from_ctx(ctx)

        me = await PlayerCombatant.from_character(char.name, controller, init, bonus, char.ac, private,
                                                  char.get_resists(), ctx, combat, char.upstream, str(ctx.author.id),
                                                  char)

        if combat.get_combatant(char.name) is not None:
            await ctx.send("Combatant already exists.")
            return

        if group is None:
            combat.add_combatant(me)
            embed.set_footer(text="Added to combat!")
        else:
            grp = combat.get_group(group, create=init)
            grp.add_combatant(me)
            embed.set_footer(text=f"Joined group {grp.name}!")

        await combat.final()
        await ctx.send(embed=embed)
        await char.commit(ctx)
Пример #5
0
    def get_sheet_embed(self):
        embed = EmbedWithCharacter(self)
        desc_details = []

        # race/class (e.g. Tiefling Bard/Warlock)
        classes = '/'.join(f"{cls} {lvl}" for cls, lvl in self.levels)
        desc_details.append(f"{self.race} {classes}")

        # prof bonus
        desc_details.append(
            f"**Proficiency Bonus**: {self.stats.prof_bonus:+}")

        # combat details
        desc_details.append(f"**AC**: {self.ac}")
        desc_details.append(f"**HP**: {self.get_hp_str()}")
        desc_details.append(
            f"**Initiative**: {self.skills.initiative.value:+}")

        # stats
        desc_details.append(str(self.stats))
        save_profs = str(self.saves)
        if save_profs:
            desc_details.append(f"**Save Proficiencies**: {save_profs}")
        skill_profs = str(self.skills)
        if skill_profs:
            desc_details.append(f"**Skill Proficiencies**: {skill_profs}")
        desc_details.append(
            f"**Senses**: passive Perception {10 + self.skills.perception.value}"
        )

        # resists
        resists = str(self.resistances)
        if resists:
            desc_details.append(resists)

        embed.description = '\n'.join(desc_details)

        # attacks
        atks = self.attacks
        atk_str = ""
        for attack in atks:
            a = f"{str(attack)}\n"
            if len(atk_str) + len(a) > 1000:
                atk_str += "[...]"
                break
            atk_str += a
        atk_str = atk_str.strip()
        if atk_str:
            embed.add_field(name="Attacks", value=atk_str)

        # sheet url?
        if self._import_version < 15:
            embed.set_footer(
                text=
                f"You are using an old sheet version ({self.sheet_type} v{self._import_version}). "
                f"Please run !update.")

        return embed
Пример #6
0
    async def spellbook(self, ctx):
        """Commands to display a character's known spells and metadata."""
        await ctx.trigger_typing()

        character: Character = await Character.from_ctx(ctx)
        embed = EmbedWithCharacter(character)
        embed.description = f"{character.name} knows {len(character.spellbook.spells)} spells."
        embed.add_field(name="DC", value=str(character.spellbook.dc))
        embed.add_field(name="Spell Attack Bonus", value=str(character.spellbook.sab))
        embed.add_field(name="Spell Slots", value=character.spellbook.slots_str() or "None")

        # dynamic help flags
        flag_show_multiple_source_help = False
        flag_show_homebrew_help = False

        spells_known = collections.defaultdict(lambda: [])
        choices = await get_spell_choices(ctx)
        for spell_ in character.spellbook.spells:
            results, strict = search(choices, spell_.name, lambda sp: sp.name, strict=True)
            if not strict:
                if len(results) > 1:
                    spells_known['unknown'].append(f"*{spell_.name} ({'*' * len(results)})*")
                    flag_show_multiple_source_help = True
                else:
                    spells_known['unknown'].append(f"*{spell_.name}*")
                flag_show_homebrew_help = True
            else:
                spell = results
                if spell.homebrew:
                    formatted = f"*{spell.name}*"
                    flag_show_homebrew_help = True
                else:
                    formatted = spell.name
                spells_known[str(spell.level)].append(formatted)

        level_name = {'0': 'Cantrips', '1': '1st Level', '2': '2nd Level', '3': '3rd Level',
                      '4': '4th Level', '5': '5th Level', '6': '6th Level',
                      '7': '7th Level', '8': '8th Level', '9': '9th Level'}
        for level, spells in sorted(list(spells_known.items()), key=lambda k: k[0]):
            if spells:
                spells.sort()
                embed.add_field(name=level_name.get(level, "Unknown"), value=', '.join(spells), inline=False)

        # dynamic help
        footer_out = []
        if flag_show_homebrew_help:
            footer_out.append("An italicized spell indicates that the spell is homebrew.")
        if flag_show_multiple_source_help:
            footer_out.append("Asterisks after a spell indicates that the spell is being provided by multiple sources.")

        if footer_out:
            embed.set_footer(text=' '.join(footer_out))

        await ctx.send(embed=embed)
Пример #7
0
    def get_sheet_embed(self):
        embed = EmbedWithCharacter(self)
        # noinspection PyListCreation
        # this could be a list literal, but it's more readable this way
        desc_details = []

        # race/class (e.g. Tiefling Bard/Warlock)
        desc_details.append(f"{self.race} {str(self.levels)}")

        # prof bonus
        desc_details.append(
            f"**Proficiency Bonus**: {self.stats.prof_bonus:+}")

        # combat details
        desc_details.append(f"**AC**: {self.ac}")
        desc_details.append(f"**HP**: {self.hp_str()}")
        desc_details.append(
            f"**Initiative**: {self.skills.initiative.value:+}")

        # stats
        desc_details.append(str(self.stats))
        save_profs = str(self.saves)
        if save_profs:
            desc_details.append(f"**Save Proficiencies**: {save_profs}")
        skill_profs = str(self.skills)
        if skill_profs:
            desc_details.append(f"**Skill Proficiencies**: {skill_profs}")
        desc_details.append(
            f"**Senses**: passive Perception {10 + self.skills.perception.value}"
        )

        # resists
        resists = str(self.resistances)
        if resists:
            desc_details.append(resists)

        embed.description = '\n'.join(desc_details)

        # attacks
        atk_str = self.attacks.build_str(self)
        if len(atk_str) > 1000:
            atk_str = f"{atk_str[:1000]}\n[...]"
        if atk_str:
            embed.add_field(name="Attacks", value=atk_str)

        # sheet url?
        if self._import_version < SHEET_VERSION:
            embed.set_footer(
                text=
                f"You are using an old sheet version ({self.sheet_type} v{self._import_version}). "
                f"Please run !update.")

        return embed
Пример #8
0
    async def game_deathsave(self, ctx, *args):
        """Commands to manage character death saves.
        __Valid Arguments__
        See `!help save`."""
        character: Character = await Character.from_ctx(ctx)

        embed = EmbedWithCharacter(character, name=False)

        args = await helpers.parse_snippets(args, ctx)
        args = await helpers.parse_with_character(ctx, character, args)
        args = argparse(args)
        checkutils.update_csetting_args(character, args)
        caster, _, _ = await targetutils.maybe_combat(ctx, character, args)
        result = checkutils.run_save('death', caster, args, embed)

        dc = result.skill_roll_result.dc or 10
        death_phrase = ''

        for save_roll in result.skill_roll_result.rolls:
            if save_roll.crit == d20.CritType.CRIT:
                character.hp = 1
            elif save_roll.crit == d20.CritType.FAIL:
                character.death_saves.fail(2)
            elif save_roll.total >= dc:
                character.death_saves.succeed()
            else:
                character.death_saves.fail()

            if save_roll.crit == d20.CritType.CRIT:
                death_phrase = f"{character.name} is UP with 1 HP!"
                break
            elif character.death_saves.is_dead():
                death_phrase = f"{character.name} is DEAD!"
                break
            elif character.death_saves.is_stable():
                death_phrase = f"{character.name} is STABLE!"
                break

        if death_phrase:
            embed.set_footer(text=death_phrase)
        embed.add_field(name="Death Saves",
                        value=str(character.death_saves),
                        inline=False)

        await character.commit(ctx)
        await ctx.send(embed=embed)
        await try_delete(ctx.message)
        if gamelog := self.bot.get_cog('GameLog'):
            await gamelog.send_save(ctx, character, result.skill_name,
                                    result.rolls)
Пример #9
0
    async def game_deathsave_save(self, ctx):
        """Adds a successful death save."""
        character: Character = await Character.from_ctx(ctx)

        embed = EmbedWithCharacter(character)
        embed.title = f'{character.name} succeeds a Death Save!'

        character.death_saves.succeed()
        await character.commit(ctx)

        if character.death_saves.is_stable():
            embed.set_footer(text=f"{character.name} is STABLE!")
        embed.description = "Added 1 successful death save."
        embed.add_field(name="Death Saves", value=str(character.death_saves))

        await ctx.send(embed=embed)
Пример #10
0
    async def game_deathsave_fail(self, ctx):
        """Adds a failed death save."""
        character: Character = await Character.from_ctx(ctx)

        embed = EmbedWithCharacter(character)
        embed.title = f'{character.name} fails a Death Save!'

        character.death_saves.fail()
        await character.commit(ctx)

        if character.death_saves.is_dead():
            embed.set_footer(text=f"{character.name} is DEAD!")
        embed.description = "Added 1 failed death save."
        embed.add_field(name="Death Saves", value=str(character.death_saves))

        await ctx.send(embed=embed)
    async def game_deathsave_fail(self, ctx):
        """Adds a failed death save."""
        character = await Character.from_ctx(ctx)

        embed = EmbedWithCharacter(character)
        embed.title = f'{character.get_name()} fails a Death Save!'

        death_phrase = ''
        if character.add_failed_ds(): death_phrase = f"{character.get_name()} is DEAD!"

        await character.commit(ctx)
        embed.description = "Added 1 failed death save."
        if death_phrase: embed.set_footer(text=death_phrase)

        embed.add_field(name="Death Saves", value=character.get_ds_str())

        await ctx.send(embed=embed)
Пример #12
0
    async def game_deathsave_save(self, ctx):
        """Adds a successful death save."""
        character = Character.from_ctx(ctx)

        embed = EmbedWithCharacter(character)
        embed.title = f'{character.get_name()} succeeds a Death Save!'

        death_phrase = ''
        if character.add_successful_ds(): death_phrase = f"{character.get_name()} is STABLE!"

        character.commit(ctx)
        embed.description = "Added 1 successful death save."
        if death_phrase: embed.set_footer(text=death_phrase)

        embed.add_field(name="Death Saves", value=character.get_ds_str())

        await self.bot.say(embed=embed)
Пример #13
0
async def send_ddb_ctas(ctx, character):
    """Sends relevant CTAs after a DDB character is imported. Only show a CTA 1/24h to not spam people."""
    ddb_user = await ctx.bot.ddb.get_ddb_user(ctx, ctx.author.id)
    if ddb_user is not None:
        ld_dict = ddb_user.to_ld_dict()
    else:
        ld_dict = {"key": str(ctx.author.id), "anonymous": True}
    gamelog_flag = await ctx.bot.ldclient.variation('cog.gamelog.cta.enabled',
                                                    ld_dict, False)

    # has the user seen this cta within the last 7d?
    if await ctx.bot.rdb.get(f"cog.sheetmanager.cta.seen.{ctx.author.id}"):
        return

    embed = EmbedWithCharacter(character)
    embed.title = "Heads up!"
    embed.description = "There's a couple of things you can do to make your experience even better!"
    embed.set_footer(text="You won't see this message again this week.")

    # link ddb user
    if ddb_user is None:
        embed.add_field(
            name="Connect Your D&D Beyond Account",
            value=
            "Visit your [Account Settings](https://www.dndbeyond.com/account) page in D&D Beyond to link your "
            "D&D Beyond and Discord accounts. This lets you use all your D&D Beyond content in Avrae for free!",
            inline=False)
    # game log
    if character.ddb_campaign_id and gamelog_flag:
        try:
            await CampaignLink.from_id(ctx.bot.mdb, character.ddb_campaign_id)
        except NoCampaignLink:
            embed.add_field(
                name="Link Your D&D Beyond Campaign",
                value=
                f"Sync rolls between a Discord channel and your D&D Beyond character sheet by linking your "
                f"campaign! Use `{ctx.prefix}campaign https://www.dndbeyond.com/campaigns/"
                f"{character.ddb_campaign_id}` in the Discord channel you want to link it to.",
                inline=False)

    if not embed.fields:
        return
    await ctx.send(embed=embed)
    await ctx.bot.rdb.setex(f"cog.sheetmanager.cta.seen.{ctx.author.id}",
                            str(time.time()), 60 * 60 * 24 * 7)
Пример #14
0
def embed_for_basic_attack(gctx,
                           action_name,
                           character,
                           to_hit_roll=None,
                           damage_roll=None):
    """
    Creates an embed for a character making an attack where the Avrae action is unknown.

    Handles inserting the correct fields for to-hit and damage.

    :type gctx: GameLogEventContext
    :type action_name: str
    :type character: cogs5e.models.character.Character
    :type to_hit_roll: ddb.dice.tree.RollRequestRoll
    :type damage_roll: ddb.dice.tree.RollRequestRoll
    """
    embed = EmbedWithCharacter(character, name=False)

    # set title
    embed.title = f'{character.get_title_name()} attacks with {action_name}!'

    # add to hit (and damage, either if it is provided or the action expects damage and it is not provided)
    meta_rolls = []
    if to_hit_roll is not None:
        meta_rolls.append(f"**To Hit**: {str(to_hit_roll.to_d20())}")

    if damage_roll is not None:
        if damage_roll.roll_kind == ddb.dice.RollKind.CRITICAL_HIT:
            meta_rolls.append(
                f"**Damage (CRIT!)**: {str(damage_roll.to_d20())}")
        else:
            meta_rolls.append(f"**Damage**: {str(damage_roll.to_d20())}")
    else:
        meta_rolls.append("**Damage**: Waiting for roll...")

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

    # set footer
    embed.set_footer(text=f"Rolled in {gctx.campaign.campaign_name}",
                     icon_url=constants.DDB_LOGO_ICON)
    return embed
Пример #15
0
    async def customcounter_summary(self, ctx, page: int = 1):
        """
        Prints a summary of all custom counters.
        Use `!cc list <page>` to view pages if you have more than 25 counters.
        """
        character: Character = await Character.from_ctx(ctx)
        embed = EmbedWithCharacter(character, title="Custom Counters")

        if character.consumables:
            # paginate if > 25
            total = len(character.consumables)
            maxpage = total // 25 + 1
            page = max(1, min(page, maxpage))
            pages = [character.consumables[i:i + 25] for i in range(0, total, 25)]
            for counter in pages[page - 1]:
                embed.add_field(name=counter.name, value=counter.full_str())
            if total > 25:
                embed.set_footer(text=f"Page [{page}/{maxpage}] | {ctx.prefix}cc list <page>")
        else:
            embed.add_field(name="No Custom Counters",
                            value=f"Check out `{ctx.prefix}help cc create` to see how to create new ones.")

        await ctx.send(embed=embed)
    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
Пример #17
0
def embed_for_action(gctx,
                     action,
                     character,
                     to_hit_roll=None,
                     damage_roll=None):
    """
    Creates an embed for a character performing some action (attack or spell).

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

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

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

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

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

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

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

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

    # set footer
    embed.set_footer(text=f"Rolled in {gctx.campaign.campaign_name}",
                     icon_url=constants.DDB_LOGO_ICON)
    return embed
Пример #18
0
    async def join(self, ctx, *, args: str = ''):
        """Adds the current active character to combat. A character must be loaded through the SheetManager module first.
        Args: adv/dis
              -b [conditional bonus]
              -phrase [flavor text]
              -p [init value]
              -h (same as !init add)
              --group (same as !init add)"""
        char = await Character.from_ctx(ctx)
        character = char.character

        # if char.get_combat_id():
        #     return await ctx.send(f"This character is already in a combat. "
        #                               f"Please leave combat in <#{char.get_combat_id()}> first.\n"
        #                               f"If this seems like an error, please `!update` your character sheet.")
        # we just ignore this for now.
        # I'll figure out a better solution when I actually need it

        skills = character.get('skills')
        if skills is None:
            return await ctx.send('You must update your character sheet first.')
        skill = 'initiative'

        embed = EmbedWithCharacter(char, False)
        embed.colour = char.get_color()

        skill_effects = character.get('skill_effects', {})
        args += ' ' + skill_effects.get(skill, '')  # dicecloud v7 - autoadv

        args = shlex.split(args)
        args = argparse(args)
        adv = args.adv()
        b = args.join('b', '+') or None
        p = args.last('p', type_=int)
        phrase = args.join('phrase', '\n') or None

        if p is None:
            if b:
                bonus = '{:+}'.format(skills[skill]) + '+' + b
                check_roll = roll('1d20' + bonus, adv=adv, inline=True)
            else:
                bonus = '{:+}'.format(skills[skill])
                check_roll = roll('1d20' + bonus, adv=adv, inline=True)

            embed.title = '{} makes an Initiative check!'.format(char.get_name())
            embed.description = check_roll.skeleton + ('\n*' + phrase + '*' if phrase is not None else '')
            init = check_roll.total
        else:
            init = p
            bonus = 0
            embed.title = "{} already rolled initiative!".format(char.get_name())
            embed.description = "Placed at initiative `{}`.".format(init)

        group = args.last('group')
        controller = str(ctx.author.id)
        private = args.last('h', type_=bool)
        bonus = roll(bonus).total

        combat = await Combat.from_ctx(ctx)

        me = await PlayerCombatant.from_character(char.get_name(), controller, init, bonus, char.get_ac(), private,
                                                  char.get_resists(), ctx, combat, char.id, str(ctx.author.id), char)

        if combat.get_combatant(char.get_name()) is not None:
            await ctx.send("Combatant already exists.")
            return

        if group is None:
            combat.add_combatant(me)
            embed.set_footer(text="Added to combat!")
        else:
            grp = combat.get_group(group, create=init)
            grp.add_combatant(me)
            embed.set_footer(text=f"Joined group {grp.name}!")

        await combat.final()
        await ctx.send(embed=embed)
        char.join_combat(str(ctx.channel.id))
        await char.commit(ctx)
Пример #19
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
Пример #20
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
    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