Example #1
0
    def __init__(self,
                 name: str,
                 stats: BaseStats = None,
                 levels: Levels = None,
                 attacks: AttackList = None,
                 skills: Skills = None,
                 saves: Saves = None,
                 resistances: Resistances = None,
                 spellbook: Spellbook = None,
                 ac: int = None,
                 max_hp: int = None,
                 hp: int = None,
                 temp_hp: int = 0,
                 creature_type: str = None):
        if stats is None:
            stats = BaseStats.default()
        if levels is None:
            levels = Levels()
        if attacks is None:
            attacks = AttackList()
        if skills is None:
            skills = Skills.default(stats)
        if saves is None:
            saves = Saves.default(stats)
        if resistances is None:
            resistances = Resistances()
        if spellbook is None:
            spellbook = Spellbook()
        if hp is None:
            hp = max_hp

        # ===== static =====
        # all actors have a name
        self._name = name
        # ability scores
        self._stats = stats
        # at least a total level
        self._levels = levels
        # attacks - list of automation actions
        self._attacks = attacks
        # skill profs/saving throws
        self._skills = skills
        self._saves = saves
        # defensive resistances
        self._resistances = resistances
        # assigned by combatant type
        self._creature_type = creature_type

        # ===== dynamic =====
        # hp/ac
        self._ac = ac
        self._max_hp = max_hp
        self._hp = hp
        self._temp_hp = temp_hp

        # spellbook
        self._spellbook = spellbook
Example #2
0
 def from_bestiary(cls, data, source):
     for key in ('traits', 'actions', 'reactions', 'legactions'):
         data[key] = [Trait(**t) for t in data.pop(key)]
     data['spellcasting'] = MonsterSpellbook.from_dict(data.pop('spellbook'))
     data['saves'] = Saves.from_dict(data['saves'])
     data['skills'] = Skills.from_dict(data['skills'])
     data['ability_scores'] = BaseStats.from_dict(data['ability_scores'])
     data['attacks'] = AttackList.from_dict(data['attacks'])
     if 'resistances' in data:
         data['resistances'] = Resistances.from_dict(data['resistances'])
     if 'display_resists' in data:
         data['display_resists'] = Resistances.from_dict(data['display_resists'], smart=False)
     else:
         data['display_resists'] = Resistances()
     if 'source' in data:
         del data['source']
     return cls(homebrew=True, source=source, **data)
