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_saves(self) -> Saves: out = {} for stat_key, save_key in zip(constants.STAT_ABBREVIATIONS, constants.SAVE_NAMES): out[save_key] = Skill(self.character_data['stats'][stat_key]['save'], prof=1 if self.character_data['stats'][stat_key]['saveProficiency'] else 0) return Saves(out)
def get_skills_and_saves(self) -> (Skills, Saves): if self.character_data is None: raise Exception('You must call get_character() first.') character = self.character_data stats = self.get_stats() NAME_SET = set(SKILL_NAMES + SAVE_NAMES) ADV_INT_MAP = {-1: False, 0: None, 1: True} profs = {} effects = collections.defaultdict(lambda: 0) # calculate profs for prof in character.get('proficiencies', []): if prof.get('enabled', False) and not prof.get('removed', False): profs[prof.get('name')] = prof.get('value') \ if prof.get('value') > profs.get(prof.get('name', 'None'), 0) \ else profs[prof.get('name')] # and effects for effect in self.character_data.get('effects', []): if effect.get('stat') in NAME_SET \ and effect.get('enabled', True) \ and not effect.get('removed', False): statname = effect.get('stat') if effect.get('operation') == 'disadvantage': effects[statname] = max(-1, effects[statname] - 1) if effect.get('operation') == 'advantage': effects[statname] = min(1, effects[statname] + 1) # assign skills skills = {} for skill in SKILL_NAMES: prof_mult = profs.get(skill, 0) base_val = floor(stats.get_mod(SKILL_MAP[skill]) + stats.prof_bonus * prof_mult) adv = ADV_INT_MAP.get(effects.get(skill)) if skill not in STAT_NAMES: value = int(self.calculate_stat(skill, base=base_val)) else: value = base_val skills[skill] = Skill( value, prof=prof_mult, adv=adv ) # and saves saves = {} for save in SAVE_NAMES: prof_mult = profs.get(save, 0) base_val = floor(stats.get_mod(SKILL_MAP[save]) + stats.prof_bonus * prof_mult) adv = ADV_INT_MAP.get(effects.get(save)) saves[save] = Skill( int(self.calculate_stat(save, base=base_val)), prof=prof_mult, adv=adv ) return Skills(skills), Saves(saves)
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_saves(self) -> Saves: out = {} for stat_key, save_key in zip(constants.STAT_ABBREVIATIONS, constants.SAVE_NAMES): stat_data = self.character_data['stats'][stat_key] adv_type = derive_adv(stat_data['saveAdv'], stat_data['saveDis']) out[save_key] = Skill( stat_data['save'], prof=1 if stat_data['saveProficiency'] else 0, adv=adv_type) return Saves(out)
def from_bestiary(cls, data): for key in ('traits', 'actions', 'reactions', 'legactions'): data[key] = [Trait(**t) for t in data.pop(key)] data['spellcasting'] = Spellbook.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 'display_resists' in data: data['display_resists'] = Resistances.from_dict( data['display_resists']) return cls(**data)
def get_skills_and_saves(self): if self.character_data is None: raise Exception('You must call get_character() first.') character = self.character_data skills = {} saves = {} is_joat = self.version == 2 and bool(character.cell("AR45").value) for cell, skill, advcell in SKILL_CELL_MAP: if isinstance(cell, int): advcell = f"F{cell}" profcell = f"H{cell}" cell = f"I{cell}" else: profcell = None try: value = int(character.cell(cell).value) except (TypeError, ValueError): raise MissingAttribute(skill) adv = None if self.version == 2 and advcell: advtype = character.cell(advcell).value if advtype in {'a', 'adv', 'advantage'}: adv = True elif advtype in {'d', 'dis', 'disadvantage'}: adv = False prof = 0 if "Save" not in skill and is_joat: prof = 0.5 if profcell: proftype = character.cell(profcell).value_unformatted if proftype == 'e': prof = 2 elif proftype and proftype != '0': prof = 1 skl_obj = Skill(value, prof, adv=adv) if "Save" in skill: saves[skill] = skl_obj else: skills[skill] = skl_obj skills = Skills(skills) saves = Saves(saves) return skills, saves
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 migrate_monster(old_monster): def spaced_to_camel(spaced): return re.sub(r"\s+(\w)", lambda m: m.group(1).upper(), spaced.lower()) for old_key in ('raw_saves', 'raw_skills'): if old_key in old_monster: del old_monster[old_key] if 'spellcasting' in old_monster and old_monster['spellcasting']: old_spellcasting = old_monster.pop('spellcasting') old_monster['spellbook'] = Spellbook( {}, {}, [SpellbookSpell(s) for s in old_spellcasting['spells']], old_spellcasting['dc'], old_spellcasting['attackBonus'], old_spellcasting['casterLevel']).to_dict() else: old_monster['spellbook'] = Spellbook({}, {}, []).to_dict() base_stats = BaseStats(0, old_monster.pop('strength'), old_monster.pop('dexterity'), old_monster.pop('constitution'), old_monster.pop('intelligence'), old_monster.pop('wisdom'), old_monster.pop('charisma')) old_monster['ability_scores'] = base_stats.to_dict() old_saves = old_monster.pop('saves') saves = Saves.default(base_stats) save_updates = {} for save, value in old_saves.items(): if value != saves[save]: save_updates[save] = value saves.update(save_updates) old_monster['saves'] = saves.to_dict() old_skills = old_monster.pop('skills') skills = Skills.default(base_stats) skill_updates = {} for skill, value in old_skills.items(): name = spaced_to_camel(skill) if value != skills[name]: skill_updates[name] = value skills.update(skill_updates) old_monster['skills'] = skills.to_dict() new_monster = Monster.from_bestiary(old_monster) return new_monster
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 get_skills_and_saves(self): if self.character_data is None: raise Exception('You must call get_character() first.') character = self.character_data skills = {} saves = {} is_joat = False all_check_bonus = 0 if self.version == (2, 0): is_joat = bool(character.value("AR45")) all_check_bonus = int(character.value("AQ26") or 0) elif self.version == (2, 1): is_joat = bool(character.value("AQ59")) all_check_bonus = int(character.value("AR58")) joat_bonus = int(is_joat and self.get_stats().prof_bonus // 2) # calculate str, dex, con, etc checks for cell, skill, advcell in BASE_ABILITY_CHECKS: try: # add bonuses manually since the cell does not include them value = int( character.value(cell)) + all_check_bonus + joat_bonus except (TypeError, ValueError): raise MissingAttribute(skill) prof = 0 if is_joat: prof = 0.5 skl_obj = Skill(value, prof) skills[skill] = skl_obj # read the value of the rest of the skills for cell, skill, advcell in SKILL_CELL_MAP: if isinstance(cell, int): advcell = f"F{cell}" profcell = f"H{cell}" cell = f"I{cell}" else: profcell = None try: value = int(character.value(cell)) except (TypeError, ValueError): raise MissingAttribute(skill) adv = None if self.version >= (2, 0) and advcell: advtype = character.unformatted_value(advcell) if advtype in {'a', 'adv', 'advantage'}: adv = True elif advtype in {'d', 'dis', 'disadvantage'}: adv = False prof = 0 if "Save" not in skill and is_joat: prof = 0.5 if profcell: proftype = character.unformatted_value(profcell) if proftype == 'e': prof = 2 elif proftype and proftype != '0': prof = 1 skl_obj = Skill(value, prof, adv=adv) if "Save" in skill: saves[skill] = skl_obj else: skills[skill] = skl_obj skills = Skills(skills) saves = Saves(saves) return skills, saves
def __init__(self, name: str, size: str, race: str, alignment: str, ac: int, armortype: str, hp: int, hitdice: str, speed: str, ability_scores: BaseStats, cr: str, xp: int, passiveperc: int = None, senses: str = '', vuln: list = None, resist: list = None, immune: list = None, condition_immune: list = None, saves: Saves = None, skills: Skills = None, languages: list = None, traits: list = None, actions: list = None, reactions: list = None, legactions: list = None, la_per_round=3, srd=True, source='homebrew', attacks: AttackList = None, proper: bool = False, image_url: str = None, spellcasting=None, page=None, display_resists: Resistances = None, **_): if vuln is None: vuln = [] if resist is None: resist = [] if immune is None: immune = [] if condition_immune is None: condition_immune = [] if saves is None: saves = Saves.default(ability_scores) if skills is None: skills = Skills.default(ability_scores) if languages is None: languages = [] 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 = Spellbook({}, {}, []) if passiveperc is None: passiveperc = 10 + skills.perception.value try: levels = Levels({"Monster": spellcasting.caster_level or int(cr)}) except ValueError: levels = None resistances = Resistances(vuln=vuln, resist=resist, immune=immune) super(Monster, self).__init__(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.srd = srd self.source = source self.proper = proper self.image_url = image_url self.page = page # this should really be by source, but oh well # resistances including notes, e.g. "Bludgeoning from nonmagical weapons" self._displayed_resistances = display_resists or resistances
def from_data(cls, data): # print(f"Parsing {data['name']}") _type = parse_type(data['type']) alignment = parse_alignment(data['alignment']) speed = parse_speed(data['speed']) ac = data['ac']['ac'] armortype = data['ac'].get('armortype') or None if not 'special' in data['hp']: hp = data['hp']['average'] hitdice = data['hp']['formula'] else: hp = 0 hitdice = data['hp']['special'] scores = BaseStats(0, data['str'] or 10, data['dex'] or 10, data['con'] or 10, data['int'] or 10, data['wis'] or 10, data['cha'] or 10) if isinstance(data['cr'], dict): cr = data['cr']['cr'] else: cr = data['cr'] # resistances vuln = parse_resists(data['vulnerable'], notated=False) if 'vulnerable' in data else None resist = parse_resists(data['resist'], notated=False) if 'resist' in data else None immune = parse_resists(data['immune'], notated=False) if 'immune' in data else None display_resists = Resistances(*[ parse_resists(data.get(r)) for r in ('resist', 'immune', 'vulnerable') ]) condition_immune = data.get('conditionImmune', []) if 'conditionImmune' in data else None languages = data.get('languages', '').split(', ') if 'languages' in data else None traits = [Trait(t['name'], t['text']) for t in data.get('trait', [])] actions = [Trait(t['name'], t['text']) for t in data.get('action', [])] legactions = [ Trait(t['name'], t['text']) for t in data.get('legendary', []) ] reactions = [ Trait(t['name'], t['text']) for t in data.get('reaction', []) ] skills = Skills.default(scores) skills.update(data['skill']) saves = Saves.default(scores) saves.update(data['save']) scores.prof_bonus = _calc_prof(scores, saves, skills) source = data['source'] proper = bool(data.get('isNamedCreature') or data.get('isNPC')) attacks = AttackList.from_dict(data.get('attacks', [])) spellcasting = data.get('spellcasting', {}) spells = [SpellbookSpell(s) for s in spellcasting.get('spells', [])] spellbook = Spellbook({}, {}, spells, spellcasting.get('dc'), spellcasting.get('attackBonus'), spellcasting.get('casterLevel', 1)) return cls(data['name'], parsesize(data['size']), _type, alignment, ac, armortype, hp, hitdice, speed, scores, cr, xp_by_cr(cr), data['passive'], data.get('senses', ''), vuln, resist, immune, condition_immune, saves, skills, languages, traits, actions, reactions, legactions, 3, data.get('srd', False), source, attacks, spellcasting=spellbook, page=data.get('page'), proper=proper, display_resists=display_resists)
def get_skills_and_saves(self): """Returns a dict of all the character's skills.""" if self.character_data is None: raise Exception('You must call get_character() first.') stats = self.get_stats() profBonus = stats.prof_bonus profs = dict() bonuses = dict() advantages = collections.defaultdict(lambda: []) for mod in self.modifiers(): mod['subType'] = mod['subType'].replace("-saving-throws", "Save") if mod['type'] == 'half-proficiency': profs[mod['subType']] = max(profs.get(mod['subType'], 0), 0.5) elif mod['type'] == 'proficiency': profs[mod['subType']] = max(profs.get(mod['subType'], 0), 1) elif mod['type'] == 'expertise': profs[mod['subType']] = 2 elif mod['type'] == 'bonus': if not mod['isGranted']: continue if mod['statId'] is not None: bonuses[mod['subType']] = bonuses.get( mod['subType'], 0) + self.stat_from_id(mod['statId']) else: bonuses[mod['subType']] = bonuses.get( mod['subType'], 0) + (mod['value'] or 0) elif mod['type'] == 'advantage' and not mod[ 'restriction']: # unconditional adv advantages[mod['subType']].append(True) elif mod['type'] == 'disadvantage' and not mod[ 'restriction']: # unconditional dis advantages[mod['subType']].append(False) profs['animalHandling'] = profs.get('animal-handling', 0) profs['sleightOfHand'] = profs.get('sleight-of-hand', 0) advantages['animalHandling'] = advantages['animal-handling'] advantages['sleightOfHand'] = advantages['sleight-of-hand'] def _simplify_adv(adv_list): adv_set = set(adv_list) if len(adv_set) == 1: return adv_set.pop() return None skills = {} for skill in SKILL_NAMES: # add proficiency and bonuses to skills relevantprof = profs.get(skill, 0) relevantbonus = bonuses.get(skill, 0) relevantadv = _simplify_adv(advantages[skill]) if 'ability-checks' in profs and skill != 'initiative': relevantprof = max(relevantprof, profs['ability-checks']) if 'ability-checks' in bonuses and skill != 'initiative': relevantbonus += bonuses['ability-checks'] skills[skill] = Skill(floor( stats.get_mod(SKILL_MAP[skill]) + (profBonus * relevantprof) + relevantbonus), relevantprof, relevantbonus, adv=relevantadv) # saves saves = {} for save in SAVE_NAMES: # add proficiency and bonuses to skills relevantprof = profs.get(save, 0) relevantbonus = bonuses.get(save, 0) relevantadv = _simplify_adv(advantages[save]) if 'saving-throws' in profs: relevantprof = max(relevantprof, profs['saving-throws']) if 'saving-throws' in bonuses: relevantbonus += bonuses['saving-throws'] saves[save] = Skill(floor( stats.get_mod(SKILL_MAP[save]) + (profBonus * relevantprof) + relevantbonus), relevantprof, relevantbonus, adv=relevantadv) # values ignored_ids = set() for charval in self.character_data['characterValues']: if charval['value'] is None: continue if charval['typeId'] == 39: # misc saving throw bonus save_id = SAVE_NAMES[charval['valueId'] - 1] save_bonus = charval['value'] saves[save_id].value += save_bonus saves[save_id].bonus += save_bonus elif charval['valueId'] in HOUSERULE_SKILL_MAP and charval[ 'valueId'] not in ignored_ids: skill_name = HOUSERULE_SKILL_MAP[charval['valueId']] if charval['typeId'] == 23: # override skills[skill_name] = Skill(charval['value']) ignored_ids.add( charval['valueId'] ) # this must be the final value so we stop looking elif charval['typeId'] in { 24, 25 }: # PROBABLY skill magic/misc bonus skills[skill_name].value += charval['value'] skills[skill_name].bonus += charval['value'] elif charval['typeId'] == 26: # proficiency stuff relevantprof = profs.get(skill_name, 0) skills[skill_name].value -= relevantprof * profBonus if charval[ 'value'] == 0: # no prof, don't need to do anything skills[skill_name].prof = 0 elif charval['value'] == 1: # half prof, round down skills[skill_name].value += profBonus // 2 skills[skill_name].prof = 0.5 elif charval['value'] == 2: # half, round up skills[skill_name].value += ceil(profBonus / 2) skills[skill_name].prof = 0.5 elif charval['value'] == 3: # full skills[skill_name].value += profBonus skills[skill_name].prof = 1 elif charval['value'] == 4: # double skills[skill_name].value += profBonus * 2 skills[skill_name].prof = 2 skills = Skills(skills) saves = Saves(saves) return skills, saves
def get_skills_and_saves(self): """Returns a dict of all the character's skills.""" if self.character_data is None: raise Exception('You must call get_character() first.') character = self.character_data stats = self.get_stats() profBonus = stats.prof_bonus profs = dict() bonuses = dict() for modtype in character['modifiers'].values( ): # calculate proficiencies in all skills for mod in modtype: mod['subType'] = mod['subType'].replace( "-saving-throws", "Save") if mod['type'] == 'half-proficiency': profs[mod['subType']] = max(profs.get(mod['subType'], 0), 0.5) elif mod['type'] == 'proficiency': profs[mod['subType']] = max(profs.get(mod['subType'], 0), 1) elif mod['type'] == 'expertise': profs[mod['subType']] = 2 elif mod['type'] == 'bonus': if not mod['isGranted']: continue if mod['statId'] is not None: bonuses[mod['subType']] = bonuses.get( mod['subType'], 0) + self.stat_from_id( mod['statId']) else: bonuses[mod['subType']] = bonuses.get( mod['subType'], 0) + (mod['value'] or 0) profs['animalHandling'] = profs.get('animal-handling', 0) profs['sleightOfHand'] = profs.get('sleight-of-hand', 0) skills = {} for skill in SKILL_NAMES: # add proficiency and bonuses to skills relevantprof = profs.get(skill, 0) relevantbonus = bonuses.get(skill, 0) if 'ability-checks' in profs: relevantprof = max(relevantprof, profs['ability-checks']) if 'ability-checks' in bonuses: relevantbonus += bonuses['ability-checks'] skills[skill] = Skill( floor( stats.get_mod(SKILL_MAP[skill]) + (profBonus * relevantprof) + relevantbonus), relevantprof, relevantbonus) saves = {} for save in SAVE_NAMES: # add proficiency and bonuses to skills relevantprof = profs.get(save, 0) relevantbonus = bonuses.get(save, 0) if 'saving-throws' in profs: relevantprof = max(relevantprof, profs['saving-throws']) if 'saving-throws' in bonuses: relevantbonus += bonuses['saving-throws'] saves[save] = Skill( floor( stats.get_mod(SKILL_MAP[save]) + (profBonus * relevantprof) + relevantbonus), relevantprof, relevantbonus) ignored_ids = set() for charval in self.character_data['characterValues']: if charval['valueId'] in HOUSERULE_SKILL_MAP and charval[ 'valueId'] not in ignored_ids: skill_name = HOUSERULE_SKILL_MAP[charval['valueId']] if charval['typeId'] == 23: # override skills[skill_name] = Skill(charval['value']) ignored_ids.add( charval['valueId'] ) # this must be the final value so we stop looking elif charval['typeId'] in { 24, 25 }: # PROBABLY skill magic/misc bonus skills[skill_name].value += charval['value'] skills[skill_name].bonus += charval['value'] elif charval['typeId'] == 26: # proficiency stuff relevantprof = profs.get(skill_name, 0) skills[skill_name].value -= relevantprof * profBonus if charval[ 'value'] == 0: # no prof, don't need to do anything skills[skill_name].prof = 0 elif charval['value'] == 1: # half prof, round down skills[skill_name].value += profBonus // 2 skills[skill_name].prof = 0.5 elif charval['value'] == 2: # half, round up skills[skill_name].value += ceil(profBonus / 2) skills[skill_name].prof = 0.5 elif charval['value'] == 3: # full skills[skill_name].value += profBonus skills[skill_name].prof = 1 elif charval['value'] == 4: # double skills[skill_name].value += profBonus * 2 skills[skill_name].prof = 2 skills = Skills(skills) saves = Saves(saves) return skills, saves