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 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 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: 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 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)
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 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)
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_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)
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 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 desc(self, ctx): """Prints or edits a description of your currently active character.""" char = await Character.from_ctx(ctx) desc = char.character['stats'].get('description', 'No description available.') 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.get_name() embed.description = desc await ctx.send(embed=embed) try: await ctx.message.delete() except: pass
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 = await Character.from_ctx(ctx) character = char.character # if char.get_combat_id(): # return await ctx.send(f"This character is already in a combat. " # f"Please leave combat in <#{char.get_combat_id()}> first.\n" # f"If this seems like an error, please `!update` your character sheet.") # we just ignore this for now. # I'll figure out a better solution when I actually need it skills = character.get('skills') if skills is None: return await ctx.send('You must update your character sheet first.') skill = 'initiative' embed = EmbedWithCharacter(char, False) embed.colour = char.get_color() skill_effects = character.get('skill_effects', {}) args += ' ' + skill_effects.get(skill, '') # dicecloud v7 - autoadv args = shlex.split(args) args = argparse(args) adv = args.adv() b = args.join('b', '+') or None p = args.last('p', type_=int) phrase = args.join('phrase', '\n') or None if p is None: if b: bonus = '{:+}'.format(skills[skill]) + '+' + b check_roll = roll('1d20' + bonus, adv=adv, inline=True) else: bonus = '{:+}'.format(skills[skill]) check_roll = roll('1d20' + bonus, adv=adv, inline=True) embed.title = '{} makes an Initiative check!'.format(char.get_name()) embed.description = check_roll.skeleton + ('\n*' + phrase + '*' if phrase is not None else '') init = check_roll.total else: init = p bonus = 0 embed.title = "{} already rolled initiative!".format(char.get_name()) embed.description = "Placed at initiative `{}`.".format(init) group = args.last('group') controller = str(ctx.author.id) private = args.last('h', type_=bool) bonus = roll(bonus).total combat = await Combat.from_ctx(ctx) me = await PlayerCombatant.from_character(char.get_name(), controller, init, bonus, char.get_ac(), private, char.get_resists(), ctx, combat, char.id, str(ctx.author.id), char) if combat.get_combatant(char.get_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) char.join_combat(str(ctx.channel.id)) await char.commit(ctx)
async def save(self, ctx, skill, *, args: str = ''): """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, *shlex.split(args)) char = await Character.from_ctx(ctx) saves = char.get_saves() if not saves: return await ctx.send('You must update your character sheet first.') try: save = next(a for a in saves.keys() if skill.lower() == a.lower()) except StopIteration: try: save = next(a for a in saves.keys() if skill.lower() in a.lower()) except StopIteration: return await ctx.send('That\'s not a valid save.') embed = EmbedWithCharacter(char, name=False) skill_effects = char.get_skill_effects() args += ' ' + skill_effects.get(save, '') # dicecloud v11 - autoadv args = await self.new_arg_stuff(args, ctx, char) adv = args.adv() 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 = format_d20(adv, char.get_setting('reroll')) if b is not None: roll_str = formatted_d20 + '{:+}'.format(saves[save]) + '+' + b else: roll_str = formatted_d20 + '{:+}'.format(saves[save]) embed.title = args.last('title', '') \ .replace('[charname]', char.get_name()) \ .replace('[sname]', camel_to_title(save)) \ or '{} makes {}!'.format(char.get_name(), a_or_an(camel_to_title(save))) 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, adv=adv, 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} Failues") else: result = roll(roll_str, adv=adv, 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 check(self, ctx, check, *, args: str = ''): """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 = await Character.from_ctx(ctx) skills = char.get_skills() if not skills: return await ctx.send('You must update your character sheet first.') try: skill = next(a for a in skills.keys() if check.lower() == a.lower())#this checks for the skill exactly except StopIteration: try: skill = next(a for a in skills.keys() if check.lower() in a.lower())#this checks for the partial name of the skill except StopIteration: try: # Probably will be fairly slow, but whatever skill = next(SKILL_ALIASES[alias] for alias in SKILL_ALIASES.keys() if check.lower() == alias.lower())#go through our alias names except StopIteration: return await ctx.send('That\'s not a valid check.') embed = EmbedWithCharacter(char, False) skill_effects = char.get_skill_effects() args += ' ' + skill_effects.get(skill, '') # dicecloud v7 - autoadv args = await self.new_arg_stuff(args, ctx, char) adv = args.adv() 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 = format_d20(adv, char.get_setting('reroll')) mc = args.last('mc', None) if mc: formatted_d20 = f"{formatted_d20}mi{mc}" mod = skills[skill] skill_name = skill if any(args.last(s, type_=bool) for s in ("str", "dex", "con", "int", "wis", "cha")): base = next(s for s in ("str", "dex", "con", "int", "wis", "cha") if args.last(s, type_=bool)) mod = mod - char.get_mod(SKILL_MAP[skill]) + char.get_mod(base) skill_name = f"{verbose_stat(base)} ({skill})" skill_name = camel_to_title(skill_name) default_title = '{} makes {} check!'.format(char.get_name(), a_or_an(skill_name)) if b is not None: roll_str = formatted_d20 + '{:+}'.format(mod) + '+' + b else: roll_str = formatted_d20 + '{:+}'.format(mod) embed.title = args.last('title', '') \ .replace('[charname]', char.get_name()) \ .replace('[cname]', skill_name) \ or default_title 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, adv=adv, 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} Failues") else: result = roll(roll_str, adv=adv, 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 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)
async def _old_cast(self, ctx, spell_name, args): spell = getSpell(spell_name) self.bot.rdb.incr('spells_looked_up_life') if spell is None: return await self.bot.say("Spell not found.", delete_after=15) if spell.get('source') == "UAMystic": return await self.bot.say("Mystic talents are not supported.") char = await Character.from_ctx(ctx) args = await scripting.parse_snippets(args, ctx) args = await char.parse_cvars(args, ctx) args = shlex.split(args) args = argparse(args) can_cast = True spell_level = int(spell.get('level', 0)) cast_level = args.last('l', spell_level, int) if not spell_level <= cast_level <= 9: return await self.bot.say("Invalid spell level.") # make sure we can cast it if not char.get_remaining_slots( cast_level) > 0 and spell_name in char.get_spell_list(): can_cast = False if args.last('i', type_=bool): can_cast = True if not can_cast: embed = EmbedWithCharacter(char) embed.title = "Cannot cast spell!" embed.description = "Not enough spell slots remaining, or spell not in known spell list!\n" \ "Use `!game longrest` to restore all spell slots, or pass `-i` to ignore restrictions." if cast_level > 0: embed.add_field(name="Spell Slots", value=char.get_remaining_slots_str(cast_level)) return await self.bot.say(embed=embed) if len(args) == 0: rolls = spell.get('roll', None) if isinstance(rolls, list): rolls = '\n'.join(rolls) \ .replace('SPELL', str(char.get_spell_ab() - char.get_prof_bonus())) \ .replace('PROF', str(char.get_prof_bonus())) rolls = rolls.split('\n') out = "**{} casts {}:** ".format( char.get_name(), spell['name']) + '\n'.join( roll(r, inline=True).skeleton for r in rolls) elif rolls is not None: rolls = rolls \ .replace('SPELL', str(char.get_spell_ab() - char.get_prof_bonus())) \ .replace('PROF', str(char.get_prof_bonus())) out = "**{} casts {}:** ".format( char.get_name(), spell['name']) + roll( rolls, inline=True).skeleton else: out = "**{} casts {}!** ".format(char.get_name(), spell['name']) else: rolls = args.get('r') roll_results = "" for r in rolls: res = roll(r, inline=True) if res.total is not None: roll_results += res.result + '\n' else: roll_results += "**Effect:** " + r out = "**{} casts {}:**\n".format(char.get_name(), spell['name']) + roll_results if not args.last('i', type_=bool): char.use_slot(cast_level) if cast_level > 0: out += f"\n**Remaining Spell Slots**: {char.get_remaining_slots_str(cast_level)}" out = "Spell not supported by new cast, falling back to old cast.\n" + out await char.commit(ctx) # make sure we save changes await self.bot.say(out) spell_cmd = self.bot.get_command('spell') if spell_cmd is None: return await self.bot.say("Lookup cog not loaded.") await ctx.invoke(spell_cmd, name=spell['name'])
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 cast(self, ctx, spell_name, *, args=''): """Casts a spell. __Valid Arguments:__ -i - Ignores Spellbook restrictions, for demonstrations or rituals. -l [level] - Specifies the level to cast the spell at. **__Save Spells__** -dc [Save DC] - Default: Pulls a cvar called `dc`. -save [Save type] - Default: The spell's default save. -d [damage] - adds additional damage. **__Attack Spells__** See `!a`. **__All Spells__** -phrase [phrase] - adds flavor text.""" try: await self.bot.delete_message(ctx.message) except: pass char = None if not '-i' in args: char = await Character.from_ctx(ctx) spell_name = await searchCharacterSpellName(spell_name, ctx, char) else: spell_name = await searchSpellNameFull(spell_name, ctx) if spell_name is None: return spell = strict_search(c.autospells, 'name', spell_name) if spell is None: return await self._old_cast(ctx, spell_name, args) # fall back to old cast if not char: char = await Character.from_ctx(ctx) args = await scripting.parse_snippets(args, ctx) args = await char.parse_cvars(args, ctx) args = shlex.split(args) args = argparse(args) can_cast = True spell_level = int(spell.get('level', 0)) cast_level = args.last('l', spell_level, int) if not spell_level <= cast_level <= 9: return await self.bot.say("Invalid spell level.") # make sure we can cast it if not char.get_remaining_slots( cast_level) > 0 and spell_name in char.get_spell_list(): can_cast = False if args.last('i', type_=bool): can_cast = True if not can_cast: embed = EmbedWithCharacter(char) embed.title = "Cannot cast spell!" embed.description = "Not enough spell slots remaining, or spell not in known spell list!\n" \ "Use `!game longrest` to restore all spell slots, or pass `-i` to ignore restrictions." if cast_level > 0: embed.add_field(name="Spell Slots", value=char.get_remaining_slots_str(cast_level)) return await self.bot.say(embed=embed) args['l'] = [cast_level] args['name'] = [char.get_name()] args['dc'] = [args.get('dc', [char.get_save_dc()])[-1]] args['casterlevel'] = [char.get_level()] args['crittype'] = [char.get_setting('crittype', 'default')] args['ab'] = [char.get_spell_ab()] args['SPELL'] = [ str( char.evaluate_cvar("SPELL") or (char.get_spell_ab() - char.get_prof_bonus())) ] result = sheet_cast(spell, args, EmbedWithCharacter(char, name=False)) embed = result['embed'] _fields = args.get('f') if type(_fields) == list: for f in _fields: title = f.split('|')[0] if '|' in f else '\u200b' value = "|".join(f.split('|')[1:]) if '|' in f else f embed.add_field(name=title, value=value) if not args.last('i', type_=bool): char.use_slot(cast_level) if cast_level > 0: embed.add_field(name="Spell Slots", value=char.get_remaining_slots_str(cast_level)) await char.commit(ctx) # make sure we save changes await self.bot.say(embed=embed)