Beispiel #1
0
    async def racefeat(self, ctx, *, name: str):
        """Looks up a racial feature."""
        guild_settings = await self.get_settings(ctx.guild)
        pm = guild_settings.get("pm_result", False)
        destination = ctx.author if pm else ctx.channel

        result, metadata = await search_and_select(ctx, c.rfeats, name, lambda e: e['name'], return_metadata=True)
        await self.add_training_data("racefeat", name, result['name'], metadata=metadata)

        embed = EmbedWithAuthor(ctx)
        embed.title = result['name']
        desc = result['text']
        desc = [desc[i:i + 1024] for i in range(0, len(desc), 1024)]
        embed.description = ''.join(desc[:2])
        for piece in desc[2:]:
            embed.add_field(name="** **", value=piece)

        await destination.send(embed=embed)
Beispiel #2
0
    async def race(self, ctx, *, name: str):
        """Looks up a race."""
        choices = compendium.fancyraces + compendium.nrace_names
        result = await self._lookup_search(ctx, choices, name, lambda e: e.name, search_type='race', is_obj=True)
        if not result:
            return

        embed = EmbedWithAuthor(ctx)
        embed.title = result.name
        embed.description = f"Source: {result.source}"
        embed.add_field(name="Speed", value=result.get_speed_str())
        embed.add_field(name="Size", value=result.size)
        if result.ability:
            embed.add_field(name="Ability Bonuses", value=result.get_asi_str())
        for t in result.get_traits():
            add_fields_from_long_text(embed, t['name'], t['text'])

        await (await self._get_destination(ctx)).send(embed=embed)
Beispiel #3
0
    async def _class(self, ctx, name: str, level: int = None):
        """Looks up a class, or all features of a certain level."""
        if level is not None and not 0 < level < 21:
            return await ctx.send("Invalid level.")

        result: gamedata.Class = await self._lookup_search3(ctx, {'class': compendium.classes}, name)

        embed = EmbedWithAuthor(ctx)
        embed.url = result.url
        if level is None:
            embed.title = result.name
            embed.add_field(name="Hit Points", value=result.hit_points)

            levels = []
            for level in range(1, 21):
                level = result.levels[level - 1]
                levels.append(', '.join([feature.name for feature in level]))

            embed.add_field(name="Starting Proficiencies", value=result.proficiencies, inline=False)
            embed.add_field(name="Starting Equipment", value=result.equipment, inline=False)

            level_features_str = ""
            for i, l in enumerate(levels):
                level_features_str += f"`{i + 1}` {l}\n"
            embed.description = level_features_str

            handle_source_footer(embed, result, f"Use {ctx.prefix}classfeat to look up a feature.",
                                 add_source_str=False)
        else:
            embed.title = f"{result.name}, Level {level}"

            level_features = result.levels[level - 1]

            for resource, value in zip(result.table.headers, result.table.levels[level - 1]):
                if value != '0':
                    embed.add_field(name=resource, value=value)

            for f in level_features:
                embed.add_field(name=f.name, value=trim_str(f.text, 1024), inline=False)

            handle_source_footer(embed, result, f"Use {ctx.prefix}classfeat to look up a feature if it is cut off.",
                                 add_source_str=False)

        await (await self._get_destination(ctx)).send(embed=embed)
Beispiel #4
0
    async def rule(self, ctx, *, name: str):
        """Looks up a rule."""
        guild_settings = await self.get_settings(ctx.message.server)
        pm = guild_settings.get("pm_result", False)

        destination = ctx.message.author if pm else ctx.message.channel

        result = await search_and_select(ctx, c.rules, name,
                                         lambda e: e['name'])

        embed = EmbedWithAuthor(ctx)
        embed.title = result['name']
        desc = result['desc']
        desc = [desc[i:i + 1024] for i in range(0, len(desc), 1024)]
        embed.description = ''.join(desc[:2])
        for piece in desc[2:]:
            embed.add_field(name="** **", value=piece)

        await self.bot.send_message(destination, embed=embed)
Beispiel #5
0
    async def subclass(self, ctx, *, name: str):
        """Looks up a subclass."""
        result: gamedata.Subclass = await self._lookup_search3(ctx, {'class': compendium.subclasses}, name,
                                                               query_type='subclass')

        embed = EmbedWithAuthor(ctx)
        embed.url = result.url
        embed.title = result.name
        embed.description = f"*Source: {result.source_str()}*"

        for level in result.levels:
            for feature in level:
                text = trim_str(feature.text, 1024)
                embed.add_field(name=feature.name, value=text, inline=False)

        handle_source_footer(embed, result, f"Use {ctx.prefix}classfeat to look up a feature if it is cut off.",
                             add_source_str=False)

        await (await self._get_destination(ctx)).send(embed=embed)
    async def token(self, ctx, *, name=None):
        """Shows a token for a monster or player. May not support all monsters."""

        if name is None:
            token_cmd = self.bot.get_command('playertoken')
            if token_cmd is None:
                return await ctx.send("Error: SheetManager cog not loaded.")
            return await ctx.invoke(token_cmd)

        monster, metadata = await select_monster_full(ctx,
                                                      name,
                                                      return_metadata=True)

        metadata['homebrew'] = monster.source == 'homebrew'
        await self.add_training_data("monster",
                                     name,
                                     monster.name,
                                     metadata=metadata)

        url = monster.get_image_url()

        if not monster.source == 'homebrew':
            embed = EmbedWithAuthor(ctx)
            embed.title = monster.name
            embed.description = f"{monster.size} monster."
            embed.set_image(url=url)
            embed.set_footer(text="This command may not support all monsters.")

            await ctx.send(embed=embed)
        else:
            if not url:
                return await ctx.channel.send("This monster has no image.")

            try:
                processed = await generate_token(url)
            except Exception as e:
                return await ctx.channel.send(f"Error generating token: {e}")

            file = discord.File(processed, filename="image.png")
            await ctx.channel.send(
                "I generated this token for you! If it seems  wrong, you can make your own at "
                "<http://rolladvantage.com/tokenstamp/>!",
                file=file)
Beispiel #7
0
    async def spell(self, ctx, *, name: str):
        """Looks up a spell."""
        choices = await get_spell_choices(ctx, filter_by_license=False)
        spell = await self._lookup_search3(ctx, {'spell': choices}, name)

        embed = EmbedWithAuthor(ctx)
        embed.url = spell.url
        color = embed.colour

        embed.title = spell.name
        school_level = f"{spell.get_level()} {spell.get_school().lower()}" if spell.level > 0 \
            else f"{spell.get_school().lower()} cantrip"
        embed.description = f"*{school_level}. " \
                            f"({', '.join(itertools.chain(spell.classes, spell.subclasses))})*"
        if spell.ritual:
            time = f"{spell.time} (ritual)"
        else:
            time = spell.time

        meta = f"**Casting Time**: {time}\n" \
               f"**Range**: {spell.range}\n" \
               f"**Components**: {spell.components}\n" \
               f"**Duration**: {spell.duration}"
        embed.add_field(name="Meta", value=meta)

        higher_levels = spell.higherlevels
        pieces = chunk_text(spell.description)

        embed.add_field(name="Description", value=pieces[0], inline=False)

        embed_queue = [embed]
        if len(pieces) > 1:
            for i, piece in enumerate(pieces[1::2]):
                temp_embed = discord.Embed()
                temp_embed.colour = color
                if (next_idx := (i + 1) * 2) < len(
                        pieces
                ):  # this is chunked into 1024 pieces, and descs can handle 2
                    temp_embed.description = piece + pieces[next_idx]
                else:
                    temp_embed.description = piece
                embed_queue.append(temp_embed)
    async def patron_roscoe(self, ctx):
        embed = EmbedWithAuthor(ctx)
        embed.title = "Roscoe's Feast"
        embed.description = "*6th level conjuration. (Cleric, Druid)*"
        embed.add_field(name="Casting Time", value="10 minutes")
        embed.add_field(name="Range", value="30 feet")
        embed.add_field(name="Components", value="V, S, M (A gem encrusted bowl worth 1000 gold pieces)")
        embed.add_field(name="Duration", value="Instantaneous")
        embed.add_field(
            name="Description",
            value="You call forth the Avatar of Roscoe who brings with him a magnificent feast of chicken and waffles.\n"
                  "The feast takes 1 hour to consume and disappears at the end of that time, and the beneficial effects "
                  "don't set in until this hour is over. Up to twelve creatures can partake of the feast.\n"
                  "A creature that partakes of the feast gains several benefits. "
                  "The creature is cured of all diseases and poison, becomes immune to poison and being frightened, and "
                  "makes all Wisdom saving throws with advantage. Its hit point maximum also increases by 2d10, and it "
                  "gains the same number of hit points. These benefits last for 24 hours.")

        embed.set_footer(text=f"Spell | Thanks Roscoe!")
        await ctx.send(embed=embed)
