Exemple #1
0
    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)
Exemple #2
0
    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")
Exemple #4
0
    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}
Exemple #6
0
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()))
Exemple #10
0
    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")
Exemple #12
0
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}
Exemple #14
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

    # 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'))
Exemple #15
0
 def stat_gen():
     stats = [roll('4d6kh3').total for _ in range(6)]
     return stats
Exemple #16
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)
    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}