async def spellbook_addall(self, ctx, _class, level: int, spell_list=None): """Adds all spells of a given level from a given class list to the spellbook override. Requires live sheet. If `spell_list` is passed, will add these spells to the list named so in Dicecloud.""" character = Character.from_ctx(ctx) if not character.live: return await self.bot.say( "This command requires a live Dicecloud sheet. To set up, share your Dicecloud " "sheet with `avrae` with edit permissions, then `!update`.") if not 0 <= level < 10: return await self.bot.say("Invalid spell level.") class_spells = [ sp for sp in c.spells if _class.lower() in [cl.lower() for cl in sp['classes'].split(', ') if not '(' in cl] ] if len(class_spells) == 0: return await self.bot.say("No spells for that class found.") level_spells = [s for s in class_spells if str(level) == s['level']] try: await DicecloudClient.getInstance().sync_add_mass_spells( character, [dicecloud_parse(s) for s in level_spells], spell_list) character.commit(ctx) except MeteorClient.MeteorClientException: return await self.bot.say( "Error: Failed to connect to Dicecloud. The site may be down.") await self.bot.say( f"{len(level_spells)} spells added to {character.get_name()}'s spell list on Dicecloud." )
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 self.bot.say("Invalid spell level.") character = 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.get_remaining_slots_str()}" elif value is None: embed.description = f"__**Remaining Level {level} Spell Slots**__\n{character.get_remaining_slots_str(level)}" else: try: if value.startswith(('+', '-')): value = character.get_remaining_slots(level) + int(value) else: value = int(value) except ValueError: return await self.bot.say(f"{value} is not a valid integer.") try: assert 0 <= value <= character.get_max_spellslots(level) except AssertionError: raise CounterOutOfBounds() character.set_remaining_slots(level, value).commit(ctx) embed.description = f"__**Remaining Level {level} Spell Slots**__\n{character.get_remaining_slots_str(level)}" await self.bot.say(embed=embed)
async def spellbook(self, ctx): """Commands to display a character's known spells and metadata.""" character = 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_hp(self, ctx, operator='', *, hp=''): """Modifies the HP of a the current active character. Synchronizes live with Dicecloud. If operator is not passed, assumes `mod`. Operators: `mod`, `set`.""" character = Character.from_ctx(ctx) if not operator == '': hp_roll = roll(hp, inline=True, show_blurbs=False) if 'mod' in operator.lower(): character.modify_hp(hp_roll.total) elif 'set' in operator.lower(): character.set_hp(hp_roll.total, True) elif 'max' in operator.lower() and not hp: character.set_hp(character.get_max_hp(), True) elif hp == '': hp_roll = roll(operator, inline=True, show_blurbs=False) hp = operator character.modify_hp(hp_roll.total) else: await self.bot.say("Incorrect operator. Use mod or set.") return character.commit(ctx) out = "{}: {}".format(character.get_name(), character.get_hp_str()) if 'd' in hp: out += '\n' + hp_roll.skeleton else: out = "{}: {}".format(character.get_name(), character.get_hp_str()) await self.bot.say(out)
async def customcounter_reset(self, ctx, *args): """Resets custom counters, hp, death saves, and spell slots. Will reset all if name is not passed, otherwise the specific passed one. A counter can only be reset if it has a maximum value. Reset hierarchy: short < long < default < none __Valid Arguments__ -h - Hides the character summary output.""" character = Character.from_ctx(ctx) try: name = args[0] except IndexError: name = None else: if name == '-h': name = None if name: try: character.reset_consumable(name).commit(ctx) except ConsumableException as e: return await self.bot.say(f"Counter could not be reset: {e}") else: return await self.bot.say( f"Counter reset to {character.get_consumable(name)['value']}." ) else: reset_consumables = character.reset_all_consumables() character.commit(ctx) await self.bot.say( f"Reset counters: {', '.join(set(reset_consumables)) or 'none'}" ) if not '-h' in args: await ctx.invoke(self.game_status)
async def spellbook_add(self, ctx, *, spell_name): """Adds a spell to the spellbook override. If character is live, will add to sheet as well.""" result = searchSpell(spell_name) if result is None: return await self.bot.say('Spell not found.') strict = result[1] results = result[0] if strict: result = results else: if len(results) == 1: result = results[0] else: result = await get_selection(ctx, [(r, r) for r in results]) if result is None: return await self.bot.say( 'Selection timed out or was cancelled.') spell = getSpell(result) character = Character.from_ctx(ctx) if character.live: try: await DicecloudClient.getInstance().sync_add_spell( character, dicecloud_parse(spell)) except MeteorClient.MeteorClientException: return await self.bot.say( "Error: Failed to connect to Dicecloud. The site may be down." ) character.add_known_spell(spell).commit(ctx) live = "Spell added to Dicecloud!" if character.live else '' await self.bot.say( f"{spell['name']} added to known spell list!\n{live}")
async def customcounter_summary(self, ctx): """Prints a summary of all custom counters.""" character = 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 self.bot.say(embed=embed)
async def customcounter_delete(self, ctx, name): """Deletes a custom counter.""" character = Character.from_ctx(ctx) try: character.delete_consumable(name).commit(ctx) except ConsumableNotFound: return await self.bot.say("Counter not found. Make sure you're using the full name, case-sensitive.") await self.bot.say(f"Deleted counter {name}.")
async def game_status(self, ctx): """Prints the status of the current active character.""" character = 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 self.bot.say(embed=embed)
async def game_deathsave_reset(self, ctx): """Resets all death saves.""" character = Character.from_ctx(ctx) character.reset_death_saves() embed = EmbedWithCharacter(character) embed.title = f'{character.get_name()} reset Death Saves!' character.commit(ctx) embed.add_field(name="Death Saves", value=character.get_ds_str()) 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.from_ctx(ctx) args = parse_args_3(args) adv = 0 if args.get('adv', [False])[-1] and args.get('dis', [False])[-1] else \ 1 if args.get('adv', [False])[-1] else \ -1 if args.get('dis', [False])[-1] else 0 b = '+'.join(args.get('b', [])) phrase = '\n'.join(args.get('phrase', [])) if b: save_roll = roll('1d20+' + b, adv=adv, inline=True) else: save_roll = roll('1d20', adv=adv, inline=True) embed = discord.Embed() embed.title = args.get( 'title', '').replace('[charname]', character.get_name()).replace( '[sname]', 'Death') or '{} makes {}!'.format( character.get_name(), "a Death Save") embed.colour = character.get_color() death_phrase = '' if save_roll.crit == 1: character.set_hp(1) death_phrase = f"{character.get_name()} is UP with 1 HP!" elif save_roll.crit == 2: if character.add_failed_ds(): death_phrase = f"{character.get_name()} is DEAD!" else: if character.add_failed_ds(): death_phrase = f"{character.get_name()} is DEAD!" elif save_roll.total >= 10: if character.add_successful_ds(): death_phrase = f"{character.get_name()} is STABLE!" else: if character.add_failed_ds(): death_phrase = f"{character.get_name()} is DEAD!" character.commit(ctx) embed.description = save_roll.skeleton + ('\n*' + phrase + '*' if phrase else '') if death_phrase: embed.set_footer(text=death_phrase) embed.add_field(name="Death Saves", value=character.get_ds_str()) if args.get('image') is not None: embed.set_thumbnail(url=args.get('image')) await self.bot.say(embed=embed)
async def game_longrest(self, ctx, *args): """Performs a long rest, resetting applicable counters. __Valid Arguments__ -h - Hides the character summary output.""" character = Character.from_ctx(ctx) reset = character.long_rest() embed = EmbedWithCharacter(character, name=False) embed.title = f"{character.get_name()} took a Long 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 spellbook_remove(self, ctx, *, spell_name): """ Removes a spell from the spellbook override. Must type in full name. """ character = Character.from_ctx(ctx) if character.live: return await self.bot.say("Just delete the spell from your character sheet!") spell = character.remove_known_spell(spell_name) if spell: character.commit(ctx) await self.bot.say(f"{spell} removed from spellbook override.") else: await self.bot.say( f"Spell not in spellbook override. Make sure you typed the full spell name. " f"To remove a spell on your sheet, just delete it from your sheet.")
async def game_thp(self, ctx, thp: int = None): """Modifies the temp HP of a the current active character. If positive, assumes set; if negative, assumes mod.""" character = Character.from_ctx(ctx) if thp is not None: if thp >= 0: character.set_temp_hp(thp) else: character.set_temp_hp(character.get_temp_hp() + thp) character.commit(ctx) out = "{}: {}".format(character.get_name(), character.get_hp_str()) await self.bot.say(out)
async def game_deathsave_fail(self, ctx): """Adds a failed death save.""" character = 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!" 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 self.bot.say(embed=embed)
async def handle_aliases(self, message): if message.content.startswith(self.bot.prefix): alias = self.bot.prefix.join( message.content.split(self.bot.prefix)[1:]).split(' ')[0] if not message.channel.is_private: command = self.aliases.get(message.author.id, {}).get(alias) or \ self.serv_aliases.get(message.server.id, {}).get(alias) else: command = self.aliases.get(message.author.id, {}).get(alias) if command: try: message.content = self.handle_alias_arguments( command, message) except UserInputError as e: return await self.bot.send_message(message.channel, f"Invalid input: {e}") # message.content = message.content.replace(alias, command, 1) ctx = Context(self.bot, message) char = None try: char = Character.from_ctx(ctx) except NoCharacter: pass try: if char: message.content = await char.parse_cvars( message.content, ctx) else: message.content = await self.parse_no_char( message.content, ctx) except EvaluationError as err: e = err.original if not isinstance(e, AvraeException): tb = f"```py\n{''.join(traceback.format_exception(type(e), e, e.__traceback__, limit=0, chain=False))}\n```" try: await self.bot.send_message(message.author, tb) except Exception: pass return await self.bot.send_message(message.channel, err) except Exception as e: return await self.bot.send_message(message.channel, e) await self.bot.process_commands(message)
async def customcounter_create(self, ctx, name, *args): """Creates a new custom counter. __Valid Arguments__ `-reset <short|long|none>` - Counter will reset to max on a short/long rest, or not ever when "none". Default - will reset on a call of `!cc reset`. `-max <max value>` - The maximum value of the counter. `-min <min value>` - The minimum value of the counter. `-type <bubble|default>` - Whether the counter displays bubbles to show remaining uses or numbers. Default - numbers.""" character = Character.from_ctx(ctx) args = parse_args_3(args) _reset = args.get('reset', [None])[-1] _max = args.get('max', [None])[-1] _min = args.get('min', [None])[-1] _type = args.get('type', [None])[-1] try: character.create_consumable(name, maxValue=_max, minValue=_min, reset=_reset, displayType=_type).commit(ctx) except InvalidArgument as e: return await self.bot.say(f"Failed to create counter: {e}") else: await self.bot.say(f"Custom counter created.")
async def test(self, ctx, *, str): """Parses `str` as if it were in an alias, for testing.""" char = Character.from_ctx(ctx) parsed = await char.parse_cvars(str, ctx) parsed = clean_content(parsed, ctx) await self.bot.say(f"{ctx.message.author.display_name}: {parsed}")
async def _old_cast(self, ctx, spell_name, args): spell = getSpell(spell_name) self.bot.db.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 = Character.from_ctx(ctx) args = parse_snippets(args, ctx) args = await char.parse_cvars(args, ctx) args = shlex.split(args) args = parse_args_3(args) can_cast = True spell_level = int(spell.get('level', 0)) try: cast_level = int(args.get('l', [spell_level])[-1]) assert spell_level <= cast_level <= 9 except (AssertionError, ValueError): return await self.bot.say("Invalid spell level.") # make sure we can cast it try: assert char.get_remaining_slots(cast_level) > 0 assert spell_name in char.get_spell_list() except AssertionError: can_cast = False if args.get('i'): 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): active_character = self.bot.db.not_json_get( 'active_characters', {}).get(ctx.message.author.id) # get user's active if active_character is not None: 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( ctx.message.author.mention, spell['name']) + '\n'.join( roll(r, inline=True).skeleton for r in rolls) elif rolls is not None: active_character = self.bot.db.not_json_get( 'active_characters', {}).get(ctx.message.author.id) # get user's active if active_character 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( ctx.message.author.mention, spell['name']) + roll( rolls, inline=True).skeleton else: out = "**{} casts {}!** ".format(ctx.message.author.mention, 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(ctx.message.author.mention, spell['name']) + roll_results if not args.get('i'): 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 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 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.from_ctx(ctx) sel = await character.select_consumable(ctx, name) if sel is None: return await self.bot.say("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 self.bot.say(embed=counterDisplayEmbed) operator = None if ' ' in modifier: m = modifier.split(' ') operator = m[0] modifier = m[-1] try: modifier = int(modifier) except ValueError: return await self.bot.say( 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 self.bot.say("Invalid operator. Use mod or set.") try: character.set_consumable(name, newValue).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 self.bot.delete_message(ctx.message) except: pass await self.bot.say(embed=resultEmbed)
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 = 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 = Character.from_ctx(ctx) args = parse_snippets(args, ctx) args = await char.parse_cvars(args, ctx) args = shlex.split(args) args = parse_args_3(args) can_cast = True spell_level = int(spell.get('level', 0)) try: cast_level = int(args.get('l', [spell_level])[-1]) assert spell_level <= cast_level <= 9 except (AssertionError, ValueError): return await self.bot.say("Invalid spell level.") # make sure we can cast it try: assert char.get_remaining_slots(cast_level) > 0 assert spell_name in char.get_spell_list() except AssertionError: can_cast = False if args.get('i'): 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.get('i'): char.use_slot(cast_level) if cast_level > 0: embed.add_field(name="Spell Slots", value=char.get_remaining_slots_str(cast_level)) char.commit(ctx) # make sure we save changes await self.bot.say(embed=embed)