async def game_hp(self, ctx, operator='', *, hp=''): """Modifies the HP of a the current active character. Synchronizes live with Dicecloud. If operator is not passed, assumes `mod`. Operators: `mod`, `set`.""" character: Character = await Character.from_ctx(ctx) if not operator == '': hp_roll = roll(hp, inline=True, show_blurbs=False) if 'mod' in operator.lower(): character.modify_hp(hp_roll.total) elif 'set' in operator.lower(): character.hp = hp_roll.total elif 'max' in operator.lower() and not hp: character.hp = character.max_hp elif hp == '': hp_roll = roll(operator, inline=True, show_blurbs=False) hp = operator character.modify_hp(hp_roll.total) else: await ctx.send("Incorrect operator. Use mod or set.") return await character.commit(ctx) out = "{}: {}".format(character.name, character.hp_str()) if 'd' in hp: out += '\n' + hp_roll.skeleton else: out = "{}: {}".format(character.name, character.hp_str()) await ctx.send(out)
async def game_deathsave(self, ctx, *args): """Commands to manage character death saves. __Valid Arguments__ See `!help save`.""" character: Character = await Character.from_ctx(ctx) args = argparse(args) adv = args.adv() b = args.join('b', '+') phrase = args.join('phrase', '\n') if b: save_roll = roll('1d20+' + b, adv=adv, inline=True) else: save_roll = roll('1d20', adv=adv, inline=True) embed = discord.Embed() embed.title = args.last('title', '') \ .replace('[charname]', character.name) \ .replace('[sname]', 'Death') \ or '{} makes {}!'.format(character.name, "a Death Save") embed.colour = character.get_color() death_phrase = '' if save_roll.crit == 1: character.hp = 1 elif save_roll.crit == 2: character.death_saves.fail(2) elif save_roll.total >= 10: character.death_saves.succeed() else: character.death_saves.fail() if save_roll.crit == 1: death_phrase = f"{character.name} is UP with 1 HP!" elif character.death_saves.is_dead(): death_phrase = f"{character.name} is DEAD!" elif character.death_saves.is_stable(): death_phrase = f"{character.name} is STABLE!" await character.commit(ctx) embed.description = save_roll.skeleton + ('\n*' + phrase + '*' if phrase else '') if death_phrase: embed.set_footer(text=death_phrase) embed.add_field(name="Death Saves", value=str(character.death_saves)) if args.last('image') is not None: embed.set_thumbnail(url=args.last('image')) await ctx.send(embed=embed)
async def rrr(self, ctx, iterations: int, rollStr, dc: int = 0, *, args=''): """Rolls dice in xdy format, given a set dc. Usage: !rrr <iterations> <xdy> <DC> [args]""" if iterations < 1 or iterations > 100: return await ctx.send("Too many or too few iterations.") adv = 0 out = [] successes = 0 if re.search('(^|\s+)(adv|dis)(\s+|$)', args) is not None: adv = 1 if re.search('(^|\s+)adv(\s+|$)', args) is not None else -1 args = re.sub('(adv|dis)(\s+|$)', '', args) for r in range(iterations): res = roll(rollStr, adv=adv, rollFor=args, inline=True) if res.plain >= dc: successes += 1 out.append(res) outStr = "Rolling {} iterations, DC {}...\n".format(iterations, dc) outStr += '\n'.join([o.skeleton for o in out]) if len(outStr) < 1500: outStr += '\n{} successes.'.format(str(successes)) else: outStr = "Rolling {} iterations, DC {}...\n[Output truncated due to length]\n".format(iterations, dc) + '{} successes.'.format( str(successes)) await try_delete(ctx.message) await ctx.send(ctx.author.mention + '\n' + outStr) await Stats.increase_stat(ctx, "dice_rolled_life")
def save(self, ability: str, adv: bool = None): """ Rolls a combatant's saving throw. :param str ability: The type of save ("str", "dexterity", etc). :param bool adv: Whether to roll the save with advantage. Rolls with advantage if ``True``, disadvantage if ``False``, or normally if ``None``. :returns: A SimpleRollResult describing the rolled save. :rtype: :class:`~cogs5e.funcs.scripting.functions.SimpleRollResult` """ try: save = self._combatant.saves.get(ability) mod = save.value except ValueError: raise InvalidSaveType sb = self._combatant.active_effects('sb') if sb: saveroll = '1d20{:+}+{}'.format(mod, '+'.join(sb)) else: saveroll = '1d20{:+}'.format(mod) adv = 0 if adv is None else 1 if adv else -1 save_roll = roll(saveroll, adv=adv, rollFor='{} Save'.format(ability[:3].upper()), inline=True, show_blurbs=False) return SimpleRollResult(save_roll.rolled, save_roll.total, save_roll.skeleton, [part.to_dict() for part in save_roll.raw_dice.parts], save_roll)
def run(self, autoctx): super(Save, self).run(autoctx) save = autoctx.args.last('save') or self.stat auto_pass = autoctx.args.last('pass', type_=bool, ephem=True) auto_fail = autoctx.args.last('fail', type_=bool, ephem=True) hide = autoctx.args.last('h', type_=bool) dc_override = None if self.dc: try: dc_override = autoctx.parse_annostr(self.dc) dc_override = int(dc_override) except (TypeError, ValueError): raise AutomationException(f"{dc_override} cannot be interpreted as a DC.") dc = autoctx.args.last('dc', type_=int) or dc_override or autoctx.dc_override or autoctx.caster.spellbook.dc if dc is None: raise NoSpellDC() try: save_skill = next(s for s in ('strengthSave', 'dexteritySave', 'constitutionSave', 'intelligenceSave', 'wisdomSave', 'charismaSave') if save.lower() in s.lower()) except StopIteration: raise InvalidSaveType() autoctx.meta_queue(f"**DC**: {dc}") if not autoctx.target.is_simple: save_blurb = f'{save_skill[:3].upper()} Save' if auto_pass: is_success = True autoctx.queue(f"**{save_blurb}:** Automatic success!") elif auto_fail: is_success = False autoctx.queue(f"**{save_blurb}:** Automatic failure!") else: saveroll = autoctx.target.get_save_dice(save_skill, adv=autoctx.args.adv(boolwise=True)) save_roll = roll(saveroll, rollFor=save_blurb, inline=True, show_blurbs=False) is_success = save_roll.total >= dc success_str = ("; Success!" if is_success else "; Failure!") if not hide: autoctx.queue(f"{save_roll.result}{success_str}") else: autoctx.add_pm(str(autoctx.ctx.author.id), f"{save_roll.result}{success_str}") autoctx.queue(f"**{save_blurb}**: 1d20...{success_str}") else: autoctx.meta_queue('{} Save'.format(save_skill[:3].upper())) is_success = False if is_success: damage = self.on_success(autoctx) else: damage = self.on_fail(autoctx) return {"total": damage}
def simple_roll(dice): """ Rolls dice and returns the total. .. note:: This function's true signature is ``roll(dice)``. :param str dice: The dice to roll. :return: The roll's total, or 0 if an error was encountered. :rtype: int """ return roll(dice).total
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 = self.dice if autoctx.is_spell: if self.cantripScale: dice = autoctx.cantrip_scale(dice) if self.higher and not autoctx.get_cast_level() == autoctx.spell.level: higher = self.higher.get(str(autoctx.get_cast_level())) if higher: dice = f"{dice}+{higher}" if not self.hidden: # -mi # (#527) if mi: dice = re.sub(r'(\d+d\d+)', rf'\1mi{mi}', dice) if d: dice = f"{dice}+{d}" if maxdmg: def maxSub(matchobj): return f"{matchobj.group(1)}d{matchobj.group(2)}mi{matchobj.group(2)}" dice = re.sub(r'(\d+)d(\d+)', maxSub, dice) rolled = roll(dice, rollFor=self.name.title(), inline=True, show_blurbs=False) if not self.hidden: autoctx.meta_queue(rolled.result) if not rolled.raw_dice: raise InvalidArgument(f"Invalid roll in meta roll: {rolled.result}") autoctx.metavars[self.name] = rolled.consolidated()
async def rollCmd(self, ctx, *, rollStr: str = '1d20'): """Rolls dice in xdy format. __Examples__ !r xdy Attack! !r xdy+z adv Attack with Advantage! !r xdy-z dis Hide with Heavy Armor! !r xdy+xdy*z !r XdYkhZ !r 4d6mi2[fire] Elemental Adept, Fire !r 2d6e6 Explode on 6 !r 10d6ra6 Spell Bombardment !r 4d6ro<3 Great Weapon Master __Supported Operators__ k (keep) p (drop) ro (reroll once) rr (reroll infinitely) mi/ma (min/max result) e (explode dice of value) ra (reroll and add) __Supported Selectors__ lX (lowest X) hX (highest X) >X/<X (greater than or less than X)""" if rollStr == '0/0': # easter eggs return await ctx.send("What do you expect me to do, destroy the universe?") adv = 0 if re.search('(^|\s+)(adv|dis)(\s+|$)', rollStr) is not None: adv = 1 if re.search('(^|\s+)adv(\s+|$)', rollStr) is not None else -1 rollStr = re.sub('(adv|dis)(\s+|$)', '', rollStr) res = roll(rollStr, adv=adv) out = res.result await try_delete(ctx.message) outStr = ctx.author.mention + ' :game_die:\n' + out if len(outStr) > 1999: await ctx.send( ctx.author.mention + ' :game_die:\n[Output truncated due to length]\n**Result:** ' + str( res.plain)) else: await ctx.send(outStr) await Stats.increase_stat(ctx, "dice_rolled_life")
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) if autoctx.is_spell: if self.cantripScale: amount = autoctx.cantrip_scale(amount) if self.higher and not autoctx.get_cast_level() == autoctx.spell.level: higher = self.higher.get(str(autoctx.get_cast_level())) if higher: amount = f"{amount}+{higher}" roll_for = "THP" if maxdmg: def maxSub(matchobj): return f"{matchobj.group(1)}d{matchobj.group(2)}mi{matchobj.group(2)}" amount = re.sub(r'(\d+)d(\d+)', maxSub, amount) dmgroll = roll(amount, rollFor=roll_for, inline=True, show_blurbs=False) autoctx.queue(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()))
async def randChar(self, ctx, level="0"): """Makes a random 5e character.""" try: level = int(level) except: await ctx.send("Invalid level.") return if level == 0: rolls = [roll("4d6kh3", inline=True) for _ in range(6)] stats = '\n'.join(r.skeleton for r in rolls) total = sum([r.total for r in rolls]) await ctx.send( f"{ctx.message.author.mention}\nGenerated random stats:\n{stats}\nTotal = `{total}`" ) return if level > 20 or level < 1: await ctx.send("Invalid level (must be 1-20).") return await self.genChar(ctx, level)
async def rr(self, ctx, iterations: int, rollStr, *, args=''): """Rolls dice in xdy format a given number of times. Usage: !rr <iterations> <xdy> [args]""" if iterations < 1 or iterations > 100: return await ctx.send("Too many or too few iterations.") adv = 0 out = [] if re.search('(^|\s+)(adv|dis)(\s+|$)', args) is not None: adv = 1 if re.search('(^|\s+)adv(\s+|$)', args) is not None else -1 args = re.sub('(adv|dis)(\s+|$)', '', args) for _ in range(iterations): res = roll(rollStr, adv=adv, rollFor=args, inline=True) out.append(res) outStr = "Rolling {} iterations...\n".format(iterations) outStr += '\n'.join([o.skeleton for o in out]) if len(outStr) < 1500: outStr += '\n{} total.'.format(sum(o.total for o in out)) else: outStr = "Rolling {} iterations...\n[Output truncated due to length]\n".format(iterations) + \ '{} total.'.format(sum(o.total for o in out)) await try_delete(ctx.message) await ctx.send(ctx.author.mention + '\n' + outStr) await Stats.increase_stat(ctx, "dice_rolled_life")
def vroll(dice, multiply=1, add=0): """ Rolls dice and returns a detailed roll result. :param str dice: The dice to roll. :param int multiply: How many times to multiply each set of dice by. :param int add: How many dice to add to each set of dice. :return: The result of the roll. :rtype: :class:`~cogs5e.funcs.scripting.functions.SimpleRollResult` """ if multiply != 1 or add != 0: def subDice(matchobj): return str((int(matchobj.group(1)) * multiply) + add) + 'd' + matchobj.group(2) dice = re.sub(r'(\d+)d(\d+)', subDice, dice) rolled = roll(dice, inline=True) try: return SimpleRollResult( rolled.rolled, rolled.total, rolled.skeleton, [part.to_dict() for part in rolled.raw_dice.parts], rolled) except AttributeError: return None
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}
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 # 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 # 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'))
def stat_gen(): stats = [roll('4d6kh3').total for _ in range(6)] return stats
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)
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: reroll = autoctx.character.get_setting('reroll') or reroll criton = autoctx.character.get_setting('criton') or criton # 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}