def from_data(cls, d): ability_scores = BaseStats.from_dict(d['ability_scores']) saves = Saves.from_dict(d['saves']) skills = Skills.from_dict(d['skills']) display_resists = Resistances.from_dict(d['display_resists'], smart=False) traits = [Trait(**t) for t in d['traits']] actions = [Trait(**t) for t in d['actions']] reactions = [Trait(**t) for t in d['reactions']] legactions = [Trait(**t) for t in d['legactions']] bonus_actions = [Trait(**t) for t in d.get('bonus_actions', [])] mythic_actions = [Trait(**t) for t in d.get('mythic_actions', [])] resistances = Resistances.from_dict(d['resistances']) attacks = AttackList.from_dict(d['attacks']) if d['spellbook'] is not None: spellcasting = MonsterSpellbook.from_dict(d['spellbook']) else: spellcasting = None return cls(d['name'], d['size'], d['race'], d['alignment'], d['ac'], d['armortype'], d['hp'], d['hitdice'], d['speed'], ability_scores, saves, skills, d['senses'], display_resists, d['condition_immune'], d['languages'], d['cr'], d['xp'], traits=traits, actions=actions, reactions=reactions, legactions=legactions, bonus_actions=bonus_actions, mythic_actions=mythic_actions, la_per_round=d['la_per_round'], passiveperc=d['passiveperc'], # augmented resistances=resistances, attacks=attacks, proper=d['proper'], image_url=d['image_url'], spellcasting=spellcasting, token_free_fp=d['token_free'], token_sub_fp=d['token_sub'], # sourcing source=d['source'], entity_id=d['id'], page=d['page'], url=d['url'], is_free=d['isFree'])
def get_resistances(self): out = {'resist': [], 'immune': [], 'vuln': []} if not self.additional: # requires 2.0 return Resistances.from_dict(out) for rownum in range(69, 80): for resist_type, col in RESIST_COLS: try: dtype = self.additional.value(f"{col}{rownum}") except IndexError: dtype = None if dtype: out[resist_type].append(dtype.lower()) return Resistances.from_dict(out)
def get_resistances(self): resist = { 'resist': set(), 'immune': set(), 'vuln': set() } for mod in self.modifiers(): if mod['type'] == 'resistance': resist['resist'].add(mod['subType'].lower()) elif mod['type'] == 'immunity': resist['immune'].add(mod['subType'].lower()) elif mod['type'] == 'vulnerability': resist['vuln'].add(mod['subType'].lower()) for override in self.character_data['customDefenseAdjustments']: if not override['type'] == 2: continue if override['id'] not in RESIST_OVERRIDE_MAP: continue dtype, rtype = RESIST_OVERRIDE_MAP[override['id']] resist[RESIST_TYPE_MAP[rtype]].add(dtype.lower()) resist = {k: list(v) for k, v in resist.items()} return Resistances.from_dict(resist)
def resistances(self): out = self._resistances.copy() out.update(Resistances.from_dict( {k: self.active_effects(k) for k in RESIST_TYPES}), overwrite=False) return out
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)
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
def get_resistances(self) -> Resistances: if self.character_data is None: raise Exception('You must call get_character() first.') out = {'resist': [], 'immune': [], 'vuln': []} for dmgType in DAMAGE_TYPES: mult = self.calculate_stat(f"{dmgType}Multiplier", 1) if mult <= 0: out['immune'].append(dmgType) elif mult < 1: out['resist'].append(dmgType) elif mult > 1: out['vuln'].append(dmgType) return Resistances.from_dict(out)
def run(self, autoctx): super(Damage, self).run(autoctx) # general arguments args = autoctx.args damage = self.damage resistances = Resistances() d_args = args.get('d', [], ephem=True) c_args = args.get('c', [], ephem=True) crit_arg = args.last('crit', None, bool, ephem=True) nocrit = args.last('nocrit', default=False, type_=bool, ephem=True) max_arg = args.last('max', None, bool, ephem=True) magic_arg = args.last('magical', None, bool, ephem=True) mi_arg = args.last('mi', None, int) dtype_args = args.get('dtype', [], ephem=True) critdice = args.last('critdice', 0, int) hide = args.last('h', type_=bool) # character-specific arguments if autoctx.character: critdice = autoctx.character.get_setting('critdice') or critdice # combat-specific arguments if not autoctx.target.is_simple: resistances = autoctx.target.get_resists().copy() resistances.update(Resistances.from_args(args, ephem=True)) # check if we actually need to run this damage roll (not in combat and roll is redundant) if autoctx.target.is_simple and self.is_meta(autoctx, True): return # add on combatant damage effects (#224) if autoctx.combatant: d_args.extend(autoctx.combatant.active_effects('d')) # check if we actually need to care about the -d tag if self.is_meta(autoctx): d_args = [] # d was likely applied in the Roll effect already # set up damage AST damage = autoctx.parse_annostr(damage) dice_ast = copy.copy(d20.parse(damage)) dice_ast = _upcast_scaled_dice(self, autoctx, dice_ast) # -mi # (#527) if mi_arg: dice_ast = d20.utils.tree_map(_mi_mapper(mi_arg), dice_ast) # -d # for d_arg in d_args: d_ast = d20.parse(d_arg) dice_ast.roll = d20.ast.BinOp(dice_ast.roll, '+', d_ast.roll) # crit # nocrit (#1216) in_crit = (autoctx.in_crit or crit_arg) and not nocrit roll_for = "Damage" if not in_crit else "Damage (CRIT!)" if in_crit: dice_ast = d20.utils.tree_map(_crit_mapper, dice_ast) if critdice and not autoctx.is_spell: # add X critdice to the leftmost node if it's dice left = d20.utils.leftmost(dice_ast) if isinstance(left, d20.ast.Dice): left.num += int(critdice) # -c # if in_crit: for c_arg in c_args: c_ast = d20.parse(c_arg) dice_ast.roll = d20.ast.BinOp(dice_ast.roll, '+', c_ast.roll) # max if max_arg: dice_ast = d20.utils.tree_map(_max_mapper, dice_ast) # evaluate damage dmgroll = roll(dice_ast) # magic arg (#853) always = {'magical'} if (autoctx.is_spell or magic_arg) else None # dtype transforms/overrides (#876) transforms = {} for dtype in dtype_args: if '>' in dtype: *froms, to = dtype.split('>') for frm in froms: transforms[frm.strip()] = to.strip() else: transforms[None] = dtype # display damage transforms (#1103) if None in transforms: autoctx.meta_queue(f"**Damage Type**: {transforms[None]}") elif transforms: for frm in transforms: autoctx.meta_queue( f"**Damage Change**: {frm} > {transforms[frm]}") # evaluate resistances do_resistances(dmgroll.expr, resistances, always, transforms) # generate output result = d20.MarkdownStringifier().stringify(dmgroll.expr) # output if not hide: autoctx.queue(f"**{roll_for}**: {result}") else: d20.utils.simplify_expr(dmgroll.expr) autoctx.queue( f"**{roll_for}**: {d20.MarkdownStringifier().stringify(dmgroll.expr)}" ) autoctx.add_pm(str(autoctx.ctx.author.id), f"**{roll_for}**: {result}") autoctx.target.damage(autoctx, dmgroll.total, allow_overheal=self.overheal) # return metadata for scripting return { 'damage': f"**{roll_for}**: {result}", 'total': dmgroll.total, 'roll': dmgroll }
def __init__( self, name: str, size: str, race: str, alignment: str, ac: int, armortype: str, hp: int, hitdice: str, speed: str, ability_scores: BaseStats, saves: Saves, skills: Skills, senses: str, display_resists: Resistances, condition_immune: list, languages: list, cr: str, xp: int, # optional traits: list = None, actions: list = None, reactions: list = None, legactions: list = None, la_per_round=3, passiveperc: int = None, # augmented resistances: Resistances = None, attacks: AttackList = None, proper: bool = False, image_url: str = None, spellcasting=None, # sourcing homebrew=False, **kwargs): if traits is None: traits = [] if actions is None: actions = [] if reactions is None: reactions = [] if legactions is None: legactions = [] if attacks is None: attacks = AttackList() if spellcasting is None: spellcasting = MonsterSpellbook() if passiveperc is None: passiveperc = 10 + skills.perception.value # old/new resist handling if resistances is None: # fall back to old-style resistances (deprecated) vuln = kwargs.get('vuln', []) resist = kwargs.get('resist', []) immune = kwargs.get('immune', []) resistances = Resistances.from_dict( dict(vuln=vuln, resist=resist, immune=immune)) try: levels = Levels({"Monster": spellcasting.caster_level or int(cr)}) except ValueError: levels = None Sourced.__init__(self, 'monster', homebrew, source=kwargs['source'], entity_id=kwargs.get('entity_id'), page=kwargs.get('page'), url=kwargs.get('url'), is_free=kwargs.get('is_free')) StatBlock.__init__(self, name=name, stats=ability_scores, attacks=attacks, skills=skills, saves=saves, resistances=resistances, spellbook=spellcasting, ac=ac, max_hp=hp, levels=levels) self.size = size self.race = race self.alignment = alignment self.armortype = armortype self.hitdice = hitdice self.speed = speed self.cr = cr self.xp = xp self.passive = passiveperc self.senses = senses self.condition_immune = condition_immune self.languages = languages self.traits = traits self.actions = actions self.reactions = reactions self.legactions = legactions self.la_per_round = la_per_round self.proper = proper self.image_url = image_url # resistances including notes, e.g. "Bludgeoning from nonmagical weapons" self._displayed_resistances = display_resists or resistances
def _get_resistances(self): return Resistances.from_dict(self.character_data['resistances'])
def _monster_factory(data, bestiary_name): ability_scores = BaseStats(data['stats']['proficiencyBonus'] or 0, data['stats']['abilityScores']['strength'] or 10, data['stats']['abilityScores']['dexterity'] or 10, data['stats']['abilityScores']['constitution'] or 10, data['stats']['abilityScores']['intelligence'] or 10, data['stats']['abilityScores']['wisdom'] or 10, data['stats']['abilityScores']['charisma'] or 10) cr = {0.125: '1/8', 0.25: '1/4', 0.5: '1/2'}.get(data['stats']['challengeRating'], str(data['stats']['challengeRating'])) num_hit_die = data['stats']['numHitDie'] hit_die_size = data['stats']['hitDieSize'] con_by_level = num_hit_die * ability_scores.get_mod('con') hp = floor(((hit_die_size + 1) / 2) * num_hit_die) + con_by_level hitdice = f"{num_hit_die}d{hit_die_size} + {con_by_level}" proficiency = data['stats']['proficiencyBonus'] if proficiency is None: raise ExternalImportError(f"Monster's proficiency bonus is nonexistent ({data['name']}).") skills = Skills.default(ability_scores) skill_updates = {} for skill in data['stats']['skills']: name = spaced_to_camel(skill['name']) if skill['proficient']: mod = skills[name].value + proficiency else: mod = skill.get('value') if mod is not None: skill_updates[name] = mod skills.update(skill_updates) saves = Saves.default(ability_scores) save_updates = {} for save in data['stats']['savingThrows']: name = save['ability'].lower() + 'Save' if save['proficient']: mod = saves.get(name).value + proficiency else: mod = save.get('value') if mod is not None: save_updates[name] = mod saves.update(save_updates) attacks = [] traits, atks = parse_critterdb_traits(data, 'additionalAbilities') attacks.extend(atks) actions, atks = parse_critterdb_traits(data, 'actions') attacks.extend(atks) reactions, atks = parse_critterdb_traits(data, 'reactions') attacks.extend(atks) legactions, atks = parse_critterdb_traits(data, 'legendaryActions') attacks.extend(atks) attacks = AttackList.from_dict(attacks) spellcasting = parse_critterdb_spellcasting(traits, ability_scores) resistances = Resistances.from_dict(dict(vuln=data['stats']['damageVulnerabilities'], resist=data['stats']['damageResistances'], immune=data['stats']['damageImmunities'])) return Monster(name=data['name'], size=data['stats']['size'], race=data['stats']['race'], alignment=data['stats']['alignment'], ac=data['stats']['armorClass'], armortype=data['stats']['armorType'], hp=hp, hitdice=hitdice, speed=data['stats']['speed'], ability_scores=ability_scores, saves=saves, skills=skills, senses=', '.join(data['stats']['senses']), resistances=resistances, display_resists=resistances, condition_immune=data['stats']['conditionImmunities'], languages=data['stats']['languages'], cr=cr, xp=data['stats']['experiencePoints'], traits=traits, actions=actions, reactions=reactions, legactions=legactions, la_per_round=data['stats']['legendaryActionsPerRound'], attacks=attacks, proper=data['flavor']['nameIsProper'], image_url=data['flavor']['imageUrl'], spellcasting=spellcasting, homebrew=True, source=bestiary_name)
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)