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 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)
async def playertoken(self, ctx): """Generates and sends a token for use on VTTs.""" char: Character = await Character.from_ctx(ctx) color_override = char.get_setting('color') if not char.image: return await ctx.send("This character has no image.") try: processed = await generate_token(char.image, color_override) 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)
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()) 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 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 = EmbedWithCharacter(char, name=False) embed.title = char.name embed.description = desc await ctx.send(embed=embed) await try_delete(ctx.message)
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) await try_delete(ctx.message)
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 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) # reliable talent (#654) rt = char.get_setting('talent', 0) and skill.prof >= 1 args['mc'] = args.get('mc') or 10 * rt # halfling luck args['ro'] = char.get_setting('reroll') 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)
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) await try_delete(ctx.message) await ctx.send(embed=result_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) # halfling luck args['ro'] = char.get_setting('reroll') 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)
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.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 _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 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.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.source == '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)