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 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_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 game_spellslot(self, ctx, level: int = None, value: str = None): """Views or sets your remaining spell slots.""" if level is not None: try: assert 0 < level < 10 except AssertionError: return await ctx.send("Invalid spell level.") character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.set_footer(text="\u25c9 = Available / \u3007 = Used") if level is None and value is None: # show remaining embed.description = f"__**Remaining Spell Slots**__\n{character.spellbook.slots_str()}" elif value is None: embed.description = f"__**Remaining Level {level} Spell Slots**__\n" \ f"{character.spellbook.slots_str(level)}" else: try: if value.startswith(('+', '-')): value = character.spellbook.get_slots(level) + int(value) else: value = int(value) except ValueError: return await ctx.send(f"{value} is not a valid integer.") try: assert 0 <= value <= character.spellbook.get_max_slots(level) except AssertionError: raise CounterOutOfBounds() character.spellbook.set_slots(level, value) await character.commit(ctx) embed.description = f"__**Remaining Level {level} Spell Slots**__\n" \ f"{character.spellbook.slots_str(level)}" await ctx.send(embed=embed)
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 join(self, ctx, *, args: str = ''): """Adds the current active character to combat. A character must be loaded through the SheetManager module first. Args: adv/dis -b [conditional bonus] -phrase [flavor text] -p [init value] -h (same as !init add) --group (same as !init add)""" char: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(char, False) embed.colour = char.get_color() args = shlex.split(args) args = argparse(args) adv = args.adv(boolwise=True) b = args.join('b', '+') or None p = args.last('p', type_=int) phrase = args.join('phrase', '\n') or None group = args.last('group') if p is None: roll_str = char.skills.initiative.d20(base_adv=adv) if b: roll_str = f"{roll_str}+{b}" check_roll = roll(roll_str, inline=True) embed.title = '{} makes an Initiative check!'.format(char.name) embed.description = check_roll.skeleton + ('\n*' + phrase + '*' if phrase is not None else '') init = check_roll.total else: init = p embed.title = "{} already rolled initiative!".format(char.name) embed.description = "Placed at initiative `{}`.".format(init) controller = str(ctx.author.id) private = args.last('h', type_=bool) bonus = char.skills.initiative.value combat = await Combat.from_ctx(ctx) me = await PlayerCombatant.from_character(char.name, controller, init, bonus, char.ac, private, char.get_resists(), ctx, combat, char.upstream, str(ctx.author.id), char) if combat.get_combatant(char.name) is not None: await ctx.send("Combatant already exists.") return if group is None: combat.add_combatant(me) embed.set_footer(text="Added to combat!") else: grp = combat.get_group(group, create=init) grp.add_combatant(me) embed.set_footer(text=f"Joined group {grp.name}!") await combat.final() await ctx.send(embed=embed) await char.commit(ctx)
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 = EmbedWithCharacter(char, image=False) embed.set_image(url="attachment://image.png") await ctx.send(file=file, embed=embed) processed.close()
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 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: [name] 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) An italicized argument means the argument supports ephemeral arguments - e.g. `-b1` applies a bonus to one save. """ 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) embed = EmbedWithCharacter(char, name=False) args = await self.new_arg_stuff(args, ctx, char) checkutils.update_csetting_args(char, args) checkutils.run_save(skill, char, args, embed) if args.last('image') is not None: embed.set_thumbnail(url=args.last('image')) # send await ctx.send(embed=embed) await try_delete(ctx.message)
async def game_spellslot(self, ctx, level: int = None, value: str = None, *args): """ Views or sets your remaining spell slots. __Valid Arguments__ nopact - Modifies normal spell slots first instead of a Pact Magic slots, if applicable. """ if level is not None: try: assert 0 < level < 10 except AssertionError: return await ctx.send("Invalid spell level.") character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) if level is None and value is None: # show remaining embed.description = f"__**Remaining Spell Slots**__\n{character.spellbook.slots_str()}" elif value is None: embed.description = f"__**Remaining Level {level} Spell Slots**__\n" \ f"{character.spellbook.slots_str(level)}" else: old_slots = character.spellbook.get_slots(level) value = maybe_mod(value, old_slots) character.spellbook.set_slots(level, value, pact='nopact' not in args) await character.commit(ctx) embed.description = f"__**Remaining Level {level} Spell Slots**__\n" \ f"{character.spellbook.slots_str(level)} ({(value - old_slots):+})" # footer - pact vs non pact if character.spellbook.max_pact_slots is not None: embed.set_footer(text=f"{constants.FILLED_BUBBLE} = Available / {constants.EMPTY_BUBBLE} = Used\n" f"{constants.FILLED_BUBBLE_ALT} / {constants.EMPTY_BUBBLE_ALT} = Pact Slot") else: embed.set_footer(text=f"{constants.FILLED_BUBBLE} = Available / {constants.EMPTY_BUBBLE} = Used") await ctx.send(embed=embed)
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 check(self, ctx, check, *args): """Rolls a check for your current active character. __Valid Arguments__ *adv/dis* *-b [conditional bonus]* -phrase [flavor text] -title [title] *note: [name] and [cname] will be replaced automatically* -dc [dc] -mc [minimum roll] -rr [iterations] str/dex/con/int/wis/cha (different skill base; e.g. Strength (Intimidation)) An italicized argument means the argument supports ephemeral arguments - e.g. `-b1` applies a bonus to one check. """ char: Character = await Character.from_ctx(ctx) skill_key = await search_and_select(ctx, SKILL_NAMES, check, lambda s: s) embed = EmbedWithCharacter(char, False) skill = char.skills[skill_key] args = await self.new_arg_stuff(args, ctx, char) checkutils.update_csetting_args(char, args, skill) checkutils.run_check(skill_key, char, args, embed) if args.last('image') is not None: embed.set_thumbnail(url=args.last('image')) await ctx.send(embed=embed) await try_delete(ctx.message)
def embed_for_caster(caster): if isinstance(caster, Character): return EmbedWithCharacter(character=caster, name=False) embed = discord.Embed() embed.colour = random.randint(0, 0xffffff) if isinstance(caster, Monster): embed.set_thumbnail(url=caster.get_image_url()) return 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=character.hp_str()) embed.add_field(name="Spell Slots", value=character.slots_str()) for counter in character.consumables: embed.add_field(name=counter.name, value=counter.full_str()) await ctx.send(embed=embed)
async def attack(self, ctx, atk_name=None, *, args: str = ''): """Rolls an attack for the current active character. __Valid Arguments__ -t "<target>" - Sets targets for the attack. You can pass as many as needed. Will target combatants if channel is in initiative. -t "<target>|<args>" - Sets a target, and also allows for specific args to apply to them. (e.g, -t "OR1|hit" to force the attack against OR1 to hit) *adv/dis* *ea* (Elven Accuracy double advantage) -ac [target ac] -t [target] *-b* [to hit bonus] -criton [a number to crit on if rolled on or above] *-d* [damage bonus] *-c* [damage bonus on crit] -rr [times to reroll] *-mi* [minimum weapon dice roll] *-resist* [damage resistance] *-immune* [damage immunity] *-vuln* [damage vulnerability] *-neutral* [damage non-resistance] *hit* (automatically hits) *miss* (automatically misses) *crit* (automatically crit) *max* (deals max damage) -h (hides name and rolled values) -phrase [flavor text] -title [title] *note: [name] and [aname] will be replaced automatically* -thumb [url] -f "Field Title|Field Text" (see !embed) [user snippet] An italicized argument means the argument supports ephemeral arguments - e.g. `-d1` applies damage to the first hit, `-b1` applies a bonus to one attack, and so on.""" if atk_name is None: return await ctx.invoke(self.attack_list) char: Character = await Character.from_ctx(ctx) args = await self.new_arg_stuff(args, ctx, char) caster, targets, combat = await targetutils.maybe_combat( ctx, char, args) attack = await search_and_select(ctx, caster.attacks, atk_name, lambda a: a.name) embed = EmbedWithCharacter(char, name=False) await attackutils.run_attack(ctx, embed, args, caster, attack, targets, combat) await ctx.send(embed=embed) try: await ctx.message.delete() except: pass
async def attack(self, ctx, atk_name=None, *, args: str = ''): """Rolls an attack for the current active character. __Valid Arguments__ -t "<target>" - Sets targets for the attack. You can pass as many as needed. Will target combatants if channel is in initiative. -t "<target>|<args>" - Sets a target, and also allows for specific args to apply to them. (e.g, -t "OR1|hit" to force the attack against OR1 to hit) *adv/dis* - Advantage or Disadvantage *ea* - Elven Accuracy double advantage -ac <target ac> - overrides target AC *-b* <to hit bonus> - adds a bonus to hit -criton <num> - a number to crit on if rolled on or above *-d* <damage bonus> - adds a bonus to damage *-c* <damage bonus on crit> - adds a bonus to crit damage -rr <times> - number of times to roll the attack against each target *-mi <value>* - minimum value of each die on the damage roll *-resist* <damage resistance> *-immune* <damage immunity> *-vuln* <damage vulnerability> *-neutral* <damage type> - ignores this damage type in resistance calculations *-dtype <damage type>* - replaces all damage types with this damage type *-dtype <old>new>* - replaces all of one damage type with another (e.g. `-dtype fire>cold`) *hit* - automatically hits *miss* - automatically misses *crit* - automatically crits if hit *max* - deals max damage *magical* - makes the damage type magical -h - hides name and rolled values -phrase <text> - adds flavour text -title <title> - changes the result title *note: `[name]` and `[aname]` will be replaced automatically* -thumb <url> - adds flavour image -f "Field Title|Field Text" - see `!help embed` <user snippet> - see `!help snippet` An italicized argument means the argument supports ephemeral arguments - e.g. `-d1` applies damage to the first hit, `-b1` applies a bonus to one attack, and so on. """ if atk_name is None: return await self.attack_list(ctx) char: Character = await Character.from_ctx(ctx) args = await self.new_arg_stuff(args, ctx, char) caster, targets, combat = await targetutils.maybe_combat( ctx, char, args) attack = await search_and_select(ctx, caster.attacks, atk_name, lambda a: a.name) embed = EmbedWithCharacter(char, name=False) await attackutils.run_attack(ctx, embed, args, caster, attack, targets, combat) await ctx.send(embed=embed) await try_delete(ctx.message)
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 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)
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 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 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: 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)