def run(self, autoctx): super(Damage, self).run(autoctx) # general arguments args = autoctx.args damage = self.damage d = args.join('d', '+', ephem=True) c = args.join('c', '+', ephem=True) resist = args.get('resist', [], ephem=True) immune = args.get('immune', [], ephem=True) vuln = args.get('vuln', [], ephem=True) neutral = args.get('neutral', [], ephem=True) crit = args.last('crit', None, bool, ephem=True) maxdmg = args.last('max', None, bool, ephem=True) mi = args.last('mi', None, int) critdice = args.last('critdice', 0, int) hide = args.last('h', type_=bool) # character-specific arguments if autoctx.character: critdice = autoctx.character.get_setting('critdice') or critdice # combat-specific arguments if not autoctx.target.is_simple: resist = resist or autoctx.target.get_resist() immune = immune or autoctx.target.get_immune() vuln = vuln or autoctx.target.get_vuln() neutral = neutral or autoctx.target.get_neutral() # check if we actually need to run this damage roll (not in combat and roll is redundant) if autoctx.target.is_simple and self.is_meta(autoctx, True): return # add on combatant damage effects (#224) if autoctx.combatant: effect_d = '+'.join(autoctx.combatant.active_effects('d')) if effect_d: if d: d = f"{d}+{effect_d}" else: d = effect_d # check if we actually need to care about the -d tag if self.is_meta(autoctx): d = None # d was likely applied in the Roll effect already damage = autoctx.parse_annostr(damage) if autoctx.is_spell: if self.cantripScale: damage = autoctx.cantrip_scale(damage) if self.higher and not autoctx.get_cast_level() == autoctx.spell.level: higher = self.higher.get(str(autoctx.get_cast_level())) if higher: damage = f"{damage}+{higher}" # crit in_crit = autoctx.in_crit or crit roll_for = "Damage" if not in_crit else "Damage (CRIT!)" def parsecrit(damage_dice, wep=False): if in_crit: def critSub(matchobj): extracritdice = critdice if (critdice and wep) else 0 return f"{int(matchobj.group(1)) * 2 + extracritdice}d{matchobj.group(2)}" damage_dice = re.sub(r'(\d+)d(\d+)', critSub, damage_dice) return damage_dice # -mi # (#527) if mi: damage = re.sub(r'(\d+d\d+)', rf'\1mi{mi}', damage) # -d # if d: damage = parsecrit(damage, wep=not autoctx.is_spell) + '+' + parsecrit(d) else: damage = parsecrit(damage, wep=not autoctx.is_spell) # -c # if c and in_crit: damage = f"{damage}+{c}" # max if maxdmg: def maxSub(matchobj): return f"{matchobj.group(1)}d{matchobj.group(2)}mi{matchobj.group(2)}" damage = re.sub(r'(\d+)d(\d+)', maxSub, damage) damage = parse_resistances(damage, resist, immune, vuln, neutral) dmgroll = roll(damage, rollFor=roll_for, inline=True, show_blurbs=False) # output if not hide: autoctx.queue(dmgroll.result) else: autoctx.queue(f"**{roll_for}**: {dmgroll.consolidated()} = `{dmgroll.total}`") autoctx.add_pm(str(autoctx.ctx.author.id), dmgroll.result) autoctx.target.damage(autoctx, dmgroll.total, allow_overheal=self.overheal) # return metadata for scripting return {'damage': dmgroll.result, 'total': dmgroll.total, 'roll': dmgroll}
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 run(self, autoctx: AutomationContext): super(Attack, self).run(autoctx) # arguments args = autoctx.args adv = args.adv(ea=True, ephem=True) crit = args.last('crit', None, bool, ephem=True) and 1 hit = args.last('hit', None, bool, ephem=True) and 1 miss = (args.last('miss', None, bool, ephem=True) and not hit) and 1 b = args.join('b', '+', ephem=True) hide = args.last('h', type_=bool) reroll = args.last('reroll', 0, int) criton = args.last('criton', 20, int) ac = args.last('ac', None, int) # character-specific arguments if autoctx.character: if 'reroll' not in args: reroll = autoctx.character.get_setting('reroll', 0) if 'criton' not in args: criton = autoctx.character.get_setting('criton', 20) # check for combatant IEffect bonus (#224) if autoctx.combatant: effect_b = '+'.join(autoctx.combatant.active_effects('b')) if effect_b and b: b = f"{b}+{effect_b}" elif effect_b: b = effect_b attack_bonus = autoctx.ab_override or autoctx.caster.spellbook.sab # explicit bonus if self.bonus: explicit_bonus = autoctx.parse_annostr(self.bonus, is_full_expression=True) try: attack_bonus = int(explicit_bonus) except (TypeError, ValueError): raise AutomationException(f"{explicit_bonus} cannot be interpreted as an attack bonus.") if attack_bonus is None and b is None: raise NoAttackBonus() # tracking damage = 0 # roll attack against autoctx.target if not (hit or miss): formatted_d20 = '1d20' if adv == 1: formatted_d20 = '2d20kh1' elif adv == 2: formatted_d20 = '3d20kh1' elif adv == -1: formatted_d20 = '2d20kl1' if reroll: formatted_d20 = f"{formatted_d20}ro{reroll}" to_hit_message = 'To Hit' if ac: to_hit_message = f'To Hit (AC {ac})' if b: toHit = roll(f"{formatted_d20}+{attack_bonus}+{b}", rollFor=to_hit_message, inline=True, show_blurbs=False) else: toHit = roll(f"{formatted_d20}+{attack_bonus}", rollFor=to_hit_message, inline=True, show_blurbs=False) # crit processing try: d20_value = next(p for p in toHit.raw_dice.parts if isinstance(p, SingleDiceGroup) and p.max_value == 20).get_total() except (StopIteration, AttributeError): d20_value = 0 if d20_value >= criton: itercrit = 1 else: itercrit = toHit.crit # -ac # target_has_ac = not autoctx.target.is_simple and autoctx.target.ac is not None if target_has_ac: ac = ac or autoctx.target.ac if itercrit == 0 and ac: if toHit.total < ac: itercrit = 2 # miss! # output if not hide: # not hidden autoctx.queue(toHit.result) elif target_has_ac: # hidden if itercrit == 2: hit_type = 'MISS' elif itercrit == 1: hit_type = 'CRIT' else: hit_type = 'HIT' autoctx.queue(f"**To Hit**: {formatted_d20}... = `{hit_type}`") autoctx.add_pm(str(autoctx.ctx.author.id), toHit.result) else: # hidden, no ac autoctx.queue(f"**To Hit**: {formatted_d20}... = `{toHit.total}`") autoctx.add_pm(str(autoctx.ctx.author.id), toHit.result) if itercrit == 2: damage += self.on_miss(autoctx) elif itercrit == 1: damage += self.on_crit(autoctx) else: damage += self.on_hit(autoctx) elif hit: autoctx.queue(f"**To Hit**: Automatic hit!") if crit: damage += self.on_crit(autoctx) else: damage += self.on_hit(autoctx) else: autoctx.queue(f"**To Hit**: Automatic miss!") damage += self.on_miss(autoctx) return {"total": damage}
def sheet_cast(spell, args, embed=None): if embed is None: embed = discord.Embed() spell_level = int(spell.get('level', 0)) spell_type = spell.get('type') # save, attack, special cast_level = args.last('l', spell_level, int) caster_name = args.last('name', 'Unnamed') phrase = args.join('phrase', '\n') dc = args.last('dc', type_=int) # save DC (int) save_skill = args.last('save') or spell.get('save', {}).get( 'save') # str, dex, etc (optional) caster_level = args.last('casterlevel', 1, int) # for cantrip scaling d = args.join('d', '+') spell_ab = sum(args.get('ab', type_=int)) casting_mod = sum(args.get('SPELL', type_=int)) total_damage = 0 upcast_dmg = None if not cast_level == spell_level: upcast_dmg = spell.get('higher_levels', {}).get(str(cast_level)) if phrase: # parse phrase embed.description = '*' + phrase + '*' else: embed.description = '~~' + ' ' * 500 + '~~' embed.title = '{} casts {}!'.format(caster_name, spell['name']) if spell_type == 'save': # save spell if not dc: raise NoSpellDC try: save_skill = next(s for s in ('strengthSave', 'dexteritySave', 'constitutionSave', 'intelligenceSave', 'wisdomSave', 'charismaSave') if save_skill.lower() in s.lower()) except StopIteration: raise InvalidSaveType save = spell['save'] if save['damage'] is None: # save against effect embed.add_field(name="DC", value=str(dc) + "\n{} Save".format(save_skill[:3].upper())) else: # damage spell dmg = save['damage'].replace("SPELL", str(casting_mod)) if spell['level'] == '0' and spell.get('scales', True): def lsub(matchobj): level = caster_level if level < 5: levelDice = "1" elif level < 11: levelDice = "2" elif level < 17: levelDice = "3" else: levelDice = "4" return levelDice + 'd' + matchobj.group(2) dmg = re.sub(r'(\d+)d(\d+)', lsub, dmg) if upcast_dmg: dmg = dmg + '+' + upcast_dmg if d: dmg = dmg + '+' + d dmgroll = roll(dmg, rollFor="Damage", inline=True, show_blurbs=False) embed.add_field(name="Damage/DC", value=dmgroll.result + "\n**DC:** {}\n{} Save".format( str(dc), save_skill[:3].upper())) total_damage = dmgroll.total elif spell['type'] == 'attack': # attack spell attack = copy.copy(spell['atk']) attack['attackBonus'] = str(spell_ab) if not attack['attackBonus']: raise NoSpellAB if spell['level'] == '0' and spell.get('scales', True): def lsub(matchobj): level = caster_level if level < 5: levelDice = "1" elif level < 11: levelDice = "2" elif level < 17: levelDice = "3" else: levelDice = "4" return levelDice + 'd' + matchobj.group(2) attack['damage'] = re.sub(r'(\d+)d(\d+)', lsub, attack['damage']) if upcast_dmg: attack['damage'] = attack['damage'] + '+' + upcast_dmg attack['damage'] = attack['damage'].replace("SPELL", str(casting_mod)) result = sheet_attack(attack, args) total_damage = result['total_damage'] for f in result['embed'].fields: embed.add_field(name=f.name, value=f.value, inline=f.inline) else: # special spell (MM/heal) attack = { "name": spell['name'], "damage": spell.get("damage", "0").replace('SPELL', str(casting_mod)), "attackBonus": None } if upcast_dmg: attack['damage'] = attack['damage'] + '+' + upcast_dmg result = sheet_attack(attack, args) total_damage = result['total_damage'] for f in result['embed'].fields: embed.add_field(name=f.name, value=f.value, inline=f.inline) spell_ctx = spell_context(spell) if spell_ctx: embed.add_field(name='Effect', value=spell_ctx) return {'embed': embed, 'total_damage': total_damage}
def default_curly_func(s): curlyout = "" for substr in re.split(ops, s): temp = substr.strip() curlyout += str(self.names.get(temp, temp)) + " " return str(roll(curlyout).total)
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
def stat_gen(): stats = [roll('4d6kh3').total for _ in range(6)] return stats
def test_infinite_loops(): r = roll("1d1e1") assert r.total == 251 # 1 + 250 rerolls
def _run_common(skill, args, embed, mod_override=None, rr_format="Check {}"): # 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, inline=True) if dc and result.total >= dc: num_successes += 1 results.append(result.total) # output if iterations > 1: embed.add_field(name=rr_format.format(str(i + 1)), value=result.skeleton) else: desc_out.append(result.skeleton) # 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=args.last('thumb')) return results
async def join(self, ctx, *, args: str = ''): """Adds the current active character to combat. A character must be loaded through the SheetManager module first. Args: adv/dis -b [conditional bonus] -phrase [flavor text] -p [init value] -h (same as !init add) --group (same as !init add)""" char: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(char, False) embed.colour = char.get_color() args = shlex.split(args) args = argparse(args) adv = args.adv(boolwise=True) b = args.join('b', '+') or None p = args.last('p', type_=int) phrase = args.join('phrase', '\n') or None group = args.last('group') if p is None: roll_str = char.skills.initiative.d20(base_adv=adv) if b: roll_str = f"{roll_str}+{b}" check_roll = roll(roll_str, inline=True) embed.title = '{} makes an Initiative check!'.format(char.name) embed.description = check_roll.skeleton + ( '\n*' + phrase + '*' if phrase is not None else '') init = check_roll.total else: init = p embed.title = "{} already rolled initiative!".format(char.name) embed.description = "Placed at initiative `{}`.".format(init) controller = str(ctx.author.id) private = args.last('h', type_=bool) bonus = char.skills.initiative.value combat = await Combat.from_ctx(ctx) me = await PlayerCombatant.from_character(char.name, controller, init, bonus, char.ac, private, char.get_resists(), ctx, combat, char.upstream, str(ctx.author.id), char) if combat.get_combatant(char.name) is not None: await ctx.send("Combatant already exists.") return if group is None: combat.add_combatant(me) embed.set_footer(text="Added to combat!") else: grp = combat.get_group(group, create=init) grp.add_combatant(me) embed.set_footer(text=f"Joined group {grp.name}!") await combat.final() await ctx.send(embed=embed) await char.commit(ctx)
def test_roll(): assert type(roll("1d20")) == DiceResult assert 0 < roll("1d20").total < 21 assert roll("3+4*(9-2)").total == 31
async def madd(self, ctx, monster_name: str, *args): """Adds a monster to combat. Args: adv/dis -b [conditional bonus] -n [number of monsters] -p [init value] -name [name scheme, use "#" for auto-numbering, ex. "Orc#"] -h (same as !init add, default true) -group (same as !init add) -npr (removes physical resistances when added) -rollhp (rolls monster HP) -hp [starting hp] -ac [starting ac]""" monster = await select_monster_full(ctx, monster_name, pm=True) self.bot.rdb.incr("monsters_looked_up_life") args = argparse(args) private = not args.last('h', type_=bool) group = args.last('group') adv = args.adv() b = args.join('b', '+') p = args.last('p', type_=int) rollhp = args.last('rollhp', False, bool) hp = args.last('hp', type_=int) ac = args.last('ac', type_=int) npr = args.last('npr', type_=bool) n = args.last('n', 1, int) name_template = args.last('name', monster.name[:2].upper() + '#') init_mod = monster.skills.initiative.value opts = {} if npr: opts['npr'] = True combat = await Combat.from_ctx(ctx) out = '' to_pm = '' recursion = 25 if n > 25 else 1 if n < 1 else n name_num = 1 for i in range(recursion): name = name_template.replace('#', str(name_num)) raw_name = name_template to_continue = False while combat.get_combatant( name ) and name_num < 100: # keep increasing to avoid duplicates if '#' in raw_name: name_num += 1 name = raw_name.replace('#', str(name_num)) else: out += "Combatant already exists.\n" to_continue = True break if to_continue: continue try: check_roll = None # to make things happy if p is None: if b: check_roll = roll(f'1d20{init_mod:+}+{b}', adv=adv, inline=True) else: check_roll = roll(f'1d20{init_mod:+}', adv=adv, inline=True) init = check_roll.total else: init = int(p) controller = str(ctx.author.id) rolled_hp = None if rollhp: rolled_hp = roll(monster.hitdice, inline=True) to_pm += f"{name} began with {rolled_hp.skeleton} HP.\n" rolled_hp = max(rolled_hp.total, 1) me = MonsterCombatant.from_monster(name, controller, init, init_mod, private, monster, ctx, combat, opts, hp=hp or rolled_hp, ac=ac) if group is None: combat.add_combatant(me) out += f"{name} was added to combat with initiative {check_roll.skeleton if p is None else p}.\n" else: grp = combat.get_group(group, create=init) grp.add_combatant(me) out += f"{name} was added to combat with initiative {grp.init} as part of group {grp.name}.\n" except Exception as e: log.error('\n'.join( traceback.format_exception(type(e), e, e.__traceback__))) out += "Error adding combatant: {}\n".format(e) await combat.final() await ctx.send(out, delete_after=15) if to_pm: await ctx.author.send(to_pm)
def sheet_damage(damage_str, args, itercrit=0, dnum=None): total_damage = 0 out = "" if dnum is None: dnum = {} d = args.join('d', '+') crittype = args.last('crittype', 'default') c = args.join('c', '+') critdice = args.last('critdice', 0, int) showmiss = args.last('showmiss', False, bool) resist = args.get('resist') immune = args.get('immune') vuln = args.get('vuln') neutral = args.get('neutral') maxdmg = args.last('max', None, bool) mi = args.last('mi', None, int) if damage_str is None and d: damage_str = '0' dmgroll = None if damage_str is not None: def parsecrit(damage_str, wep=False): if itercrit == 1: if crittype == '2x': critDice = f"({damage_str})*2" if c: critDice += '+' + c else: def critSub(matchobj): extracritdice = critdice if critdice and wep else 0 return f"{int(matchobj.group(1)) * 2 + extracritdice}d{matchobj.group(2)}" critDice = re.sub(r'(\d+)d(\d+)', critSub, damage_str) else: critDice = damage_str return critDice if mi: damage_str = re.sub(r'(\d+d\d+)', rf'\1mi{mi}', damage_str) # -d, -d# parsing if d: damage = parsecrit(damage_str, wep=True) + '+' + parsecrit(d) else: damage = parsecrit(damage_str, wep=True) for dice, numHits in dnum.items(): if not itercrit == 2 and numHits > 0: damage += '+' + parsecrit(dice) dnum[dice] -= 1 if maxdmg: def maxSub(matchobj): return f"{matchobj.group(1)}d{matchobj.group(2)}mi{matchobj.group(2)}" damage = re.sub(r'(\d+)d(\d+)', maxSub, damage) # crit parsing rollFor = "Damage" if itercrit == 1: if c: damage += '+' + c rollFor = "Damage (CRIT!)" elif itercrit == 2: rollFor = "Damage (Miss!)" # resist parsing damage = parse_resistances(damage, resist, immune, vuln, neutral) # actual roll if itercrit == 2 and not showmiss: 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 return {'damage': out, 'total': total_damage, 'roll': dmgroll}
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
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() 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 ctx.send('That\'s not a valid check.') embed = discord.Embed() embed.colour = random.randint(0, 0xffffff) args = await scripting.parse_snippets(args, ctx) 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() 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, 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')) 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 _old_cast(self, ctx, spell_name, args): spell = getSpell(spell_name) self.bot.rdb.incr('spells_looked_up_life') if spell is None: return await self.bot.say("Spell not found.", delete_after=15) if spell.get('source') == "UAMystic": return await self.bot.say("Mystic talents are not supported.") char = await Character.from_ctx(ctx) args = await scripting.parse_snippets(args, ctx) args = await char.parse_cvars(args, ctx) args = shlex.split(args) args = argparse(args) can_cast = True spell_level = int(spell.get('level', 0)) cast_level = args.last('l', spell_level, int) if not spell_level <= cast_level <= 9: return await self.bot.say("Invalid spell level.") # make sure we can cast it if not char.get_remaining_slots( cast_level) > 0 and spell_name in char.get_spell_list(): can_cast = False if args.last('i', type_=bool): can_cast = True if not can_cast: embed = EmbedWithCharacter(char) embed.title = "Cannot cast spell!" embed.description = "Not enough spell slots remaining, or spell not in known spell list!\n" \ "Use `!game longrest` to restore all spell slots, or pass `-i` to ignore restrictions." if cast_level > 0: embed.add_field(name="Spell Slots", value=char.get_remaining_slots_str(cast_level)) return await self.bot.say(embed=embed) if len(args) == 0: rolls = spell.get('roll', None) if isinstance(rolls, list): rolls = '\n'.join(rolls) \ .replace('SPELL', str(char.get_spell_ab() - char.get_prof_bonus())) \ .replace('PROF', str(char.get_prof_bonus())) rolls = rolls.split('\n') out = "**{} casts {}:** ".format( char.get_name(), spell['name']) + '\n'.join( roll(r, inline=True).skeleton for r in rolls) elif rolls is not None: rolls = rolls \ .replace('SPELL', str(char.get_spell_ab() - char.get_prof_bonus())) \ .replace('PROF', str(char.get_prof_bonus())) out = "**{} casts {}:** ".format( char.get_name(), spell['name']) + roll( rolls, inline=True).skeleton else: out = "**{} casts {}!** ".format(char.get_name(), spell['name']) else: rolls = args.get('r') roll_results = "" for r in rolls: res = roll(r, inline=True) if res.total is not None: roll_results += res.result + '\n' else: roll_results += "**Effect:** " + r out = "**{} casts {}:**\n".format(char.get_name(), spell['name']) + roll_results if not args.last('i', type_=bool): char.use_slot(cast_level) if cast_level > 0: out += f"\n**Remaining Spell Slots**: {char.get_remaining_slots_str(cast_level)}" out = "Spell not supported by new cast, falling back to old cast.\n" + out await char.commit(ctx) # make sure we save changes await self.bot.say(out) spell_cmd = self.bot.get_command('spell') if spell_cmd is None: return await self.bot.say("Lookup cog not loaded.") await ctx.invoke(spell_cmd, name=spell['name'])
async def monster_save(self, ctx, monster_name, save, *, 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() args = await scripting.parse_snippets(args, ctx) 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 ctx.send('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]) if not args.last('h', type_=bool): default_title = f'{monster_name} makes {a_or_an(camel_to_title(save))}!' else: default_title = f"An unknown creature 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')) 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
def simple_roll(rollStr): return roll(rollStr).total
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 ("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_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
def verbose_roll(rollStr): rolled = roll(rollStr, inline=True) return SimpleRollResult(rolled.rolled, rolled.total, rolled.skeleton, [part.to_dict() for part in rolled.raw_dice.parts])
def run(self, autoctx: AutomationContext): super(Attack, self).run(autoctx) args = autoctx.args 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 rr = min(args.last('rr', 1, int), 25) b = args.join('b', '+') reroll = args.last('reroll', 0, int) criton = args.last('criton', 20, int) # check for combatant IEffect bonus (#224) if autoctx.combatant: effect_b = '+'.join(autoctx.combatant.active_effects('b')) if effect_b: if b: b = f"{b}+{effect_b}" else: b = effect_b explicit_bonus = None if self.bonus: explicit_bonus = autoctx.evaluator.parse(self.bonus, autoctx.metavars) try: explicit_bonus = int(explicit_bonus) except (TypeError, ValueError): raise AutomationException( f"{explicit_bonus} cannot be interpreted as an attack bonus." ) sab = explicit_bonus or autoctx.ab_override or autoctx.caster.spellcasting.sab if not sab: raise NoSpellAB() # roll attack(s) against autoctx.target for iteration in range(rr): if rr > 1: autoctx.queue(f"**Attack {iteration + 1}**") if not (hit or miss): formatted_d20 = '1d20' if adv == 1: formatted_d20 = '2d20kh1' elif adv == 2: formatted_d20 = '3d20kh1' elif adv == -1: formatted_d20 = '2d20kl1' if reroll: formatted_d20 = f"{formatted_d20}ro{reroll}" if b: toHit = roll(f"{formatted_d20}+{sab}+{b}", rollFor='To Hit', inline=True, show_blurbs=False) else: toHit = roll(f"{formatted_d20}+{sab}", rollFor='To Hit', inline=True, show_blurbs=False) autoctx.queue(toHit.result) # crit processing try: d20_value = next(p for p in toHit.raw_dice.parts if isinstance(p, SingleDiceGroup) and p.max_value == 20).get_total() except StopIteration: d20_value = 0 if d20_value >= criton: itercrit = 1 else: itercrit = toHit.crit if autoctx.target.target and autoctx.target.ac is not None: if toHit.total < autoctx.target.ac and itercrit == 0: itercrit = 2 # miss! if itercrit == 2: self.on_miss(autoctx) elif itercrit == 1: self.on_crit(autoctx) else: self.on_hit(autoctx) elif hit: autoctx.queue(f"**To Hit**: Automatic hit!") if crit: self.on_crit(autoctx) else: self.on_hit(autoctx) else: autoctx.queue(f"**To Hit**: Automatic miss!") self.on_miss(autoctx)
def run(self, autoctx): super(Damage, self).run(autoctx) args = autoctx.args damage = self.damage d = args.join('d', '+') c = args.join('c', '+') resist = args.get('resist', []) immune = args.get('immune', []) vuln = args.get('vuln', []) neutral = args.get('neutral', []) crit = args.last('crit', None, bool) maxdmg = args.last('max', None, bool) mi = args.last('mi', None, int) if autoctx.target.target: resist = resist or autoctx.target.get_resist() immune = immune or autoctx.target.get_immune() vuln = vuln or autoctx.target.get_vuln() neutral = neutral or autoctx.target.get_neutral() # check if we actually need to run this damage roll (not in combat and roll is redundant) if not autoctx.target.target and self.is_meta(autoctx, True): return # add on combatant damage effects (#224) if autoctx.combatant: effect_d = '+'.join(autoctx.combatant.active_effects('d')) if effect_d: if d: d = f"{d}+{effect_d}" else: d = effect_d # check if we actually need to care about the -d tag if self.is_meta(autoctx): d = None # d was likely applied in the Roll effect already damage = autoctx.parse_annostr(damage) if self.cantripScale: damage = autoctx.cantrip_scale(damage) if self.higher and not autoctx.get_cast_level() == autoctx.spell.level: higher = self.higher.get(str(autoctx.get_cast_level())) if higher: damage = f"{damage}+{higher}" # -mi # (#527) if mi: damage = re.sub(r'(\d+d\d+)', rf'\1mi{mi}', damage) if d: damage = f"{damage}+{d}" roll_for = "Damage" if autoctx.in_crit or crit: def critSub(matchobj): return f"{int(matchobj.group(1)) * 2}d{matchobj.group(2)}" damage = re.sub(r'(\d+)d(\d+)', critSub, damage) roll_for = "Damage (CRIT!)" if c: damage = f"{damage}+{c}" if maxdmg: def maxSub(matchobj): return f"{matchobj.group(1)}d{matchobj.group(2)}mi{matchobj.group(2)}" damage = re.sub(r'(\d+)d(\d+)', maxSub, damage) damage = parse_resistances(damage, resist, immune, vuln, neutral) dmgroll = roll(damage, rollFor=roll_for, inline=True, show_blurbs=False) autoctx.queue(dmgroll.result) autoctx.target.damage(autoctx, dmgroll.total)