Beispiel #9
0
    async def background(self, ctx, *, name: str):
        """Looks up a background."""
        try:
            guild_id = ctx.message.server.id
            pm = self.settings.get(guild_id, {}).get("pm_result", False)
            srd = self.settings.get(guild_id, {}).get("srd", False)
        except:
            pm = False
            srd = False

        result = await search_and_select(ctx,
                                         c.backgrounds,
                                         name,
                                         lambda e: e['name'],
                                         srd=srd)

        if not result['srd'] and srd:
            return await self.send_srd_error(ctx, result)

        embed = EmbedWithAuthor(ctx)
        embed.title = result['name']
        embed.description = f"*Source: {result.get('source', 'Unknown')}*"

        ignored_fields = [
            'suggested characteristics', 'personality trait', 'ideal', 'bond',
            'flaw', 'specialty', 'harrowing event'
        ]
        for trait in result['trait']:
            if trait['name'].lower() in ignored_fields: continue
            text = '\n'.join(t for t in trait['text'] if t)
            text = textwrap.shorten(text, width=1020, placeholder="...")
            embed.add_field(name=trait['name'], value=text)

        # do stuff here
        if pm:
            await self.bot.send_message(ctx.message.author, embed=embed)
        else:
            await self.bot.say(embed=embed)
Beispiel #10
0
    async def token(self, ctx, *, name=None):
        """Shows a monster's image."""

        if name is None:
            token_cmd = self.bot.get_command('playertoken')
            if token_cmd is None:
                return await ctx.send("Error: SheetManager cog not loaded.")
            return await ctx.invoke(token_cmd)

        choices = await get_monster_choices(ctx, filter_by_license=False)
        monster = await self._lookup_search3(ctx, {'monster': choices}, name)
        await Stats.increase_stat(ctx, "monsters_looked_up_life")

        url = monster.get_image_url()
        embed = EmbedWithAuthor(ctx)
        embed.title = monster.name
        embed.description = f"{monster.size} monster."

        if not url:
            return await ctx.channel.send("This monster has no image.")

        embed.set_image(url=url)
        await ctx.send(embed=embed)
    async def condition(self, ctx, *, name: str):
        """Looks up a condition."""
        guild_settings = await self.get_settings(ctx.guild)
        pm = guild_settings.get("pm_result", False)

        destination = ctx.author if pm else ctx.channel

        result, metadata = await search_and_select(ctx,
                                                   c.conditions,
                                                   name,
                                                   lambda e: e['name'],
                                                   return_metadata=True)

        await self.add_training_data("condition",
                                     name,
                                     result['name'],
                                     metadata=metadata)

        embed = EmbedWithAuthor(ctx)
        embed.title = result['name']
        embed.description = result['desc']

        await destination.send(embed=embed)
Beispiel #12
0
    async def subclass(self, ctx, name: str):
        """Looks up a subclass."""
        choices = compendium.subclasses + compendium.nsubclass_names
        result = await self._lookup_search(ctx, choices, name, lambda e: e['name'], search_type='subclass')
        if not result:
            return

        embed = EmbedWithAuthor(ctx)
        embed.title = result['name']
        embed.description = f"*Source: {result['source']}*"

        for level_features in result['subclassFeatures']:
            for feature in level_features:
                for entry in feature['entries']:
                    if not isinstance(entry, dict): continue
                    if not entry.get('type') == 'entries': continue
                    text = parse_data_entry(entry['entries'])
                    embed.add_field(name=entry['name'], value=(text[:1019] + "...") if len(text) > 1023 else text,
                                    inline=False)

        embed.set_footer(text=f"Use {ctx.prefix}classfeat to look up a feature if it is cut off.")

        await (await self._get_destination(ctx)).send(embed=embed)
    async def subclass(self, ctx, name: str):
        """Looks up a subclass."""
        guild_settings = await self.get_settings(ctx.guild)
        pm = guild_settings.get("pm_result", False)
        destination = ctx.author if pm else ctx.channel

        result, metadata = await search_and_select(ctx,
                                                   c.subclasses,
                                                   name,
                                                   lambda e: e['name'],
                                                   return_metadata=True)
        await self.add_training_data("subclass",
                                     name,
                                     result['name'],
                                     metadata=metadata)

        embed = EmbedWithAuthor(ctx)
        embed.title = result['name']
        embed.description = f"*Source: {result['source']}*"

        for level_features in result['subclassFeatures']:
            for feature in level_features:
                for entry in feature['entries']:
                    if not isinstance(entry, dict): continue
                    if not entry.get('type') == 'entries': continue
                    text = parse_data_entry(entry['entries'])
                    embed.add_field(
                        name=entry['name'],
                        value=(text[:1019] +
                               "...") if len(text) > 1023 else text)

        embed.set_footer(
            text=
            f"Use {ctx.prefix}classfeat to look up a feature if it is cut off."
        )

        await destination.send(embed=embed)
Beispiel #14
0
    async def rule(self, ctx, *, name: str = None):
        """Looks up a rule."""
        destination = await self._get_destination(ctx)

        if name is None:
            return await self._show_reference_options(ctx, destination)

        options = []
        for actiontype in compendium.rule_references:
            if name == actiontype['type']:
                return await self._show_action_options(ctx, actiontype, destination)
            else:
                options.extend(actiontype['items'])

        result, metadata = await search_and_select(ctx, options, name, lambda e: e['fullName'], return_metadata=True)
        await self.add_training_data("reference", name, result['fullName'], metadata=metadata)

        embed = EmbedWithAuthor(ctx)
        embed.title = result['fullName']
        embed.description = f"*{result['short']}*"
        add_fields_from_long_text(embed, "Description", result['desc'])
        embed.set_footer(text=f"Rule | {result['source']}")

        await destination.send(embed=embed)
Beispiel #15
0
    async def race(self, ctx, *, name: str):
        """Looks up a race."""
        guild_settings = await self.get_settings(ctx.guild)
        pm = guild_settings.get("pm_result", False)
        destination = ctx.author if pm else ctx.channel

        result, metadata = await search_and_select(ctx, c.fancyraces, name, lambda e: e.name, return_metadata=True)
        await self.add_training_data("race", name, result.name, metadata=metadata)

        embed = EmbedWithAuthor(ctx)
        embed.title = result.name
        embed.description = f"Source: {result.source}"
        embed.add_field(name="Speed", value=result.get_speed_str())
        embed.add_field(name="Size", value=result.size)
        if result.ability:
            embed.add_field(name="Ability Bonuses", value=result.get_asi_str())
        for t in result.get_traits():
            f_text = t['text']
            f_text = [f_text[i:i + 1024] for i in range(0, len(f_text), 1024)]
            embed.add_field(name=t['name'], value=f_text[0])
            for piece in f_text[1:]:
                embed.add_field(name="** **", value=piece)

        await destination.send(embed=embed)
