def __init__(self, owner: str, upstream: str, active: bool, sheet_type: str, import_version: int, name: str, description: str, image: str, stats: dict, levels: dict, attacks: list, skills: dict, resistances: dict, saves: dict, ac: int, max_hp: int, hp: int, temp_hp: int, cvars: dict, options: dict, overrides: dict, consumables: list, death_saves: dict, spellbook: dict, live, race: str, background: str, **kwargs): if kwargs: log.warning(f"Unused kwargs: {kwargs}") # sheet metadata self._owner = owner self._upstream = upstream self._active = active self._sheet_type = sheet_type self._import_version = import_version # main character info self.name = name self._description = description self._image = image self.stats = BaseStats.from_dict(stats) self.levels = Levels.from_dict(levels) self._attacks = [Attack.from_dict(atk) for atk in attacks] self.skills = Skills.from_dict(skills) self.resistances = Resistances.from_dict(resistances) self.saves = Saves.from_dict(saves) # hp/ac self.ac = ac self.max_hp = max_hp self._hp = hp self._temp_hp = temp_hp # customization self.cvars = cvars self.options = CharOptions.from_dict(options) self.overrides = ManualOverrides.from_dict(overrides) # ccs self.consumables = [ CustomCounter.from_dict(self, cons) for cons in consumables ] self.death_saves = DeathSaves.from_dict(death_saves) # spellbook spellbook = Spellbook.from_dict(spellbook) super(Character, self).__init__(spellbook) # live sheet integrations self._live = live integration = INTEGRATION_MAP.get(live) if integration: self._live_integration = integration(self) else: self._live_integration = None # misc research things self.race = race self.background = background
def parse_critterdb_spellcasting(traits): known_spells = [] usual_dc = (0, 0) # dc, number of spells using dc usual_sab = (0, 0) # same thing caster_level = 1 for trait in traits: if not 'Spellcasting' in trait.name: continue desc = trait.desc level_match = re.search(r"is a (\d+)[stndrh]{2}-level", desc) ab_dc_match = re.search(r"spell save DC (\d+), [+-](\d+) to hit", desc) spells = [] for spell_match in re.finditer( r"(?:(?:(?:\d[stndrh]{2}\slevel)|(?:Cantrip))\s(?:\(.+\))|(?:At will)|(?:\d/day)): (.+)$", desc, re.MULTILINE): spell_texts = spell_match.group(1).split(', ') for spell_text in spell_texts: s = spell_text.strip('* _') spells.append(s.lower()) if level_match: caster_level = max(caster_level, int(level_match.group(1))) if ab_dc_match: ab = int(ab_dc_match.group(2)) dc = int(ab_dc_match.group(1)) if len(spells) > usual_dc[1]: usual_dc = (dc, len(spells)) if len(spells) > usual_sab[1]: usual_sab = (ab, len(spells)) known_spells.extend(s for s in spells if s not in known_spells) dc = usual_dc[0] sab = usual_sab[0] log.debug(f"Lvl {caster_level}; DC: {dc}; SAB: {sab}; Spells: {known_spells}") spells = [SpellbookSpell(s) for s in known_spells] spellbook = Spellbook({}, {}, spells, dc, sab, caster_level) return spellbook
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']) return cls(**data)
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'] vuln = parse_resists(data['vulnerable']) if 'vulnerable' in data else None resist = parse_resists(data['resist']) if 'resist' in data else None immune = parse_resists(data['immune']) if 'immune' in data else None condition_immune = data.get('conditionImmune', []) if 'conditionImmune' in data else None raw_resists = { "vuln": parse_resists(data['vulnerable'], False) if 'vulnerable' in data else [], "resist": parse_resists(data['resist'], False) if 'resist' in data else [], "immune": parse_resists(data['immune'], False) if 'immune' in data else [] } 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']) source = data['source'] proper = bool(data.get('isNamedCreature') or data.get('isNPC')) attacks = 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, raw_resists=raw_resists)
def get_spellbook(self): if self.character_data is None: raise Exception('You must call get_character() first.') spellcasterLevel = 0 castingClasses = 0 spellMod = 0 pactSlots = 0 pactLevel = 1 for _class in self.character_data['classes']: castingAbility = _class['definition']['spellCastingAbilityId'] or \ (_class['subclassDefinition'] or {}).get('spellCastingAbilityId') if castingAbility: castingClasses += 1 casterMult = CASTER_TYPES.get(_class['definition']['name'], 1) spellcasterLevel += _class['level'] * casterMult spellMod = max(spellMod, self.stat_from_id(castingAbility)) if _class['definition']['name'] == 'Warlock': pactSlots = pact_slots_by_level(_class['level']) pactLevel = pact_level_by_level(_class['level']) if castingClasses > 1: spellcasterLevel = floor(spellcasterLevel) else: if spellcasterLevel >= 1: spellcasterLevel = ceil(spellcasterLevel) else: spellcasterLevel = 0 log.debug(f"Caster level: {spellcasterLevel}") slots = {} for lvl in range(1, 10): slots[str(lvl)] = SLOTS_PER_LEVEL[lvl](spellcasterLevel) slots[str(pactLevel)] += pactSlots prof = self.get_stats().prof_bonus attack_bonus_bonus = self.get_stat("spell-attacks") dc = 8 + spellMod + prof sab = spellMod + prof + attack_bonus_bonus spellnames = [] for src in self.character_data['classSpells']: spellnames.extend(s['definition']['name'].replace('\u2019', "'") for s in src['spells']) for src in self.character_data['spells'].values(): spellnames.extend(s['definition']['name'].replace('\u2019', "'") for s in src) spells = [] for value in spellnames: result = search(c.spells, value, lambda sp: sp.name, strict=True) if result and result[0] and result[1]: spells.append(SpellbookSpell(result[0].name, True)) elif len(value) > 2: spells.append(SpellbookSpell(value)) spellbook = Spellbook(slots, slots, spells, dc, sab, self.get_levels().total_level) return spellbook
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 get_spellbook(self): if self.character_data is None: raise Exception('You must call get_character() first.') # max slots slots = { '1': int(self.character_data.cell('AK101').value or 0), '2': int(self.character_data.cell('E107').value or 0), '3': int(self.character_data.cell('AK113').value or 0), '4': int(self.character_data.cell('E119').value or 0), '5': int(self.character_data.cell('AK124').value or 0), '6': int(self.character_data.cell('E129').value or 0), '7': int(self.character_data.cell('AK134').value or 0), '8': int(self.character_data.cell('E138').value or 0), '9': int(self.character_data.cell('AK142').value or 0) } # spells C96:AH143 potential_spells = self.character_data.range( "D96:AH143") # returns a matrix, the docs lie if self.additional: potential_spells.extend(self.additional.range("D17:AH64")) spells = [] for row in potential_spells: for cell in row: if cell.value and not cell.value in IGNORED_SPELL_VALUES: value = cell.value.strip() result = search(c.spells, value, lambda sp: sp.name, strict=True) if result and result[0] and result[1]: spells.append(SpellbookSpell(result[0].name, True)) elif len(value) > 2: spells.append(SpellbookSpell(value)) try: dc = int(self.character_data.cell('AB91').value or 0) except ValueError: dc = None try: sab = int(self.character_data.cell('AI91').value or 0) except ValueError: sab = None spellbook = Spellbook(slots, slots, spells, dc, sab, self.total_level) return spellbook
def get_spellbook(self): if self.character_data is None: raise Exception('You must call get_character() first.') # max slots slots = { '1': int(self.character_data.value("AK101") or 0), '2': int(self.character_data.value("E107") or 0), '3': int(self.character_data.value("AK113") or 0), '4': int(self.character_data.value("E119") or 0), '5': int(self.character_data.value("AK124") or 0), '6': int(self.character_data.value("E129") or 0), '7': int(self.character_data.value("AK134") or 0), '8': int(self.character_data.value("E138") or 0), '9': int(self.character_data.value("AK142") or 0) } # spells C96:AH143 potential_spells = self.character_data.value_range("D96:AH143") if self.additional: potential_spells.extend(self.additional.value_range("D17:AH64")) spells = [] for value in potential_spells: value = value.strip() if len(value) > 2 and value not in IGNORED_SPELL_VALUES: log.debug(f"Searching for spell {value}") result, strict = search(compendium.spells, value, lambda sp: sp.name, strict=True) if result and strict: spells.append(SpellbookSpell(result.name, True)) else: spells.append(SpellbookSpell(value.strip())) try: dc = int(self.character_data.value("AB91") or 0) except ValueError: dc = None try: sab = int(self.character_data.value("AI91") or 0) except ValueError: sab = None spellbook = Spellbook(slots, slots, spells, dc, sab, self.total_level) return spellbook
def get_spellbook(self): if self.character_data is None: raise Exception('You must call get_character() first.') spellnames = [ s.get('name', '') for s in self.character_data.get('spells', []) if not s.get('removed', False) ] slots = {} for lvl in range(1, 10): num_slots = int(self.calculate_stat(f"level{lvl}SpellSlots")) slots[str(lvl)] = num_slots spells = [] for spell in spellnames: result = search(compendium.spells, spell.strip(), lambda sp: sp.name) if result and result[0] and result[1]: spells.append(SpellbookSpell.from_spell(result[0])) else: spells.append(SpellbookSpell(spell.strip())) spell_lists = [(0, 0)] # ab, dc for sl in self.character_data.get('spellLists', []): try: ab = int(self.evaluator.eval(sl.get('attackBonus'))) dc = int(self.evaluator.eval(sl.get('saveDC'))) spell_lists.append((ab, dc)) except: pass sl = sorted(spell_lists, key=lambda k: k[0], reverse=True)[0] sab = sl[0] dc = sl[1] spellbook = Spellbook(slots, slots, spells, dc, sab, self.get_levels().total_level) log.debug(f"Completed parsing spellbook: {spellbook.to_dict()}") return spellbook
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: list = None, proper: bool = False, image_url: str = None, spellcasting=None, page=None, raw_resists: dict = 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 = [] if spellcasting is None: spellcasting = Spellbook({}, {}, []) if passiveperc is None: passiveperc = 10 + skills.perception.value if raw_resists is None: raw_resists = {} self.name = name self.size = size self.race = race self.alignment = alignment self.ac = ac self.armortype = armortype self.hp = hp self.hitdice = hitdice self.speed = speed self.ability_scores = ability_scores self.cr = cr self.xp = xp self.passive = passiveperc self.senses = senses self.vuln = vuln self.resist = resist self.immume = immune self.condition_immune = condition_immune self.saves = saves self.skills = skills 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.attacks = attacks self.proper = proper self.image_url = image_url self.spellbook = spellcasting self.page = page # this should really be by source, but oh well self.raw_resists = raw_resists