Ejemplo n.º 1
0
    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}
Ejemplo n.º 2
0
    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
Ejemplo n.º 3
0
    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}
Ejemplo n.º 5
0
 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)
Ejemplo n.º 6
0
    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
Ejemplo n.º 7
0
 def stat_gen():
     stats = [roll('4d6kh3').total for _ in range(6)]
     return stats
Ejemplo n.º 8
0
def test_infinite_loops():
    r = roll("1d1e1")
    assert r.total == 251  # 1 + 250 rerolls
Ejemplo n.º 9
0
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
Ejemplo n.º 10
0
    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)
Ejemplo n.º 11
0
def test_roll():
    assert type(roll("1d20")) == DiceResult
    assert 0 < roll("1d20").total < 21
    assert roll("3+4*(9-2)").total == 31
Ejemplo n.º 12
0
    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)
Ejemplo n.º 13
0
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}
Ejemplo n.º 14
0
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
Ejemplo n.º 15
0
    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
Ejemplo n.º 16
0
    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'])
Ejemplo n.º 17
0
    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
Ejemplo n.º 18
0
def simple_roll(rollStr):
    return roll(rollStr).total
Ejemplo n.º 19
0
    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
Ejemplo n.º 20
0
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])
Ejemplo n.º 21
0
    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)
Ejemplo n.º 22
0
    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)