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) 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 monster_cast(self, ctx, monster_name, spell_name, *args): """ Casts a spell as a monster. __Valid Arguments__ -i - Ignores Spellbook restrictions, for demonstrations or rituals. -l <level> - Specifies the level to cast the spell at. noconc - Ignores concentration requirements. -h - Hides rolled values. **__Save Spells__** -dc <Save DC> - Overrides the spell save DC. -save <Save type> - Overrides the spell save type. -d <damage> - Adds additional damage. pass - Target automatically succeeds save. fail - Target automatically fails save. adv/dis - Target makes save at advantage/disadvantage. **__Attack Spells__** See `!a`. **__All Spells__** -phrase <phrase> - adds flavor text. -title <title> - changes the title of the cast. Replaces [sname] with spell name. -thumb <url> - adds an image to the cast. -dur <duration> - changes the duration of any effect applied by the spell. -mod <spellcasting mod> - sets the value of the spellcasting ability modifier. int/wis/cha - different skill base for DC/AB (will not account for extra bonuses) """ await try_delete(ctx.message) monster: Monster = await select_monster_full(ctx, monster_name) args = await helpers.parse_snippets(args, ctx) args = await helpers.parse_with_statblock(ctx, monster, args) args = argparse(args) if not args.last('i', type_=bool): try: spell = await select_spell_full(ctx, spell_name, list_filter=lambda s: s.name in monster.spellbook) except NoSelectionElements: return await ctx.send(f"No matching spells found in the creature's spellbook. Cast again " f"with the `-i` argument to ignore restrictions!") else: spell = await select_spell_full(ctx, spell_name) caster, targets, combat = await targetutils.maybe_combat(ctx, monster, args) result = await spell.cast(ctx, caster, targets, args, combat=combat) # embed display embed = result['embed'] embed.colour = random.randint(0, 0xffffff) if not args.last('h', type_=bool) and 'thumb' not in args: embed.set_thumbnail(url=monster.get_image_url()) handle_source_footer(embed, monster, add_source_str=False) # save changes: combat state if combat: await combat.final() await ctx.send(embed=embed)
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)
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 classfeat(self, ctx, *, name: str): """Looks up a class feature.""" result: SourcedTrait = await self._lookup_search3(ctx, {'class': compendium.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)
async def racefeat(self, ctx, *, name: str): """Looks up a racial feature.""" result: SourcedTrait = 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)
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)
async def monster_check(self, ctx, monster_name, check, *args): await try_delete(ctx.message) monster: Monster = await select_monster_full(ctx, monster_name) args = await helpers.parse_snippets(args, ctx, statblock=monster) args = argparse(args) skill_key = await search_and_select(ctx, SKILL_NAMES, check, lambda s: s) embed = embed_for_monster(monster, args) checkutils.run_check(skill_key, monster, args, embed) handle_source_footer(embed, monster, add_source_str=False) await 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 monster_save(self, ctx, monster_name, save_stat, *args): await try_delete(ctx.message) monster: Monster = await select_monster_full(ctx, monster_name) args = await helpers.parse_snippets(args, ctx, statblock=monster) args = argparse(args) embed = discord.Embed() embed.colour = random.randint(0, 0xffffff) if not args.last('h', type_=bool): embed.set_thumbnail(url=monster.get_image_url()) checkutils.run_save(save_stat, monster, args, embed) handle_source_footer(embed, monster, add_source_str=False) await ctx.send(embed=embed)
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 monster_check(self, ctx, monster_name, check, *args): monster: Monster = await select_monster_full(ctx, monster_name) skill_key = await search_and_select(ctx, SKILL_NAMES, check, lambda s: s) embed = discord.Embed() embed.colour = random.randint(0, 0xffffff) args = await helpers.parse_snippets(args, ctx, statblock=monster) args = argparse(args) if not args.last('h', type_=bool): embed.set_thumbnail(url=monster.get_image_url()) checkutils.run_check(skill_key, monster, args, embed) handle_source_footer(embed, monster, add_source_str=False) await ctx.send(embed=embed) await try_delete(ctx.message)
async def monster_cast(self, ctx, monster_name, spell_name, *args): await try_delete(ctx.message) monster: Monster = await select_monster_full(ctx, monster_name) args = await helpers.parse_snippets(args, ctx) args = await helpers.parse_with_statblock(ctx, monster, args) args = argparse(args) if not args.last('i', type_=bool): try: spell = await select_spell_full( ctx, spell_name, list_filter=lambda s: s.name in monster.spellbook) except NoSelectionElements: return await ctx.send( f"No matching spells found in the creature's spellbook. Cast again " f"with the `-i` argument to ignore restrictions!") else: spell = await select_spell_full(ctx, spell_name) caster, targets, combat = await targetutils.maybe_combat( ctx, monster, args) result = await spell.cast(ctx, caster, targets, args, combat=combat) # embed display embed = result.embed embed.colour = random.randint(0, 0xffffff) if not args.last('h', type_=bool) and 'thumb' not in args: embed.set_thumbnail(url=monster.get_image_url()) handle_source_footer(embed, monster, add_source_str=False) # save changes: combat state if combat: await combat.final() await ctx.send(embed=embed)
async def monster_check(self, ctx, monster_name, check, *args): """Rolls a check for a monster. __Valid Arguments__ *adv/dis* *-b [conditional bonus]* -phrase [flavor text] -title [title] *note: [name] and [cname] will be replaced automatically* -thumb [thumbnail URL] -dc [dc] -rr [iterations] str/dex/con/int/wis/cha (different skill base; e.g. Strength (Intimidation)) -h (hides name and image of monster) An italicized argument means the argument supports ephemeral arguments - e.g. `-b1` applies a bonus to one check. """ monster: Monster = await select_monster_full(ctx, monster_name) skill_key = await search_and_select(ctx, SKILL_NAMES, check, lambda s: s) embed = discord.Embed() embed.colour = random.randint(0, 0xffffff) args = await helpers.parse_snippets(args, ctx) args = await helpers.parse_with_statblock(ctx, monster, args) args = argparse(args) if not args.last('h', type_=bool): embed.set_thumbnail(url=monster.get_image_url()) checkutils.run_check(skill_key, monster, args, embed) handle_source_footer(embed, monster, add_source_str=False) await ctx.send(embed=embed) await try_delete(ctx.message)
async def monster_atk(self, ctx, monster_name, atk_name=None, *, args=''): if atk_name is None or atk_name == 'list': return await self.monster_atk_list(ctx, monster_name) await try_delete(ctx.message) monster = await select_monster_full(ctx, monster_name) attacks = monster.attacks attack = await search_and_select(ctx, attacks, atk_name, lambda a: a.name) args = await helpers.parse_snippets(args, ctx, statblock=monster) args = argparse(args) embed = embed_for_monster(monster, args) caster, targets, combat = await targetutils.maybe_combat( ctx, monster, args) await actionutils.run_attack(ctx, embed, args, caster, attack, targets, combat) handle_source_footer(embed, monster, add_source_str=False) await ctx.send(embed=embed)
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)
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 monster_atk(self, ctx, monster_name, atk_name=None, *, args=''): """Rolls a monster's attack. __Valid Arguments__ -t "<target>" - Sets targets for the attack. You can pass as many as needed. Will target combatants if channel is in initiative. -t "<target>|<args>" - Sets a target, and also allows for specific args to apply to them. (e.g, -t "OR1|hit" to force the attack against OR1 to hit) *adv/dis* - Advantage or Disadvantage *ea* - Elven Accuracy double advantage -ac <target ac> - overrides target AC *-b* <to hit bonus> - adds a bonus to hit -criton <num> - a number to crit on if rolled on or above *-d* <damage bonus> - adds a bonus to damage *-c* <damage bonus on crit> - adds a bonus to crit damage -rr <times> - number of times to roll the attack against each target *-mi <value>* - minimum value of each die on the damage roll *-resist* <damage resistance> *-immune* <damage immunity> *-vuln* <damage vulnerability> *-neutral* <damage type> - ignores this damage type in resistance calculations *-dtype <damage type>* - replaces all damage types with this damage type *-dtype <old>new>* - replaces all of one damage type with another (e.g. `-dtype fire>cold`) *hit* - automatically hits *miss* - automatically misses *crit* - automatically crits if hit *max* - deals max damage *magical* - makes the damage type magical -h - hides name, rolled values, and monster details -phrase <text> - adds flavour text -title <title> - changes the result title *note: `[name]` and `[aname]` will be replaced automatically* -thumb <url> - adds flavour image -f "Field Title|Field Text" - see `!help embed` <user snippet> - see `!help snippet` An italicized argument means the argument supports ephemeral arguments - e.g. `-d1` applies damage to the first hit, `-b1` applies a bonus to one attack, and so on. """ if atk_name is None or atk_name == 'list': return await self.monster_atk_list(ctx, monster_name) await try_delete(ctx.message) monster = await select_monster_full(ctx, monster_name) attacks = monster.attacks attack = await search_and_select(ctx, attacks, atk_name, lambda a: a.name) args = await helpers.parse_snippets(args, ctx) args = await helpers.parse_with_statblock(ctx, monster, args) args = argparse(args) embed = discord.Embed() if not args.last('h', type_=bool): embed.set_thumbnail(url=monster.get_image_url()) caster, targets, combat = await targetutils.maybe_combat(ctx, monster, args) await attackutils.run_attack(ctx, embed, args, caster, attack, targets, combat) embed.colour = random.randint(0, 0xffffff) handle_source_footer(embed, monster, add_source_str=False) await ctx.send(embed=embed)