Example #3
0
    def run(self, autoctx):
        super(Damage, self).run(autoctx)
        # general arguments
        args = autoctx.args
        damage = self.damage
        resistances = Resistances()
        d_args = args.get('d', [], ephem=True)
        c_args = args.get('c', [], ephem=True)
        crit_arg = args.last('crit', None, bool, ephem=True)
        nocrit = args.last('nocrit', default=False, type_=bool, ephem=True)
        max_arg = args.last('max', None, bool, ephem=True)
        magic_arg = args.last('magical', None, bool, ephem=True)
        mi_arg = args.last('mi', None, int)
        dtype_args = args.get('dtype', [], ephem=True)
        critdice = args.last('critdice', 0, int)
        hide = args.last('h', type_=bool)

        # character-specific arguments
        if autoctx.character:
            critdice = autoctx.character.get_setting('critdice') or critdice

        # combat-specific arguments
        if not autoctx.target.is_simple:
            resistances = autoctx.target.get_resists().copy()
        resistances.update(Resistances.from_args(args, ephem=True))

        # check if we actually need to run this damage roll (not in combat and roll is redundant)
        if autoctx.target.is_simple and self.is_meta(autoctx, True):
            return

        # add on combatant damage effects (#224)
        if autoctx.combatant:
            d_args.extend(autoctx.combatant.active_effects('d'))

        # check if we actually need to care about the -d tag
        if self.is_meta(autoctx):
            d_args = []  # d was likely applied in the Roll effect already

        # set up damage AST
        damage = autoctx.parse_annostr(damage)
        dice_ast = copy.copy(d20.parse(damage))
        dice_ast = _upcast_scaled_dice(self, autoctx, dice_ast)

        # -mi # (#527)
        if mi_arg:
            dice_ast = d20.utils.tree_map(_mi_mapper(mi_arg), dice_ast)

        # -d #
        for d_arg in d_args:
            d_ast = d20.parse(d_arg)
            dice_ast.roll = d20.ast.BinOp(dice_ast.roll, '+', d_ast.roll)

        # crit
        # nocrit (#1216)
        in_crit = (autoctx.in_crit or crit_arg) and not nocrit
        roll_for = "Damage" if not in_crit else "Damage (CRIT!)"
        if in_crit:
            dice_ast = d20.utils.tree_map(_crit_mapper, dice_ast)
            if critdice and not autoctx.is_spell:
                # add X critdice to the leftmost node if it's dice
                left = d20.utils.leftmost(dice_ast)
                if isinstance(left, d20.ast.Dice):
                    left.num += int(critdice)

        # -c #
        if in_crit:
            for c_arg in c_args:
                c_ast = d20.parse(c_arg)
                dice_ast.roll = d20.ast.BinOp(dice_ast.roll, '+', c_ast.roll)

        # max
        if max_arg:
            dice_ast = d20.utils.tree_map(_max_mapper, dice_ast)

        # evaluate damage
        dmgroll = roll(dice_ast)

        # magic arg (#853)
        always = {'magical'} if (autoctx.is_spell or magic_arg) else None
        # dtype transforms/overrides (#876)
        transforms = {}
        for dtype in dtype_args:
            if '>' in dtype:
                *froms, to = dtype.split('>')
                for frm in froms:
                    transforms[frm.strip()] = to.strip()
            else:
                transforms[None] = dtype
        # display damage transforms (#1103)
        if None in transforms:
            autoctx.meta_queue(f"**Damage Type**: {transforms[None]}")
        elif transforms:
            for frm in transforms:
                autoctx.meta_queue(
                    f"**Damage Change**: {frm} > {transforms[frm]}")

        # evaluate resistances
        do_resistances(dmgroll.expr, resistances, always, transforms)

        # generate output
        result = d20.MarkdownStringifier().stringify(dmgroll.expr)

        # output
        if not hide:
            autoctx.queue(f"**{roll_for}**: {result}")
        else:
            d20.utils.simplify_expr(dmgroll.expr)
            autoctx.queue(
                f"**{roll_for}**: {d20.MarkdownStringifier().stringify(dmgroll.expr)}"
            )
            autoctx.add_pm(str(autoctx.ctx.author.id),
                           f"**{roll_for}**: {result}")

        autoctx.target.damage(autoctx,
                              dmgroll.total,
                              allow_overheal=self.overheal)

        # return metadata for scripting
        return {
            'damage': f"**{roll_for}**: {result}",
            'total': dmgroll.total,
            'roll': dmgroll
        }
