async def run_attack(ctx, embed, args, caster, attack, targets, combat): """Runs an attack: adds title, handles -f and -thumb args, commits combat, runs automation, edits embed.""" if not args.last('h', type_=bool): name = caster.get_title_name() else: name = "An unknown creature" if not attack.proper: attack_name = a_or_an(attack.name) else: attack_name = attack.name verb = attack.verb or "attacks with" if args.last('title') is not None: embed.title = args.last('title') \ .replace('[name]', name) \ .replace('[aname]', attack_name) else: embed.title = f'{name} {verb} {attack_name}!' await attack.automation.run(ctx, embed, caster, targets, args, combat=combat, title=embed.title) if combat: await combat.final() embeds.add_fields_from_args(embed, args.get('f')) if 'thumb' in args: embed.set_thumbnail(url=args.last('thumb'))
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. noconc - Ignores concentration requirements. -h - Hides rolled values. **__Save Spells__** -dc <Save DC> - Overrides the spell save DC. -save <Save type> - Overrides the spell save type. -d <damage> - Adds additional damage. pass - Target automatically succeeds save. fail - Target automatically fails save. adv/dis - Target makes save at advantage/disadvantage. **__Attack Spells__** See `!a`. **__All Spells__** -phrase <phrase> - adds flavor text. -title <title> - changes the title of the cast. Replaces [sname] with spell name. -thumb <url> - adds an image to the cast. -dur <duration> - changes the duration of any effect applied by the spell. -mod <spellcasting mod> - sets the value of the spellcasting ability modifier. int/wis/cha - different skill base for DC/AB (will not account for extra bonuses) """ try: await ctx.message.delete() except: pass char: Character = await Character.from_ctx(ctx) args = await helpers.parse_snippets(args, ctx) args = await char.parse_cvars(args, ctx) args = argparse(args) if not args.last('i', type_=bool): spell = await select_spell_full( ctx, spell_name, list_filter=lambda s: s.name in char.spellbook) else: spell = await select_spell_full(ctx, spell_name) caster, targets, combat = await targetutils.maybe_combat( ctx, char, args) result = await spell.cast(ctx, caster, targets, args, combat=combat) embed = result['embed'] embed.colour = char.get_color() embed.set_thumbnail(url=char.image) add_fields_from_args(embed, args.get('f')) if 'thumb' in args: embed.set_thumbnail(url=args.last('thumb')) # save changes: combat state, spell slot usage await char.commit(ctx) if combat: await combat.final() await ctx.send(embed=embed)
async def _run_common(ctx, embed, args, caster, action, targets, combat): """ Common automation runner for attacks/actions :type ctx: discord.ext.commands.Context :type embed: discord.Embed :type args: utils.argparser.ParsedArguments :type caster: cogs5e.models.sheet.statblock.StatBlock :type action: cogs5e.models.sheet.attack.Attack or cogs5e.models.sheet.action.Action :type targets: list of str or list of cogs5e.models.sheet.statblock.StatBlock :type combat: None or cogs5e.models.initiative.Combat :rtype: cogs5e.models.automation.AutomationResult """ result = await action.automation.run(ctx, embed, caster, targets, args, combat=combat, title=embed.title) if combat: await combat.final() # commit character only if we have not already committed it via combat final if result.caster_needs_commit and hasattr( caster, 'commit') and not (combat and caster in combat.get_combatants()): await caster.commit(ctx) embeds.add_fields_from_args(embed, args.get('f')) if 'thumb' in args: embed.set_thumbnail(url=maybe_http_url(args.last('thumb', ''))) return result
async def monster_atk(self, ctx, monster_name, atk_name=None, *, args=''): """Rolls a monster's attack. __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 -ac [target ac] -b [to hit bonus] -d [damage bonus] -d# [applies damage to the first # hits] -rr [times to reroll] -t [target] -phrase [flavor text] crit (automatically crit) -h (hides monster name, image, and rolled values) """ if atk_name is None or atk_name == 'list': return await ctx.invoke(self.monster_atk_list, monster_name) await try_delete(ctx.message) monster = await select_monster_full(ctx, monster_name) attacks = monster.attacks monster_name = monster.get_title_name() attack = await search_and_select(ctx, attacks, atk_name, lambda a: a.name) args = await helpers.parse_snippets(args, ctx) args = argparse(args) if not args.last('h', type_=bool): name = monster_name image = args.last('thumb') or monster.get_image_url() else: name = "An unknown creature" image = None embed = discord.Embed() if args.last('title') is not None: embed.title = args.last('title') \ .replace('[name]', name) \ .replace('[aname]', attack.name) else: embed.title = '{} attacks with {}!'.format(name, a_or_an(attack.name)) if image: embed.set_thumbnail(url=image) caster, targets, combat = await targetutils.maybe_combat(ctx, monster, args) await attack.automation.run(ctx, embed, caster, targets, args, combat=combat, title=embed.title) if combat: await combat.final() _fields = args.get('f') embeds.add_fields_from_args(embed, _fields) embed.colour = random.randint(0, 0xffffff) if monster.source == 'homebrew': embeds.add_homebrew_footer(embed) await ctx.send(embed=embed)
async def _cast(self, ctx, combatant_name, spell_name, args): args = await scripting.parse_snippets(args, ctx) combat = await Combat.from_ctx(ctx) if combatant_name is None: combatant = combat.current_combatant if combatant is None: return await ctx.send( f"You must start combat with `{ctx.prefix}init next` first." ) else: try: combatant = await combat.select_combatant( combatant_name, "Select the caster.") if combatant is None: return await ctx.send("Combatant not found.") except SelectionException: return await ctx.send("Combatant not found.") if isinstance(combatant, CombatantGroup): return await ctx.send("Groups cannot cast spells.") is_character = isinstance(combatant, PlayerCombatant) if is_character and combatant.character_owner == str(ctx.author.id): args = await combatant.character.parse_cvars(args, ctx) args = shlex.split(args) args = argparse(args) if not args.last('i', type_=bool): spell = await select_spell_full( ctx, spell_name, list_filter=lambda s: s.name.lower( ) in combatant.spellcasting.lower_spells) else: spell = await select_spell_full(ctx, spell_name) targets = [] for i, t in enumerate(args.get('t')): target = await combat.select_combatant(t, f"Select target #{i + 1}.", select_group=True) if isinstance(target, CombatantGroup): targets.extend(target.get_combatants()) else: targets.append(target) result = await spell.cast(ctx, combatant, targets, args, combat=combat) embed = result['embed'] embed.colour = random.randint( 0, 0xffffff) if not is_character else combatant.character.get_color() add_fields_from_args(embed, args.get('f')) await ctx.send(embed=embed) await combat.final()
async def run_attack(ctx, embed, args, caster, attack, targets, combat): """ Runs an attack: adds title, handles -f and -thumb args, commits combat, runs automation, edits embed. :type ctx: discord.ext.commands.Context :type embed: discord.Embed :type args: utils.argparser.ParsedArguments :type caster: cogs5e.models.sheet.statblock.StatBlock :type attack: cogs5e.models.sheet.attack.Attack :type targets: list of str or list of cogs5e.models.sheet.statblock.StatBlock :type combat: None or cogs5e.models.initiative.Combat :rtype: cogs5e.models.automation.AutomationResult """ if not args.last('h', type_=bool): name = caster.get_title_name() else: name = "An unknown creature" if not attack.proper: attack_name = a_or_an(attack.name) else: attack_name = attack.name verb = attack.verb or "attacks with" if args.last('title') is not None: embed.title = args.last('title') \ .replace('[name]', name) \ .replace('[aname]', attack_name) else: embed.title = f'{name} {verb} {attack_name}!' # arg overrides (#1163) arg_defaults = { 'criton': attack.criton, 'phrase': attack.phrase, 'thumb': attack.thumb, 'c': attack.extra_crit_damage } args.update_nx(arg_defaults) result = await attack.automation.run(ctx, embed, caster, targets, args, combat=combat, title=embed.title) if combat: await combat.final() embeds.add_fields_from_args(embed, args.get('f')) if 'thumb' in args: embed.set_thumbnail(url=args.last('thumb')) return result
async def monster_atk(self, ctx, monster_name, atk_name='list', *, args=''): """Rolls a monster's attack. Attack name can be "list" for a list of all of the monster's attacks. Valid Arguments: adv/dis -ac [target ac] -b [to hit bonus] -d [damage bonus] -d# [applies damage to the first # hits] -rr [times to reroll] -t [target] -phrase [flavor text] crit (automatically crit)""" try: await self.bot.delete_message(ctx.message) except: pass monster = await select_monster_full(ctx, monster_name) self.bot.rdb.incr('monsters_looked_up_life') attacks = monster.attacks monster_name = monster.get_title_name() if atk_name == 'list': attacks_string = '\n'.join( "**{0}:** +{1} To Hit, {2} damage.".format( a['name'], a['attackBonus'], a['damage'] or 'no') for a in attacks) return await self.bot.say("{}'s attacks:\n{}".format( monster_name, attacks_string)) attack = fuzzy_search(attacks, 'name', atk_name) if attack is None: return await self.bot.say("No attack with that name found.", delete_after=15) args = shlex.split(args) args = argparse(args) args['name'] = [monster_name] args['image'] = args.get('image') or [monster.get_image_url()] attack['details'] = attack.get('desc') or attack.get('details') result = sheet_attack(attack, args) embed = result['embed'] embed.colour = random.randint(0, 0xffffff) embeds.add_fields_from_args(embed, args.get('f')) if monster.source == 'homebrew': embed.set_footer(text="Homebrew content.", icon_url="https://avrae.io/static/homebrew.png") await self.bot.say(embed=embed)
async def monster_atk(self, ctx, monster_name, atk_name='list', *, args=''): """Rolls a monster's attack. Attack name can be "list" for a list of all of the monster's attacks. __Valid Arguments__ adv/dis -ac [target ac] -b [to hit bonus] -d [damage bonus] -d# [applies damage to the first # hits] -rr [times to reroll] -t [target] -phrase [flavor text] crit (automatically crit) -h (hides monster name, image, and attack details)""" try: await ctx.message.delete() except: pass monster = await select_monster_full(ctx, monster_name) self.bot.rdb.incr('monsters_looked_up_life') attacks = monster.attacks monster_name = monster.get_title_name() if atk_name == 'list': attacks_string = '\n'.join("**{0}:** +{1} To Hit, {2} damage.".format(a['name'], a['attackBonus'], a['damage'] or 'no') for a in attacks) return await ctx.send("{}'s attacks:\n{}".format(monster_name, attacks_string)) attack = await search_and_select(ctx, attacks, atk_name, lambda a: a['name']) args = await scripting.parse_snippets(args, ctx) args = argparse(args) if not args.last('h', type_=bool): args['name'] = monster_name args['image'] = args.get('image') or monster.get_image_url() else: args['name'] = "An unknown creature" attack['details'] = attack.get('desc') or attack.get('details') result = sheet_attack(attack, args) embed = result['embed'] embed.colour = random.randint(0, 0xffffff) embeds.add_fields_from_args(embed, args.get('f')) if monster.source == 'homebrew': embed.set_footer(text="Homebrew content.", icon_url="https://avrae.io/assets/img/homebrew.png") if args.last('h', type_=bool): try: await ctx.author.send(embed=result['full_embed']) except: pass await ctx.send(embed=embed)
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. noconc - Ignores concentration requirements. **__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. -title [title] - changes the title of the cast. Replaces [sname] with spell name. -dur [duration] - changes the duration of any effect applied by the spell. -mod [spellcasting mod] - sets the value of the spellcasting ability modifier. int/wis/cha - different skill base for DC/AB (will not account for extra bonuses) """ try: await ctx.message.delete() except: pass char: Character = await Character.from_ctx(ctx) args = await scripting.parse_snippets(args, ctx) args = await char.parse_cvars(args, ctx) args = argparse(args) if not args.last('i', type_=bool): spell = await select_spell_full(ctx, spell_name, list_filter=lambda s: s.name in char.spellbook) else: spell = await select_spell_full(ctx, spell_name) caster, targets, combat = await targetutils.maybe_combat(ctx, char, args.get('t')) result = await spell.cast(ctx, caster, targets, args, combat=combat) embed = result['embed'] embed.colour = char.get_color() embed.set_thumbnail(url=char.image) add_fields_from_args(embed, args.get('f')) # save changes: spell slot usage if combat: await combat.final() else: await char.commit(ctx) await ctx.send(embed=embed)
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. -title [title] - changes the title of the cast. Replaces [sname] with spell name. -dur [duration] - changes the duration of any effect applied by the spell. int/wis/cha - different skill base for DC/AB (will not account for extra bonuses)""" try: await ctx.message.delete() except: pass char = await Character.from_ctx(ctx) if not '-i' in args: spell = await select_spell_full( ctx, spell_name, list_filter=lambda s: s.name in char.get_spell_list()) else: spell = await select_spell_full(ctx, spell_name) args = await scripting.parse_snippets(args, ctx) args = await char.parse_cvars(args, ctx) args = shlex.split(args) args = argparse(args) result = await spell.cast(ctx, char, None, args) embed = result['embed'] embed.colour = char.get_color() embed.set_thumbnail(url=char.get_image()) add_fields_from_args(embed, args.get('f')) await char.commit(ctx) # make sure we save changes await ctx.send(embed=embed)
async def embed(self, ctx, *, args): """Creates and prints an Embed. __Valid Arguments__ -title <title> -desc <description text> -thumb <image url> -image <image url> -footer <footer text> -f "<Field Title>|<Field Text>[|inline]" (e.g. "Donuts|I have 15 donuts|inline" for an inline field, or "Donuts|I have 15 donuts" for one with its own line.) -color <hex color> -t <timeout (0..600)> """ try: await ctx.message.delete() except: pass embed = embeds.EmbedWithAuthor(ctx) args = argparse(args) embed.title = args.last('title') embed.description = args.last('desc') embed.set_thumbnail(url=args.last('thumb', '') if 'http' in str(args.last('thumb')) else '') embed.set_image(url=args.last('image', '') if 'http' in str(args.last('image')) else '') embed.set_footer(text=args.last('footer', '')) try: embed.colour = int(args.last('color', "0").strip('#'), base=16) except: pass embeds.add_fields_from_args(embed, args.get('f')) timeout = 0 if 't' in args: try: timeout = min(max(args.last('t', type_=int), 0), 600) except: pass if timeout: await ctx.send(embed=embed, delete_after=timeout) else: await ctx.send(embed=embed)
def _run_common(skill, args, embed, mod_override=None, rr_format="Check {}"): """ Runs a roll for a given Skill. :rtype: SkillRollResult """ # ephemeral support: adv, b # phrase phrase = args.join('phrase', '\n') # num rolls iterations = min(args.last('rr', 1, int), 25) # dc dc = args.last('dc', type_=int) # ro ro = args.last('ro', type_=int) # mc mc = args.last('mc', type_=int) desc_out = [] num_successes = 0 results = [] # add DC text if dc: desc_out.append(f"**DC {dc}**") for i in range(iterations): # advantage adv = args.adv(boolwise=True, ephem=True) # roll bonus b = args.join('b', '+', ephem=True) # set up dice roll_str = skill.d20(base_adv=adv, reroll=ro, min_val=mc, mod_override=mod_override) if b is not None: roll_str = f"{roll_str}+{b}" # roll result = roll(roll_str) if dc and result.total >= dc: num_successes += 1 results.append(result) # output if iterations > 1: embed.add_field(name=rr_format.format(str(i + 1)), value=result.result) else: desc_out.append(result.result) # phrase if phrase: desc_out.append(f"*{phrase}*") # DC footer if iterations > 1 and dc: embed.set_footer( text= f"{num_successes} Successes | {iterations - num_successes} Failures" ) elif dc: embed.set_footer(text="Success!" if num_successes else "Failure!") # build embed embed.description = '\n'.join(desc_out) embeds.add_fields_from_args(embed, args.get('f')) if 'thumb' in args: embed.set_thumbnail(url=maybe_http_url(args.last('thumb', ''))) return SkillRollResult(rolls=results, iterations=iterations, dc=dc, successes=num_successes)
async def monster_save(self, ctx, monster_name, save, *args): """Rolls a save for a monster. Args: adv/dis -b [conditional bonus] -phrase [flavor text] -title [title] *note: [mname] and [cname] will be replaced automatically* -dc [dc] -rr [iterations]""" monster: Monster = await select_monster_full(ctx, monster_name) self.bot.db.incr('monsters_looked_up_life') monster_name = monster.get_title_name() saves = monster.saves try: save = next(a for a in saves.keys() if save.lower() == a.lower()) except StopIteration: try: save = next(a for a in saves.keys() if save.lower() in a.lower()) except StopIteration: return await self.bot.say('That\'s not a valid save.') embed = discord.Embed() embed.colour = random.randint(0, 0xffffff) args = parse_args_3(args) adv = 0 if args.get( 'adv', []) and args.get('dis', []) else 1 if args.get( 'adv', False) else -1 if args.get('dis', False) else 0 b = "+".join(args.get('b', [])) or None phrase = '\n'.join(args.get('phrase', [])) or None iterations = min(int(args.get('rr', [1])[-1]), 25) try: dc = int(args.get('dc', [None])[-1]) except (ValueError, TypeError): dc = None num_successes = 0 if b is not None: roll_str = '1d20{:+}'.format(saves[save]) + '+' + b else: roll_str = '1d20{:+}'.format(saves[save]) embed.title = args.get('title', [''])[-1].replace('[mname]', monster_name).replace('[sname]', re.sub( r'((?<=[a-z])[A-Z]|(?<!\A)[A-Z](?=[a-z]))', r' \1', save).title()) or \ '{} makes {}!'.format(monster_name, a_or_an(re.sub( r'((?<=[a-z])[A-Z]|(?<!\A)[A-Z](?=[a-z]))', r' \1', save).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.get('image') is not None: embed.set_thumbnail(url=args.get('image')) else: embed.set_thumbnail(url=monster.get_image_url()) if monster.source == 'homebrew': embed.set_footer(text="Homebrew content.", icon_url="https://avrae.io/static/homebrew.png") await self.bot.say(embed=embed) try: await self.bot.delete_message(ctx.message) except: pass
async def monster_save(self, ctx, monster_name, save_stat, *args): """Rolls a save for a monster. __Valid Arguments__ adv/dis -b [conditional bonus] -phrase [flavor text] -title [title] *note: [mname] and [cname] will be replaced automatically* -dc [dc] -rr [iterations] -h (hides name and image of monster)""" monster: Monster = await select_monster_full(ctx, monster_name) self.bot.rdb.incr('monsters_looked_up_life') monster_name = monster.get_title_name() try: save = monster.saves.get(save_stat) save_name = f"{verbose_stat(save_stat[:3]).title()} Save" except ValueError: return await ctx.send('That\'s not a valid save.') embed = discord.Embed() embed.colour = random.randint(0, 0xffffff) args = await scripting.parse_snippets(args, ctx) args = argparse(args) 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) if b: roll_str = f"{formatted_d20}+{b}" else: roll_str = formatted_d20 if not args.last('h', type_=bool): default_title = f'{monster_name} makes {a_or_an(save_name)}!' else: default_title = f"An unknown creature makes {a_or_an(save_name)}!" embed.title = args.last('title', '') \ .replace('[mname]', monster_name) \ .replace('[sname]', save_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, 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, 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')) elif not args.last('h', type_=bool): embed.set_thumbnail(url=monster.get_image_url()) if monster.source == 'homebrew': embed.set_footer( text="Homebrew content.", icon_url="https://avrae.io/assets/img/homebrew.png") await ctx.send(embed=embed) try: await ctx.message.delete() except: pass
async def monster_atk(self, ctx, monster_name, atk_name=None, *, args=''): """Rolls a monster's attack. __Valid Arguments__ adv/dis -ac [target ac] -b [to hit bonus] -d [damage bonus] -d# [applies damage to the first # hits] -rr [times to reroll] -t [target] -phrase [flavor text] crit (automatically crit) -h (hides monster name, image, and attack details)""" if atk_name is None or atk_name == 'list': return await ctx.invoke(self.monster_atk_list, monster_name) try: await ctx.message.delete() except: pass monster = await select_monster_full(ctx, monster_name) attacks = monster.attacks monster_name = monster.get_title_name() attack = await search_and_select(ctx, attacks, atk_name, lambda a: a['name']) args = await scripting.parse_snippets(args, ctx) args = argparse(args) if not args.last('h', type_=bool): name = monster_name image = args.get('image') or monster.get_image_url() else: name = "An unknown creature" image = None attack = Attack.from_old(attack) embed = discord.Embed() if args.last('title') is not None: embed.title = args.last('title') \ .replace('[name]', name) \ .replace('[aname]', attack.name) else: embed.title = '{} attacks with {}!'.format(name, a_or_an(attack.name)) if image: embed.set_thumbnail(url=image) caster, targets, combat = await targetutils.maybe_combat( ctx, monster, args.get('t')) await Automation.from_attack(attack).run(ctx, embed, caster, targets, args, combat=combat, title=embed.title) if combat: await combat.final() _fields = args.get('f') embeds.add_fields_from_args(embed, _fields) embed.colour = random.randint(0, 0xffffff) if monster.source == 'homebrew': embeds.add_homebrew_footer(embed) await ctx.send(embed=embed)
async def check(self, ctx, check, *args): """Rolls a check for your current active character. __Valid Arguments__ adv/dis -b [conditional bonus] -mc [minimum roll] -phrase [flavor text] -title [title] *note: [charname] and [cname] will be replaced automatically* -dc [dc] -rr [iterations] str/dex/con/int/wis/cha (different skill base; e.g. Strength (Intimidation)) """ char: Character = await Character.from_ctx(ctx) skill_key = await search_and_select(ctx, SKILL_NAMES, check, lambda s: s) skill_name = camel_to_title(skill_key) embed = EmbedWithCharacter(char, False) skill = char.skills[skill_key] args = await self.new_arg_stuff(args, ctx, char) # advantage adv = args.adv(boolwise=True) # roll bonus b = args.join('b', '+') # phrase phrase = args.join('phrase', '\n') # num rolls iterations = min(args.last('rr', 1, int), 25) # dc dc = args.last('dc', type_=int) # reliable talent (#654) rt = char.get_setting('talent', 0) and skill.prof >= 1 mc = args.last('mc') or 10 * rt # halfling luck ro = char.get_setting('reroll') num_successes = 0 mod = skill.value formatted_d20 = skill.d20(base_adv=adv, reroll=ro, min_val=mc, base_only=True) if any(args.last(s, type_=bool) for s in STAT_ABBREVIATIONS): base = next(s for s in STAT_ABBREVIATIONS if args.last(s, type_=bool)) mod = mod - char.get_mod(SKILL_MAP[skill_key]) + char.get_mod(base) skill_name = f"{verbose_stat(base)} ({skill_name})" if b is not None: roll_str = f"{formatted_d20}{mod:+}+{b}" else: roll_str = f"{formatted_d20}{mod:+}" if args.last('title'): embed.title = args.last('title', '') \ .replace('[charname]', char.name) \ .replace('[cname]', skill_name) else: embed.title = f'{char.name} makes {a_or_an(skill_name)} check!' if iterations > 1: embed.description = (f"**DC {dc}**\n" if dc else '') + ( '*' + phrase + '*' if phrase is not None else '') for i in range(iterations): result = roll(roll_str, inline=True) if dc and result.total >= dc: num_successes += 1 embed.add_field(name=f"Check {i + 1}", value=result.skeleton) if dc: embed.set_footer( text= f"{num_successes} Successes | {iterations - num_successes} Failures" ) else: result = roll(roll_str, inline=True) if dc: embed.set_footer( text="Success!" if result.total >= dc else "Failure!") embed.description = ( f"**DC {dc}**\n" if dc else '') + result.skeleton + ( '\n*' + phrase + '*' if phrase is not None else '') embeds.add_fields_from_args(embed, args.get('f')) if args.last('image') is not None: embed.set_thumbnail(url=args.last('image')) await ctx.send(embed=embed) try: await ctx.message.delete() except: pass
async def cast(self, ctx, caster, targets, args, combat=None): """ Casts this spell. :param ctx: The context of the casting. :param caster: The caster of this spell. :type caster: :class:`~cogs5e.models.sheet.statblock.StatBlock` :param targets: A list of targets :type targets: list of :class:`~cogs5e.models.sheet.statblock.StatBlock` :param args: Args :type args: :class:`~utils.argparser.ParsedArguments` :param combat: The combat the spell was cast in, if applicable. :rtype: CastResult """ # generic args l = args.last('l', self.level, int) i = args.last('i', type_=bool) title = args.last('title') nopact = args.last('nopact', type_=bool) # meta checks if not self.level <= l <= 9: raise SpellException("Invalid spell level.") # caster spell-specific overrides dc_override = None ab_override = None spell_override = None is_prepared = True spellbook_spell = caster.spellbook.get_spell(self) if spellbook_spell is not None: dc_override = spellbook_spell.dc ab_override = spellbook_spell.sab spell_override = spellbook_spell.mod is_prepared = spellbook_spell.prepared if not i: # if I'm a warlock, and I didn't have any slots of this level anyway (#655) # automatically scale up to our pact slot level (or the next available level s.t. max > 0) if l > 0 \ and l == self.level \ and not caster.spellbook.get_max_slots(l) \ and not caster.spellbook.can_cast(self, l): if caster.spellbook.pact_slot_level is not None: l = caster.spellbook.pact_slot_level else: l = next((sl for sl in range(l, 6) if caster.spellbook.get_max_slots(sl)), l) # only scale up to l5 args['l'] = l # can I cast this spell? if not caster.spellbook.can_cast(self, l): embed = EmbedWithAuthor(ctx) embed.title = "Cannot cast spell!" if not caster.spellbook.get_slots(l): # out of spell slots err = ( f"You don't have enough level {l} slots left! Use `-l <level>` to cast at a different " f"level, `{ctx.prefix}g lr` to take a long rest, or `-i` to ignore spell slots!" ) elif self.name not in caster.spellbook: # don't know spell err = ( f"You don't know this spell! Use `{ctx.prefix}sb add {self.name}` to add it to your " f"spellbook, or pass `-i` to ignore restrictions.") else: # ? err = ( "Not enough spell slots remaining, or spell not in known spell list!\n" f"Use `{ctx.prefix}game longrest` to restore all spell slots if this is a character, " f"or pass `-i` to ignore restrictions.") embed.description = err if l > 0: embed.add_field(name="Spell Slots", value=caster.spellbook.remaining_casts_of( self, l)) return CastResult(embed=embed, success=False, automation_result=None) # #1000: is this spell prepared (soft check)? if not is_prepared: skip_prep_conf = await confirm( ctx, f"{self.name} is not prepared. Do you want to cast it anyway? (Reply with yes/no)", delete_msgs=True) if not skip_prep_conf: embed = EmbedWithAuthor( ctx, title=f"Cannot cast spell!", description= f"{self.name} is not prepared! Prepare it on your character sheet and use " f"`{ctx.prefix}update` to mark it as prepared, or use `-i` to ignore restrictions." ) return CastResult(embed=embed, success=False, automation_result=None) # use resource caster.spellbook.cast(self, l, pact=not nopact) # base stat stuff mod_arg = args.last("mod", type_=int) with_arg = args.last("with") stat_override = '' if mod_arg is not None: mod = mod_arg prof_bonus = caster.stats.prof_bonus dc_override = 8 + mod + prof_bonus ab_override = mod + prof_bonus spell_override = mod elif with_arg is not None: if with_arg not in STAT_ABBREVIATIONS: raise InvalidArgument( f"{with_arg} is not a valid stat to cast with.") mod = caster.stats.get_mod(with_arg) dc_override = 8 + mod + caster.stats.prof_bonus ab_override = mod + caster.stats.prof_bonus spell_override = mod stat_override = f" with {verbose_stat(with_arg)}" # begin setup embed = discord.Embed() if title: embed.title = title.replace('[name]', caster.name) \ .replace('[aname]', self.name) \ .replace('[sname]', self.name) \ .replace('[verb]', 'casts') # #1514, [aname] is action name now, #1587, add verb to action/cast else: embed.title = f"{caster.get_title_name()} casts {self.name}{stat_override}!" if targets is None: targets = [None] # concentration noconc = args.last("noconc", type_=bool) conc_conflict = None conc_effect = None if all((self.concentration, isinstance(caster, BaseCombatant), combat, not noconc)): duration = args.last('dur', self.get_combat_duration(), int) conc_effect = Effect.new(combat, caster, self.name, duration, "", True) effect_result = caster.add_effect(conc_effect) conc_conflict = effect_result['conc_conflict'] # run automation_result = None if self.automation and self.automation.effects: title = f"{caster.name} cast {self.name}!" automation_result = await self.automation.run( ctx, embed, caster, targets, args, combat, self, conc_effect=conc_effect, ab_override=ab_override, dc_override=dc_override, spell_override=spell_override, title=title) else: # no automation, display spell description phrase = args.join('phrase', '\n') if phrase: embed.description = f"*{phrase}*" embed.add_field(name="Description", value=smart_trim(self.description), inline=False) embed.set_footer(text="No spell automation found.") if l != self.level and self.higherlevels: embed.add_field(name="At Higher Levels", value=smart_trim(self.higherlevels), inline=False) if l > 0 and not i: embed.add_field(name="Spell Slots", value=caster.spellbook.remaining_casts_of(self, l)) if conc_conflict: conflicts = ', '.join(e.name for e in conc_conflict) embed.add_field(name="Concentration", value=f"Dropped {conflicts} due to concentration.") if 'thumb' in args: embed.set_thumbnail(url=maybe_http_url(args.last('thumb', ''))) elif self.image: embed.set_thumbnail(url=self.image) add_fields_from_args(embed, args.get('f')) gamedata.lookuputils.handle_source_footer(embed, self, add_source_str=False) return CastResult(embed=embed, success=True, automation_result=automation_result)
async def attack(self, ctx, atk_name=None, *, args: str = ''): """Rolls an attack for the current active character. __Valid Arguments__ adv/dis adv#/dis# (applies adv to the first # attacks) 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] -d# [applies damage to the first # hits] -c [damage bonus on crit] -rr [times to reroll] -mi [minimum weapon dice roll] -rd [extra dice rolled for damage] -rh [extra dice rolled for to hit] -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) -phrase [flavor text] -title [title] *note: [charname], [aname], and [target] will be replaced automatically* -f "Field Title|Field Text" (see !embed) -h (hides attack details) [user snippet]""" if atk_name is None: return await ctx.invoke(self.attack_list) char = await Character.from_ctx(ctx) attacks = char.get_attacks() try: # fuzzy search for atk_name attack = next(a for a in attacks if atk_name.lower() == a.get('name').lower()) except StopIteration: try: attack = next(a for a in attacks if atk_name.lower() in a.get('name').lower()) except StopIteration: return await ctx.send('No attack with that name found.') args = await self.new_arg_stuff(args, ctx, char) args['name'] = char.get_name() args['criton'] = args.last('criton') or char.get_setting('criton', 20) args['reroll'] = char.get_setting('reroll', 0) args['critdice'] = int(char.get_setting('hocrit', False)) + char.get_setting('critdice', 0) args['crittype'] = char.get_setting('crittype', 'default') if attack.get('details') is not None: try: attack['details'] = await char.parse_cvars(attack['details'], ctx) except AvraeException: pass # failed to eval, probably DDB nonsense if args.last('rd') is not None: attack["damage"] = (attack.get("damage") + "+" + args.last('rd')) result = sheet_attack(attack, args, EmbedWithCharacter(char, name=False)) embed = result['embed'] if args.last('h', type_=bool): try: await ctx.author.send(embed=result['full_embed']) except: pass _fields = args.get('f') embeds.add_fields_from_args(embed, _fields) 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: 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 attack(self, ctx, atk_name=None, *, args: str = ''): """Rolls an attack for the current active character. __Valid Arguments__ adv/dis adv#/dis# (applies adv to the first # attacks) 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] -d# [applies damage to the first # hits] -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) -phrase [flavor text] -title [title] *note: [charname], [aname], and [target] will be replaced automatically* -f "Field Title|Field Text" (see !embed) -h (hides attack details) [user snippet]""" if atk_name is None: return await ctx.invoke(self.attack_list) char: Character = await Character.from_ctx(ctx) attack = await search_and_select(ctx, char.attacks, atk_name, lambda a: a.name) args = await self.new_arg_stuff(args, ctx, char) args['name'] = char.name args['criton'] = args.last('criton') or char.get_setting('criton', 20) args['reroll'] = char.get_setting('reroll', 0) args['critdice'] = char.get_setting('critdice', 0) args['crittype'] = char.get_setting('crittype', 'default') result = sheet_attack(attack.to_old(), args, EmbedWithCharacter(char, name=False)) embed = result['embed'] if args.last('h', type_=bool): try: await ctx.author.send(embed=result['full_embed']) except: pass _fields = args.get('f') embeds.add_fields_from_args(embed, _fields) await ctx.send(embed=embed) try: await ctx.message.delete() except: pass
async def monster_cast(self, ctx, monster_name, spell_name, *args): """ Casts a spell as a monster. __Valid Arguments__ -i - Ignores Spellbook restrictions, for demonstrations or rituals. -l <level> - Specifies the level to cast the spell at. noconc - Ignores concentration requirements. -h - Hides rolled values. **__Save Spells__** -dc <Save DC> - Overrides the spell save DC. -save <Save type> - Overrides the spell save type. -d <damage> - Adds additional damage. pass - Target automatically succeeds save. fail - Target automatically fails save. adv/dis - Target makes save at advantage/disadvantage. **__Attack Spells__** See `!a`. **__All Spells__** -phrase <phrase> - adds flavor text. -title <title> - changes the title of the cast. Replaces [sname] with spell name. -thumb <url> - adds an image to the cast. -dur <duration> - changes the duration of any effect applied by the spell. -mod <spellcasting mod> - sets the value of the spellcasting ability modifier. int/wis/cha - different skill base for DC/AB (will not account for extra bonuses) """ await try_delete(ctx.message) monster: Monster = await select_monster_full(ctx, monster_name) args = await helpers.parse_snippets(args, ctx) args = argparse(args) if not args.last('i', type_=bool): spell = await select_spell_full( ctx, spell_name, list_filter=lambda s: s.name in monster.spellbook) else: spell = await select_spell_full(ctx, spell_name) caster, targets, combat = await targetutils.maybe_combat( ctx, monster, args) result = await spell.cast(ctx, caster, targets, args, combat=combat) # embed display embed = result['embed'] embed.colour = random.randint(0, 0xffffff) add_fields_from_args(embed, args.get('f')) if args.last('thumb') is not None: embed.set_thumbnail(url=args.last('thumb')) elif not args.last('h', type_=bool): embed.set_thumbnail(url=monster.get_image_url()) if monster.source == 'homebrew': embeds.add_homebrew_footer(embed) # save changes: combat state if combat: await combat.final() await ctx.send(embed=embed)
async def _attack(self, ctx, combatant_name, target_name, atk_name, args): args = await scripting.parse_snippets(args, ctx) combat = await Combat.from_ctx(ctx) try: target = await combat.select_combatant(target_name, "Select the target.") if target is None: return await ctx.send("Target not found.") except SelectionException: return await ctx.send("Target not found.") if combatant_name is None: combatant = combat.current_combatant if combatant is None: return await ctx.send(f"You must start combat with `{ctx.prefix}init next` first.") else: try: combatant = await combat.select_combatant(combatant_name, "Select the attacker.") if combatant is None: return await ctx.send("Combatant not found.") except SelectionException: return await ctx.send("Combatant not found.") attacks = combatant.attacks if '-custom' in args: attack = {'attackBonus': None, 'damage': None, 'name': atk_name} else: try: attack = await get_selection(ctx, [(a['name'], a) for a in attacks if atk_name.lower() in a['name'].lower()], message="Select your attack.") except SelectionException: return await ctx.send("Attack not found.") is_player = isinstance(combatant, PlayerCombatant) if is_player and combatant.character_owner == str(ctx.author.id): args = await combatant.character.parse_cvars(args, ctx) args = argparse(shlex.split(args)) # set up all the arguments args['name'] = combatant.name if target.ac is not None: args['ac'] = target.ac args['t'] = target.name args['resist'] = args.get('resist') or target.resists['resist'] args['immune'] = args.get('immune') or target.resists['immune'] args['vuln'] = args.get('vuln') or target.resists['vuln'] args['neutral'] = args.get('neutral') or target.resists['neutral'] if is_player: args['c'] = combatant.character.get_setting('critdmg') or args.get('c') args['reroll'] = combatant.character.get_setting('reroll') or 0 args['crittype'] = combatant.character.get_setting('crittype') or 'default' args['critdice'] = (combatant.character.get_setting('critdice') or 0) + int( combatant.character.get_setting('hocrit', False)) args['criton'] = combatant.character.get_setting('criton') or args.get('criton') result = sheet_attack(attack, args) embed = result['embed'] if args.last('h', type_=bool): try: controller = ctx.guild.get_member(int(combatant.controller)) await controller.send(embed=result['full_embed']) except: pass if is_player: embed.colour = combatant.character.get_color() else: embed.colour = random.randint(0, 0xffffff) if target.ac is not None and target.hp is not None: target.mod_hp(-result['total_damage'], overheal=False) if target.ac is not None: if target.hp is not None: embed.set_footer(text="{}: {}".format(target.name, target.get_hp_str())) if target.isPrivate: try: controller = ctx.guild.get_member(int(target.controller)) await controller.send( f"{combatant.name} attacked with a {attack['name']}!" f"\n{target.name}'s HP: {target.get_hp_str(True)}") except: pass else: embed.set_footer(text="Dealt {} damage to {}!".format(result['total_damage'], target.name)) if target.is_concentrating() and result['total_damage'] > 0: dcs = [] for atk in result['raw_attacks']: if atk['crit'] == 2: continue dcs.append(int(max(atk['damage'] / 2, 10))) if len(dcs) > 1: dcs = ', '.join(map(str, dcs)) embed.add_field(name="Concentration", value=f"Check your concentration (DCs {dcs})!") else: embed.add_field(name="Concentration", value=f"Check your concentration (DC {dcs[0]})!") else: embed.set_footer(text="Target AC not set.") embeds.add_fields_from_args(embed, args.get('f', [])) await ctx.send(embed=embed) await combat.final()
async def monster_check(self, ctx, monster_name, check, *args): """Rolls a check for a monster. Args: adv/dis -b [conditional bonus] -phrase [flavor text] -title [title] *note: [mname] and [cname] will be replaced automatically* -dc [dc] -rr [iterations] str/dex/con/int/wis/cha (different skill base; e.g. Strength (Intimidation))""" monster: Monster = await select_monster_full(ctx, monster_name) self.bot.rdb.incr('monsters_looked_up_life') monster_name = monster.get_title_name() skills = monster.skills try: skill = next(a for a in skills.keys() if check.lower() == a.lower()) except StopIteration: try: skill = next(a for a in skills.keys() if check.lower() in a.lower()) except StopIteration: return await self.bot.say('That\'s not a valid check.') embed = discord.Embed() embed.colour = random.randint(0, 0xffffff) args = argparse(args) adv = args.adv() b = args.join('b', '+') phrase = args.join('phrase', '\n') formatted_d20 = '1d20' if adv == 0 else '2d20' + ( 'kh1' if adv == 1 else 'kl1') iterations = min(args.last('rr', 1, int), 25) dc = args.last('dc', type_=int) num_successes = 0 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 - monster.get_mod( SKILL_MAP[skill]) + monster.get_mod(base) skill_name = f"{verbose_stat(base)} ({skill})" skill_name = skill_name.title() default_title = '{} makes {} check!'.format(monster_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('[mname]', monster_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')) else: embed.set_thumbnail(url=monster.get_image_url()) if monster.source == 'homebrew': embed.set_footer(text="Homebrew content.", icon_url="https://avrae.io/static/homebrew.png") await self.bot.say(embed=embed) try: await self.bot.delete_message(ctx.message) 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 monster_save(self, ctx, monster_name, save, *args): """Rolls a save for a monster. Args: adv/dis -b [conditional bonus] -phrase [flavor text] -title [title] *note: [mname] and [cname] will be replaced automatically* -dc [dc] -rr [iterations]""" monster: Monster = await select_monster_full(ctx, monster_name) self.bot.rdb.incr('monsters_looked_up_life') monster_name = monster.get_title_name() saves = monster.saves try: save = next(a for a in saves.keys() if save.lower() == a.lower()) except StopIteration: try: save = next(a for a in saves.keys() if save.lower() in a.lower()) except StopIteration: return await self.bot.say('That\'s not a valid save.') embed = discord.Embed() embed.colour = random.randint(0, 0xffffff) args = argparse(args) 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 if b is not None: roll_str = '1d20{:+}'.format(saves[save]) + '+' + b else: roll_str = '1d20{:+}'.format(saves[save]) default_title = f'{monster_name} makes {a_or_an(camel_to_title(save))}!' embed.title = args.last('title', '') \ .replace('[mname]', monster_name) \ .replace('[sname]', camel_to_title(save)) \ 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')) else: embed.set_thumbnail(url=monster.get_image_url()) if monster.source == 'homebrew': embed.set_footer(text="Homebrew content.", icon_url="https://avrae.io/static/homebrew.png") await self.bot.say(embed=embed) try: await self.bot.delete_message(ctx.message) except: pass
async def attack(self, ctx, atk_name=None, *, args: str = ''): """Rolls an attack for the current active character. __Valid Arguments__ adv/dis adv#/dis# (applies adv to the first # attacks) 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] -d# [applies damage to the first # hits] -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) -phrase [flavor text] -title [title] *note: [name] and [aname] will be replaced automatically* -f "Field Title|Field Text" (see !embed) [user snippet]""" if atk_name is None: return await ctx.invoke(self.attack_list) char: Character = await Character.from_ctx(ctx) attack = await search_and_select(ctx, char.attacks, atk_name, lambda a: a.name) args = await self.new_arg_stuff(args, ctx, char) embed = EmbedWithCharacter(char, name=False) if args.last('title') is not None: embed.title = args.last('title') \ .replace('[name]', char.name) \ .replace('[aname]', attack.name) else: embed.title = '{} attacks with {}!'.format(char.name, a_or_an(attack.name)) caster, targets, combat = await targetutils.maybe_combat( ctx, char, args.get('t')) await Automation.from_attack(attack).run(ctx, embed, caster, targets, args, combat=combat, title=embed.title) if combat: await combat.final() _fields = args.get('f') embeds.add_fields_from_args(embed, _fields) await ctx.send(embed=embed) try: await ctx.message.delete() except: pass
async def monster_check(self, ctx, monster_name, check, *args): """Rolls a check for a monster. __Valid Arguments__ adv/dis -b [conditional bonus] -phrase [flavor text] -title [title] *note: [mname] and [cname] will be replaced automatically* -dc [dc] -rr [iterations] str/dex/con/int/wis/cha (different skill base; e.g. Strength (Intimidation)) -h (hides name and image of monster)""" monster: Monster = await select_monster_full(ctx, monster_name) self.bot.rdb.incr('monsters_looked_up_life') monster_name = monster.get_title_name() skill_key = await search_and_select(ctx, SKILL_NAMES, check, lambda s: s) skill_name = camel_to_title(skill_key) embed = discord.Embed() embed.colour = random.randint(0, 0xffffff) args = await scripting.parse_snippets(args, ctx) args = argparse(args) 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 skill = monster.skills[skill_key] mod = skill.value formatted_d20 = skill.d20(base_adv=adv, base_only=True) 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 - monster.get_mod( SKILL_MAP[skill_key]) + monster.get_mod(base) skill_name = f"{verbose_stat(base)} ({skill_name})" skill_name = skill_name.title() if not args.last('h', type_=bool): default_title = '{} makes {} check!'.format( monster_name, a_or_an(skill_name)) else: default_title = f"An unknown creature makes {a_or_an(skill_name)} check!" 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('[mname]', monster_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, 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, 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')) elif not args.last('h', type_=bool): embed.set_thumbnail(url=monster.get_image_url()) if monster.source == 'homebrew': embed.set_footer( text="Homebrew content.", icon_url="https://avrae.io/assets/img/homebrew.png") await ctx.send(embed=embed) try: await ctx.message.delete() except: pass
async def cast(self, ctx, caster, targets, args, combat=None): """ Casts this spell. :param ctx: The context of the casting. :param caster: The caster of this spell. :type caster: :class:`~cogs5e.models.sheet.statblock.StatBlock` :param targets: A list of targets :type targets: list of :class:`~cogs5e.models.sheet.statblock.StatBlock` :param args: Args :type args: :class:`~utils.argparser.ParsedArguments` :param combat: The combat the spell was cast in, if applicable. :return: {embed: Embed} """ # generic args l = args.last('l', self.level, int) i = args.last('i', type_=bool) title = args.last('title') # meta checks if not self.level <= l <= 9: raise SpellException("Invalid spell level.") # caster spell-specific overrides dc_override = None ab_override = None spell_override = None spellbook_spell = caster.spellbook.get_spell(self) if spellbook_spell is not None: dc_override = spellbook_spell.dc ab_override = spellbook_spell.sab spell_override = spellbook_spell.mod if not i: # if I'm a warlock, and I didn't have any slots of this level anyway (#655) # automatically scale up to the next level s.t. our slots are not 0 if l > 0 \ and l == self.level \ and not caster.spellbook.get_max_slots(l) \ and not caster.spellbook.can_cast(self, l): l = next((sl for sl in range(l, 6) if caster.spellbook.get_max_slots(sl)), l) # only scale up to l5 args['l'] = l # can I cast this spell? if not caster.spellbook.can_cast(self, l): embed = EmbedWithAuthor(ctx) embed.title = "Cannot cast spell!" if not caster.spellbook.get_slots(l): # out of spell slots err = f"You don't have enough level {l} slots left! Use `-l <level>` to cast at a different level, " \ f"`{ctx.prefix}g lr` to take a long rest, or `-i` to ignore spell slots!" elif self.name not in caster.spellbook: # don't know spell err = f"You don't know this spell! Use `{ctx.prefix}sb add {self.name}` to add it to your spellbook, " \ f"or pass `-i` to ignore restrictions." else: # ? err = "Not enough spell slots remaining, or spell not in known spell list!\n" \ f"Use `{ctx.prefix}game longrest` to restore all spell slots if this is a character, " \ f"or pass `-i` to ignore restrictions." embed.description = err if l > 0: embed.add_field(name="Spell Slots", value=caster.spellbook.remaining_casts_of( self, l)) return {"embed": embed} # use resource caster.spellbook.cast(self, l) # character setup character = None if isinstance(caster, PlayerCombatant): character = caster.character elif isinstance(caster, Character): character = caster # base stat stuff mod_arg = args.last("mod", type_=int) stat_override = '' if mod_arg is not None: mod = mod_arg if character: prof_bonus = character.stats.prof_bonus else: prof_bonus = 0 dc_override = 8 + mod + prof_bonus ab_override = mod + prof_bonus spell_override = mod elif character and 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 = character.stats.get_mod(base) dc_override = 8 + mod + character.stats.prof_bonus ab_override = mod + character.stats.prof_bonus spell_override = mod stat_override = f" with {verbose_stat(base)}" if spell_override is None and (caster.spellbook.sab is None or caster.spellbook.dc is None): raise SpellException( "This caster does not have the ability to cast spells.") # begin setup embed = discord.Embed() if title: embed.title = title.replace('[sname]', self.name) else: embed.title = f"{caster.get_title_name()} casts {self.name}{stat_override}!" if targets is None: targets = [None] # concentration noconc = args.last("noconc", type_=bool) conc_conflict = None conc_effect = None if all( (self.concentration, isinstance(caster, Combatant), combat, not noconc)): duration = args.last('dur', self.get_combat_duration(), int) conc_effect = initiative.Effect.new(combat, caster, self.name, duration, "", True) effect_result = caster.add_effect(conc_effect) conc_conflict = effect_result['conc_conflict'] if self.automation and self.automation.effects: title = f"{caster.name} cast {self.name}!" await self.automation.run(ctx, embed, caster, targets, args, combat, self, conc_effect=conc_effect, ab_override=ab_override, dc_override=dc_override, spell_override=spell_override, title=title) else: phrase = args.join('phrase', '\n') if phrase: embed.description = f"*{phrase}*" text = self.description if len(text) > 1020: text = f"{text[:1020]}..." embed.add_field(name="Description", value=text, inline=False) if l != self.level and self.higherlevels: embed.add_field(name="At Higher Levels", value=self.higherlevels, inline=False) embed.set_footer(text="No spell automation found.") if l > 0 and not i: embed.add_field(name="Spell Slots", value=caster.spellbook.remaining_casts_of(self, l)) if conc_conflict: conflicts = ', '.join(e.name for e in conc_conflict) embed.add_field(name="Concentration", value=f"Dropped {conflicts} due to concentration.") if 'thumb' in args: embed.set_thumbnail(url=args.last('thumb')) elif self.image: embed.set_thumbnail(url=self.image) add_fields_from_args(embed, args.get('f')) if self.source == 'homebrew': add_homebrew_footer(embed) return {"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 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) if args.last('title') is not None: embed.title = args.last('title') \ .replace('[name]', char.name) \ .replace('[aname]', attack.name) else: embed.title = '{} attacks with {}!'.format(char.name, a_or_an(attack.name)) await attack.automation.run(ctx, embed, caster, targets, args, combat=combat, title=embed.title) if combat: await combat.final() _fields = args.get('f') embeds.add_fields_from_args(embed, _fields) if 'thumb' in args: embed.set_thumbnail(url=args.last('thumb')) await ctx.send(embed=embed) try: await ctx.message.delete() except: pass