def run(self, autoctx): super(TempHP, self).run(autoctx) args = autoctx.args amount = self.amount maxdmg = args.last('max', None, bool, ephem=True) # 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 amount = autoctx.parse_annostr(amount) dice_ast = copy.copy(d20.parse(amount)) dice_ast = _upcast_scaled_dice(self, autoctx, dice_ast) if maxdmg: dice_ast = d20.utils.tree_map(_max_mapper, dice_ast) dmgroll = roll(dice_ast) autoctx.queue(f"**THP**: {dmgroll.result}") if autoctx.target.combatant: autoctx.target.combatant.temp_hp = max(dmgroll.total, 0) autoctx.footer_queue("{}: {}".format( autoctx.target.combatant.name, autoctx.target.combatant.hp_str())) elif autoctx.target.character: autoctx.target.character.temp_hp = max(dmgroll.total, 0) autoctx.footer_queue("{}: {}".format( autoctx.target.character.name, autoctx.target.character.hp_str()))
def _upcast_scaled_dice(effect, autoctx, dice_ast): """Scales the dice of the cast to its appropriate amount (handling cantrip scaling and higher level addition).""" if autoctx.is_spell: if effect.cantripScale: level = autoctx.caster.spellbook.caster_level if level < 5: level_dice = 1 elif level < 11: level_dice = 2 elif level < 17: level_dice = 3 else: level_dice = 4 def mapper(node): if isinstance(node, d20.ast.Dice): node.num = level_dice return node dice_ast = d20.utils.tree_map(mapper, dice_ast) if effect.higher and not autoctx.get_cast_level( ) == autoctx.spell.level: higher = effect.higher.get(str(autoctx.get_cast_level())) if higher: higher_ast = d20.parse(higher) dice_ast.roll = d20.ast.BinOp(dice_ast.roll, '+', higher_ast.roll) return dice_ast
async def _roll_many(self, ctx, iterations, roll_str, dc=None, adv=None): if iterations < 1 or iterations > 100: return await ctx.send("Too many or too few iterations.") if adv is None: adv = d20.AdvType.NONE results = [] successes = 0 ast = d20.parse(roll_str) roller = d20.Roller(context=PersistentRollContext()) for _ in range(iterations): res = roller.roll(ast, advantage=adv) if dc is not None and res.total >= dc: successes += 1 results.append(res) if dc is None: header = f"Rolling {iterations} iterations..." footer = f"{sum(o.total for o in results)} total." else: header = f"Rolling {iterations} iterations, DC {dc}..." footer = f"{successes} successes, {sum(o.total for o in results)} total." result_strs = '\n'.join([str(o) for o in results]) out = f"{header}\n{result_strs}\n{footer}" if len(out) > 1500: one_result = str(results[0])[:100] one_result = f"{one_result}..." if len(one_result) > 100 else one_result out = f"{header}\n{one_result}\n{footer}" await try_delete(ctx.message) await ctx.send(f"{ctx.author.mention}\n{out}") await Stats.increase_stat(ctx, "dice_rolled_life")
def run(self, autoctx): super(Roll, self).run(autoctx) d = autoctx.args.join('d', '+', ephem=True) maxdmg = autoctx.args.last('max', None, bool, ephem=True) mi = autoctx.args.last('mi', None, int) # 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 dice_ast = copy.copy(d20.parse(autoctx.parse_annostr(self.dice))) dice_ast = upcast_scaled_dice(self, autoctx, dice_ast) if not self.hidden: # -mi # (#527) if mi: dice_ast = d20.utils.tree_map(mi_mapper(mi), dice_ast) if d: d_ast = d20.parse(d) dice_ast.roll = d20.ast.BinOp(dice_ast.roll, '+', d_ast.roll) if maxdmg: dice_ast = d20.utils.tree_map(max_mapper, dice_ast) rolled = roll(dice_ast) if not self.hidden: autoctx.meta_queue(f"**{self.name.title()}**: {rolled.result}") simplified_expr = copy.deepcopy(rolled.expr) d20.utils.simplify_expr(simplified_expr) simplified = RerollableStringifier().stringify(simplified_expr.roll) autoctx.metavars[self.name] = simplified autoctx.metavars['lastRoll'] = rolled.total # #1335 return RollResult(result=rolled.total, roll=rolled, simplified=simplified, hidden=self.hidden)
async def _snippet_before_edit(ctx, name=None, delete=False): if delete: return confirmation = None # special arg checking if not name: return name = name.lower() if name in SPECIAL_ARGS or name.startswith('-'): confirmation = f"**Warning:** Creating a snippet named `{name}` will prevent you from using the built-in `{name}` argument in Avrae commands.\nAre you sure you want to create this snippet? (Reply with yes/no)" # roll string checking try: d20.parse(name) except d20.RollSyntaxError: pass else: confirmation = f"**Warning:** Creating a snippet named `{name}` might cause hidden problems if you try to use the same roll in other commands.\nAre you sure you want to create this snippet? (Reply with yes/no)" if confirmation is not None: if not await confirm(ctx, confirmation): raise InvalidArgument('Ok, cancelling.')
async def _roll_many(ctx, iterations, roll_str, dc=None, adv=None): if iterations < 1 or iterations > 100: return await ctx.send("Too many or too few iterations.") if adv is None: adv = d20.AdvType.NONE results = [] successes = 0 ast = d20.parse(roll_str, allow_comments=True) roller = d20.Roller(context=PersistentRollContext()) for _ in range(iterations): res = roller.roll(ast, advantage=adv) if dc is not None and res.total >= dc: successes += 1 results.append(res) if dc is None: header = f"Rolling {iterations} iterations..." footer = f"{sum(o.total for o in results)} total." else: header = f"Rolling {iterations} iterations, DC {dc}..." footer = f"{successes} successes, {sum(o.total for o in results)} total." if ast.comment: header = f"{ast.comment}: {header}" result_strs = '\n'.join(str(o) for o in results) out = f"{header}\n{result_strs}\n{footer}" if len(out) > 1500: one_result = str(results[0]) out = f"{header}\n{one_result}\n[{len(results) - 1} results omitted for output size.]\n{footer}" await try_delete(ctx.message) await ctx.send( f"{ctx.author.mention}\n{out}", allowed_mentions=discord.AllowedMentions(users=[ctx.author])) await Stats.increase_stat(ctx, "dice_rolled_life")
def run(self, autoctx): super(TempHP, self).run(autoctx) if autoctx.target is None: raise TargetException( "Tried to add temp HP without a target! Make sure all TempHP effects are inside " "of a Target effect.") args = autoctx.args amount = self.amount maxdmg = args.last('max', None, bool, ephem=True) # 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 amount = autoctx.parse_annostr(amount) dice_ast = copy.copy(d20.parse(amount)) dice_ast = utils.upcast_scaled_dice(self, autoctx, dice_ast) if maxdmg: dice_ast = d20.utils.tree_map(utils.max_mapper, dice_ast) dmgroll = d20.roll(dice_ast) thp_amount = max(dmgroll.total, 0) autoctx.queue(f"**THP**: {dmgroll.result}") autoctx.metavars['lastTempHp'] = thp_amount # #1335 if autoctx.target.combatant: autoctx.target.combatant.temp_hp = thp_amount autoctx.footer_queue("{}: {}".format( autoctx.target.combatant.name, autoctx.target.combatant.hp_str())) elif autoctx.target.character: autoctx.target.character.temp_hp = thp_amount autoctx.footer_queue("{}: {}".format( autoctx.target.character.name, autoctx.target.character.hp_str())) return TempHPResult(amount=thp_amount, amount_roll=dmgroll)
def run(self, autoctx): super(Damage, self).run(autoctx) # general arguments args = autoctx.args damage = self.damage resistances = Resistances() d_args = args.get('d', [], ephem=True) c_args = args.get('c', [], ephem=True) crit_arg = args.last('crit', None, bool, ephem=True) nocrit = args.last('nocrit', default=False, type_=bool, ephem=True) max_arg = args.last('max', None, bool, ephem=True) magic_arg = args.last('magical', None, bool, ephem=True) mi_arg = args.last('mi', None, int) dtype_args = args.get('dtype', [], ephem=True) 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: resistances = autoctx.target.get_resists().copy() resistances.update(Resistances.from_args(args, ephem=True)) # 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: d_args.extend(autoctx.combatant.active_effects('d')) # check if we actually need to care about the -d tag if self.is_meta(autoctx): d_args = [] # d was likely applied in the Roll effect already # set up damage AST damage = autoctx.parse_annostr(damage) dice_ast = copy.copy(d20.parse(damage)) dice_ast = _upcast_scaled_dice(self, autoctx, dice_ast) # -mi # (#527) if mi_arg: dice_ast = d20.utils.tree_map(_mi_mapper(mi_arg), dice_ast) # -d # for d_arg in d_args: d_ast = d20.parse(d_arg) dice_ast.roll = d20.ast.BinOp(dice_ast.roll, '+', d_ast.roll) # crit # nocrit (#1216) in_crit = (autoctx.in_crit or crit_arg) and not nocrit roll_for = "Damage" if not in_crit else "Damage (CRIT!)" if in_crit: dice_ast = d20.utils.tree_map(_crit_mapper, dice_ast) if critdice and not autoctx.is_spell: # add X critdice to the leftmost node if it's dice left = d20.utils.leftmost(dice_ast) if isinstance(left, d20.ast.Dice): left.num += int(critdice) # -c # if in_crit: for c_arg in c_args: c_ast = d20.parse(c_arg) dice_ast.roll = d20.ast.BinOp(dice_ast.roll, '+', c_ast.roll) # max if max_arg: dice_ast = d20.utils.tree_map(_max_mapper, dice_ast) # evaluate damage dmgroll = roll(dice_ast) # magic arg (#853) always = {'magical'} if (autoctx.is_spell or magic_arg) else None # dtype transforms/overrides (#876) transforms = {} for dtype in dtype_args: if '>' in dtype: *froms, to = dtype.split('>') for frm in froms: transforms[frm.strip()] = to.strip() else: transforms[None] = dtype # display damage transforms (#1103) if None in transforms: autoctx.meta_queue(f"**Damage Type**: {transforms[None]}") elif transforms: for frm in transforms: autoctx.meta_queue( f"**Damage Change**: {frm} > {transforms[frm]}") # evaluate resistances do_resistances(dmgroll.expr, resistances, always, transforms) # generate output result = d20.MarkdownStringifier().stringify(dmgroll.expr) # output if not hide: autoctx.queue(f"**{roll_for}**: {result}") else: d20.utils.simplify_expr(dmgroll.expr) autoctx.queue( f"**{roll_for}**: {d20.MarkdownStringifier().stringify(dmgroll.expr)}" ) autoctx.add_pm(str(autoctx.ctx.author.id), f"**{roll_for}**: {result}") autoctx.target.damage(autoctx, dmgroll.total, allow_overheal=self.overheal) # return metadata for scripting return { 'damage': f"**{roll_for}**: {result}", 'total': dmgroll.total, 'roll': dmgroll }
def new(cls, character, name, minv=None, maxv=None, reset=None, display_type=None, live_id=None, reset_to=None, reset_by=None): if reset not in ('short', 'long', 'none', None): raise InvalidArgument("Invalid reset.") if any(c in name for c in ".$"): raise InvalidArgument("Invalid character in CC name.") if display_type == 'bubble' and (maxv is None or minv is None): raise InvalidArgument( "Bubble display requires a max and min value.") # sanity checks if maxv is None and reset not in ('none', None): raise InvalidArgument("Reset passed but no maximum passed.") if reset_to is not None and reset_by is not None: raise InvalidArgument( "Both `resetto` and `resetby` arguments found.") min_value = None if minv is not None: min_value = character.evaluate_math(minv) max_value = None if maxv is not None: max_value = character.evaluate_math(maxv) if min_value is not None and max_value < min_value: raise InvalidArgument("Max value is less than min value.") if max_value == 0: raise InvalidArgument("Max value cannot be 0.") reset_to_value = None if reset_to is not None: reset_to_value = character.evaluate_math(reset_to) if min_value is not None and reset_to_value < min_value: raise InvalidArgument("Reset to value is less than min value.") if max_value is not None and reset_to_value > max_value: raise InvalidArgument( "Reset to value is greater than max value.") if reset_by is not None: try: d20.parse(reset_by) except d20.RollSyntaxError: raise InvalidArgument( f"{reset_by} (`resetby`) cannot be interpreted as a number or dice string." ) # set initial value initial_value = max(0, min_value or 0) if reset_to_value is not None: initial_value = reset_to_value elif max_value is not None: initial_value = max_value return cls(character, name.strip(), initial_value, minv, maxv, reset, display_type, live_id, reset_to, reset_by)
def new(cls, character, name, minv=None, maxv=None, reset=None, display_type=None, live_id=None, reset_to=None, reset_by=None, title=None, desc=None): if reset not in ('short', 'long', 'none', None): raise InvalidArgument("Invalid reset.") if any(c in name for c in ".$"): raise InvalidArgument("Invalid character in CC name.") if display_type == 'bubble' and (maxv is None or minv is None): raise InvalidArgument( "Bubble display requires a max and min value.") # sanity checks if reset not in ('none', None) and (maxv is None and reset_to is None and reset_by is None): raise InvalidArgument( "Reset passed but no valid reset value (`max`, `resetto`, `resetby`) passed." ) if reset_to is not None and reset_by is not None: raise InvalidArgument( "Both `resetto` and `resetby` arguments found.") if not name.strip(): raise InvalidArgument("The name of the counter can not be empty.") min_value = None if minv is not None: min_value = character.evaluate_math(minv) max_value = None if maxv is not None: max_value = character.evaluate_math(maxv) if min_value is not None and max_value < min_value: raise InvalidArgument("Max value is less than min value.") if max_value == 0: raise InvalidArgument("Max value cannot be 0.") reset_to_value = None if reset_to is not None: reset_to_value = character.evaluate_math(reset_to) if min_value is not None and reset_to_value < min_value: raise InvalidArgument("Reset to value is less than min value.") if max_value is not None and reset_to_value > max_value: raise InvalidArgument( "Reset to value is greater than max value.") if reset_by is not None: try: d20.parse(str(reset_by)) except d20.RollSyntaxError: raise InvalidArgument( f"{reset_by} (`resetby`) cannot be interpreted as a number or dice string." ) # set initial value initial_value = max(0, min_value or 0) if reset_to_value is not None: initial_value = reset_to_value elif max_value is not None: initial_value = max_value # length checks if desc and len(desc) > 1024: raise InvalidArgument( 'Description must be less than 1024 characters.') if title and len(title) >= 256: raise InvalidArgument('Title must be less than 256 characters.') if len(name) > 256: raise InvalidArgument('Name must be less than 256 characters.') return cls(character, name.strip(), initial_value, minv, maxv, reset, display_type, live_id, reset_to, reset_by, title, desc)
def run(self, autoctx): super().run(autoctx) if autoctx.target is None: raise TargetException( "Tried to do damage without a target! Make sure all Damage effects are inside " "of a Target effect." ) # general arguments args = autoctx.args damage = self.damage resistances = Resistances() d_args = args.get('d', [], ephem=True) c_args = args.get('c', [], ephem=True) crit_arg = args.last('crit', None, bool, ephem=True) nocrit = args.last('nocrit', default=False, type_=bool, ephem=True) max_arg = args.last('max', None, bool, ephem=True) magic_arg = args.last('magical', None, bool, ephem=True) silvered_arg = args.last('silvered', None, bool, ephem=True) mi_arg = args.last('mi', None, int) dtype_args = args.get('dtype', [], ephem=True) critdice = sum(args.get('critdice', type_=int)) hide = args.last('h', type_=bool) # character-specific arguments if autoctx.character and 'critdice' not in args: critdice = autoctx.character.options.extra_crit_dice # combat-specific arguments if not autoctx.target.is_simple: resistances = autoctx.target.get_resists().copy() resistances.update(Resistances.from_args(args, ephem=True)) # 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): return # add on combatant damage effects (#224) if autoctx.combatant: d_args.extend(autoctx.combatant.active_effects('d')) # check if we actually need to care about the -d tag if self.contains_roll_meta(autoctx): d_args = [] # d was likely applied in the Roll effect already # set up damage AST damage = autoctx.parse_annostr(damage) dice_ast = copy.copy(d20.parse(damage)) dice_ast = utils.upcast_scaled_dice(self, autoctx, dice_ast) # -mi # (#527) if mi_arg: dice_ast = d20.utils.tree_map(utils.mi_mapper(mi_arg), dice_ast) # -d # for d_arg in d_args: d_ast = d20.parse(d_arg) dice_ast.roll = d20.ast.BinOp(dice_ast.roll, '+', d_ast.roll) # crit # nocrit (#1216) # Disable critical damage in saves (#1556) in_crit = (autoctx.in_crit or crit_arg) and not (nocrit or autoctx.in_save) if in_crit: dice_ast = d20.utils.tree_map(utils.crit_mapper, dice_ast) if critdice and not autoctx.is_spell: # add X critdice to the leftmost node if it's dice left = d20.utils.leftmost(dice_ast) if isinstance(left, d20.ast.Dice): left.num += int(critdice) # -c # if in_crit: for c_arg in c_args: c_ast = d20.parse(c_arg) dice_ast.roll = d20.ast.BinOp(dice_ast.roll, '+', c_ast.roll) # max if max_arg: dice_ast = d20.utils.tree_map(utils.max_mapper, dice_ast) # evaluate damage dmgroll = d20.roll(dice_ast) # magic arg (#853), magical effect (#1063) # silvered arg (#1544) always = set() magical_effect = autoctx.combatant and autoctx.combatant.active_effects('magical') if magical_effect or autoctx.is_spell or magic_arg: always.add('magical') silvered_effect = autoctx.combatant and autoctx.combatant.active_effects('silvered') if silvered_effect or silvered_arg: always.add('silvered') # dtype transforms/overrides (#876) transforms = {} for dtype in dtype_args: if '>' in dtype: *froms, to = dtype.split('>') for frm in froms: transforms[frm.strip()] = to.strip() else: transforms[None] = dtype # display damage transforms (#1103) if None in transforms: autoctx.meta_queue(f"**Damage Type**: {transforms[None]}") elif transforms: for frm in transforms: autoctx.meta_queue(f"**Damage Change**: {frm} > {transforms[frm]}") # evaluate resistances do_resistances(dmgroll.expr, resistances, always, transforms) # determine healing/damage, stringify expr result = d20.MarkdownStringifier().stringify(dmgroll.expr) if dmgroll.total < 0: roll_for = "Healing" else: roll_for = "Damage" # output roll_for = roll_for if not in_crit else f"{roll_for} (CRIT!)" if not hide: autoctx.queue(f"**{roll_for}**: {result}") else: d20.utils.simplify_expr(dmgroll.expr) autoctx.queue(f"**{roll_for}**: {d20.MarkdownStringifier().stringify(dmgroll.expr)}") autoctx.add_pm(str(autoctx.ctx.author.id), f"**{roll_for}**: {result}") autoctx.target.damage(autoctx, dmgroll.total, allow_overheal=self.overheal) # #1335 autoctx.metavars['lastDamage'] = dmgroll.total return DamageResult(damage=dmgroll.total, damage_roll=dmgroll, in_crit=in_crit)
def test_example1(slf): slf.assertEqual(water_roughness(parse(example1)), 273)
def test_example1(slf): slf.assertEqual(corner_ids(parse(example1)), 20899048083289)
def get_roll_comment(expr): """Gets the dice and comment from a roll expression.""" result = d20.parse(expr, allow_comments=True) return str(result.roll), (result.comment or '')