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) srd = guild_settings.get("srd", False) destination = ctx.author if pm else ctx.channel result, metadata = await search_and_select(ctx, c.subclasses, name, lambda e: e['name'], srd=srd, return_metadata=True) metadata['srd'] = srd await self.add_training_data("subclass", name, result['name'], metadata=metadata) if not result.get('srd') and srd: return await self.send_srd_error(ctx, result) 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)
async def feat(self, ctx, *, name: str): """Looks up a feat.""" guild_settings = await self.get_settings(ctx.guild) pm = guild_settings.get("pm_result", False) srd = guild_settings.get("srd", False) destination = ctx.author if pm else ctx.channel result, metadata = await search_and_select(ctx, c.feats, name, lambda e: e['name'], return_metadata=True) metadata['srd'] = srd await self.add_training_data("feat", name, result['name'], metadata=metadata) if not result['srd'] and srd: return await self.send_srd_error(ctx, result) embed = EmbedWithAuthor(ctx) embed.title = result['name'] if result['prerequisite']: embed.add_field(name="Prerequisite", value=result['prerequisite']) if result['ability']: embed.add_field(name="Ability Improvement", value=f"Increase your {result['ability']} score by 1, up to a maximum of 20.") _name = 'Description' for piece in [result['desc'][i:i + 1024] for i in range(0, len(result['desc']), 1024)]: embed.add_field(name=_name, value=piece) _name = '** **' embed.set_footer(text=f"Feat | {result['source']} {result['page']}") await destination.send(embed=embed)
async def subclass(self, ctx, name: str): """Looks up a subclass.""" guild_settings = await self.get_settings(ctx.message.server) pm = guild_settings.get("pm_result", False) srd = guild_settings.get("srd", False) destination = ctx.message.author if pm else ctx.message.channel result = await search_and_select(ctx, c.subclasses, name, lambda e: e['name'], srd=srd) if not result.get('srd') and srd: return await self.send_srd_error(ctx, result) embed = EmbedWithAuthor(ctx) embed.title = result['name'] 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="Use !classfeat to look up a feature if it is cut off.") await self.bot.send_message(destination, embed=embed)
async def background(self, ctx, *, name: str): """Looks up a background.""" guild_settings = await self.get_settings(ctx.guild) pm = guild_settings.get("pm_result", False) srd = guild_settings.get("srd", False) result, metadata = await search_and_select(ctx, c.backgrounds, name, lambda e: e.name, srd=srd and (lambda e: e.srd), return_metadata=True) metadata['srd'] = srd await self.add_training_data("background", name, result.name, metadata=metadata) if not result.srd and srd: return await self.send_srd_error(ctx, result) embed = EmbedWithAuthor(ctx) embed.title = result.name embed.set_footer(text=f"Background | {result.source} {result.page}") ignored_fields = ['suggested characteristics', 'personality trait', 'ideal', 'bond', 'flaw', 'specialty', 'harrowing event'] for trait in result.traits: if trait['name'].lower() in ignored_fields: continue text = trait['text'] text = textwrap.shorten(text, width=1020, placeholder="...") embed.add_field(name=trait['name'], value=text) # do stuff here if pm: await ctx.author.send(embed=embed) else: await ctx.send(embed=embed)
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) embed.set_footer(text=f"Item | {item.source_str()}") if item.homebrew: add_homebrew_footer(embed) await Stats.increase_stat(ctx, "items_looked_up_life") await (await self._get_destination(ctx)).send(embed=embed)
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 handle_alias_required_licenses(ctx, err): 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: embed.title = "D&D Beyond is currently unavailable" embed.description = f"I was unable to communicate with D&D Beyond to confirm access to:\n" \ f"{', '.join(e.name for e in err.entities)}" else: embed.title = f"Connect your D&D Beyond account to use this customization!" embed.url = "https://www.dndbeyond.com/account" embed.description = \ "This customization requires access to one or more entities that are not in the SRD.\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: missing_source_ids = {e.source for e in err.entities} if len(err.entities) == 1: # 1 entity, display entity piecemeal embed.title = f"Purchase {err.entities[0].name} on D&D Beyond to use this customization!" marketplace_url = err.entities[0].marketplace_url elif len(missing_source_ids ) == 1: # 1 source, recommend purchasing source missing_source = next(iter(missing_source_ids)) embed.title = f"Purchase {long_source_name(missing_source)} on D&D Beyond to use this customization!" marketplace_url = f"https://www.dndbeyond.com/marketplace?utm_source=avrae&utm_medium=marketplacelink" else: # more than 1 source embed.title = f"Purchase {len(missing_source_ids)} sources on D&D Beyond to use this customization!" marketplace_url = "https://www.dndbeyond.com/marketplace?utm_source=avrae&utm_medium=marketplacelink" missing = natural_join( [f"[{e.name}]({e.marketplace_url})" for e in err.entities], "and") if len(missing) > 1400: missing = f"{len(err.entities)} items" missing_sources = natural_join( [long_source_name(e) for e in missing_source_ids], "and") embed.description = \ f"To use this customization and gain access to more integrations in Avrae, unlock **{missing}** by " \ f"purchasing {missing_sources} on D&D Beyond.\n\n" \ f"[Go to Marketplace]({marketplace_url})" embed.url = 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)
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 embed.set_footer( text=f"Use {ctx.prefix}classfeat to look up a feature.") 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) 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 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) embed.set_footer(text=f"Class Feature | {result.source_str()}") 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 self.bot.say("Error: SheetManager cog not loaded." ) return await ctx.invoke(token_cmd) try: guild_id = ctx.message.server.id srd = self.settings.get(guild_id, {}).get("srd", False) except: srd = False monster = await select_monster_full(ctx, name, srd=srd) if not monster.srd and srd: e = EmbedWithAuthor(ctx) e.title = monster.name e.description = "Token not available." return await self.bot.say(embed=e) 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 self.bot.say(embed=embed) else: if not url: return await self.bot.send_message( ctx.message.channel, "This monster has no image.") try: processed = await generate_token(url) except Exception as e: return await self.bot.send_message( ctx.message.channel, f"Error generating token: {e}") await self.bot.send_file( ctx.message.channel, processed, content="I generated this token for you! If it seems " "wrong, you can make your own at " "<http://rolladvantage.com/tokenstamp/>!", filename="image.png")
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) embed.set_footer(text=f"Feat | {result.source_str()}") 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) embed.set_footer(text=f"Race Feature | {result.source_str()}") 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 embed.set_footer(text=f"Background | {result.source_str()}") 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 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) embed.set_footer(text=f"Race | {result.source_str()}") 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) guild_settings = await self.get_settings(ctx.guild) srd = guild_settings.get("srd", False) monster, metadata = await select_monster_full(ctx, name, srd=srd, return_metadata=True) metadata['srd'] = srd metadata['homebrew'] = monster.source == 'homebrew' await self.add_training_data("monster", name, monster.name, metadata=metadata) if not monster.srd and srd: e = EmbedWithAuthor(ctx) e.title = monster.name e.description = "Token not available." return await ctx.send(embed=e) 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)
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 background(self, ctx, *, name: str): """Looks up a background.""" choices = compendium.backgrounds + compendium.nbackground_names result = await self._lookup_search(ctx, choices, name, lambda e: e.name, search_type='background', is_obj=True) if not result: return embed = EmbedWithAuthor(ctx) embed.title = result.name embed.set_footer(text=f"Background | {result.source} {result.page}") ignored_fields = ['suggested characteristics', 'personality trait', 'ideal', 'bond', 'flaw', 'specialty', 'harrowing event'] for trait in result.traits: if trait['name'].lower() in ignored_fields: continue text = trait['text'] text = textwrap.shorten(text, width=1020, placeholder="...") embed.add_field(name=trait['name'], value=text, inline=False) await (await self._get_destination(ctx)).send(embed=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)
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() embed = EmbedWithAuthor(ctx) embed.title = monster.name embed.description = f"{monster.size} monster." if not monster.source == 'homebrew': 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") embed.set_image(url="attachment://image.png") await ctx.send(file=file, 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) 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.""" 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 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)
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)
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)
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}
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}
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)
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)
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)
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)