Ejemplo n.º 1
0
    async def spellbook(self, ctx):
        """Commands to display a character's known spells and metadata."""
        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.source == '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)
Ejemplo n.º 2
0
    async def customcounter(self, ctx, name=None, *, modifier=None):
        """Commands to implement custom counters.
        When called on its own, if modifier is supplied, increases the counter *name* by *modifier*.
        If modifier is not supplied, prints the value and metadata of the counter *name*.
        """
        if name is None:
            return await self.customcounter_summary(ctx)
        character: Character = await Character.from_ctx(ctx)
        counter = await character.select_consumable(ctx, name)

        cc_embed_title = counter.title if counter.title is not None else counter.name

        # replace [name] in title
        cc_embed_title = cc_embed_title.replace('[name]', character.name)

        if modifier is None:  # display value
            counter_display_embed = EmbedWithCharacter(character)
            counter_display_embed.add_field(name=counter.name,
                                            value=counter.full_str())
            if counter.desc:
                counter_display_embed.add_field(name='Description',
                                                value=counter.desc,
                                                inline=False)
            return await ctx.send(embed=counter_display_embed)

        operator = None
        if ' ' in modifier:
            m = modifier.split(' ')
            operator = m[0]
            modifier = m[-1]

        change = ''
        old_value = counter.value
        try:
            modifier = int(modifier)
        except ValueError:
            return await ctx.send(
                f"Could not modify counter: {modifier} is not a number")
        result_embed = EmbedWithCharacter(character)
        if not operator or operator == 'mod':
            new_value = counter.value + modifier
        elif operator == 'set':
            new_value = modifier
        else:
            return await ctx.send("Invalid operator. Use mod or set.")

        counter.set(new_value)
        await character.commit(ctx)

        delta = f"({counter.value - old_value:+})"
        if new_value - counter.value:  # we overflowed somewhere
            out = f"{str(counter)} {delta}\n({abs(new_value - counter.value)} overflow)"
        else:
            out = f"{str(counter)} {delta}"

        result_embed.add_field(name=cc_embed_title, value=out)

        if counter.desc:
            result_embed.add_field(name='Description',
                                   value=counter.desc,
                                   inline=False)

        await try_delete(ctx.message)
        await ctx.send(embed=result_embed)
Ejemplo n.º 3
0
    async def _rest(self, ctx, rest_type, *args):
        """
        Runs a rest.

        :param ctx: The Context.
        :param character: The Character.
        :param rest_type: "long", "short", "all"
        :param args: a list of args.
        """
        character: Character = await Character.from_ctx(ctx)
        old_hp = character.hp
        old_slots = {
            lvl: character.spellbook.get_slots(lvl)
            for lvl in range(1, 10)
        }

        embed = EmbedWithCharacter(character, name=False)
        if rest_type == 'long':
            reset_counters = character.long_rest()
            embed.title = f"{character.name} took a Long Rest!"
        elif rest_type == 'short':
            reset_counters = character.short_rest()
            embed.title = f"{character.name} took a Short Rest!"
        elif rest_type == 'all':
            reset_counters = character.reset_all_consumables()
            embed.title = f"{character.name} reset all counters!"
        else:
            raise ValueError(f"Invalid rest type: {rest_type}")

        if '-h' in args:
            values = ', '.join(
                set(ctr.name for ctr, _ in reset_counters)
                | {"Hit Points", "Death Saves", "Spell Slots"})
            embed.add_field(name="Reset Values", value=values)
        else:
            # hp
            hp_delta = character.hp - old_hp
            hp_delta_str = ""
            if hp_delta:
                hp_delta_str = f" ({hp_delta:+})"
            embed.add_field(name="Hit Points",
                            value=f"{character.hp_str()}{hp_delta_str}")

            # slots
            slots_out = []
            slots_delta = {
                lvl: character.spellbook.get_slots(lvl) - old_slots[lvl]
                for lvl in range(1, 10)
            }
            for lvl in range(1, 10):
                if character.spellbook.get_max_slots(lvl):
                    if slots_delta[lvl]:
                        slots_out.append(
                            f"{character.spellbook.slots_str(lvl)} ({slots_delta[lvl]:+})"
                        )
                    else:
                        slots_out.append(character.spellbook.slots_str(lvl))
            if slots_out:
                embed.add_field(name="Spell Slots", value='\n'.join(slots_out))

            # ccs
            counters_out = []
            for counter, result in reset_counters:
                if result.new_value != result.old_value:
                    counters_out.append(
                        f"{counter.name}: {str(counter)} ({result.delta})")
                else:
                    counters_out.append(f"{counter.name}: {str(counter)}")
            if counters_out:
                embed.add_field(name="Reset Counters",
                                value='\n'.join(counters_out))

        await character.commit(ctx)
        await ctx.send(embed=embed)
