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)
async def list(self, ctx): embed = EmbedWithAuthor(ctx) has_at_least_1 = False user_objs = await self.personal_cls.get_ctx_map(ctx) user_obj_names = list(user_objs.keys()) if user_obj_names: has_at_least_1 = True embeds.add_fields_from_long_text( embed, f"Your {self.obj_name_pl.title()}", ', '.join(sorted(user_obj_names))) async for subscription_doc in self.workshop_sub_meth(ctx): try: the_collection = await workshop.WorkshopCollection.from_id( ctx, subscription_doc['object_id']) except workshop.CollectionNotFound: continue if bindings := subscription_doc[self.binding_key]: has_at_least_1 = True embed.add_field(name=the_collection.name, value=', '.join( sorted(ab['name'] for ab in bindings)), inline=False)
async def item_lookup(self, ctx, *, name): """Looks up an item.""" choices = await get_item_choices(ctx, filter_by_license=False) item = await self._lookup_search3(ctx, {'magic-item': choices}, name, query_type='item') embed = EmbedWithAuthor(ctx) embed.title = item.name embed.url = item.url embed.description = item.meta if item.attunement: if item.attunement 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.attunement}", inline=False) text = trim_str(item.desc, 5500) add_fields_from_long_text(embed, "Description", text) if item.image: embed.set_thumbnail(url=item.image) handle_source_footer(embed, item, "Item") await Stats.increase_stat(ctx, "items_looked_up_life") await (await self._get_destination(ctx)).send(embed=embed)
async def _view(self, ctx, name): collectable = await helpers.get_collectable_named( ctx, name, self.personal_cls, self.workshop_cls, self.workshop_sub_meth, self.is_alias, self.obj_name, self.obj_name_pl, self.name) if collectable is None: return await ctx.send(f"No {self.obj_name} named {name} found.") elif isinstance(collectable, self.personal_cls): # personal await send_long_code_text( ctx, outside_codeblock=f'**{name}**:', inside_codeblock= f"{ctx.prefix}{self.obj_copy_command} {collectable.name} {collectable.code}", codeblock_language='py') return else: # collection embed = EmbedWithAuthor(ctx) the_collection = await collectable.load_collection(ctx) owner = await user_from_id(ctx, the_collection.owner) embed.title = f"{ctx.prefix}{name}" if self.is_alias else name embed.description = f"From {the_collection.name} by {owner}.\n" \ f"[View on Workshop]({the_collection.url})" embeds.add_fields_from_long_text( embed, "Help", collectable.docs or "No documentation.") if isinstance(collectable, workshop.WorkshopAlias): await collectable.load_subcommands(ctx) if collectable.subcommands: subcommands = "\n".join(f"**{sc.name}** - {sc.short_docs}" for sc in collectable.subcommands) embed.add_field(name="Subcommands", value=subcommands, inline=False) return await ctx.send(embed=embed)
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) text = spell.description higher_levels = spell.higherlevels 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], inline=False) 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: add_fields_from_long_text(embed_queue[-1], "At Higher Levels", higher_levels) embed_queue[-1].set_footer(text=f"Spell | {spell.source_str()}") if spell.homebrew: add_homebrew_footer(embed_queue[-1]) if spell.image: embed_queue[0].set_thumbnail(url=spell.image) await Stats.increase_stat(ctx, "spells_looked_up_life") destination = await self._get_destination(ctx) for embed in embed_queue: await destination.send(embed=embed)
def add_action_field(title, action_source): action_texts = ( f"**{action.name}**: {action.build_str(caster=caster, automation_only=not verbose)}" for action in action_source) action_text = '\n'.join(action_texts) embeds.add_fields_from_long_text(embed, field_name=title, text=action_text)
async def spell(self, ctx, *, name: str): """Looks up a spell.""" spell, metadata = await select_spell_full(ctx, name, return_metadata=True, extra_choices=compendium.nspell_names, selectkey=self.nsrd_selectkey_obj) metadata['homebrew'] = spell.source == 'homebrew' await self.add_training_data("spell", name, spell.name, metadata=metadata, srd=spell.srd) if not (metadata['homebrew'] or spell.srd): return await self._non_srd(ctx, spell, "spell") 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 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], inline=False) 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: add_fields_from_long_text(embed_queue[-1], "At Higher Levels", 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) destination = await self._get_destination(ctx) for embed in embed_queue: await destination.send(embed=embed)
async def feat(self, ctx, *, name: str): """Looks up a feat.""" result: gamedata.Feat = await self._lookup_search3(ctx, {'feat': compendium.feats}, name) embed = EmbedWithAuthor(ctx) embed.title = result.name embed.url = result.url if result.prerequisite: embed.add_field(name="Prerequisite", value=result.prerequisite, inline=False) add_fields_from_long_text(embed, "Description", result.desc) handle_source_footer(embed, result, "Feat") await (await self._get_destination(ctx)).send(embed=embed)
async def race(self, ctx, *, name: str): """Looks up a race.""" result: gamedata.Race = await self._lookup_search3(ctx, {'race': compendium.races, 'subrace': compendium.subraces}, name, 'race') embed = EmbedWithAuthor(ctx) embed.title = result.name embed.url = result.url embed.add_field(name="Speed", value=result.speed) embed.add_field(name="Size", value=result.size) for t in result.traits: add_fields_from_long_text(embed, t.name, t.text) handle_source_footer(embed, result, "Race") await (await self._get_destination(ctx)).send(embed=embed)
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)
async def feat(self, ctx, *, name: str): """Looks up a feat.""" choices = compendium.feats + compendium.nfeat_names result = await self._lookup_search(ctx, choices, name, lambda e: e['name'], search_type='feat') if not result: return embed = EmbedWithAuthor(ctx) embed.title = result['name'] if result['prerequisite']: embed.add_field(name="Prerequisite", value=result['prerequisite'], inline=False) if result['ability']: embed.add_field(name="Ability Improvement", value=f"Increase your {result['ability']} score by 1, up to a maximum of 20.", inline=False) add_fields_from_long_text(embed, "Description", result['desc']) embed.set_footer(text=f"Feat | {result['source']} {result['page']}") await (await self._get_destination(ctx)).send(embed=embed)
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)
class Lookup(commands.Cog): """Commands to help look up items, status effects, rules, etc.""" def __init__(self, bot): self.bot = bot # ==== rules/references ==== @staticmethod async def _show_reference_options(ctx, destination): embed = EmbedWithAuthor(ctx) embed.title = "Rules" categories = ', '.join(a['type'] for a in compendium.rule_references) embed.description = f"Use `{ctx.prefix}{ctx.invoked_with} <category>` to look at all actions of " \ f"a certain type.\nCategories: {categories}" for actiontype in compendium.rule_references: embed.add_field(name=actiontype['fullName'], value=', '.join(a['name'] for a in actiontype['items']), inline=False) await destination.send(embed=embed) @staticmethod async def _show_action_options(ctx, actiontype, destination): embed = EmbedWithAuthor(ctx) embed.title = actiontype['fullName'] actions = [] for action in actiontype['items']: actions.append(f"**{action['name']}** - *{action['short']}*") embed.description = '\n'.join(actions) await destination.send(embed=embed) @commands.command(aliases=['status']) async def condition(self, ctx, *, name: str = None): """Looks up a condition.""" if not name: name = 'condition' else: name = f"Condition: {name}" # this is an invoke instead of an alias to make more sense in docs await self.rule(ctx, name=name) @commands.command(aliases=['reference']) 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) # ==== feats ==== @commands.command() async def feat(self, ctx, *, name: str): """Looks up a feat.""" result: gamedata.Feat = await self._lookup_search3( ctx, {'feat': compendium.feats}, name) embed = EmbedWithAuthor(ctx) embed.title = result.name embed.url = result.url if result.prerequisite: embed.add_field(name="Prerequisite", value=result.prerequisite, inline=False) add_fields_from_long_text(embed, "Description", result.desc) handle_source_footer(embed, result, "Feat") await (await self._get_destination(ctx)).send(embed=embed) # ==== races / racefeats ==== @commands.command() async def racefeat(self, ctx, *, name: str): """Looks up a racial feature.""" result: RaceFeature = await self._lookup_search3( ctx, { 'race': compendium.rfeats, 'subrace': compendium.subrfeats }, name, 'racefeat') embed = EmbedWithAuthor(ctx) embed.title = result.name embed.url = result.url set_maybe_long_desc(embed, result.text) handle_source_footer(embed, result, "Race Feature") await (await self._get_destination(ctx)).send(embed=embed) @commands.command() async def race(self, ctx, *, name: str): """Looks up a race.""" result: gamedata.Race = await self._lookup_search3( ctx, { 'race': compendium.races, 'subrace': compendium.subraces }, name, 'race') embed = EmbedWithAuthor(ctx) embed.title = result.name embed.url = result.url embed.add_field(name="Speed", value=result.speed) embed.add_field(name="Size", value=result.size) for t in result.traits: add_fields_from_long_text(embed, t.name, t.text) handle_source_footer(embed, result, "Race") await (await self._get_destination(ctx)).send(embed=embed) # ==== classes / classfeats ==== @commands.command() async def classfeat(self, ctx, *, name: str): """Looks up a class feature.""" result: ClassFeature = await self._lookup_search3( ctx, { 'class': compendium.cfeats, 'class-feature': compendium.optional_cfeats }, name, query_type='classfeat') embed = EmbedWithAuthor(ctx) embed.title = result.name embed.url = result.url set_maybe_long_desc(embed, result.text) handle_source_footer(embed, result, "Class Feature") await (await self._get_destination(ctx)).send(embed=embed) @commands.command(name='class') 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_features = result.levels[level - 1] feature_names = [feature.name for feature in level_features] if level in result.subclass_feature_levels: feature_names.append(f"{result.subclass_title} Feature") levels.append(', '.join(feature_names)) level_features_str = "" for i, l in enumerate(levels): level_features_str += f"`{i + 1}` {l}\n" embed.description = level_features_str available_ocfs = await available(ctx, result.optional_features, entity_type='class-feature') if available_ocfs: ocf_names = ', '.join(ocf.name for ocf in available_ocfs) embed.add_field(name="Optional Class Features", value=ocf_names, inline=False) embed.add_field(name="Starting Proficiencies", value=result.proficiencies, inline=False) embed.add_field(name="Starting Equipment", value=result.equipment, inline=False) 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) @commands.command() 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) # ==== backgrounds ==== @commands.command() async def background(self, ctx, *, name: str): """Looks up a background.""" result: gamedata.Background = await self._lookup_search3( ctx, {'background': compendium.backgrounds}, name) embed = EmbedWithAuthor(ctx) embed.url = result.url embed.title = result.name handle_source_footer(embed, result, "Background") for trait in result.traits: text = trim_str(trait.text, 1024) embed.add_field(name=trait.name, value=text, inline=False) await (await self._get_destination(ctx)).send(embed=embed) # ==== monsters ==== @commands.command() 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 __Valid Arguments__ -h - Shows the obfuscated stat block, even if you can see the full stat block.""" guild_settings = await self.get_settings(ctx.guild) pm = guild_settings.get("pm_result", False) pm_dm = guild_settings.get("pm_dm", False) req_dm_monster = guild_settings.get("req_dm_monster", True) visible_roles = {'gm', 'game master', 'dm', 'dungeon master'} if req_dm_monster and ctx.guild: visible = True if visible_roles.intersection( set(str(r).lower() for r in ctx.author.roles)) else False else: visible = True # #817 -h arg for monster lookup if name.endswith(' -h'): name = name[:-3] visible = False choices = await get_monster_choices(ctx, filter_by_license=False) monster = await self._lookup_search3(ctx, {'monster': choices}, name) embed_queue = [EmbedWithAuthor(ctx)] color = embed_queue[-1].colour embed_queue[-1].title = monster.name embed_queue[-1].url = monster.url def safe_append(title, desc): if len(desc) < 1024: embed_queue[-1].add_field(name=title, value=desc, inline=False) 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 = chunk_text(desc, max_chunk_size=2048) 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 = '\n\n'.join(f"**{a.name}:** {a.desc}" for a in monster.traits) if trait: safe_append("Special Abilities", trait) if monster.actions: action = '\n\n'.join(f"**{a.name}:** {a.desc}" for a in monster.actions) if action: safe_append("Actions", action) if monster.bonus_actions: bonus_action = '\n\n'.join(f"**{a.name}:** {a.desc}" for a in monster.bonus_actions) if bonus_action: safe_append("Bonus Actions", bonus_action) if monster.reactions: reaction = '\n\n'.join(f"**{a.name}:** {a.desc}" for a in monster.reactions) if reaction: safe_append("Reactions", reaction) if monster.legactions: proper_name = f'The {monster.name}' if not monster.proper else monster.name legendary = [ f"{proper_name} can take {monster.la_per_round} legendary actions, choosing from " f"the options below. Only one legendary action can be used at a time and only at the " f"end of another creature's turn. {proper_name} regains spent legendary actions at " f"the start of its turn." ] for a in monster.legactions: if a.name: legendary.append(f"**{a.name}:** {a.desc}") else: legendary.append(a.desc) if legendary: safe_append("Legendary Actions", '\n\n'.join(legendary)) if monster.mythic_actions: mythic_action = '\n\n'.join(f"**{a.name}:** {a.desc}" for a in monster.mythic_actions) if mythic_action: safe_append("Mythic Actions", mythic_action) else: hp = monster.hp ac = monster.ac size = monster.size _type = monster.creature_type 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.bonus_actions: embed_queue[-1].add_field(name="Bonus Actions", value=str(len( monster.bonus_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))) handle_source_footer(embed_queue[-1], monster, "Creature") embed_queue[0].set_thumbnail(url=monster.get_image_url()) await Stats.increase_stat(ctx, "monsters_looked_up_life") for embed in embed_queue: if pm or (visible and pm_dm and req_dm_monster): await ctx.author.send(embed=embed) else: await ctx.send(embed=embed) @commands.command() async def monimage(self, ctx, *, name): """Shows a monster's image.""" 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) @commands.command() async def token(self, ctx, name=None, *args): """ Shows a monster or your character's token. __Valid Arguments__ -border <plain|none (player token only)> - Overrides the token border. """ if name is None or name.startswith('-'): token_cmd = self.bot.get_command('playertoken') if token_cmd is None: return await ctx.send("Error: SheetManager cog not loaded.") if name: args = (name, *args) return await ctx.invoke(token_cmd, *args) # select monster 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") # select border ddb_user = await self.bot.ddb.get_ddb_user(ctx, ctx.author.id) is_subscriber = ddb_user and ddb_user.is_subscriber token_args = argparse(args) if monster.homebrew: # homebrew: generate token if not monster.get_image_url(): return await ctx.send("This monster has no image.") try: image = await img.generate_token(monster.get_image_url(), is_subscriber, token_args) except Exception as e: return await ctx.send(f"Error generating token: {e}") else: # official monsters token_url = monster.get_token_url(is_subscriber) if token_args.last('border') == 'plain': token_url = monster.get_token_url(False) if not token_url: return await ctx.send("This monster has no image.") image = await img.fetch_monster_image(token_url) embed = EmbedWithAuthor(ctx) embed.title = monster.name embed.description = f"{monster.size} monster." file = discord.File(image, filename="image.png") embed.set_image(url="attachment://image.png") await ctx.send(embed=embed, file=file) # ==== spells ==== @commands.command() 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) if higher_levels: add_fields_from_long_text(embed_queue[-1], "At Higher Levels", higher_levels) handle_source_footer(embed_queue[-1], spell, "Spell") if spell.image: embed_queue[0].set_thumbnail(url=spell.image) await Stats.increase_stat(ctx, "spells_looked_up_life") destination = await self._get_destination(ctx) for embed in embed_queue: await destination.send(embed=embed)
async def item_lookup(self, ctx, *, name): """Looks up an item.""" try: pack = await Pack.from_ctx(ctx) custom_items = pack.get_search_formatted_items() pack_id = pack.id except NoActiveBrew: custom_items = [] pack_id = None choices = list(itertools.chain(compendium.items, custom_items)) if ctx.guild: async for servpack in ctx.bot.mdb.packs.find({"server_active": str(ctx.guild.id)}): if servpack['_id'] != pack_id: choices.extend(Pack.from_dict(servpack).get_search_formatted_items()) # #881 - display nSRD names choices.extend(compendium.nitem_names) result, metadata = await search_and_select(ctx, choices, name, lambda e: e['name'], selectkey=self.nsrd_selectkey, return_metadata=True) metadata['homebrew'] = result.get('source') == 'homebrew' await self.add_training_data("item", name, result['name'], metadata=metadata, srd=result['srd']) if not (metadata['homebrew'] or result['srd']): return await self._non_srd(ctx, result, "item") embed = EmbedWithAuthor(ctx) item = 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()}**: {compendium.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 compendium.itemprops: proptext += f"**{a.title()}**: {compendium.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']}", inline=False) 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] + "..." add_fields_from_long_text(embed, "Description", text) await Stats.increase_stat(ctx, "items_looked_up_life") await (await self._get_destination(ctx)).send(embed=embed)
async def send_action_list(destination, caster, attacks=None, actions=None, embed=None, args=None): """ Sends the list of actions and attacks given to the given destination. :type destination: discord.abc.Messageable :type caster: cogs5e.models.sheet.statblock.StatBlock :type attacks: cogs5e.models.sheet.attack.AttackList :type actions: cogs5e.models.sheet.action.Actions :type embed: discord.Embed :type args: Iterable[str] """ if embed is None: embed = discord.Embed(color=caster.get_color(), title=f"{caster.get_title_name()}'s Actions") if args is None: args = () # arg setup verbose = '-v' in args display_attacks = 'attacks' in args display_actions = 'actions' in args display_bonus = 'bonus' in args display_reactions = 'reactions' in args display_other = 'other' in args is_display_filtered = any((display_attacks, display_actions, display_bonus, display_reactions, display_other)) filtered_action_type_strs = list( itertools.compress(('attacks', 'actions', 'bonus actions', 'reactions', 'other actions'), (display_attacks, display_actions, display_bonus, display_reactions, display_other))) # action display if attacks and (display_attacks or not is_display_filtered): atk_str = attacks.build_str(caster) embeds.add_fields_from_long_text(embed, field_name="Attacks", text=atk_str) # since the sheet displays the description regardless of entitlements, we do here too def add_action_field(title, action_source): action_texts = ( f"**{action.name}**: {action.build_str(caster=caster, automation_only=not verbose)}" for action in action_source) action_text = '\n'.join(action_texts) embeds.add_fields_from_long_text(embed, field_name=title, text=action_text) if actions is not None: if actions.full_actions and (display_actions or not is_display_filtered): add_action_field("Actions", actions.full_actions) if actions.bonus_actions and (display_bonus or not is_display_filtered): add_action_field("Bonus Actions", actions.bonus_actions) if actions.reactions and (display_reactions or not is_display_filtered): add_action_field("Reactions", actions.reactions) if actions.other_actions and (display_other or not is_display_filtered): add_action_field("Other", actions.other_actions) # misc helper displays if not embed.fields: if is_display_filtered: embed.description = f"{caster.get_title_name()} has no {natural_join(filtered_action_type_strs, 'or')}." else: embed.description = f"{caster.get_title_name()} has no actions." elif is_display_filtered: embed.description = f"Only displaying {natural_join(filtered_action_type_strs, 'and')}." if not verbose and actions: embed.set_footer( text="Use the -v argument to view each action's full description.") await destination.send(embed=embed)