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 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 dice_roll_save(self, gctx, roll_request): """Save: Display like ``!s``.""" save_name = roll_request.action if save_name in constants.STAT_ABBREVIATIONS: save_name = verbose_stat(save_name) await self._dice_roll_embed_common(gctx, roll_request, "{name} makes {save} Save!", save=a_or_an(save_name.title()))
async def dice_roll_check(self, gctx, roll_request): """Check: Display like ``!c``. Requires character - if not imported falls back to default roll.""" check_name = roll_request.action if check_name in constants.STAT_ABBREVIATIONS: check_name = verbose_stat(check_name) await self._dice_roll_embed_common(gctx, roll_request, "{name} makes {check} check!", check=a_or_an(check_name.title()))
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 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
def get_title_name(self): """Returns a monster's name for use in embed titles.""" return a_or_an(self.name, upper=True) if not self.proper else self.name
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 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
def sheet_attack(attack, args, embed=None): """ :param attack: (dict) The attack to roll :param args: (dict) Metadata arguments :param embed: (discord.Embed) if supplied, will use as base. If None, will create one. :returns: a dict with structure {"embed": discord.Embed(), "result": {metadata}}""" # print(args) if embed is None: embed = discord.Embed() total_damage = 0 dnum_keys = [k for k in args.keys() if re.match(r'd\d+', k)] # ['d1', 'd2'...] dnum = {} for k in dnum_keys: # parse d# args for dmg in args[k].split('|'): try: dnum[dmg] = int(k.split('d')[-1]) except ValueError: embed.title = "Error" embed.colour = 0xff0000 embed.description = "Malformed tag: {}".format(k) return {"embed": embed, "total_damage": 0} advnum_keys = [k for k in args.keys() if re.match(r'(adv|dis)\d+', k)] advnum = {} for k in advnum_keys: # parse adv# args m = re.match(r'(adv|dis)(\d+)', k) _adv = m.group(1) num = int(m.group(2)) advnum[_adv] = num if args.get('phrase') is not None: # parse phrase embed.description = '*' + args.get('phrase') + '*' else: embed.description = '~~' + ' ' * 500 + '~~' if args.get('title') is not None: embed.title = args.get('title').replace( '[charname]', args.get('name')).replace( '[aname]', attack.get('name')).replace('[target]', args.get('t', '')) elif args.get('t') is not None: # parse target embed.title = '{} attacks with {} at {}!'.format( args.get('name'), a_or_an(attack.get('name')), args.get('t')) else: embed.title = '{} attacks with {}!'.format(args.get('name'), a_or_an(attack.get('name'))) for arg in ('rr', 'ac'): # parse reroll/ac try: args[arg] = int(args.get(arg, None)) except (ValueError, TypeError): args[arg] = None args['adv'] = 0 if args.get( 'adv', False) and args.get('dis', False) else 1 if args.get( 'adv', False) else -1 if args.get('dis', False) else 0 args['adv'] = 2 if args.get( 'ea', False) and not args.get('dis', False) else args['adv'] args['crit'] = 1 if args.get('crit', False) else None args['hit'] = 1 if args.get('hit', False) else None for r in range(args.get('rr', 1) or 1): # start rolling attacks out = '' itercrit = 0 if attack.get('attackBonus') is None and args.get('b') is not None: attack['attackBonus'] = '0' if attack.get('attackBonus') is not None and not args.get('hit'): adv = args.get('adv') for _adv, numHits in advnum.items(): if numHits > 0: adv = 1 if _adv == 'adv' else -1 advnum[_adv] -= 1 formatted_d20 = ('1d20' if adv == 0 else '2d20' + ( 'kh1' if adv == 1 else 'kl1') if not adv == 2 else '3d20kh1') \ + ('ro{}'.format(args.get('reroll', 0)) if int(args.get('reroll', 0)) else '') if args.get('b') is not None: toHit = roll(f'{formatted_d20}+' + attack.get('attackBonus') + '+' + args.get('b'), rollFor='To Hit', inline=True, show_blurbs=False) else: toHit = roll(f'{formatted_d20}+' + attack.get('attackBonus'), rollFor='To Hit', inline=True, show_blurbs=False) try: parts = len(toHit.raw_dice.parts) except: parts = 0 if parts > 0: out += toHit.result + '\n' try: raw = next(p for p in toHit.raw_dice.parts if isinstance(p, SingleDiceGroup) and p.max_value == 20).get_total() except StopIteration: raw = 0 if raw >= (int(args.get('criton', 20)) or 20): itercrit = 1 else: itercrit = toHit.crit if args.get('ac') is not None: if toHit.total < args.get('ac') and itercrit == 0: itercrit = 2 # miss! if args.get('crit') and itercrit < 2: itercrit = args.get('crit', 0) else: # output wherever was there if error out += "**To Hit**: " + attack.get('attackBonus') + '\n' else: if args.get('hit'): out += "**To Hit**: Automatic hit!\n" if args.get('crit'): itercrit = args.get('crit', 0) else: itercrit = 0 if attack.get('damage') is None and args.get('d') is not None: attack['damage'] = '0' if attack.get('damage') is not None: def parsecrit(damage_str, wep=False): if itercrit == 1: if args.get('crittype') == '2x': critDice = f"({damage_str})*2" if args.get('c') is not None: critDice += '+' + args.get('c', '') else: def critSub(matchobj): hocrit = 1 if args.get('hocrit') and wep else 0 return str(int(matchobj.group(1)) * 2 + hocrit) + 'd' + matchobj.group(2) critDice = re.sub(r'(\d+)d(\d+)', critSub, damage_str) else: critDice = damage_str return critDice # -d, -d# parsing if args.get('d') is not None: damage = parsecrit(attack.get('damage'), wep=True) + '+' + parsecrit(args.get('d')) else: damage = parsecrit(attack.get('damage'), wep=True) for dice, numHits in dnum.items(): if not itercrit == 2 and numHits > 0: damage += '+' + parsecrit(dice) dnum[dice] -= 1 # crit parsing rollFor = "Damage" if itercrit == 1: if args.get('c') is not None: damage += '+' + args.get('c', '') rollFor = "Damage (CRIT!)" elif itercrit == 2: rollFor = "Damage (Miss!)" # resist parsing if 'resist' in args or 'immune' in args or 'vuln' in args: resistances = args.get('resist', '').split('|') immunities = args.get('immune', '').split('|') vulnerabilities = args.get('vuln', '').split('|') damage = parse_resistances(damage, resistances, immunities, vulnerabilities) # actual roll if itercrit == 2 and not args.get('showmiss', False): out += '**Miss!**\n' else: dmgroll = roll(damage, rollFor=rollFor, inline=True, show_blurbs=False) out += dmgroll.result + '\n' if not itercrit == 2: # if we actually hit total_damage += dmgroll.total if out is not '': if (args.get('rr', 1) or 1) > 1: embed.add_field(name='Attack {}'.format(r + 1), value=out, inline=False) else: embed.add_field(name='Attack', value=out, inline=False) if (args.get('rr', 1) or 1) > 1 and attack.get('damage') is not None: embed.add_field(name='Total Damage', value=str(total_damage)) if attack.get('details') is not None: embed.add_field(name='Effect', value=(attack.get('details', ''))) if args.get('image') is not None: embed.set_thumbnail(url=args.get('image')) return {'embed': embed, 'total_damage': total_damage}
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 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
def sheet_attack(attack, args, embed=None): """ :param attack: (dict) The attack to roll :param args: (dict) Metadata arguments :param embed: (discord.Embed) if supplied, will use as base. If None, will create one. :returns: a dict with structure {"embed": discord.Embed(), "result": {metadata}}""" # print(args) if embed is None: embed = discord.Embed() total_damage = 0 advnum_keys = [k for k in args if re.match(r'(adv|dis)\d+', k) and args.last(k, type_=bool)] advnum = {} for k in advnum_keys: # parse adv# args m = re.match(r'(adv|dis)(\d+)', k) _adv = m.group(1) num = int(m.group(2)) advnum[_adv] = num dnum_keys = [k for k in args if re.match(r'd\d+', k)] # ['d1', 'd2'...] dnum = {} for k in dnum_keys: # parse d# args for dmg in args.get(k): try: dnum[dmg] = int(k.split('d')[-1]) except ValueError: embed.title = "Error" embed.colour = 0xff0000 embed.description = "Malformed tag: {}".format(k) return {"embed": embed, "total_damage": 0} if args.get('phrase'): # parse phrase embed.description = '*' + args.join('phrase', '\n') + '*' else: embed.description = '~~' + ' ' * 500 + '~~' if args.last('title') is not None: embed.title = args.last('title') \ .replace('[charname]', args.last('name')) \ .replace('[aname]', attack.get('name')) \ .replace('[target]', args.last('t', '')) elif args.last('t') is not None: # parse target embed.title = '{} attacks with {} at {}!'.format(args.last('name'), a_or_an(attack.get('name')), args.last('t')) else: embed.title = '{} attacks with {}!'.format(args.last('name'), a_or_an(attack.get('name'))) if args.last('image') is not None: embed.set_thumbnail(url=args.last('image')) adv = args.adv(True) crit = args.last('crit', None, bool) and 1 hit = args.last('hit', None, bool) and 1 miss = (args.last('miss', None, bool) and not hit) and 1 ac = args.last('ac', type_=int) criton = args.last('criton', 20, int) rr = min(args.last('rr', 1, int), 25) reroll = args.last('reroll', 0, int) b = args.join('b', '+') h = args.last('h', None, bool) if h: hidden_embed = copy.copy(embed) else: hidden_embed = discord.Embed() # less memory? idek we don't use it anyway raw_attacks = [] for r in range(rr): # start rolling attacks out = '' hidden_out = '' itercrit = 0 if attack.get('attackBonus') is None and b: attack['attackBonus'] = '0' if attack.get('attackBonus') is not None and not (hit or miss): iteradv = adv for _adv, numHits in advnum.items(): if numHits > 0: iteradv = 1 if _adv == 'adv' else -1 advnum[_adv] -= 1 formatted_d20 = format_d20(iteradv, reroll) if b: toHit = roll(f"{formatted_d20}+{attack.get('attackBonus')}+{b}", rollFor='To Hit', inline=True, show_blurbs=False) else: toHit = roll(f"{formatted_d20}+{attack.get('attackBonus')}", rollFor='To Hit', inline=True, show_blurbs=False) try: parts = len(toHit.raw_dice.parts) except: parts = 0 if parts > 0: out += toHit.result + '\n' try: raw = next(p for p in toHit.raw_dice.parts if isinstance(p, SingleDiceGroup) and p.max_value == 20).get_total() except StopIteration: raw = 0 if raw >= criton: itercrit = 1 else: itercrit = toHit.crit if ac is not None: if toHit.total < ac and itercrit == 0: itercrit = 2 # miss! if crit and itercrit < 2: itercrit = crit if ac: hidden_out += f"**To Hit**: {formatted_d20}... = `{HIT_DICT[itercrit]}`\n" else: hidden_out += f"**To Hit**: {formatted_d20}... = `{toHit.total}`\n" else: # output wherever was there if error out += "**To Hit**: " + attack.get('attackBonus') + '\n' hidden_out += "**To Hit**: Unknown" else: if hit: out += "**To Hit**: Automatic hit!\n" elif miss: out += "**To Hit**: Automatic miss!\n" if crit: itercrit = crit else: if miss: itercrit = 2 else: itercrit = 0 res = sheet_damage(attack.get('damage'), args, itercrit, dnum) out += res['damage'] if res['roll']: hidden_out += f"**Damage**: {res['roll'].consolidated()} = `{res['roll'].total}`" else: hidden_out += res['damage'] total_damage += res['total'] raw_attacks.append({'damage': res['total'], 'crit': itercrit}) if out is not '': if rr > 1: embed.add_field(name='Attack {}'.format(r + 1), value=out, inline=False) hidden_embed.add_field(name='Attack {}'.format(r + 1), value=hidden_out, inline=False) else: embed.add_field(name='Attack', value=out, inline=False) hidden_embed.add_field(name='Attack', value=hidden_out, inline=False) if rr > 1 and attack.get('damage') is not None: embed.add_field(name='Total Damage', value=str(total_damage)) hidden_embed.add_field(name='Total Damage', value=str(total_damage)) if attack.get('details'): embed.add_field(name='Effect', value=attack['details'] if len(attack['details']) < 1020 else f"{attack['details'][:1020]}...") out = {'embed': embed, 'total_damage': total_damage, 'full_embed': embed, 'raw_attacks': raw_attacks} if h: out['embed'] = hidden_embed return out
def sheet_attack(attack, args, embed=None): """ :param attack: (dict) The attack to roll :param args: (dict) Metadata arguments :param embed: (discord.Embed) if supplied, will use as base. If None, will create one. :returns: a dict with structure {"embed": discord.Embed(), "result": {metadata}}""" # print(args) if embed is None: embed = discord.Embed() total_damage = 0 advnum_keys = [k for k in args.keys() if re.match(r'(adv|dis)\d+', k)] advnum = {} for k in advnum_keys: # parse adv# args m = re.match(r'(adv|dis)(\d+)', k) _adv = m.group(1) num = int(m.group(2)) advnum[_adv] = num dnum_keys = [k for k in args.keys() if re.match(r'd\d+', k) ] # ['d1', 'd2'...] TODO should probably move this dnum = {} for k in dnum_keys: # parse d# args for dmg in args[k].split('|'): try: dnum[dmg] = int(k.split('d')[-1]) except ValueError: embed.title = "Error" embed.colour = 0xff0000 embed.description = "Malformed tag: {}".format(k) return {"embed": embed, "total_damage": 0} if args.get('phrase') is not None: # parse phrase embed.description = '*' + args.get('phrase') + '*' else: embed.description = '~~' + ' ' * 500 + '~~' if args.get('title') is not None: embed.title = args.get('title').replace( '[charname]', args.get('name')).replace( '[aname]', attack.get('name')).replace('[target]', args.get('t', '')) elif args.get('t') is not None: # parse target embed.title = '{} attacks with {} at {}!'.format( args.get('name'), a_or_an(attack.get('name')), args.get('t')) else: embed.title = '{} attacks with {}!'.format(args.get('name'), a_or_an(attack.get('name'))) for arg in ('rr', 'ac'): # parse reroll/ac try: args[arg] = int(args.get(arg, None)) except (ValueError, TypeError): args[arg] = None args['adv'] = 0 if args.get( 'adv', False) and args.get('dis', False) else 1 if args.get( 'adv', False) else -1 if args.get('dis', False) else 0 args['adv'] = 2 if args.get( 'ea', False) and not args.get('dis', False) else args['adv'] args['crit'] = 1 if args.get('crit', False) else None args['hit'] = 1 if args.get('hit', False) else None args['miss'] = 1 if args.get('miss', False) and not args.get('hit') else None for r in range(args.get('rr', 1) or 1): # start rolling attacks out = '' itercrit = 0 if attack.get('attackBonus') is None and args.get('b') is not None: attack['attackBonus'] = '0' if attack.get('attackBonus') is not None and not args.get( 'hit') and not args.get('miss'): adv = args.get('adv') for _adv, numHits in advnum.items(): if numHits > 0: adv = 1 if _adv == 'adv' else -1 advnum[_adv] -= 1 formatted_d20 = ('1d20' if adv == 0 else '2d20' + ( 'kh1' if adv == 1 else 'kl1') if not adv == 2 else '3d20kh1') \ + ('ro{}'.format(args.get('reroll', 0)) if int(args.get('reroll', 0)) else '') if args.get('b') is not None: toHit = roll(f'{formatted_d20}+' + attack.get('attackBonus') + '+' + args.get('b'), rollFor='To Hit', inline=True, show_blurbs=False) else: toHit = roll(f'{formatted_d20}+' + attack.get('attackBonus'), rollFor='To Hit', inline=True, show_blurbs=False) try: parts = len(toHit.raw_dice.parts) except: parts = 0 if parts > 0: out += toHit.result + '\n' try: raw = next(p for p in toHit.raw_dice.parts if isinstance(p, SingleDiceGroup) and p.max_value == 20).get_total() except StopIteration: raw = 0 if raw >= int(args.get('criton', 20) or 20): itercrit = 1 else: itercrit = toHit.crit if args.get('ac') is not None: if toHit.total < args.get('ac') and itercrit == 0: itercrit = 2 # miss! if args.get('crit') and itercrit < 2: itercrit = args.get('crit', 0) else: # output wherever was there if error out += "**To Hit**: " + attack.get('attackBonus') + '\n' else: if args.get('hit'): out += "**To Hit**: Automatic hit!\n" elif args.get('miss'): out += "**To Hit**: Automatic miss!\n" if args.get('crit'): itercrit = args.get('crit', 0) else: if args.get("miss"): itercrit = 2 else: itercrit = 0 res = sheet_damage(attack.get('damage'), args, itercrit, dnum) out += res['damage'] total_damage += res['total'] if out is not '': if (args.get('rr', 1) or 1) > 1: embed.add_field(name='Attack {}'.format(r + 1), value=out, inline=False) else: embed.add_field(name='Attack', value=out, inline=False) if (args.get('rr', 1) or 1) > 1 and attack.get('damage') is not None: embed.add_field(name='Total Damage', value=str(total_damage)) if attack.get('details') is not None: embed.add_field(name='Effect', value=(attack.get('details', ''))) if args.get('image') is not None: embed.set_thumbnail(url=args.get('image')) return {'embed': embed, 'total_damage': total_damage}
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)
def embed_for_action(gctx, action, character, to_hit_roll=None, damage_roll=None): """ Creates an embed for a character performing some action (attack or spell). Handles inserting the correct fields for to-hit and damage based on the action's automation and whether the rolls are present. :type gctx: GameLogEventContext :type action: Attack or gamedata.spell.Spell :type character: cogs5e.models.character.Character :type to_hit_roll: ddb.dice.tree.RollRequestRoll :type damage_roll: ddb.dice.tree.RollRequestRoll """ embed = EmbedWithCharacter(character, name=False) automation = action.automation waiting_for_damage = False # set title if isinstance(action, Attack): attack_name = a_or_an( action.name) if not action.proper else action.name verb = action.verb or "attacks with" embed.title = f'{character.get_title_name()} {verb} {attack_name}!' else: # spell embed.title = f'{character.get_title_name()} casts {action.name}!' # add to hit (and damage, either if it is provided or the action expects damage and it is not provided) meta_rolls = [] if to_hit_roll is not None: meta_rolls.append(f"**To Hit**: {str(to_hit_roll.to_d20())}") if damage_roll is not None: if damage_roll.roll_kind == ddb.dice.RollKind.CRITICAL_HIT: meta_rolls.append( f"**Damage (CRIT!)**: {str(damage_roll.to_d20())}") else: meta_rolls.append(f"**Damage**: {str(damage_roll.to_d20())}") elif automation_has_damage(automation): meta_rolls.append("**Damage**: Waiting for roll...") waiting_for_damage = True # add dcs, texts if automation: for effect in automation_dfg(automation, enter_filter=action_enter_filter): # break if we see a damage and are waiting on a damage roll if effect.type == 'damage' and waiting_for_damage: break # note: while certain fields here are AnnotatedStrings, it should never be annotated directly from the sheet # and GameLog events cannot trigger custom attacks, so this should be fine # save: add the DC if effect.type == 'save': meta_rolls.append( f"**DC**: {effect.dc}\n{effect.stat[:3].upper()} Save") # text: add the text as a field elif effect.type == 'text': embed.add_field(name="Effect", value=effect.text, inline=False) embed.insert_field_at(0, name="Meta", value='\n'.join(meta_rolls), inline=False) # set footer embed.set_footer(text=f"Rolled in {gctx.campaign.campaign_name}", icon_url=constants.DDB_LOGO_ICON) return 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