Beispiel #16
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)
Beispiel #17
0
    async def _class(self, ctx, name: str, level: int = None):
        """Looks up a class, or all features of a certain level."""
        try:
            guild_id = ctx.message.server.id
            pm = self.settings.get(guild_id, {}).get("pm_result", False)
            srd = self.settings.get(guild_id, {}).get("srd", False)
        except:
            pm = False
            srd = False
        destination = ctx.message.author if pm else ctx.message.channel

        if level is not None and not 0 < level < 21:
            return await self.bot.say("Invalid level.")

        result = await search_and_select(ctx,
                                         c.classes,
                                         name,
                                         lambda e: e['name'],
                                         srd=srd)

        if not result['srd'] and srd:
            return await self.send_srd_error(ctx, result)

        embed = EmbedWithAuthor(ctx)
        if level is None:
            embed.title = result['name']
            embed.add_field(name="Hit Die", value=f"1d{result['hd']['faces']}")
            embed.add_field(name="Saving Throws",
                            value=', '.join(
                                ABILITY_MAP.get(p)
                                for p in result['proficiency']))

            levels = []
            starting_profs = f"You are proficient with the following items, " \
                             f"in addition to any proficiencies provided by your race or background.\n" \
                             f"Armor: {', '.join(result['startingProficiencies'].get('armor', ['None']))}\n" \
                             f"Weapons: {', '.join(result['startingProficiencies'].get('weapons', ['None']))}\n" \
                             f"Tools: {', '.join(result['startingProficiencies'].get('tools', ['None']))}\n" \
                             f"Skills: Choose {result['startingProficiencies']['skills']['choose']} from " \
                             f"{', '.join(result['startingProficiencies']['skills']['from'])}"

            equip_choices = '\n'.join(
                f"• {i}" for i in result['startingEquipment']['default'])
            gold_alt = f"Alternatively, you may start with {result['startingEquipment']['goldAlternative']} gp " \
                       f"to buy your own equipment." if 'goldAlternative' in result['startingEquipment'] else ''
            starting_items = f"You start with the following items, plus anything provided by your background.\n" \
                             f"{equip_choices}\n" \
                             f"{gold_alt}"
            for level in range(1, 21):
                level_str = []
                level_features = result['classFeatures'][level - 1]
                for feature in level_features:
                    level_str.append(feature.get('name'))
                levels.append(', '.join(level_str))

            embed.add_field(name="Starting Proficiencies",
                            value=starting_profs)
            embed.add_field(name="Starting Equipment", value=starting_items)

            level_features_str = ""
            for i, l in enumerate(levels):
                level_features_str += f"`{i+1}` {l}\n"
            embed.description = level_features_str

            embed.set_footer(text="Use !classfeat to look up a feature.")
        else:
            embed.title = f"{result['name']}, Level {level}"

            level_resources = {}
            level_features = result['classFeatures'][level - 1]

            for table in result['classTableGroups']:
                relevant_row = table['rows'][level - 1]
                for i, col in enumerate(relevant_row):
                    level_resources[table['colLabels'][i]] = parse_data_entry(
                        [col])

            for res_name, res_value in level_resources.items():
                embed.add_field(name=res_name, value=res_value)

            for f in level_features:
                text = parse_data_entry(f['entries'])
                embed.add_field(name=f['name'],
                                value=(text[:1019] +
                                       "...") if len(text) > 1023 else text)

            embed.set_footer(
                text="Use !classfeat to look up a feature if it is cut off.")

        await self.bot.send_message(destination, embed=embed)
Beispiel #18
0
    async def send_character_details(self, ctx, final_level, race=None, _class=None, subclass=None, background=None):
        loadingMessage = await ctx.channel.send("Generating character, please wait...")
        color = random.randint(0, 0xffffff)

        # Name Gen
        #    DMG name gen
        name = self.old_name_gen()
        # Stat Gen
        #    4d6d1
        #        reroll if too low/high
        stats = [roll('4d6kh3').total for _ in range(6)]
        await ctx.author.send("**Stats for {0}:** `{1}`".format(name, stats))
        # Race Gen
        #    Racial Features
        race = race or random.choice(await get_race_choices(ctx))

        embed = EmbedWithAuthor(ctx)
        embed.title = race.name
        embed.add_field(name="Speed", value=race.speed)
        embed.add_field(name="Size", value=race.size)
        for t in race.traits:
            embeds.add_fields_from_long_text(embed, t.name, t.text)
        embed.set_footer(text=f"Race | {race.source_str()}")

        embed.colour = color
        await ctx.author.send(embed=embed)

        # Class Gen
        #    Class Features

        # class
        _class = _class or random.choice(await available(ctx, compendium.classes, 'class'))
        subclass = subclass or (random.choice(subclass_choices)
                                if (subclass_choices := await available(ctx, _class.subclasses, 'class')) else None)
        embed = EmbedWithAuthor(ctx)

        embed.title = _class.name
        embed.add_field(name="Hit Points", value=_class.hit_points)

        levels = []
        for level in range(1, final_level + 1):
            level = _class.levels[level - 1]
            levels.append(', '.join([feature.name for feature in level]))

        embed.add_field(name="Starting Proficiencies", value=_class.proficiencies, inline=False)
        embed.add_field(name="Starting Equipment", value=_class.equipment, inline=False)

        level_features_str = ""
        for i, l in enumerate(levels):
            level_features_str += f"`{i + 1}` {l}\n"
        embed.description = level_features_str
        await ctx.author.send(embed=embed)

        # level table
        embed = EmbedWithAuthor(ctx)
        embed.title = f"{_class.name}, Level {final_level}"

        for resource, value in zip(_class.table.headers, _class.table.levels[final_level - 1]):
            if value != '0':
                embed.add_field(name=resource, value=value)

        embed.colour = color
        await ctx.author.send(embed=embed)

        # features
        embed_queue = [EmbedWithAuthor(ctx)]
        num_fields = 0

        def inc_fields(ftext):
            nonlocal num_fields
            num_fields += 1
            if num_fields > 25:
                embed_queue.append(EmbedWithAuthor(ctx))
                num_fields = 0
            if len(str(embed_queue[-1].to_dict())) + len(ftext) > 5800:
                embed_queue.append(EmbedWithAuthor(ctx))
                num_fields = 0

        def add_levels(source):
            for level in range(1, final_level + 1):
                level_features = source.levels[level - 1]
                for f in level_features:
                    for field in embeds.get_long_field_args(f.text, f.name):
                        inc_fields(field['value'])
                        embed_queue[-1].add_field(**field)

        add_levels(_class)
        if subclass:
            add_levels(subclass)

        for embed in embed_queue:
            embed.colour = color
            await ctx.author.send(embed=embed)

        # Background Gen
        #    Inventory/Trait Gen
        background = background or random.choice(await available(ctx, compendium.backgrounds, 'background'))
        embed = EmbedWithAuthor(ctx)
        embed.title = background.name
        embed.set_footer(text=f"Background | {background.source_str()}")

        ignored_fields = ['suggested characteristics', 'personality trait', 'ideal', 'bond', 'flaw', 'specialty',
                          'harrowing event']
        for trait in background.traits:
            if trait.name.lower() in ignored_fields:
                continue
            text = textwrap.shorten(trait.text, width=1020, placeholder="...")
            embed.add_field(name=trait.name, value=text, inline=False)
        embed.colour = color
        await ctx.author.send(embed=embed)

        out = f"{ctx.author.mention}\n" \
              f"{name}, {race.name} {subclass.name if subclass else ''} {_class.name} {final_level}. " \
              f"{background.name} Background.\n" \
              f"Stat Array: `{stats}`\nI have PM'd you full character details."

        await loadingMessage.edit(content=out)
