async def _dice_roll_embed_common(self, gctx, roll_request, title_fmt: str, **fmt_kwargs): """ Common method to display a character embed based on some check/save result. Note: {name} will be formatted with the character's name in title_fmt. """ # check for loaded character character = await gctx.get_character() if character is None: await self.dice_roll_roll( gctx, roll_request, comment_getter=lambda rr: f"{roll_request.action}: {rr.roll_type.value.title()}") return # only listen to the first roll the_roll = roll_request.rolls[0] # send embed embed = embeds.EmbedWithCharacter(character, name=False) embed.title = title_fmt.format(name=character.get_title_name(), **fmt_kwargs) embed.description = str(the_roll.to_d20()) embed.set_footer(text=f"Rolled in {gctx.campaign.campaign_name}", icon_url=constants.DDB_LOGO_ICON) await gctx.channel.send(embed=embed)
async def save(self, ctx, skill, *args): if skill == 'death': ds_cmd = self.bot.get_command('game deathsave') if ds_cmd is None: return await ctx.send("Error: GameTrack cog not loaded.") return await ctx.invoke(ds_cmd, *args) char: Character = await Character.from_ctx(ctx) args = await self.new_arg_stuff(args, ctx, char) hide = args.last('h', type_=bool) embed = embeds.EmbedWithCharacter(char, name=False, image=not hide) checkutils.update_csetting_args(char, args) caster = await targetutils.maybe_combat_caster(ctx, char) result = checkutils.run_save(skill, caster, args, embed) # send await ctx.send(embed=embed) await try_delete(ctx.message) if gamelog := self.bot.get_cog('GameLog'): await gamelog.send_save(ctx, char, result.skill_name, result.rolls)
async def playertoken(self, ctx, *args): """ Generates and sends a token for use on VTTs. __Valid Arguments__ -border <gold|plain|none> - Chooses the token border. """ char: Character = await Character.from_ctx(ctx) if not char.image: return await ctx.send("This character has no image.") token_args = argparse(args) ddb_user = await self.bot.ddb.get_ddb_user(ctx, ctx.author.id) is_subscriber = ddb_user and ddb_user.is_subscriber try: processed = await img.generate_token(char.image, is_subscriber, token_args) except Exception as e: return await ctx.send(f"Error generating token: {e}") file = discord.File(processed, filename="image.png") embed = embeds.EmbedWithCharacter(char, image=False) embed.set_image(url="attachment://image.png") await ctx.send(file=file, embed=embed) processed.close()
async def action(self, ctx, atk_name=None, *, args: str = ''): if atk_name is None: return await self.action_list(ctx) char: Character = await Character.from_ctx(ctx) args = await self.new_arg_stuff(args, ctx, char) hide = args.last('h', type_=bool) embed = embeds.EmbedWithCharacter(char, name=False, image=not hide) caster, targets, combat = await targetutils.maybe_combat( ctx, char, args) # we select from caster attacks b/c a combat effect could add some attack_or_action = await actionutils.select_action( ctx, atk_name, attacks=caster.attacks, actions=char.actions) if isinstance(attack_or_action, Attack): result = await actionutils.run_attack(ctx, embed, args, caster, attack_or_action, targets, combat) else: result = await actionutils.run_action(ctx, embed, args, caster, attack_or_action, targets, combat) await ctx.send(embed=embed) await try_delete(ctx.message) if (gamelog := self.bot.get_cog('GameLog')) and result is not None: await gamelog.send_automation(ctx, char, attack_or_action.name, result)
async def get_content(self): outbound, inbound = await self.can_do_character_sync() embed = embeds.EmbedWithCharacter( self.character, title=f"Character Settings ({self.character.name}) / Sync Settings" ) if outbound: embed.add_field( name="Outbound Sync", value=f"**{self.settings.sync_outbound}**\n" f"*If this is enabled, updates to your character's HP, spell slots, custom counters, and more " f"will be sent to your sheet provider live.*", inline=False) if inbound: embed.add_field( name="Inbound Sync", value=f"**{self.settings.sync_inbound}**\n" f"*If this is enabled, if you change your character's HP, spell slots, custom counters, or more " f"on your sheet provider, they will be updated here as well.*", inline=False) if not (outbound or inbound): embed.description = ( "Character sync is not supported by your sheet provider (and I have no idea how you got to this menu). " "Press the Back button to go back, and come tell us how you got here on the [Development Discord]" "(https://support.avrae.io).") return {"embed": embed}
async def get_content(self): embed = embeds.EmbedWithCharacter( self.character, title=f"Character Settings for {self.character.name}") embed.add_field( name="Cosmetic Settings", value=f"**Embed Color**: {color_setting_desc(self.settings.color)}\n" f"**Show Character Image**: {self.settings.embed_image}", inline=False) embed.add_field( name="Gameplay Settings", value=f"**Crit Range**: {crit_range_desc(self.settings.crit_on)}\n" f"**Extra Crit Dice**: {self.settings.extra_crit_dice}\n" f"**Reroll**: {self.settings.reroll}\n" f"**Ignore Crits**: {self.settings.ignore_crit}\n" f"**Reliable Talent**: {self.settings.talent}\n" f"**Reset All Spell Slots on Short Rest**: {self.settings.srslots}", inline=False) outbound, inbound = await self.can_do_character_sync() if inbound or outbound: sync_desc_lines = [] if outbound: sync_desc_lines.append( f"**Outbound Sync**: {self.settings.sync_outbound}") if inbound: sync_desc_lines.append( f"**Inbound Sync**: {self.settings.sync_inbound}") embed.add_field(name="Character Sync Settings", value='\n'.join(sync_desc_lines), inline=False) return {"embed": embed}
async def _active_character_embed(ctx): """Creates an embed to be displayed when the active character is checked""" active_character: Character = await ctx.get_character() embed = embeds.EmbedWithCharacter(active_character) desc = (f"Your current active character is {active_character.name}. " f"All of your checks, saves and actions will use this character's stats.") if (link := active_character.get_sheet_url()) is not None: desc = f"{desc}\n[Go to Character Sheet]({link})"
async def send_ddb_ctas(ctx, character): """Sends relevant CTAs after a DDB character is imported. Only show a CTA 1/24h to not spam people.""" ddb_user = await ctx.bot.ddb.get_ddb_user(ctx, ctx.author.id) if ddb_user is not None: ld_dict = ddb_user.to_ld_dict() else: ld_dict = {"key": str(ctx.author.id), "anonymous": True} gamelog_flag = await ctx.bot.ldclient.variation('cog.gamelog.cta.enabled', ld_dict, False) # get server settings for whether to pull up campaign settings if ctx.guild is not None: guild_settings = await ctx.get_server_settings() show_campaign_cta = guild_settings.show_campaign_cta else: show_campaign_cta = False # has the user seen this cta within the last 7d? if await ctx.bot.rdb.get(f"cog.sheetmanager.cta.seen.{ctx.author.id}"): return embed = embeds.EmbedWithCharacter(character) embed.title = "Heads up!" embed.description = "There's a couple of things you can do to make your experience even better!" embed.set_footer(text="You won't see this message again this week.") # link ddb user if ddb_user is None: embed.add_field( name="Connect Your D&D Beyond Account", value= "Visit your [Account Settings](https://www.dndbeyond.com/account) page in D&D Beyond to link your " "D&D Beyond and Discord accounts. This lets you use all your D&D Beyond content in Avrae for free!", inline=False) # game log if character.ddb_campaign_id and gamelog_flag and show_campaign_cta: try: await CampaignLink.from_id(ctx.bot.mdb, character.ddb_campaign_id) except NoCampaignLink: embed.add_field( name="Link Your D&D Beyond Campaign", value= f"Sync rolls between a Discord channel and your D&D Beyond character sheet by linking your " f"campaign! Use `{ctx.prefix}campaign https://www.dndbeyond.com/campaigns/" f"{character.ddb_campaign_id}` in the Discord channel you want to link it to.\n" f"This message can be disabled in `{ctx.prefix}server_settings`.", inline=False) if not embed.fields: return await ctx.send(embed=embed) await ctx.bot.rdb.setex(f"cog.sheetmanager.cta.seen.{ctx.author.id}", str(time.time()), 60 * 60 * 24 * 7)
async def send_sync_result(self, gctx: GameLogEventContext, char: Character, hp_result: SyncHPResult, death_save_result: SyncDeathSavesResult): embed = embeds.EmbedWithCharacter(char) # --- hp --- if hp_result.changed: embed.add_field(name="Hit Points", value=hp_result.message) # --- death saves --- if death_save_result.changed: embed.add_field(name="Death Saves", value=str(char.death_saves)) embed.set_footer(text=f"Updated in {gctx.campaign.campaign_name}", icon_url=constants.DDB_LOGO_ICON) await gctx.send(embed=embed)
async def get_content(self): embed = embeds.EmbedWithCharacter( self.character, title= f"Character Settings ({self.character.name}) / Gameplay Settings") embed.add_field( name="Crit Range", value=f"**{crit_range_desc(self.settings.crit_on)}**\n" f"*If an attack roll's natural roll (the value on the d20 before modifiers) lands in this range, " f"the attack will be counted as a crit.*", inline=False) embed.add_field( name="Extra Crit Dice", value=f"**{self.settings.extra_crit_dice}**\n" f"*How many additional dice to add to a weapon's damage dice on a crit (in addition to doubling the " f"dice).*", inline=False) embed.add_field( name="Reroll", value=f"**{self.settings.reroll}**\n" f"*If an attack, save, or ability check's natural roll lands on this number, the die will be " f"rerolled up to once.*", inline=False) embed.add_field( name="Ignore Crits", value=f"**{self.settings.ignore_crit}**\n" f"*If this is enabled, any attack against your character will not have its damage dice doubled on a " f"critical hit.*", inline=False) embed.add_field( name="Reliable Talent", value=f"**{self.settings.talent}**\n" f"*If this is enabled, any d20 roll on an ability check that lets you add your proficiency bonus " f"will be treated as a 10 if it rolls 9 or lower.*", inline=False) sr_slot_note = "" if self.character.spellbook.max_pact_slots is not None: sr_slot_note = " Note that your pact slots will reset on a short rest even if this setting is disabled." embed.add_field( name="Reset All Spell Slots on Short Rest", value=f"**{self.settings.srslots}**\n" f"*If this is enabled, all of your spell slots (including non-pact slots) will reset on a short " f"rest.{sr_slot_note}*", inline=False) return {"embed": embed}
async def action_list(self, ctx, *args): """ Lists the active character's actions. __Valid Arguments__ -v - Verbose: Displays each action's character sheet description rather than the effect summary. attack - Only displays the available attacks. action - Only displays the available actions. bonus - Only displays the available bonus actions. reaction - Only displays the available reactions. other - Only displays the available actions that have another activation time. """ char: Character = await Character.from_ctx(ctx) caster = await targetutils.maybe_combat_caster(ctx, char) embed = embeds.EmbedWithCharacter(char, name=False) embed.title = f"{char.name}'s Actions" await actionutils.send_action_list( ctx, caster=caster, attacks=caster.attacks, actions=char.actions, embed=embed, args=args)
async def get_content(self): embed = embeds.EmbedWithCharacter( self.character, title= f"Character Settings ({self.character.name}) / Cosmetic Settings") embed.add_field( name="Embed Color", value=f"**{color_setting_desc(self.settings.color)}**\n" f"*This color will appear on the left side of your character's check, save, actions, and some " f"other embeds (like this one!).*", inline=False) embed.add_field( name="Show Character Image", value=f"**{self.settings.embed_image}**\n" f"*If this is disabled, your character's portrait will not appear on the right side of their " f"checks, saves, actions, and some other embeds.*", inline=False) return {"embed": embed}
async def desc(self, ctx): """Prints or edits a description of your currently active character.""" char: Character = await Character.from_ctx(ctx) desc = char.description if not desc: desc = 'No description available.' if len(desc) > 2048: desc = desc[:2044] + '...' elif len(desc) < 2: desc = 'No description available.' embed = embeds.EmbedWithCharacter(char, name=False) embed.title = char.name embed.description = desc await ctx.send(embed=embed) await try_delete(ctx.message)
async def check(self, ctx, check, *args): char: Character = await Character.from_ctx(ctx) skill_key = await search_and_select(ctx, SKILL_NAMES, check, lambda s: s) args = await self.new_arg_stuff(args, ctx, char) hide = args.last('h', type_=bool) embed = embeds.EmbedWithCharacter(char, name=False, image=not hide) skill = char.skills[skill_key] checkutils.update_csetting_args(char, args, skill) caster = await targetutils.maybe_combat_caster(ctx, char) result = checkutils.run_check(skill_key, caster, args, embed) await ctx.send(embed=embed) await try_delete(ctx.message) if gamelog := self.bot.get_cog('GameLog'): await gamelog.send_check(ctx, char, result.skill_name, result.rolls)