async def customcounter_summary(self, ctx): """Prints a summary of all custom counters.""" character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) for counter in character.consumables: embed.add_field(name=counter.name, value=counter.full_str()) await ctx.send(embed=embed)
async def customcounter_summary(self, ctx, page: int = 0): """ Prints a summary of all custom counters. Use `!cc list <page>` to view pages if you have more than 25 counters. """ character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character, title="Custom Counters") # Check that we're not over the field limit total = len(character.consumables) if total > 25: # Discord Field limit page = max(0, page - 1) # Humans count from 1 maxpage = total // 25 start = min(page * 25, total - 25) end = max(start + 25, total) # Build the current page embed.set_footer( text= f"Page [{page + 1}/{maxpage + 1}] | {ctx.prefix}cc list <page>" ) for counter in character.consumables[start:end]: embed.add_field(name=counter.name, value=counter.full_str()) else: for counter in character.consumables: embed.add_field(name=counter.name, value=counter.full_str()) await ctx.send(embed=embed)
async def customcounter_summary(self, ctx): """Prints a summary of all custom counters.""" character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) for name, counter in character.get_all_consumables().items(): val = self._get_cc_value(character, counter) embed.add_field(name=name, value=val) await ctx.send(embed=embed)
def get_sheet_embed(self): embed = EmbedWithCharacter(self) desc_details = [] # race/class (e.g. Tiefling Bard/Warlock) classes = '/'.join(f"{cls} {lvl}" for cls, lvl in self.levels) desc_details.append(f"{self.race} {classes}") # prof bonus desc_details.append( f"**Proficiency Bonus**: {self.stats.prof_bonus:+}") # combat details desc_details.append(f"**AC**: {self.ac}") desc_details.append(f"**HP**: {self.get_hp_str()}") desc_details.append( f"**Initiative**: {self.skills.initiative.value:+}") # stats desc_details.append(str(self.stats)) save_profs = str(self.saves) if save_profs: desc_details.append(f"**Save Proficiencies**: {save_profs}") skill_profs = str(self.skills) if skill_profs: desc_details.append(f"**Skill Proficiencies**: {skill_profs}") desc_details.append( f"**Senses**: passive Perception {10 + self.skills.perception.value}" ) # resists resists = str(self.resistances) if resists: desc_details.append(resists) embed.description = '\n'.join(desc_details) # attacks atks = self.attacks atk_str = "" for attack in atks: a = f"{str(attack)}\n" if len(atk_str) + len(a) > 1000: atk_str += "[...]" break atk_str += a atk_str = atk_str.strip() if atk_str: embed.add_field(name="Attacks", value=atk_str) # sheet url? if self._import_version < 15: embed.set_footer( text= f"You are using an old sheet version ({self.sheet_type} v{self._import_version}). " f"Please run !update.") return embed
async def game_status(self, ctx): """Prints the status of the current active character.""" character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.add_field(name="Hit Points", value=f"{character.hp}/{character.max_hp}") embed.add_field(name="Spell Slots", value=character.get_remaining_slots_str()) for counter in character.consumables: embed.add_field(name=counter.name, value=counter.full_str()) await ctx.send(embed=embed)
async def game_deathsave_reset(self, ctx): """Resets all death saves.""" character: Character = await Character.from_ctx(ctx) character.death_saves.reset() await character.commit(ctx) embed = EmbedWithCharacter(character) embed.title = f'{character.name} reset Death Saves!' embed.add_field(name="Death Saves", value=str(character.death_saves)) await ctx.send(embed=embed)
def get_sheet_embed(self): embed = EmbedWithCharacter(self) # noinspection PyListCreation # this could be a list literal, but it's more readable this way desc_details = [] # race/class (e.g. Tiefling Bard/Warlock) desc_details.append(f"{self.race} {str(self.levels)}") # prof bonus desc_details.append( f"**Proficiency Bonus**: {self.stats.prof_bonus:+}") # combat details desc_details.append(f"**AC**: {self.ac}") desc_details.append(f"**HP**: {self.hp_str()}") desc_details.append( f"**Initiative**: {self.skills.initiative.value:+}") # stats desc_details.append(str(self.stats)) save_profs = str(self.saves) if save_profs: desc_details.append(f"**Save Proficiencies**: {save_profs}") skill_profs = str(self.skills) if skill_profs: desc_details.append(f"**Skill Proficiencies**: {skill_profs}") desc_details.append( f"**Senses**: passive Perception {10 + self.skills.perception.value}" ) # resists resists = str(self.resistances) if resists: desc_details.append(resists) embed.description = '\n'.join(desc_details) # attacks atk_str = self.attacks.build_str(self) if len(atk_str) > 1000: atk_str = f"{atk_str[:1000]}\n[...]" if atk_str: embed.add_field(name="Attacks", value=atk_str) # sheet url? if self._import_version < SHEET_VERSION: embed.set_footer( text= f"You are using an old sheet version ({self.sheet_type} v{self._import_version}). " f"Please run !update.") return embed
async def game_deathsave_reset(self, ctx): """Resets all death saves.""" character = await Character.from_ctx(ctx) character.reset_death_saves() embed = EmbedWithCharacter(character) embed.title = f'{character.get_name()} reset Death Saves!' await character.commit(ctx) embed.add_field(name="Death Saves", value=character.get_ds_str()) await self.bot.say(embed=embed)
async def game_shortrest(self, ctx, *args): """Performs a short rest, resetting applicable counters. __Valid Arguments__ -h - Hides the character summary output.""" character = Character.from_ctx(ctx) reset = character.short_rest() embed = EmbedWithCharacter(character, name=False) embed.title = f"{character.get_name()} took a Short Rest!" embed.add_field(name="Reset Values", value=', '.join(set(reset))) character.commit(ctx) await self.bot.say(embed=embed) if not '-h' in args: await ctx.invoke(self.game_status)
async def game_longrest(self, ctx, *args): """Performs a long rest, resetting applicable counters. __Valid Arguments__ -h - Hides the character summary output.""" character: Character = await Character.from_ctx(ctx) reset = character.long_rest() embed = EmbedWithCharacter(character, name=False) embed.title = f"{character.name} took a Long Rest!" embed.add_field(name="Reset Values", value=', '.join(set(reset))) await character.commit(ctx) await ctx.send(embed=embed) if not '-h' in args: await ctx.invoke(self.game_status)
async def spellbook(self, ctx): """Commands to display a character's known spells and metadata.""" character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.description = f"{character.get_name()} knows {len(character.get_spell_list())} spells." embed.add_field(name="DC", value=str(character.get_save_dc())) embed.add_field(name="Spell Attack Bonus", value=str(character.get_spell_ab())) embed.add_field(name="Spell Slots", value=character.get_remaining_slots_str() or "None") spells_known = {} for spell_name in character.get_spell_list(): spell = strict_search(c.spells, 'name', spell_name) spells_known[spell['level']] = spells_known.get( spell['level'], []) + [spell_name] level_name = { '0': 'Cantrips', '1': '1st Level', '2': '2nd Level', '3': '3rd Level', '4': '4th Level', '5': '5th Level', '6': '6th Level', '7': '7th Level', '8': '8th Level', '9': '9th Level' } for level, spells in sorted(list(spells_known.items()), key=lambda k: k[0]): if spells: embed.add_field(name=level_name.get(level, "Unknown Level"), value=', '.join(spells)) await self.bot.say(embed=embed)
async def game_deathsave(self, ctx, *args): """Commands to manage character death saves. __Valid Arguments__ See `!help save`.""" character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character, name=False) args = await helpers.parse_snippets(args, ctx) args = await helpers.parse_with_character(ctx, character, args) args = argparse(args) checkutils.update_csetting_args(character, args) caster, _, _ = await targetutils.maybe_combat(ctx, character, args) result = checkutils.run_save('death', caster, args, embed) dc = result.skill_roll_result.dc or 10 death_phrase = '' for save_roll in result.skill_roll_result.rolls: if save_roll.crit == d20.CritType.CRIT: character.hp = 1 elif save_roll.crit == d20.CritType.FAIL: character.death_saves.fail(2) elif save_roll.total >= dc: character.death_saves.succeed() else: character.death_saves.fail() if save_roll.crit == d20.CritType.CRIT: death_phrase = f"{character.name} is UP with 1 HP!" break elif character.death_saves.is_dead(): death_phrase = f"{character.name} is DEAD!" break elif character.death_saves.is_stable(): death_phrase = f"{character.name} is STABLE!" break if death_phrase: embed.set_footer(text=death_phrase) embed.add_field(name="Death Saves", value=str(character.death_saves), inline=False) await character.commit(ctx) await ctx.send(embed=embed) await try_delete(ctx.message) if gamelog := self.bot.get_cog('GameLog'): await gamelog.send_save(ctx, character, result.skill_name, result.rolls)
async def spellbook(self, ctx): """Commands to display a character's known spells and metadata.""" character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.description = f"{character.name} knows {len(character.spellbook.spells)} spells." embed.add_field(name="DC", value=str(character.spellbook.dc)) embed.add_field(name="Spell Attack Bonus", value=str(character.spellbook.sab)) embed.add_field(name="Spell Slots", value=character.get_remaining_slots_str() or "None") spells_known = collections.defaultdict(lambda: []) choices = await get_spell_choices(ctx) for spell_ in character.spellbook.spells: spell = await get_castable_spell(ctx, spell_.name, choices) if spell is None and spell_.strict: continue elif spell is None: spells_known['unknown'].append(f"*{spell_.name}*") else: if spell.source == 'homebrew': formatted = f"*{spell.name}*" else: formatted = spell.name spells_known[str(spell.level)].append(formatted) level_name = {'0': 'Cantrips', '1': '1st Level', '2': '2nd Level', '3': '3rd Level', '4': '4th Level', '5': '5th Level', '6': '6th Level', '7': '7th Level', '8': '8th Level', '9': '9th Level'} for level, spells in sorted(list(spells_known.items()), key=lambda k: k[0]): if spells: spells.sort() embed.add_field(name=level_name.get(level, "Unknown"), value=', '.join(spells)) await ctx.send(embed=embed)
async def customcounter(self, ctx, name=None, *, modifier=None): """Commands to implement custom counters. When called on its own, if modifier is supplied, increases the counter *name* by *modifier*. If modifier is not supplied, prints the value and metadata of the counter *name*.""" if name is None: return await ctx.invoke(self.bot.get_command("customcounter list")) character: Character = await Character.from_ctx(ctx) counter = await character.select_consumable(ctx, name) if modifier is None: # display value counter_display_embed = EmbedWithCharacter(character) counter_display_embed.add_field(name=counter.name, value=counter.full_str()) return await ctx.send(embed=counter_display_embed) operator = None if ' ' in modifier: m = modifier.split(' ') operator = m[0] modifier = m[-1] try: modifier = int(modifier) except ValueError: return await ctx.send( f"Could not modify counter: {modifier} is not a number") result_embed = EmbedWithCharacter(character) if not operator or operator == 'mod': new_value = counter.value + modifier elif operator == 'set': new_value = modifier else: return await ctx.send("Invalid operator. Use mod or set.") counter.set(new_value) await character.commit(ctx) if new_value - counter.value: out = f"{str(counter)}\n({abs(new_value - counter.value)} overflow)" else: out = str(counter) result_embed.add_field(name=counter.name, value=out) try: await ctx.message.delete() except: pass await ctx.send(embed=result_embed)
async def customcounter(self, ctx, name=None, *, modifier=None): """Commands to implement custom counters. When called on its own, if modifier is supplied, increases the counter *name* by *modifier*. If modifier is not supplied, prints the value and metadata of the counter *name*. """ if name is None: return await self.customcounter_summary(ctx) character: Character = await Character.from_ctx(ctx) counter = await character.select_consumable(ctx, name) cc_embed_title = counter.title if counter.title is not None else counter.name # replace [name] in title cc_embed_title = cc_embed_title.replace('[name]', character.name) if modifier is None: # display value counter_display_embed = EmbedWithCharacter(character) counter_display_embed.add_field(name=counter.name, value=counter.full_str()) if counter.desc: counter_display_embed.add_field(name='Description', value=counter.desc, inline=False) return await ctx.send(embed=counter_display_embed) operator = None if ' ' in modifier: m = modifier.split(' ') operator = m[0] modifier = m[-1] change = '' old_value = counter.value try: modifier = int(modifier) except ValueError: return await ctx.send(f"Could not modify counter: {modifier} is not a number") result_embed = EmbedWithCharacter(character) if not operator or operator == 'mod': new_value = counter.value + modifier elif operator == 'set': new_value = modifier else: return await ctx.send("Invalid operator. Use mod or set.") counter.set(new_value) await character.commit(ctx) delta = f"({counter.value - old_value:+})" if new_value - counter.value: # we overflowed somewhere out = f"{str(counter)} {delta}\n({abs(new_value - counter.value)} overflow)" else: out = f"{str(counter)} {delta}" result_embed.add_field(name=cc_embed_title, value=out) if counter.desc: result_embed.add_field(name='Description', value=counter.desc, inline=False) await try_delete(ctx.message) await ctx.send(embed=result_embed)
async def game_deathsave_fail(self, ctx): """Adds a failed death save.""" character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.title = f'{character.name} fails a Death Save!' character.death_saves.fail() await character.commit(ctx) if character.death_saves.is_dead(): embed.set_footer(text=f"{character.name} is DEAD!") embed.description = "Added 1 failed death save." embed.add_field(name="Death Saves", value=str(character.death_saves)) await ctx.send(embed=embed)
async def game_deathsave_save(self, ctx): """Adds a successful death save.""" character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.title = f'{character.name} succeeds a Death Save!' character.death_saves.succeed() await character.commit(ctx) if character.death_saves.is_stable(): embed.set_footer(text=f"{character.name} is STABLE!") embed.description = "Added 1 successful death save." embed.add_field(name="Death Saves", value=str(character.death_saves)) await ctx.send(embed=embed)
async def game_deathsave_fail(self, ctx): """Adds a failed death save.""" character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.title = f'{character.get_name()} fails a Death Save!' death_phrase = '' if character.add_failed_ds(): death_phrase = f"{character.get_name()} is DEAD!" await character.commit(ctx) embed.description = "Added 1 failed death save." if death_phrase: embed.set_footer(text=death_phrase) embed.add_field(name="Death Saves", value=character.get_ds_str()) await ctx.send(embed=embed)
async def game_deathsave_save(self, ctx): """Adds a successful death save.""" character = Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.title = f'{character.get_name()} succeeds a Death Save!' death_phrase = '' if character.add_successful_ds(): death_phrase = f"{character.get_name()} is STABLE!" character.commit(ctx) embed.description = "Added 1 successful death save." if death_phrase: embed.set_footer(text=death_phrase) embed.add_field(name="Death Saves", value=character.get_ds_str()) await self.bot.say(embed=embed)
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) # 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 = 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: 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.", 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 spellbook(self, ctx): """Commands to display a character's known spells and metadata.""" character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.description = f"{character.get_name()} knows {len(character.get_spell_list())} spells." embed.add_field(name="DC", value=str(character.get_save_dc())) embed.add_field(name="Spell Attack Bonus", value=str(character.get_spell_ab())) embed.add_field(name="Spell Slots", value=character.get_remaining_slots_str() or "None") spells_known = {} choices = await get_spell_choices(ctx) for spell_ in character.get_raw_spells(): if isinstance(spell_, str): spell, strict = search(c.spells, spell_, lambda sp: sp.name) if spell is None or not strict: continue spells_known[str(spell.level)] = spells_known.get( str(spell.level), []) + [spell.name] else: spellname = spell_['name'] strict = spell_['strict'] spell = await get_castable_spell(ctx, spellname, choices) if spell is None and strict: continue elif spell is None: spells_known['unknown'] = spells_known.get( 'unknown', []) + [f"*{spellname}*"] else: if spell.source == 'homebrew': formatted = f"*{spell.name}*" else: formatted = spell.name spells_known[str(spell.level)] = spells_known.get( str(spell.level), []) + [formatted] level_name = { '0': 'Cantrips', '1': '1st Level', '2': '2nd Level', '3': '3rd Level', '4': '4th Level', '5': '5th Level', '6': '6th Level', '7': '7th Level', '8': '8th Level', '9': '9th Level' } for level, spells in sorted(list(spells_known.items()), key=lambda k: k[0]): if spells: spells.sort() embed.add_field(name=level_name.get(level, "Unknown"), value=', '.join(spells)) await ctx.send(embed=embed)
async def spellbook(self, ctx): """Commands to display a character's known spells and metadata.""" await ctx.trigger_typing() character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.description = f"{character.name} knows {len(character.spellbook.spells)} spells." embed.add_field(name="DC", value=str(character.spellbook.dc)) embed.add_field(name="Spell Attack Bonus", value=str(character.spellbook.sab)) embed.add_field(name="Spell Slots", value=character.spellbook.slots_str() or "None") # dynamic help flags flag_show_multiple_source_help = False flag_show_homebrew_help = False spells_known = collections.defaultdict(lambda: []) choices = await get_spell_choices(ctx) for spell_ in character.spellbook.spells: results, strict = search(choices, spell_.name, lambda sp: sp.name, strict=True) if not strict: if len(results) > 1: spells_known['unknown'].append(f"*{spell_.name} ({'*' * len(results)})*") flag_show_multiple_source_help = True else: spells_known['unknown'].append(f"*{spell_.name}*") flag_show_homebrew_help = True else: spell = results if spell.homebrew: formatted = f"*{spell.name}*" flag_show_homebrew_help = True else: formatted = spell.name spells_known[str(spell.level)].append(formatted) level_name = {'0': 'Cantrips', '1': '1st Level', '2': '2nd Level', '3': '3rd Level', '4': '4th Level', '5': '5th Level', '6': '6th Level', '7': '7th Level', '8': '8th Level', '9': '9th Level'} for level, spells in sorted(list(spells_known.items()), key=lambda k: k[0]): if spells: spells.sort() embed.add_field(name=level_name.get(level, "Unknown"), value=', '.join(spells), inline=False) # dynamic help footer_out = [] if flag_show_homebrew_help: footer_out.append("An italicized spell indicates that the spell is homebrew.") if flag_show_multiple_source_help: footer_out.append("Asterisks after a spell indicates that the spell is being provided by multiple sources.") if footer_out: embed.set_footer(text=' '.join(footer_out)) await ctx.send(embed=embed)
async def game_status(self, ctx): """Prints the status of the current active character.""" character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.add_field(name="Hit Points", value=character.hp_str()) embed.add_field(name="Spell Slots", value=character.spellbook.slots_str()) if character.death_saves.successes != 0 or character.death_saves.fails != 0: embed.add_field(name="Death Saves", value=str(character.death_saves)) for counter in character.consumables: embed.add_field(name=counter.name, value=counter.full_str()) await ctx.send(embed=embed)
def embed_for_basic_attack(gctx, action_name, character, to_hit_roll=None, damage_roll=None): """ Creates an embed for a character making an attack where the Avrae action is unknown. Handles inserting the correct fields for to-hit and damage. :type gctx: GameLogEventContext :type action_name: str :type character: cogs5e.models.character.Character :type to_hit_roll: ddb.dice.tree.RollRequestRoll :type damage_roll: ddb.dice.tree.RollRequestRoll """ embed = EmbedWithCharacter(character, name=False) # set title embed.title = f'{character.get_title_name()} attacks with {action_name}!' # add to hit (and damage, either if it is provided or the action expects damage and it is not provided) meta_rolls = [] if to_hit_roll is not None: meta_rolls.append(f"**To Hit**: {str(to_hit_roll.to_d20())}") if damage_roll is not None: if damage_roll.roll_kind == ddb.dice.RollKind.CRITICAL_HIT: meta_rolls.append( f"**Damage (CRIT!)**: {str(damage_roll.to_d20())}") else: meta_rolls.append(f"**Damage**: {str(damage_roll.to_d20())}") else: meta_rolls.append("**Damage**: Waiting for roll...") embed.add_field(name="Meta", value='\n'.join(meta_rolls), inline=False) # set footer embed.set_footer(text=f"Rolled in {gctx.campaign.campaign_name}", icon_url=constants.DDB_LOGO_ICON) return embed
async def customcounter_summary(self, ctx, page: int = 1): """ Prints a summary of all custom counters. Use `!cc list <page>` to view pages if you have more than 25 counters. """ character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character, title="Custom Counters") if character.consumables: # paginate if > 25 total = len(character.consumables) maxpage = total // 25 + 1 page = max(1, min(page, maxpage)) pages = [character.consumables[i:i + 25] for i in range(0, total, 25)] for counter in pages[page - 1]: embed.add_field(name=counter.name, value=counter.full_str()) if total > 25: embed.set_footer(text=f"Page [{page}/{maxpage}] | {ctx.prefix}cc list <page>") else: embed.add_field(name="No Custom Counters", value=f"Check out `{ctx.prefix}help cc create` to see how to create new ones.") await ctx.send(embed=embed)
async def game_status(self, ctx): """Prints the status of the current active character.""" character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.add_field(name="Hit Points", value=f"{character.get_current_hp()}/{character.get_max_hp()}") embed.add_field(name="Spell Slots", value=character.get_remaining_slots_str()) for name, counter in character.get_all_consumables().items(): val = self._get_cc_value(character, counter) embed.add_field(name=name, value=val) await ctx.send(embed=embed)
async def check(self, ctx, check, *args): """Rolls a check for your current active character. __Valid Arguments__ adv/dis -b [conditional bonus] -mc [minimum roll] -phrase [flavor text] -title [title] *note: [charname] and [cname] will be replaced automatically* -dc [dc] -rr [iterations] str/dex/con/int/wis/cha (different skill base; e.g. Strength (Intimidation)) """ char: Character = await Character.from_ctx(ctx) skill_key = await search_and_select(ctx, SKILL_NAMES, check, lambda s: s) skill_name = camel_to_title(skill_key) embed = EmbedWithCharacter(char, False) skill = char.skills[skill_key] args = await self.new_arg_stuff(args, ctx, char) # advantage adv = args.adv(boolwise=True) # roll bonus b = args.join('b', '+') # phrase phrase = args.join('phrase', '\n') # num rolls iterations = min(args.last('rr', 1, int), 25) # dc dc = args.last('dc', type_=int) # reliable talent (#654) rt = char.get_setting('talent', 0) and skill.prof >= 1 mc = args.last('mc') or 10 * rt # halfling luck ro = char.get_setting('reroll') num_successes = 0 mod = skill.value formatted_d20 = skill.d20(base_adv=adv, reroll=ro, min_val=mc, base_only=True) if 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 = mod - char.get_mod(SKILL_MAP[skill_key]) + char.get_mod(base) skill_name = f"{verbose_stat(base)} ({skill_name})" if b is not None: roll_str = f"{formatted_d20}{mod:+}+{b}" else: roll_str = f"{formatted_d20}{mod:+}" if args.last('title'): embed.title = args.last('title', '') \ .replace('[charname]', char.name) \ .replace('[cname]', skill_name) else: embed.title = f'{char.name} makes {a_or_an(skill_name)} check!' if iterations > 1: embed.description = (f"**DC {dc}**\n" if dc else '') + ( '*' + phrase + '*' if phrase is not None else '') for i in range(iterations): result = roll(roll_str, inline=True) if dc and result.total >= dc: num_successes += 1 embed.add_field(name=f"Check {i + 1}", value=result.skeleton) if dc: embed.set_footer( text= f"{num_successes} Successes | {iterations - num_successes} Failures" ) else: result = roll(roll_str, inline=True) if dc: embed.set_footer( text="Success!" if result.total >= dc else "Failure!") embed.description = ( f"**DC {dc}**\n" if dc else '') + result.skeleton + ( '\n*' + phrase + '*' if phrase is not None else '') embeds.add_fields_from_args(embed, args.get('f')) if args.last('image') is not None: embed.set_thumbnail(url=args.last('image')) await ctx.send(embed=embed) try: await ctx.message.delete() except: pass
async def save(self, ctx, skill, *args): """Rolls a save for your current active character. __Valid Arguments__ adv/dis -b [conditional bonus] -phrase [flavor text] -title [title] *note: [charname] and [sname] will be replaced automatically* -image [image URL] -dc [dc] (does not apply to Death Saves) -rr [iterations] (does not apply to Death Saves)""" 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) try: save = char.saves.get(skill) except ValueError: return await ctx.send('That\'s not a valid save.') embed = EmbedWithCharacter(char, name=False) args = await self.new_arg_stuff(args, ctx, char) adv = args.adv(boolwise=True) b = args.join('b', '+') phrase = args.join('phrase', '\n') iterations = min(args.last('rr', 1, int), 25) dc = args.last('dc', type_=int) num_successes = 0 formatted_d20 = save.d20(base_adv=adv, reroll=char.get_setting('reroll')) if b: roll_str = f"{formatted_d20}+{b}" else: roll_str = formatted_d20 save_name = f"{verbose_stat(skill[:3]).title()} Save" if args.last('title'): embed.title = args.last('title', '') \ .replace('[charname]', char.name) \ .replace('[sname]', save_name) else: embed.title = f'{char.name} makes {a_or_an(save_name)}!' if iterations > 1: embed.description = (f"**DC {dc}**\n" if dc else '') + ( '*' + phrase + '*' if phrase is not None else '') for i in range(iterations): result = roll(roll_str, inline=True) if dc and result.total >= dc: num_successes += 1 embed.add_field(name=f"Save {i + 1}", value=result.skeleton) if dc: embed.set_footer( text= f"{num_successes} Successes | {iterations - num_successes} Failures" ) else: result = roll(roll_str, inline=True) if dc: embed.set_footer( text="Success!" if result.total >= dc else "Failure!") embed.description = ( f"**DC {dc}**\n" if dc else '') + result.skeleton + ( '\n*' + phrase + '*' if phrase is not None else '') embeds.add_fields_from_args(embed, args.get('f')) if args.last('image') is not None: embed.set_thumbnail(url=args.last('image')) await ctx.send(embed=embed) try: await ctx.message.delete() except: pass
async def _rest(self, ctx, rest_type, *args): """ Runs a rest. :param ctx: The Context. :param character: The Character. :param rest_type: "long", "short", "all" :param args: a list of args. """ character: Character = await Character.from_ctx(ctx) old_hp = character.hp old_slots = { lvl: character.spellbook.get_slots(lvl) for lvl in range(1, 10) } embed = EmbedWithCharacter(character, name=False) if rest_type == 'long': reset = character.long_rest() embed.title = f"{character.name} took a Long Rest!" elif rest_type == 'short': reset = character.short_rest() embed.title = f"{character.name} took a Short Rest!" elif rest_type == 'all': reset = character.reset_all_consumables() embed.title = f"{character.name} reset all counters!" else: raise ValueError(f"Invalid rest type: {rest_type}") if '-h' in args: values = ', '.join( set(ctr.name for ctr, _ in reset) | {"Hit Points", "Death Saves", "Spell Slots"}) embed.add_field(name="Reset Values", value=values) else: # hp hp_delta = character.hp - old_hp hp_delta_str = "" if hp_delta: hp_delta_str = f" ({hp_delta:+})" embed.add_field(name="Hit Points", value=f"{character.hp_str()}{hp_delta_str}") # slots slots_out = [] slots_delta = { lvl: character.spellbook.get_slots(lvl) - old_slots[lvl] for lvl in range(1, 10) } for lvl in range(1, 10): if character.spellbook.get_max_slots(lvl): if slots_delta[lvl]: slots_out.append( f"{character.spellbook.slots_str(lvl)} ({slots_delta[lvl]:+})" ) else: slots_out.append(character.spellbook.slots_str(lvl)) if slots_out: embed.add_field(name="Spell Slots", value='\n'.join(slots_out)) # ccs displayed_counters = set() counters_out = [] for counter, delta in reset: if counter.name in displayed_counters: continue displayed_counters.add(counter.name) if delta: counters_out.append( f"{counter.name}: {str(counter)} ({delta:+})") else: counters_out.append(f"{counter.name}: {str(counter)}") if counters_out: embed.add_field(name="Reset Counters", value='\n'.join(counters_out)) await character.commit(ctx) await ctx.send(embed=embed)
async def customcounter(self, ctx, name=None, *, modifier=None): """Commands to implement custom counters. When called on its own, if modifier is supplied, increases the counter *name* by *modifier*. If modifier is not supplied, prints the value and metadata of the counter *name*.""" if name is None: return await ctx.invoke(self.bot.get_command("customcounter list")) character = await Character.from_ctx(ctx) sel = await character.select_consumable(ctx, name) if sel is None: return await ctx.send("Selection timed out or was cancelled.") name = sel[0] counter = sel[1] assert character is not None assert counter is not None if modifier is None: # display value counterDisplayEmbed = EmbedWithCharacter(character) val = self._get_cc_value(character, counter) counterDisplayEmbed.add_field(name=name, value=val) return await ctx.send(embed=counterDisplayEmbed) operator = None if ' ' in modifier: m = modifier.split(' ') operator = m[0] modifier = m[-1] try: modifier = int(modifier) except ValueError: return await ctx.send( f"Could not modify counter: {modifier} is not a number") resultEmbed = EmbedWithCharacter(character) if not operator or operator == 'mod': consValue = int(counter.get('value', 0)) newValue = consValue + modifier elif operator == 'set': newValue = modifier else: return await ctx.send("Invalid operator. Use mod or set.") try: character.set_consumable(name, newValue) await character.commit(ctx) _max = self._get_cc_max(character, counter) actualValue = int(character.get_consumable(name).get('value', 0)) if counter.get('type') == 'bubble': assert _max not in ('N/A', None) numEmpty = _max - counter.get('value', 0) filled = '\u25c9' * counter.get('value', 0) empty = '\u3007' * numEmpty out = f"{filled}{empty}" else: out = f"{counter.get('value', 0)}" if (not _max in (None, 'N/A')) and not counter.get('type') == 'bubble': resultEmbed.description = f"**__{name}__**\n{out}/{_max}" else: resultEmbed.description = f"**__{name}__**\n{out}" if newValue - actualValue: resultEmbed.description += f"\n({abs(newValue - actualValue)} overflow)" except CounterOutOfBounds: resultEmbed.description = f"Could not modify counter: new value out of bounds" try: await ctx.message.delete() except: pass await ctx.send(embed=resultEmbed)