Example #4
0
    def run(self, autoctx):
        super().run(autoctx)
        if autoctx.target is None:
            raise TargetException(
                "Tried to do damage without a target! Make sure all Damage effects are inside "
                "of a Target effect."
            )
        # general arguments
        args = autoctx.args
        damage = self.damage
        resistances = Resistances()
        d_args = args.get('d', [], ephem=True)
        c_args = args.get('c', [], ephem=True)
        crit_arg = args.last('crit', None, bool, ephem=True)
        nocrit = args.last('nocrit', default=False, type_=bool, ephem=True)
        max_arg = args.last('max', None, bool, ephem=True)
        magic_arg = args.last('magical', None, bool, ephem=True)
        silvered_arg = args.last('silvered', None, bool, ephem=True)
        mi_arg = args.last('mi', None, int)
        dtype_args = args.get('dtype', [], ephem=True)
        critdice = sum(args.get('critdice', type_=int))
        hide = args.last('h', type_=bool)

        # character-specific arguments
        if autoctx.character and 'critdice' not in args:
            critdice = autoctx.character.options.extra_crit_dice

        # combat-specific arguments
        if not autoctx.target.is_simple:
            resistances = autoctx.target.get_resists().copy()
        resistances.update(Resistances.from_args(args, ephem=True))

        # check if we actually need to run this damage roll (not in combat and roll is redundant)
        if autoctx.target.is_simple and self.is_meta(autoctx):
            return

        # add on combatant damage effects (#224)
        if autoctx.combatant:
            d_args.extend(autoctx.combatant.active_effects('d'))

        # check if we actually need to care about the -d tag
        if self.contains_roll_meta(autoctx):
            d_args = []  # d was likely applied in the Roll effect already

        # set up damage AST
        damage = autoctx.parse_annostr(damage)
        dice_ast = copy.copy(d20.parse(damage))
        dice_ast = utils.upcast_scaled_dice(self, autoctx, dice_ast)

        # -mi # (#527)
        if mi_arg:
            dice_ast = d20.utils.tree_map(utils.mi_mapper(mi_arg), dice_ast)

        # -d #
        for d_arg in d_args:
            d_ast = d20.parse(d_arg)
            dice_ast.roll = d20.ast.BinOp(dice_ast.roll, '+', d_ast.roll)

        # crit
        # nocrit (#1216)
        # Disable critical damage in saves (#1556)
        in_crit = (autoctx.in_crit or crit_arg) and not (nocrit or autoctx.in_save)
        if in_crit:
            dice_ast = d20.utils.tree_map(utils.crit_mapper, dice_ast)
            if critdice and not autoctx.is_spell:
                # add X critdice to the leftmost node if it's dice
                left = d20.utils.leftmost(dice_ast)
                if isinstance(left, d20.ast.Dice):
                    left.num += int(critdice)

        # -c #
        if in_crit:
            for c_arg in c_args:
                c_ast = d20.parse(c_arg)
                dice_ast.roll = d20.ast.BinOp(dice_ast.roll, '+', c_ast.roll)

        # max
        if max_arg:
            dice_ast = d20.utils.tree_map(utils.max_mapper, dice_ast)

        # evaluate damage
        dmgroll = d20.roll(dice_ast)

        # magic arg (#853), magical effect (#1063)
        # silvered arg (#1544)
        always = set()
        magical_effect = autoctx.combatant and autoctx.combatant.active_effects('magical')
        if magical_effect or autoctx.is_spell or magic_arg:
            always.add('magical')
        silvered_effect = autoctx.combatant and autoctx.combatant.active_effects('silvered')
        if silvered_effect or silvered_arg:
            always.add('silvered')
        # dtype transforms/overrides (#876)
        transforms = {}
        for dtype in dtype_args:
            if '>' in dtype:
                *froms, to = dtype.split('>')
                for frm in froms:
                    transforms[frm.strip()] = to.strip()
            else:
                transforms[None] = dtype
        # display damage transforms (#1103)
        if None in transforms:
            autoctx.meta_queue(f"**Damage Type**: {transforms[None]}")
        elif transforms:
            for frm in transforms:
                autoctx.meta_queue(f"**Damage Change**: {frm} > {transforms[frm]}")

        # evaluate resistances
        do_resistances(dmgroll.expr, resistances, always, transforms)

        # determine healing/damage, stringify expr
        result = d20.MarkdownStringifier().stringify(dmgroll.expr)
        if dmgroll.total < 0:
            roll_for = "Healing"
        else:
            roll_for = "Damage"

        # output
        roll_for = roll_for if not in_crit else f"{roll_for} (CRIT!)"
        if not hide:
            autoctx.queue(f"**{roll_for}**: {result}")
        else:
            d20.utils.simplify_expr(dmgroll.expr)
            autoctx.queue(f"**{roll_for}**: {d20.MarkdownStringifier().stringify(dmgroll.expr)}")
            autoctx.add_pm(str(autoctx.ctx.author.id), f"**{roll_for}**: {result}")

        autoctx.target.damage(autoctx, dmgroll.total, allow_overheal=self.overheal)

        # #1335
        autoctx.metavars['lastDamage'] = dmgroll.total
        return DamageResult(damage=dmgroll.total, damage_roll=dmgroll, in_crit=in_crit)