Beispiel #19
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 #20
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: cogs5e.models.caster.Spellcaster
        :param targets: A list of targets (Combatants)
        :param args: Args
        :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)
        phrase = args.join('phrase', '\n')
        title = args.last('title')

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

        if not (caster.can_cast(self, l) or i):
            embed = EmbedWithAuthor(ctx)
            embed.title = "Cannot cast spell!"
            embed.description = "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, " \
                                "or pass `-i` to ignore restrictions."
            if l > 0:
                embed.add_field(name="Spell Slots", value=caster.remaining_casts_of(self, l))
            return {"embed": embed}

        if not i:
            caster.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)
        dc_override = None
        ab_override = None
        spell_override = None
        stat_override = ''
        if mod_arg is not None:
            mod = mod_arg
            dc_override = 8 + mod + character.get_prof_bonus()
            ab_override = mod + character.get_prof_bonus()
            spell_override = mod
        elif character and 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 = character.get_mod(base)
            dc_override = 8 + mod + character.get_prof_bonus()
            ab_override = mod + character.get_prof_bonus()
            spell_override = mod
            stat_override = f" with {verbose_stat(base)}"

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

        if phrase:
            embed.description = f"*{phrase}*"

        conc_conflict = None
        conc_effect = None
        if self.concentration and isinstance(caster, Combatant) and combat:
            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:
            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)
        else:
            text = self.description
            if len(text) > 1020:
                text = f"{text[:1020]}..."
            embed.add_field(name="Description", value=text)
            if l != self.level and self.higherlevels:
                embed.add_field(name="At Higher Levels", value=self.higherlevels)
            embed.set_footer(text="No spell automation found.")

        if l > 0 and not i:
            embed.add_field(name="Spell Slots", value=caster.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 self.image:
            embed.set_thumbnail(url=self.image)

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

        return {"embed": embed}
Beispiel #21
0
async def handle_required_license(ctx, err):
    """
    Logs a unlicensed search and displays a prompt.

    :type ctx: discord.ext.commands.Context
    :type err: cogs5e.models.errors.RequiresLicense
    """
    result = err.entity

    await ctx.bot.mdb.analytics_nsrd_lookup.update_one(
        {
            "type": result.entity_type,
            "name": result.name
        }, {"$inc": {
            "num_lookups": 1
        }},
        upsert=True)

    embed = EmbedWithAuthor(ctx)
    if not err.has_connected_ddb:
        # was the user blocked from nSRD by a feature flag?
        ddb_user = await ctx.bot.ddb.get_ddb_user(ctx, ctx.author.id)
        if ddb_user is None:
            blocked_by_ff = False
        else:
            blocked_by_ff = not (await ctx.bot.ldclient.variation(
                "entitlements-enabled", ddb_user.to_ld_dict(), False))

        if blocked_by_ff:
            # get the message from feature flag
            # replacements:
            # $entity_type$, $entity_name$, $source$, $long_source$
            unavailable_title = await ctx.bot.ldclient.variation(
                "entitlements-disabled-header", ddb_user.to_ld_dict(),
                f"{result.name} is not available")
            unavailable_desc = await ctx.bot.ldclient.variation(
                "entitlements-disabled-message", ddb_user.to_ld_dict(),
                f"{result.name} is currently unavailable")

            embed.title = unavailable_title \
                .replace('$entity_type$', result.entity_type) \
                .replace('$entity_name$', result.name) \
                .replace('$source$', result.source) \
                .replace('$long_source$', long_source_name(result.source))
            embed.description = unavailable_desc \
                .replace('$entity_type$', result.entity_type) \
                .replace('$entity_name$', result.name) \
                .replace('$source$', result.source) \
                .replace('$long_source$', long_source_name(result.source))
        else:
            embed.title = f"Connect your D&D Beyond account to view {result.name}!"
            embed.url = "https://www.dndbeyond.com/account"
            embed.description = \
                "It looks like you don't have your Discord account connected to your D&D Beyond account!\n" \
                "Linking your account means that you'll be able to use everything you own on " \
                "D&D Beyond in Avrae for free - you can link your accounts " \
                "[here](https://www.dndbeyond.com/account)."
            embed.set_footer(
                text=
                "Already linked your account? It may take up to a minute for Avrae to recognize the "
                "link.")
    else:
        embed.title = f"Purchase {result.name} on D&D Beyond to view it here!"
        embed.description = \
            f"To see and search this {result.entity_type}'s full details, unlock **{result.name}** by " \
            f"purchasing {long_source_name(result.source)} on D&D Beyond.\n\n" \
            f"[Go to Marketplace]({result.marketplace_url})"
        embed.url = result.marketplace_url

        embed.set_footer(
            text=
            "Already purchased? It may take up to a minute for Avrae to recognize the "
            "purchase.")
    await ctx.send(embed=embed)
Beispiel #22
0
    async def item_lookup(self, ctx, *, name):
        """Looks up an item."""
        try:
            guild_id = ctx.message.server.id
            pm = self.settings.get(guild_id, {}).get("pm_result", False)
            srd = self.settings.get(guild_id, {}).get("srd", False)
        except:
            pm = False
            srd = False

        self.bot.db.incr('items_looked_up_life')

        result = await search_and_select(ctx,
                                         c.items,
                                         name,
                                         lambda e: e['name'],
                                         srd=srd)

        embed = EmbedWithAuthor(ctx)
        item = result

        if not item['srd'] and srd:
            return await self.send_srd_error(ctx, result)

        name = item['name']
        damage = ''
        extras = ''
        properties = []
        proptext = ""

        if 'type' in item:
            type_ = ', '.join(i for i in (
                [ITEM_TYPES.get(t, 'n/a') for t in item['type'].split(',')] +
                ["Wondrous Item" if item.get('wondrous') else '']) if i)
            for iType in item['type'].split(','):
                if iType in ('M', 'R', 'GUN'):
                    damage = f"{item.get('dmg1', 'n/a')} {DMGTYPES.get(item.get('dmgType'), 'n/a')}" \
                        if 'dmg1' in item and 'dmgType' in item else ''
                    type_ += f', {item.get("weaponCategory")}'
                if iType == 'S': damage = f"AC +{item.get('ac', 'n/a')}"
                if iType == 'LA': damage = f"AC {item.get('ac', 'n/a')} + DEX"
                if iType == 'MA':
                    damage = f"AC {item.get('ac', 'n/a')} + DEX (Max 2)"
                if iType == 'HA': damage = f"AC {item.get('ac', 'n/a')}"
                if iType == 'SHP':  # ships
                    for p in ("CREW", "PASS", "CARGO", "DMGT", "SHPREP"):
                        a = PROPS.get(p, 'n/a')
                        proptext += f"**{a.title()}**: {c.itemprops[p]}\n"
                    extras = f"Speed: {item.get('speed')}\nCarrying Capacity: {item.get('carryingcapacity')}\n" \
                             f"Crew {item.get('crew')}, AC {item.get('vehAc')}, HP {item.get('vehHp')}"
                    if 'vehDmgThresh' in item:
                        extras += f", Damage Threshold {item['vehDmgThresh']}"
                if iType == 'siege weapon':
                    extras = f"Size: {SIZES.get(item.get('size'), 'Unknown')}\n" \
                             f"AC {item.get('ac')}, HP {item.get('hp')}\n" \
                             f"Immunities: {item.get('immune')}"
        else:
            type_ = ', '.join(
                i for i in ("Wondrous Item" if item.get('wondrous') else '',
                            item.get('technology')) if i)
        rarity = str(item.get('rarity')).replace('None', '')
        if 'tier' in item:
            if rarity:
                rarity += f', {item["tier"]}'
            else:
                rarity = item['tier']
        type_and_rarity = type_ + (f", {rarity}" if rarity else '')
        value = (item.get('value', 'n/a') +
                 (', ' if 'weight' in item else '')) if 'value' in item else ''
        weight = (item.get('weight', 'n/a') + (' lb.' if item.get('weight') == '1' else ' lbs.')) \
            if 'weight' in item else ''
        weight_and_value = value + weight
        for prop in item.get('property', []):
            if not prop: continue
            a = b = prop
            a = PROPS.get(a, 'n/a')
            if b in c.itemprops:
                proptext += f"**{a.title()}**: {c.itemprops[b]}\n"
            if b == 'V': a += " (" + item.get('dmg2', 'n/a') + ")"
            if b in ('T', 'A'): a += " (" + item.get('range', 'n/a') + "ft.)"
            if b == 'RLD': a += " (" + item.get('reload', 'n/a') + " shots)"
            properties.append(a)
        properties = ', '.join(properties)
        damage_and_properties = f"{damage} - {properties}" if properties else damage
        damage_and_properties = (' --- ' + damage_and_properties) if weight_and_value and damage_and_properties else \
            damage_and_properties

        embed.title = name
        desc = f"*{type_and_rarity}*\n{weight_and_value}{damage_and_properties}\n{extras}"
        embed.description = parse_data_entry(desc)

        if 'reqAttune' in item:
            if item['reqAttune'] is True:  # can be truthy, but not true
                embed.add_field(name="Attunement",
                                value=f"Requires Attunement")
            else:
                embed.add_field(
                    name="Attunement",
                    value=f"Requires Attunement {item['reqAttune']}")

        text = parse_data_entry(item.get('entries', []))
        if proptext:
            text = f"{text}\n{proptext}"
        if len(text) > 5500:
            text = text[:5500] + "..."

        field_name = "Description"
        for piece in [text[i:i + 1024] for i in range(0, len(text), 1024)]:
            embed.add_field(name=field_name, value=piece)
            field_name = "** **"

        embed.set_footer(
            text=
            f"Item | {item.get('source', 'Unknown')} {item.get('page', 'Unknown')}"
        )

        if pm:
            await self.bot.send_message(ctx.message.author, embed=embed)
        else:
            await self.bot.say(embed=embed)
Beispiel #23
0
    async def monster(self, ctx, *, name: str):
        """Looks up a monster.
        Generally requires a Game Master role to show full stat block.
        Game Master Roles: GM, DM, Game Master, Dungeon Master"""

        try:
            guild_id = ctx.message.server.id
            pm = self.settings.get(guild_id, {}).get("pm_result", False)
            srd = self.settings.get(guild_id, {}).get("srd", False)
            visible_roles = ['gm', 'game master', 'dm', 'dungeon master']
            if self.settings.get(guild_id, {}).get("req_dm_monster", True):
                visible = True if any(
                    ro in [str(r).lower() for r in ctx.message.author.roles]
                    for ro in visible_roles) else False
            else:
                visible = True
        except:
            visible = True
            pm = False
            srd = False

        self.bot.db.incr('monsters_looked_up_life')
        monster = await select_monster_full(ctx, name, srd=srd)

        embed_queue = [EmbedWithAuthor(ctx)]
        color = embed_queue[-1].colour

        embed_queue[-1].title = monster.name

        if not monster.srd and srd:
            e = EmbedWithAuthor(ctx)
            e.title = monster.name
            e.description = "Description not available."
            return await self.bot.say(embed=e)

        def safe_append(title, desc):
            if len(desc) < 1024:
                embed_queue[-1].add_field(name=title, value=desc)
            elif len(desc) < 2048:
                # noinspection PyTypeChecker
                # I'm adding an Embed to a list of Embeds, shut up.
                embed_queue.append(
                    discord.Embed(colour=color, description=desc, title=title))
            else:
                # noinspection PyTypeChecker
                embed_queue.append(discord.Embed(colour=color, title=title))
                trait_all = [
                    desc[i:i + 2040] for i in range(0, len(desc), 2040)
                ]
                embed_queue[-1].description = trait_all[0]
                for t in trait_all[1:]:
                    # noinspection PyTypeChecker
                    embed_queue.append(
                        discord.Embed(colour=color, description=t))

        if visible:
            embed_queue[-1].description = monster.get_meta()
            if monster.traits:
                trait = ""
                for a in monster.traits:
                    trait += f"**{a.name}:** {a.desc}\n"
                if trait:
                    safe_append("Special Abilities", trait)
            if monster.actions:
                action = ""
                for a in monster.actions:
                    action += f"**{a.name}:** {a.desc}\n"
                if action:
                    safe_append("Actions", action)
            if monster.reactions:
                reaction = ""
                for a in monster.reactions:
                    reaction += f"**{a.name}:** {a.desc}\n"
                if reaction:
                    safe_append("Reactions", reaction)
            if monster.legactions:
                legendary = ""
                for a in monster.legactions:
                    if a.name:
                        legendary += f"**{a.name}:** {a.desc}\n"
                    else:
                        legendary += f"{a.desc}\n"
                if legendary:
                    safe_append("Legendary Actions", legendary)

        else:
            hp = monster.hp
            ac = monster.ac
            size = monster.size
            _type = monster.race
            if hp < 10:
                hp = "Very Low"
            elif 10 <= hp < 50:
                hp = "Low"
            elif 50 <= hp < 100:
                hp = "Medium"
            elif 100 <= hp < 200:
                hp = "High"
            elif 200 <= hp < 400:
                hp = "Very High"
            elif 400 <= hp:
                hp = "Ludicrous"

            if ac < 6:
                ac = "Very Low"
            elif 6 <= ac < 9:
                ac = "Low"
            elif 9 <= ac < 15:
                ac = "Medium"
            elif 15 <= ac < 17:
                ac = "High"
            elif 17 <= ac < 22:
                ac = "Very High"
            elif 22 <= ac:
                ac = "Untouchable"

            languages = len(monster.languages)

            embed_queue[-1].description = f"{size} {_type}.\n" \
                                          f"**AC:** {ac}.\n**HP:** {hp}.\n**Speed:** {monster.speed}\n" \
                                          f"{monster.get_hidden_stat_array()}\n" \
                                          f"**Languages:** {languages}\n"

            if monster.traits:
                embed_queue[-1].add_field(name="Special Abilities",
                                          value=str(len(monster.traits)))

            if monster.actions:
                embed_queue[-1].add_field(name="Actions",
                                          value=str(len(monster.actions)))

            if monster.reactions:
                embed_queue[-1].add_field(name="Reactions",
                                          value=str(len(monster.reactions)))

            if monster.legactions:
                embed_queue[-1].add_field(name="Legendary Actions",
                                          value=str(len(monster.legactions)))

        if monster.source == 'homebrew':
            embed_queue[-1].set_footer(
                text="Homebrew content.",
                icon_url="https://avrae.io/static/homebrew.png")

        embed_queue[0].set_thumbnail(url=monster.get_image_url())

        for embed in embed_queue:
            if pm:
                await self.bot.send_message(ctx.message.author, embed=embed)
            else:
                await self.bot.say(embed=embed)
    async def genChar(self, ctx, final_level, race=None, _class=None, subclass=None, background=None):
        loadingMessage = await self.bot.send_message(ctx.message.channel, "Generating character, please wait...")
        color = random.randint(0, 0xffffff)

        # Name Gen
        #    DMG name gen
        name = self.nameGen()
        # Stat Gen
        #    4d6d1
        #        reroll if too low/high
        stats = self.genStats()
        await self.bot.send_message(ctx.message.author, "**Stats for {0}:** `{1}`".format(name, stats))
        # Race Gen
        #    Racial Features
        race = race or random.choice([r for r in c.fancyraces if r.source in ('PHB', 'VGM', 'MTF')])

        embed = EmbedWithAuthor(ctx)
        embed.title = race.name
        embed.description = f"Source: {race.source}"
        embed.add_field(name="Speed", value=race.get_speed_str())
        embed.add_field(name="Size", value=race.size)
        embed.add_field(name="Ability Bonuses", value=race.get_asi_str())
        for t in race.get_traits():
            f_text = t['text']
            f_text = [f_text[i:i + 1024] for i in range(0, len(f_text), 1024)]
            embed.add_field(name=t['name'], value=f_text[0])
            for piece in f_text[1:]:
                embed.add_field(name="** **", value=piece)

        embed.colour = color
        await self.bot.send_message(ctx.message.author, embed=embed)

        # Class Gen
        #    Class Features
        _class = _class or random.choice([cl for cl in c.classes if not 'UA' in cl.get('source')])
        subclass = subclass or random.choice([s for s in _class['subclasses'] if not 'UA' in s['source']])
        embed = EmbedWithAuthor(ctx)
        embed.title = f"{_class['name']} ({subclass['name']})"
        embed.add_field(name="Hit Die", value=f"1d{_class['hd']['faces']}")
        embed.add_field(name="Saving Throws", value=', '.join(ABILITY_MAP.get(p) for p in _class['proficiency']))

        levels = []
        starting_profs = f"You are proficient with the following items, " \
                         f"in addition to any proficiencies provided by your race or background.\n" \
                         f"Armor: {', '.join(_class['startingProficiencies'].get('armor', ['None']))}\n" \
                         f"Weapons: {', '.join(_class['startingProficiencies'].get('weapons', ['None']))}\n" \
                         f"Tools: {', '.join(_class['startingProficiencies'].get('tools', ['None']))}\n" \
                         f"Skills: Choose {_class['startingProficiencies']['skills']['choose']} from " \
                         f"{', '.join(_class['startingProficiencies']['skills']['from'])}"

        equip_choices = '\n'.join(f"• {i}" for i in _class['startingEquipment']['default'])
        gold_alt = f"Alternatively, you may start with {_class['startingEquipment']['goldAlternative']} gp " \
                   f"to buy your own equipment." if 'goldAlternative' in _class['startingEquipment'] else ''
        starting_items = f"You start with the following items, plus anything provided by your background.\n" \
                         f"{equip_choices}\n" \
                         f"{gold_alt}"

        for level in range(1, final_level + 1):
            level_str = []
            level_features = _class['classFeatures'][level - 1]
            for feature in level_features:
                level_str.append(feature.get('name'))
            levels.append(', '.join(level_str))

        embed.add_field(name="Starting Proficiencies", value=starting_profs)
        embed.add_field(name="Starting Equipment", value=starting_items)

        level_features_str = ""
        for i, l in enumerate(levels):
            level_features_str += f"`{i+1}` {l}\n"
        embed.description = level_features_str

        embed.colour = color
        await self.bot.send_message(ctx.message.author, embed=embed)

        embed = EmbedWithAuthor(ctx)
        level_resources = {}
        for table in _class['classTableGroups']:
            relevant_row = table['rows'][final_level - 1]
            for i, col in enumerate(relevant_row):
                level_resources[table['colLabels'][i]] = parse_data_entry([col])

        for res_name, res_value in level_resources.items():
            embed.add_field(name=res_name, value=res_value)

        embed.colour = color
        await self.bot.send_message(ctx.message.author, embed=embed)

        embed_queue = [EmbedWithAuthor(ctx)]
        num_subclass_features = 0
        num_fields = 0

        def inc_fields(text):
            nonlocal num_fields
            num_fields += 1
            if num_fields > 25:
                embed_queue.append(EmbedWithAuthor(ctx))
                num_fields = 0
            if len(str(embed_queue[-1].to_dict())) + len(text) > 5800:
                embed_queue.append(EmbedWithAuthor(ctx))
                num_fields = 0

        for level in range(1, final_level + 1):
            level_features = _class['classFeatures'][level - 1]
            for f in level_features:
                if f.get('gainSubclassFeature'):
                    num_subclass_features += 1
                text = parse_data_entry(f['entries'])
                text = [text[i:i + 1024] for i in range(0, len(text), 1024)]
                inc_fields(text[0])
                embed_queue[-1].add_field(name=f['name'], value=text[0])
                for piece in text[1:]:
                    inc_fields(piece)
                    embed_queue[-1].add_field(name="\u200b", value=piece)
        for num in range(num_subclass_features):
            level_features = subclass['subclassFeatures'][num]
            for feature in level_features:
                for entry in feature.get('entries', []):
                    if not isinstance(entry, dict): continue
                    if not entry.get('type') == 'entries': continue
                    fe = {'name': entry['name'],
                          'text': parse_data_entry(entry['entries'])}
                    text = [fe['text'][i:i + 1024] for i in range(0, len(fe['text']), 1024)]
                    inc_fields(text[0])
                    embed_queue[-1].add_field(name=fe['name'], value=text[0])
                    for piece in text[1:]:
                        inc_fields(piece)
                        embed_queue[-1].add_field(name="\u200b", value=piece)

        for embed in embed_queue:
            embed.colour = color
            await self.bot.send_message(ctx.message.author, embed=embed)

        # Background Gen
        #    Inventory/Trait Gen
        background = background or random.choice(c.backgrounds)
        embed = EmbedWithAuthor(ctx)
        embed.title = background['name']
        embed.description = f"*Source: {background.get('source', 'Unknown')}*"

        ignored_fields = ['suggested characteristics', 'specialty',
                          'harrowing event']
        for trait in background['trait']:
            if trait['name'].lower() in ignored_fields: continue
            text = '\n'.join(t for t in trait['text'] if t)
            text = [text[i:i + 1024] for i in range(0, len(text), 1024)]
            embed.add_field(name=trait['name'], value=text[0])
            for piece in text[1:]:
                embed.add_field(name="\u200b", value=piece)
        embed.colour = color
        await self.bot.send_message(ctx.message.author, embed=embed)

        out = "{6}\n{0}, {1} {7} {2} {3}. {4} Background.\nStat Array: `{5}`\nI have PM'd you full character details.".format(
            name, race.name, _class['name'], final_level, background['name'], stats, ctx.message.author.mention,
            subclass['name'])

        await self.bot.edit_message(loadingMessage, out)
Beispiel #25
0
    async def _class(self, ctx, name: str, level: int = None):
        """Looks up a class, or all features of a certain level."""
        if level is not None and not 0 < level < 21:
            return await ctx.send("Invalid level.")

        choices = compendium.classes + compendium.nclass_names
        result = await self._lookup_search(ctx, choices, name, lambda e: e['name'], search_type='class')
        if not result:
            return

        embed = EmbedWithAuthor(ctx)
        if level is None:
            embed.title = result['name']
            embed.add_field(name="Hit Die", value=f"1d{result['hd']['faces']}")
            embed.add_field(name="Saving Throws", value=', '.join(ABILITY_MAP.get(p) for p in result['proficiency']))

            levels = []
            starting_profs = f"You are proficient with the following items, " \
                             f"in addition to any proficiencies provided by your race or background.\n" \
                             f"Armor: {', '.join(result['startingProficiencies'].get('armor', ['None']))}\n" \
                             f"Weapons: {', '.join(result['startingProficiencies'].get('weapons', ['None']))}\n" \
                             f"Tools: {', '.join(result['startingProficiencies'].get('tools', ['None']))}\n" \
                             f"Skills: Choose {result['startingProficiencies']['skills']['choose']} from " \
                             f"{', '.join(result['startingProficiencies']['skills']['from'])}"

            equip_choices = '\n'.join(f"• {i}" for i in result['startingEquipment']['default'])
            gold_alt = f"Alternatively, you may start with {result['startingEquipment']['goldAlternative']} gp " \
                       f"to buy your own equipment." if 'goldAlternative' in result['startingEquipment'] else ''
            starting_items = f"You start with the following items, plus anything provided by your background.\n" \
                             f"{equip_choices}\n" \
                             f"{gold_alt}"
            for level in range(1, 21):
                level_str = []
                level_features = result['classFeatures'][level - 1]
                for feature in level_features:
                    level_str.append(feature.get('name'))
                levels.append(', '.join(level_str))

            embed.add_field(name="Starting Proficiencies", value=starting_profs, inline=False)
            embed.add_field(name="Starting Equipment", value=starting_items, inline=False)

            level_features_str = ""
            for i, l in enumerate(levels):
                level_features_str += f"`{i + 1}` {l}\n"
            embed.description = level_features_str

            embed.set_footer(text=f"Use {ctx.prefix}classfeat to look up a feature.")
        else:
            embed.title = f"{result['name']}, Level {level}"

            level_resources = {}
            level_features = result['classFeatures'][level - 1]

            for table in result.get('classTableGroups', []):
                relevant_row = table['rows'][level - 1]
                for i, col in enumerate(relevant_row):
                    level_resources[table['colLabels'][i]] = parse_data_entry([col])

            for res_name, res_value in level_resources.items():
                if res_value != '0':
                    embed.add_field(name=res_name, value=res_value)

            for f in level_features:
                text = parse_data_entry(f['entries'])
                embed.add_field(name=f['name'], value=(text[:1019] + "...") if len(text) > 1023 else text, inline=False)

            embed.set_footer(text=f"Use {ctx.prefix}classfeat to look up a feature if it is cut off.")

        await (await self._get_destination(ctx)).send(embed=embed)
Beispiel #26
0
    async def serve(self, ctx, name):
        # get the personal alias/snippet
        if self.is_alias:
            personal_obj = await helpers.get_personal_alias_named(ctx, name)
            check_coro = _servalias_before_edit
        else:
            personal_obj = await helpers.get_personal_snippet_named(ctx, name)
            check_coro = _servsnippet_before_edit

        if personal_obj is None:
            return await ctx.send(
                f"You do not have {a_or_an(self.obj_name)} named `{name}`.")
        await check_coro(ctx, name)

        # If the alias is a workshop alias we need to get the workshopCollection and set it as active.
        if not isinstance(personal_obj, self.personal_cls):
            await personal_obj.load_collection(ctx)
            collection = personal_obj.collection
            response = await confirm(
                ctx,
                f"This action will subscribe the server to the `{collection.name}` workshop collection, found at "
                f"<{collection.url}>. This will add {collection.alias_count} aliases and "
                f"{collection.snippet_count} snippets to the server. Do you want to continue? (Reply with yes/no)"
            )
            if not response:
                return await ctx.send("Ok, aborting.")
            await collection.set_server_active(
                ctx)  # this loads the aliases/snippets

            embed = EmbedWithAuthor(ctx)
            embed.title = f"Subscribed to {collection.name}"
            embed.url = collection.url
            embed.description = collection.description
            if collection.aliases:
                embed.add_field(name="Server Aliases",
                                value=", ".join(
                                    sorted(a.name
                                           for a in collection.aliases)))
            if collection.snippets:
                embed.add_field(name="Server Snippets",
                                value=", ".join(
                                    sorted(a.name
                                           for a in collection.snippets)))
            return await ctx.send(embed=embed)

        # else it's a personal alias/snippet
        if self.is_alias:
            existing_server_obj = await personal.Servalias.get_named(
                personal_obj.name, ctx)
            server_obj = personal.Servalias.new(personal_obj.name,
                                                personal_obj.code,
                                                ctx.guild.id)
        else:
            existing_server_obj = await personal.Servsnippet.get_named(
                personal_obj.name, ctx)
            server_obj = personal.Servsnippet.new(personal_obj.name,
                                                  personal_obj.code,
                                                  ctx.guild.id)

        # check if it overwrites anything
        if existing_server_obj is not None and not await confirm(
                ctx,
                f"There is already an existing server {self.obj_name} named `{name}`. Do you want to overwrite it? "
                f"(Reply with yes/no)"):
            return await ctx.send("Ok, aborting.")

        await server_obj.commit(ctx.bot.mdb)
        out = f'Server {self.obj_name} `{server_obj.name}` added.' \
              f'```py\n{ctx.prefix}{self.obj_copy_command} {server_obj.name} {server_obj.code}\n```'

        if len(out) > 2000:
            out = f'Server {self.obj_name} `{server_obj.name}` added.' \
                  f'Command output too long to display.'

        await ctx.send(out)
Beispiel #27
0
    async def spell(self, ctx, *, name: str):
        """Looks up a spell."""

        try:
            guild_id = ctx.message.server.id
            pm = self.settings.get(guild_id, {}).get("pm_result", False)
            srd = self.settings.get(guild_id, {}).get("srd", False)
        except:
            pm = False
            srd = False

        self.bot.db.incr('spells_looked_up_life')

        result = await search_and_select(ctx,
                                         c.spells,
                                         name,
                                         lambda e: e['name'],
                                         return_key=True,
                                         srd=srd)
        result = getSpell(result)

        spellDesc = []
        embed = EmbedWithAuthor(ctx)
        color = embed.colour
        spell = copy.copy(result)

        def parseschool(school):
            if school == "A": return "abjuration"
            if school == "EV": return "evocation"
            if school == "EN": return "enchantment"
            if school == "I": return "illusion"
            if school == "D": return "divination"
            if school == "N": return "necromancy"
            if school == "T": return "transmutation"
            if school == "C": return "conjuration"
            return school

        def parsespelllevel(level):
            if level == "0": return "cantrip"
            if level == "2": return level + "nd level"
            if level == "3": return level + "rd level"
            if level == "1": return level + "st level"
            return level + "th level"

        spell['school'] = parseschool(spell.get('school'))
        spell['ritual'] = spell.get('ritual', 'no').lower()

        embed.title = spell['name']

        if spell.get("source") == "UAMystic":
            embed.description = "*{level} Mystic Talent. ({classes})*".format(
                **spell)
        else:
            spell['level'] = parsespelllevel(spell['level'])
            embed.description = "*{level} {school}. ({classes})*".format(
                **spell)
            embed.add_field(name="Casting Time", value=spell['time'])
            embed.add_field(name="Range", value=spell['range'])
            embed.add_field(name="Components", value=spell['components'])
            embed.add_field(name="Duration", value=spell['duration'])
            embed.add_field(name="Ritual", value=spell['ritual'])

        if isinstance(spell['text'], list):
            for a in spell["text"]:
                if a is '': continue
                spellDesc.append(
                    a.replace("At Higher Levels: ", "**At Higher Levels:** ").
                    replace(
                        "This spell can be found in the Elemental Evil Player's Companion",
                        ""))
        else:
            spellDesc.append(spell['text'].replace(
                "At Higher Levels: ", "**At Higher Levels:** "
            ).replace(
                "This spell can be found in the Elemental Evil Player's Companion",
                ""))

        text = '\n'.join(spellDesc)
        if "**At Higher Levels:** " in text:
            text, higher_levels = text.split("**At Higher Levels:** ", 1)
        elif "At Higher Levels" in text:
            text, higher_levels = text.split("At Higher Levels", 1)
            text = text.strip('*\n')
            higher_levels = higher_levels.strip('* \n.:')
        else:
            higher_levels = None

        if not spell['srd'] and srd:
            text = "No description available."
            higher_levels = ''

        if len(text) > 1020:
            pieces = [text[:1020]] + [
                text[i:i + 2040] for i in range(1020, len(text), 2040)
            ]
        else:
            pieces = [text]

        embed.add_field(name="Description", value=pieces[0])

        embed_queue = [embed]
        if len(pieces) > 1:
            for piece in pieces[1:]:
                temp_embed = discord.Embed()
                temp_embed.colour = color
                temp_embed.description = piece
                embed_queue.append(temp_embed)

        if higher_levels:
            embed_queue[-1].add_field(name="At Higher Levels",
                                      value=higher_levels)

        for embed in embed_queue:
            if pm:
                await self.bot.send_message(ctx.message.author, embed=embed)
            else:
                await self.bot.say(embed=embed)
Beispiel #28
0
    async def spell(self, ctx, *, name: str):
        """Looks up a spell."""
        if name.lower().strip() == 'roscoe\'s feast':
            return await ctx.invoke(self.bot.get_command('patron_roscoe'))

        guild_settings = await self.get_settings(ctx.guild)
        pm = guild_settings.get("pm_result", False)
        srd = guild_settings.get("srd", False)

        self.bot.rdb.incr('spells_looked_up_life')

        spell, metadata = await select_spell_full(ctx, name, srd=srd, search_func=ml_spell_search, return_metadata=True)

        metadata['srd'] = srd
        metadata['homebrew'] = spell.source == 'homebrew'
        await self.add_training_data("spell", name, spell.name, metadata=metadata)

        embed = EmbedWithAuthor(ctx)
        color = embed.colour

        embed.title = spell.name
        embed.description = f"*{spell.get_level()} {spell.get_school().lower()}. " \
                            f"({', '.join(itertools.chain(spell.classes, spell.subclasses))})*"
        if spell.ritual:
            time = f"{spell.time} (ritual)"
        else:
            time = spell.time
        embed.add_field(name="Casting Time", value=time)
        embed.add_field(name="Range", value=spell.range)
        embed.add_field(name="Components", value=spell.components)
        embed.add_field(name="Duration", value=spell.duration)

        text = spell.description
        higher_levels = spell.higherlevels

        if not spell.srd and srd:
            text = "No description available."
            higher_levels = ''

        if len(text) > 1020:
            pieces = [text[:1020]] + [text[i:i + 2040] for i in range(1020, len(text), 2040)]
        else:
            pieces = [text]

        embed.add_field(name="Description", value=pieces[0])

        embed_queue = [embed]
        if len(pieces) > 1:
            for piece in pieces[1:]:
                temp_embed = discord.Embed()
                temp_embed.colour = color
                temp_embed.description = piece
                embed_queue.append(temp_embed)

        if higher_levels:
            embed_queue[-1].add_field(name="At Higher Levels", value=higher_levels)

        if spell.source == 'homebrew':
            embed_queue[-1].set_footer(text="Homebrew content.", icon_url=HOMEBREW_ICON)
        else:
            embed_queue[-1].set_footer(text=f"Spell | {spell.source} {spell.page}")

        if spell.image:
            embed_queue[0].set_thumbnail(url=spell.image)

        for embed in embed_queue:
            if pm:
                await ctx.author.send(embed=embed)
            else:
                await ctx.send(embed=embed)
Beispiel #29
0
 async def send_srd_error(self, ctx, data):
     e = EmbedWithAuthor(ctx)
     e.title = data['name']
     e.description = "Description not available."
     return await self.bot.say(embed=e)
Beispiel #30
0
    async def item_lookup(self, ctx, *, name):
        """Looks up an item."""
        guild_settings = await self.get_settings(ctx.guild)
        pm = guild_settings.get("pm_result", False)
        srd = guild_settings.get("srd", False)

        self.bot.rdb.incr('items_looked_up_life')

        try:
            pack = await Pack.from_ctx(ctx)
            custom_items = pack.get_search_formatted_items()
        except NoActiveBrew:
            custom_items = []
        choices = list(itertools.chain(c.items, custom_items))
        if ctx.guild:
            async for servpack in ctx.bot.mdb.packs.find({"server_active": str(ctx.guild.id)}):
                choices.extend(Pack.from_dict(servpack).get_search_formatted_items())

        def get_homebrew_formatted_name(_item):
            if _item.get('source') == 'homebrew':
                return f"{_item['name']} ({HOMEBREW_EMOJI})"
            return _item['name']

        result, metadata = await search_and_select(ctx, choices, name, lambda e: e['name'], srd=srd,
                                                   selectkey=get_homebrew_formatted_name, return_metadata=True)

        metadata['srd'] = srd
        metadata['homebrew'] = result.get('source') == 'homebrew'
        await self.add_training_data("item", name, result['name'], metadata=metadata)

        embed = EmbedWithAuthor(ctx)
        item = result

        if not item['srd'] and srd:
            return await self.send_srd_error(ctx, result)

        name = item['name']
        proptext = ""

        if not item.get('source') == 'homebrew':
            damage = ''
            extras = ''
            properties = []

            if 'type' in item:
                type_ = ', '.join(
                    i for i in ([ITEM_TYPES.get(t, 'n/a') for t in item['type'].split(',')] +
                                ["Wondrous Item" if item.get('wondrous') else ''])
                    if i)
                for iType in item['type'].split(','):
                    if iType in ('M', 'R', 'GUN'):
                        damage = f"{item.get('dmg1', 'n/a')} {DMGTYPES.get(item.get('dmgType'), 'n/a')}" \
                            if 'dmg1' in item and 'dmgType' in item else ''
                        type_ += f', {item.get("weaponCategory")}'
                    if iType == 'S': damage = f"AC +{item.get('ac', 'n/a')}"
                    if iType == 'LA': damage = f"AC {item.get('ac', 'n/a')} + DEX"
                    if iType == 'MA': damage = f"AC {item.get('ac', 'n/a')} + DEX (Max 2)"
                    if iType == 'HA': damage = f"AC {item.get('ac', 'n/a')}"
                    if iType == 'SHP':  # ships
                        for p in ("CREW", "PASS", "CARGO", "DMGT", "SHPREP"):
                            a = PROPS.get(p, 'n/a')
                            proptext += f"**{a.title()}**: {c.itemprops[p]}\n"
                        extras = f"Speed: {item.get('speed')}\nCarrying Capacity: {item.get('carryingcapacity')}\n" \
                                 f"Crew {item.get('crew')}, AC {item.get('vehAc')}, HP {item.get('vehHp')}"
                        if 'vehDmgThresh' in item:
                            extras += f", Damage Threshold {item['vehDmgThresh']}"
                    if iType == 'siege weapon':
                        extras = f"Size: {SIZES.get(item.get('size'), 'Unknown')}\n" \
                                 f"AC {item.get('ac')}, HP {item.get('hp')}\n" \
                                 f"Immunities: {item.get('immune')}"
            else:
                type_ = ', '.join(
                    i for i in ("Wondrous Item" if item.get('wondrous') else '', item.get('technology')) if i)
            rarity = str(item.get('rarity')).replace('None', '')
            if 'tier' in item:
                if rarity:
                    rarity += f', {item["tier"]}'
                else:
                    rarity = item['tier']
            type_and_rarity = type_ + (f", {rarity}" if rarity else '')
            value = (item.get('value', 'n/a') + (', ' if 'weight' in item else '')) if 'value' in item else ''
            weight = (item.get('weight', 'n/a') + (' lb.' if item.get('weight') == '1' else ' lbs.')) \
                if 'weight' in item else ''
            weight_and_value = value + weight
            for prop in item.get('property', []):
                if not prop: continue
                a = b = prop
                a = PROPS.get(a, 'n/a')
                if b in c.itemprops:
                    proptext += f"**{a.title()}**: {c.itemprops[b]}\n"
                if b == 'V': a += " (" + item.get('dmg2', 'n/a') + ")"
                if b in ('T', 'A'): a += " (" + item.get('range', 'n/a') + "ft.)"
                if b == 'RLD': a += " (" + item.get('reload', 'n/a') + " shots)"
                properties.append(a)
            properties = ', '.join(properties)
            damage_and_properties = f"{damage} - {properties}" if properties else damage
            damage_and_properties = (' --- ' + damage_and_properties) if weight_and_value and damage_and_properties else \
                damage_and_properties

            meta = f"*{type_and_rarity}*\n{weight_and_value}{damage_and_properties}\n{extras}"
            text = item['desc']

            if 'reqAttune' in item:
                if item['reqAttune'] is True:  # can be truthy, but not true
                    embed.add_field(name="Attunement", value=f"Requires Attunement")
                else:
                    embed.add_field(name="Attunement", value=f"Requires Attunement {item['reqAttune']}")

            embed.set_footer(text=f"Item | {item.get('source', 'Unknown')} {item.get('page', 'Unknown')}")
        else:
            meta = item['meta']
            text = item['desc']
            if 'image' in item:
                embed.set_thumbnail(url=item['image'])
            add_homebrew_footer(embed)

        embed.title = name
        embed.description = meta  # no need to render, has been prerendered

        if proptext:
            text = f"{text}\n{proptext}"
        if len(text) > 5500:
            text = text[:5500] + "..."

        field_name = "Description"
        for piece in [text[i:i + 1024] for i in range(0, len(text), 1024)]:
            embed.add_field(name=field_name, value=piece)
            field_name = "** **"

        if pm:
            await ctx.author.send(embed=embed)
        else:
            await ctx.send(embed=embed)