Ejemplo n.º 4
0
    async def spellbook(self, ctx):
        """Commands to display a character's known spells and metadata."""
        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.get_remaining_slots_str() or "None")
        spells_known = collections.defaultdict(lambda: [])
        choices = await get_spell_choices(ctx)
        for spell_ in character.spellbook.spells:
            spell = await get_castable_spell(ctx, spell_.name, choices, strict=True)
            if spell is None:
                spells_known['unknown'].append(f"*{spell_.name}*")
            else:
                if spell.source == 'homebrew':
                    formatted = f"*{spell.name}*"
                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))
        await ctx.send(embed=embed)
Ejemplo n.º 5
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)
Ejemplo n.º 6
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 ("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_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
Ejemplo n.º 7
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
Ejemplo n.º 8
0
    async def customcounter(self, ctx, name=None, *, modifier=None):
        """Commands to implement custom counters.
        If a modifier is not supplied, prints the value and metadata of the counter *name*.
        Otherwise, changes the counter *name* by *modifier*. Supports dice.

        The following can be put after the counter *name* to change how the *modifier* is applied:
        `mod` - Add *modifier* counter value
        `set` - Sets the counter value to *modifier*

        *Ex:*
        `!cc Test 1`
        `!cc Test -2*2d4`
        `!cc Test set 1d4`

        """
        if name is None:
            return await self.customcounter_summary(ctx)
        character: Character = await Character.from_ctx(ctx)
        counter = await character.select_consumable(ctx, name)

        cc_embed_title = counter.title if counter.title is not None else counter.name

        # replace [name] in title
        cc_embed_title = cc_embed_title.replace('[name]', character.name)

        if modifier is None:  # display value
            counter_display_embed = EmbedWithCharacter(character)
            counter_display_embed.add_field(name=counter.name, value=counter.full_str())
            if counter.desc:
                counter_display_embed.add_field(name='Description', value=counter.desc, inline=False)
            return await ctx.send(embed=counter_display_embed)

        operator = None
        if ' ' in modifier:
            m = modifier.split(' ')
            operator = m[0]
            modifier = m[-1]

        roll_text = ''
        try:
            result = int(modifier)
        except ValueError:
            try:  # if we're not a number, are we dice
                roll_result = d20.roll(str(modifier))
                result = roll_result.total
                roll_text = f"\nRoll: {roll_result}"
            except d20.RollSyntaxError:
                raise InvalidArgument(
                    f"Could not modify counter: {modifier} cannot be interpreted as a number or dice string.")

        old_value = counter.value
        result_embed = EmbedWithCharacter(character)
        if not operator or operator == 'mod':
            new_value = counter.value + result
        elif operator == 'set':
            new_value = result
        else:
            return await ctx.send("Invalid operator. Use mod or set.")

        counter.set(new_value)
        await character.commit(ctx)

        delta = f"({counter.value - old_value:+})"
        out = f"{str(counter)} {delta}{roll_text}"

        if new_value - counter.value:  # we overflowed somewhere
            out += f"\n({abs(new_value - counter.value)} overflow)"

        result_embed.add_field(name=cc_embed_title, value=out)

        if counter.desc:
            result_embed.add_field(name='Description', value=counter.desc, inline=False)

        await try_delete(ctx.message)
        await ctx.send(embed=result_embed)
Ejemplo n.º 9
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
Ejemplo n.º 10
0
    async def _old_cast(self, ctx, spell_name, args):
        spell = getSpell(spell_name)
        self.bot.rdb.incr('spells_looked_up_life')
        if spell is None:
            return await self.bot.say("Spell not found.", delete_after=15)
        if spell.get('source') == "UAMystic":
            return await self.bot.say("Mystic talents are not supported.")

        char = await Character.from_ctx(ctx)

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

        can_cast = True
        spell_level = int(spell.get('level', 0))
        cast_level = args.last('l', spell_level, int)
        if not spell_level <= cast_level <= 9:
            return await self.bot.say("Invalid spell level.")

        # make sure we can cast it
        if not char.get_remaining_slots(
                cast_level) > 0 and spell_name in char.get_spell_list():
            can_cast = False

        if args.last('i', type_=bool):
            can_cast = True

        if not can_cast:
            embed = EmbedWithCharacter(char)
            embed.title = "Cannot cast spell!"
            embed.description = "Not enough spell slots remaining, or spell not in known spell list!\n" \
                                "Use `!game longrest` to restore all spell slots, or pass `-i` to ignore restrictions."
            if cast_level > 0:
                embed.add_field(name="Spell Slots",
                                value=char.get_remaining_slots_str(cast_level))
            return await self.bot.say(embed=embed)

        if len(args) == 0:
            rolls = spell.get('roll', None)
            if isinstance(rolls, list):
                rolls = '\n'.join(rolls) \
                    .replace('SPELL', str(char.get_spell_ab() - char.get_prof_bonus())) \
                    .replace('PROF', str(char.get_prof_bonus()))
                rolls = rolls.split('\n')
                out = "**{} casts {}:** ".format(
                    char.get_name(), spell['name']) + '\n'.join(
                        roll(r, inline=True).skeleton for r in rolls)
            elif rolls is not None:
                rolls = rolls \
                    .replace('SPELL', str(char.get_spell_ab() - char.get_prof_bonus())) \
                    .replace('PROF', str(char.get_prof_bonus()))
                out = "**{} casts {}:** ".format(
                    char.get_name(), spell['name']) + roll(
                        rolls, inline=True).skeleton
            else:
                out = "**{} casts {}!** ".format(char.get_name(),
                                                 spell['name'])
        else:
            rolls = args.get('r')
            roll_results = ""
            for r in rolls:
                res = roll(r, inline=True)
                if res.total is not None:
                    roll_results += res.result + '\n'
                else:
                    roll_results += "**Effect:** " + r
            out = "**{} casts {}:**\n".format(char.get_name(),
                                              spell['name']) + roll_results

        if not args.last('i', type_=bool):
            char.use_slot(cast_level)
        if cast_level > 0:
            out += f"\n**Remaining Spell Slots**: {char.get_remaining_slots_str(cast_level)}"

        out = "Spell not supported by new cast, falling back to old cast.\n" + out
        await char.commit(ctx)  # make sure we save changes
        await self.bot.say(out)
        spell_cmd = self.bot.get_command('spell')
        if spell_cmd is None:
            return await self.bot.say("Lookup cog not loaded.")
        await ctx.invoke(spell_cmd, name=spell['name'])
Ejemplo n.º 11
0
    async def spellbook(self, ctx, *args):
        """
        Commands to display a character's known spells and metadata.
        __Valid Arguments__
        all - Display all of a character's known spells, including unprepared ones.
        """
        await ctx.trigger_typing()

        character: Character = await Character.from_ctx(ctx)
        ep = embeds.EmbedPaginator(EmbedWithCharacter(character))
        ep.add_field(name="DC", value=str(character.spellbook.dc), inline=True)
        ep.add_field(name="Spell Attack Bonus", value=str(character.spellbook.sab), inline=True)
        ep.add_field(name="Spell Slots", value=character.spellbook.slots_str() or "None", inline=True)

        show_unprepared = 'all' in args
        known_count = len(character.spellbook.spells)
        prepared_count = sum(1 for spell in character.spellbook.spells if spell.prepared)

        if known_count == prepared_count:
            ep.add_description(f"{character.name} knows {known_count} spells.")
        else:
            ep.add_description(f"{character.name} has {prepared_count} spells prepared and knows {known_count} spells.")

        # dynamic help flags
        flag_show_multiple_source_help = False
        flag_show_homebrew_help = False
        flag_show_prepared_help = False
        flag_show_prepared_underline_help = False

        spells_known = collections.defaultdict(lambda: [])
        choices = await get_spell_choices(ctx)
        for sb_spell in character.spellbook.spells:
            if not (sb_spell.prepared or show_unprepared):
                flag_show_prepared_help = True
                continue

            # homebrew / multisource formatting
            results, strict = search(choices, sb_spell.name, lambda sp: sp.name, strict=True)
            if not strict:
                known_level = 'unknown'
                if len(results) > 1:
                    formatted = f"*{sb_spell.name} ({'*' * len(results)})*"
                    flag_show_multiple_source_help = True
                else:
                    formatted = f"*{sb_spell.name}*"
                flag_show_homebrew_help = True
            else:
                spell = results
                known_level = str(spell.level)
                if spell.homebrew:
                    formatted = f"*{spell.name}*"
                    flag_show_homebrew_help = True
                else:
                    formatted = spell.name

            # prepared formatting
            if show_unprepared and sb_spell.prepared:
                formatted = f"__{formatted}__"
                flag_show_prepared_underline_help = True

            spells_known[known_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(key=lambda s: s.lstrip('*_'))
                ep.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 flag_show_prepared_help:
            footer_out.append(f"Unprepared spells were not shown. Use \"{ctx.prefix}spellbook all\" to view them!")
        if flag_show_prepared_underline_help:
            footer_out.append("Prepared spells are marked with an underline.")

        if footer_out:
            ep.set_footer(value=' '.join(footer_out))
        await ep.send_to(ctx)
Ejemplo n.º 12
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."""
        try:
            await self.bot.delete_message(ctx.message)
        except:
            pass

        char = None
        if not '-i' in args:
            char = await Character.from_ctx(ctx)
            spell_name = await searchCharacterSpellName(spell_name, ctx, char)
        else:
            spell_name = await searchSpellNameFull(spell_name, ctx)

        if spell_name is None: return

        spell = strict_search(c.autospells, 'name', spell_name)
        if spell is None:
            return await self._old_cast(ctx, spell_name,
                                        args)  # fall back to old cast

        if not char: char = await Character.from_ctx(ctx)

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

        can_cast = True
        spell_level = int(spell.get('level', 0))
        cast_level = args.last('l', spell_level, int)
        if not spell_level <= cast_level <= 9:
            return await self.bot.say("Invalid spell level.")

        # make sure we can cast it
        if not char.get_remaining_slots(
                cast_level) > 0 and spell_name in char.get_spell_list():
            can_cast = False

        if args.last('i', type_=bool):
            can_cast = True

        if not can_cast:
            embed = EmbedWithCharacter(char)
            embed.title = "Cannot cast spell!"
            embed.description = "Not enough spell slots remaining, or spell not in known spell list!\n" \
                                "Use `!game longrest` to restore all spell slots, or pass `-i` to ignore restrictions."
            if cast_level > 0:
                embed.add_field(name="Spell Slots",
                                value=char.get_remaining_slots_str(cast_level))
            return await self.bot.say(embed=embed)

        args['l'] = [cast_level]
        args['name'] = [char.get_name()]
        args['dc'] = [args.get('dc', [char.get_save_dc()])[-1]]
        args['casterlevel'] = [char.get_level()]
        args['crittype'] = [char.get_setting('crittype', 'default')]
        args['ab'] = [char.get_spell_ab()]
        args['SPELL'] = [
            str(
                char.evaluate_cvar("SPELL")
                or (char.get_spell_ab() - char.get_prof_bonus()))
        ]

        result = sheet_cast(spell, args, EmbedWithCharacter(char, name=False))

        embed = result['embed']

        _fields = args.get('f')
        if type(_fields) == list:
            for f in _fields:
                title = f.split('|')[0] if '|' in f else '\u200b'
                value = "|".join(f.split('|')[1:]) if '|' in f else f
                embed.add_field(name=title, value=value)

        if not args.last('i', type_=bool):
            char.use_slot(cast_level)
        if cast_level > 0:
            embed.add_field(name="Spell Slots",
                            value=char.get_remaining_slots_str(cast_level))

        await char.commit(ctx)  # make sure we save changes
        await self.bot.say(embed=embed)
Ejemplo n.º 13
0
    async def customcounter(self, ctx, name=None, *, modifier=None):
        """Commands to implement custom counters.
        When called on its own, if modifier is supplied, increases the counter *name* by *modifier*.
        If modifier is not supplied, prints the value and metadata of the counter *name*."""
        if name is None:
            return await ctx.invoke(self.bot.get_command("customcounter list"))
        character = await Character.from_ctx(ctx)
        sel = await character.select_consumable(ctx, name)
        if sel is None:
            return await self.bot.say("Selection timed out or was cancelled.")

        name = sel[0]
        counter = sel[1]

        assert character is not None
        assert counter is not None

        if modifier is None:  # display value
            counterDisplayEmbed = EmbedWithCharacter(character)
            val = self._get_cc_value(character, counter)
            counterDisplayEmbed.add_field(name=name, value=val)
            return await self.bot.say(embed=counterDisplayEmbed)

        operator = None
        if ' ' in modifier:
            m = modifier.split(' ')
            operator = m[0]
            modifier = m[-1]

        try:
            modifier = int(modifier)
        except ValueError:
            return await self.bot.say(
                f"Could not modify counter: {modifier} is not a number")
        resultEmbed = EmbedWithCharacter(character)
        if not operator or operator == 'mod':
            consValue = int(counter.get('value', 0))
            newValue = consValue + modifier
        elif operator == 'set':
            newValue = modifier
        else:
            return await self.bot.say("Invalid operator. Use mod or set.")
        try:
            character.set_consumable(name, newValue)
            await character.commit(ctx)
            _max = self._get_cc_max(character, counter)
            actualValue = int(character.get_consumable(name).get('value', 0))

            if counter.get('type') == 'bubble':
                assert _max not in ('N/A', None)
                numEmpty = _max - counter.get('value', 0)
                filled = '\u25c9' * counter.get('value', 0)
                empty = '\u3007' * numEmpty
                out = f"{filled}{empty}"
            else:
                out = f"{counter.get('value', 0)}"
            if (not _max
                    in (None, 'N/A')) and not counter.get('type') == 'bubble':
                resultEmbed.description = f"**__{name}__**\n{out}/{_max}"
            else:
                resultEmbed.description = f"**__{name}__**\n{out}"

            if newValue - actualValue:
                resultEmbed.description += f"\n({abs(newValue - actualValue)} overflow)"
        except CounterOutOfBounds:
            resultEmbed.description = f"Could not modify counter: new value out of bounds"
        try:
            await self.bot.delete_message(ctx.message)
        except:
            pass
        await self.bot.say(embed=resultEmbed)
Ejemplo n.º 14
0
    async def spellbook(self, ctx):
        """Commands to display a character's known spells and metadata."""
        character = await Character.from_ctx(ctx)
        embed = EmbedWithCharacter(character)
        embed.description = f"{character.get_name()} knows {len(character.get_spell_list())} spells."
        embed.add_field(name="DC", value=str(character.get_save_dc()))
        embed.add_field(name="Spell Attack Bonus",
                        value=str(character.get_spell_ab()))
        embed.add_field(name="Spell Slots",
                        value=character.get_remaining_slots_str() or "None")
        spells_known = {}
        for spell_name in character.get_spell_list():
            spell = strict_search(c.spells, 'name', spell_name)
            if spell is None:
                continue
            spells_known[spell['level']] = spells_known.get(
                spell['level'], []) + [spell_name]

        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:
                embed.add_field(name=level_name.get(level, "Unknown Level"),
                                value=', '.join(spells))
        await self.bot.say(embed=embed)
Ejemplo n.º 15
0
    async def spellbook(self, ctx):
        """Commands to display a character's known spells and metadata."""
        character = await Character.from_ctx(ctx)
        embed = EmbedWithCharacter(character)
        embed.description = f"{character.get_name()} knows {len(character.get_spell_list())} spells."
        embed.add_field(name="DC", value=str(character.get_save_dc()))
        embed.add_field(name="Spell Attack Bonus",
                        value=str(character.get_spell_ab()))
        embed.add_field(name="Spell Slots",
                        value=character.get_remaining_slots_str() or "None")
        spells_known = {}
        choices = await get_spell_choices(ctx)
        for spell_ in character.get_raw_spells():
            if isinstance(spell_, str):
                spell, strict = search(c.spells, spell_, lambda sp: sp.name)
                if spell is None or not strict:
                    continue
                spells_known[str(spell.level)] = spells_known.get(
                    str(spell.level), []) + [spell.name]
            else:
                spellname = spell_['name']
                strict = spell_['strict']
                spell = await get_castable_spell(ctx, spellname, choices)
                if spell is None and strict:
                    continue
                elif spell is None:
                    spells_known['unknown'] = spells_known.get(
                        'unknown', []) + [f"*{spellname}*"]
                else:
                    if spell.source == 'homebrew':
                        formatted = f"*{spell.name}*"
                    else:
                        formatted = spell.name
                    spells_known[str(spell.level)] = spells_known.get(
                        str(spell.level), []) + [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))
        await ctx.send(embed=embed)
Ejemplo n.º 16
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)