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())) # dc try: dc = int(self.character_data.value("AB91") or 0) except ValueError: dc = None # sab try: sab = int(self.character_data.value("AI91") or 0) except ValueError: sab = None # spellcasting mod spell_mod_value = self.character_data.value("U91") spell_mod = None if spell_mod_value: # it might be in the form of a ability name, or an int, wjdk try: spell_mod = self.get_stats().get_mod(spell_mod_value) except ValueError: try: spell_mod = int(spell_mod_value) except (TypeError, ValueError): spell_mod = None spellbook = Spellbook(slots, slots, spells, dc, sab, self.total_level, spell_mod) return spellbook
def add_known_spell(self, spell, dc: int = None, sab: int = None, mod: int = None): """Adds a spell to the character's known spell list.""" if spell.name in self.spellbook: raise InvalidArgument("You already know this spell.") sbs = SpellbookSpell.from_spell(spell, dc, sab, mod) self.spellbook.spells.append(sbs) self.overrides.spells.append(sbs)
def extract_spells(text): extracted = [] spell_names = text.split(', ') for name in spell_names: # remove any (parenthetical stuff) except (UA) name = re.sub(r'\((?!ua\)).+\)', '', name.lower()) s = name.strip('* _').replace('.', '').replace('$', '') try: real_name = next(sp for sp in gd.compendium.spells if sp.name.lower() == s).name strict = True except StopIteration: real_name = s strict = False extracted.append( SpellbookSpell(real_name, strict=strict, dc=type_dc, sab=type_sab, mod=type_casting_ability)) type_spells.extend(extracted) return extracted
def get_spellbook(self): if self.character_data is None: raise Exception('You must call get_character() first.') potential_spells = [s 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 spell_lists = {} # list_id: (ab, dc, scam) for sl in self.character_data.get('spellLists', []): try: ab_calc = sl.get('attackBonus') ab = int(self.evaluator.eval(ab_calc)) dc = int(self.evaluator.eval(sl.get('saveDC'))) try: scam = self.get_stats().get_mod(next(m for m in STAT_NAMES if m in ab_calc)) except StopIteration: scam = 0 spell_lists[sl['_id']] = (ab, dc, scam) except: pass sab, dc, scam = sorted(spell_lists.values(), key=lambda k: k[0], reverse=True)[0] if spell_lists else (0, 0, 0) spells = [] for spell in potential_spells: spell_list_id = spell['parent']['id'] spell_ab, spell_dc, spell_mod = spell_lists.get(spell_list_id, (None, None, None)) if spell_ab == sab: spell_ab = None if spell_dc == dc: spell_dc = None if spell_mod == scam: spell_mod = None result, strict = search(compendium.spells, spell['name'].strip(), lambda sp: sp.name, strict=True) if result and strict: spells.append(SpellbookSpell.from_spell(result, sab=spell_ab, dc=spell_dc, mod=spell_mod)) else: spells.append(SpellbookSpell(spell['name'].strip(), sab=spell_ab, dc=spell_dc, mod=spell_mod)) spellbook = Spellbook(slots, slots, spells, dc, sab, self.get_levels().total_level, scam) log.debug(f"Completed parsing spellbook: {spellbook.to_dict()}") return spellbook
def add_known_spell(self, spell): """Adds a spell to the character's known spell list. :param spell (Spell) - the Spell. :returns self""" if spell.name in self.spellbook: raise InvalidArgument("You already know this spell.") sbs = SpellbookSpell.from_spell(spell) self.spellbook.spells.append(sbs) self.overrides.spells.append(sbs)
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, 0)] # ab, dc, scam for sl in self.character_data.get('spellLists', []): try: ab_calc = sl.get('attackBonus') ab = int(self.evaluator.eval(ab_calc)) dc = int(self.evaluator.eval(sl.get('saveDC'))) scam = self.get_stats().get_mod( next(m for m in STAT_NAMES if m in ab_calc)) spell_lists.append((ab, dc, scam)) except: pass sab, dc, scam = sorted(spell_lists, key=lambda k: k[0], reverse=True)[0] spellbook = Spellbook(slots, slots, spells, dc, sab, self.get_levels().total_level, scam) log.debug(f"Completed parsing spellbook: {spellbook.to_dict()}") return spellbook
def _get_spellbook(self): spellbook = self.character_data['spellbook'] max_slots = {'1': 0, '2': 0, '3': 0, '4': 0, '5': 0, '6': 0, '7': 0, '8': 0, '9': 0} slots = {'1': 0, '2': 0, '3': 0, '4': 0, '5': 0, '6': 0, '7': 0, '8': 0, '9': 0} for slot in spellbook['slots']: slots[str(slot['level'])] = slot['remaining'] max_slots[str(slot['level'])] = slot['available'] dcs = [] sabs = [] mods = [] spells = [] for spell in spellbook['spells']: spell_ab = spell['sab'] spell_dc = spell['dc'] spell_mod = spell['mod'] if spell_ab is not None: sabs.append(spell_ab) if spell_dc is not None: dcs.append(spell_dc) if spell_mod is not None: mods.append(spell_mod) result = next((s for s in compendium.spells if s.entity_id == spell['id']), None) if result: spells.append(SpellbookSpell.from_spell(result, sab=spell_ab, dc=spell_dc, mod=spell_mod)) else: spells.append(SpellbookSpell(spell['name'].strip(), sab=spell_ab, dc=spell_dc, mod=spell_mod)) dc = max(dcs, key=dcs.count, default=None) sab = max(sabs, key=sabs.count, default=None) smod = max(mods, key=mods.count, default=None) return Spellbook(slots, max_slots, spells, dc, sab, self._get_levels().total_level, smod)
def extract_spells(text): extracted = [] spell_names = text.split(', ') for name in spell_names: s = name.strip('* _') try: real_name = next(sp for sp in compendium.spells if sp.name.lower() == s).name strict = True except StopIteration: real_name = s strict = False extracted.append( SpellbookSpell(real_name, strict=strict, dc=type_dc, sab=type_sab, mod=type_casting_ability)) type_spells.extend(extracted) return extracted
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 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 _get_spellbook(self): spellbook = self.character_data['spellbook'] max_slots = { '1': 0, '2': 0, '3': 0, '4': 0, '5': 0, '6': 0, '7': 0, '8': 0, '9': 0 } slots = { '1': 0, '2': 0, '3': 0, '4': 0, '5': 0, '6': 0, '7': 0, '8': 0, '9': 0 } for slot in spellbook['slots']: slots[str(slot['level'])] = slot['remaining'] max_slots[str(slot['level'])] = slot['available'] dcs = [] sabs = [] mods = [] spells = [] for spell in spellbook['spells']: spell_ab = spell['sab'] spell_dc = spell['dc'] spell_mod = spell['mod'] spell_prepared = spell['prepared'] or 'noprep' in self.args if spell_ab is not None: sabs.append(spell_ab) if spell_dc is not None: dcs.append(spell_dc) if spell_mod is not None: mods.append(spell_mod) result = compendium.lookup_entity(gamedata.Spell.entity_type, spell['id']) if result: spells.append( SpellbookSpell.from_spell(result, sab=spell_ab, dc=spell_dc, mod=spell_mod, prepared=spell_prepared)) else: spells.append( SpellbookSpell(spell['name'].strip(), sab=spell_ab, dc=spell_dc, mod=spell_mod, prepared=spell_prepared)) dc = max(dcs, key=dcs.count, default=None) sab = max(sabs, key=sabs.count, default=None) smod = max(mods, key=mods.count, default=None) # assumption: a character will only ever have one pact slot level, with a given number of slots of that level pact_slot_level = None num_pact_slots = None max_pact_slots = None if spellbook['pactSlots']: pact_info = spellbook['pactSlots'][0] pact_slot_level = pact_info['level'] max_pact_slots = pact_info['available'] num_pact_slots = max_pact_slots - pact_info['used'] return Spellbook(slots, max_slots, spells, dc, sab, self._get_levels().total_level, smod, pact_slot_level=pact_slot_level, num_pact_slots=num_pact_slots, max_pact_slots=max_pact_slots)
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_spellbook(self): if self.character_data is None: raise Exception('You must call get_character() first.') spellcasterLevel = 0 castingClasses = 0 spell_mod = 0 pactSlots = 0 pactLevel = 1 hasSpells = False for _class in self.character_data['classes']: castingAbility = _class['definition']['spellCastingAbilityId'] or \ (_class['subclassDefinition'] or {}).get('spellCastingAbilityId') if castingAbility: casterMult = CASTER_TYPES.get(_class['definition']['name'], 1) spellcasterLevel += _class['level'] * casterMult castingClasses += 1 if casterMult else 0 # warlock multiclass fix spell_mod = max(spell_mod, self.stat_from_id(castingAbility)) class_features = { cf['name'] for cf in _class['definition']['classFeatures'] if cf['requiredLevel'] <= _class['level'] } if _class['subclassDefinition']: class_features.update({ cf['name'] for cf in _class['subclassDefinition']['classFeatures'] if cf['requiredLevel'] <= _class['level'] }) hasSpells = 'Spellcasting' in class_features or hasSpells 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 hasSpells: 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 save_dc_bonus = max(self.get_stat("spell-save-dc"), self.get_stat("warlock-spell-save-dc")) attack_bonus_bonus = max(self.get_stat("spell-attacks"), self.get_stat("warlock-spell-attacks")) dc = 8 + spell_mod + prof + save_dc_bonus sab = spell_mod + 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(compendium.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, spell_mod or None) return spellbook
def migrate(character): name = character['stats']['name'] sheet_type = character.get('type') import_version = character.get('version') print(f"Migrating {name} - {sheet_type} v{import_version}") owner = character['owner'] upstream = character['upstream'] active = character['active'] description = character['stats'].get('description', "No description") image = character['stats']['image'] stats = { "prof_bonus": character['stats']['proficiencyBonus'], "strength": character['stats']['strength'], "dexterity": character['stats']['dexterity'], "constitution": character['stats']['constitution'], "intelligence": character['stats']['intelligence'], "wisdom": character['stats']['wisdom'], "charisma": character['stats']['charisma'] } # classes classes = {} for c, l in character['levels'].items(): if c.endswith("Level"): classes[c[:-5]] = l for cls, lvl in list(classes.items())[:]: if any(inv in cls for inv in ".$"): classes.pop(cls) levels = {"total_level": character['levels']['level'], "classes": classes} # attacks attacks = [] for a in character['attacks']: try: bonus = int(a['attackBonus']) bonus_calc = None except (ValueError, TypeError): bonus = None bonus_calc = a['attackBonus'] atk = { "name": a['name'], "bonus": bonus, "damage": a['damage'], "details": a.get('details'), "bonus_calc": bonus_calc } attacks.append(atk) # skills and saves skills = {} for skill_name in SKILL_NAMES: value = character['skills'][skill_name] skefct = character.get('skill_effects', {}).get(skill_name) adv = True if skefct == 'adv' else False if skefct == 'dis' else None skl = Skill(value, 0, 0, adv) skills[skill_name] = skl.to_dict() saves = {} for save_name in SAVE_NAMES: value = character['saves'][save_name] skefct = character.get('skill_effects', {}).get(save_name) adv = True if skefct == 'adv' else False if skefct == 'dis' else None skl = Skill(value, 0, 0, adv) saves[save_name] = skl.to_dict() # combat resistances = { "resist": character.get('resist', []), "immune": character.get('immune', []), "vuln": character.get('vuln', []) } ac = character.get('armor', 10) max_hp = character.get('hp', 4) # you get 4 hp if your character is that old hp = character.get('consumables', {}).get('hp', {}).get('value', max_hp) temp_hp = character.get('consumables', {}).get('temphp', {}).get('value', 0) cvars = character.get('cvars', {}) options = {"options": character.get('settings', {})} # overrides override_attacks = [] for a in character.get('overrides', {}).get('attacks', []): try: bonus = int(a['attackBonus']) bonus_calc = None except (ValueError, TypeError): bonus = None bonus_calc = a['attackBonus'] atk = { "name": a['name'], "bonus": bonus, "damage": a['damage'], "details": a['details'], "bonus_calc": bonus_calc } override_attacks.append(atk) override_spells = [] for old_spell in character.get('overrides', {}).get('spells', []): if isinstance(old_spell, dict): spl = SpellbookSpell(old_spell['name'], old_spell['strict']) else: spl = SpellbookSpell(old_spell, True) override_spells.append(spl.to_dict()) overrides = { "desc": character.get('overrides', {}).get('desc'), "image": character.get('overrides', {}).get('image'), "attacks": override_attacks, "spells": override_spells } # other things consumables = [] for cname, cons in character.get('consumables', {}).get('custom', {}).items(): value = cons['value'] minv = cons.get('min') maxv = cons.get('max') reset = cons.get('reset') display_type = cons.get('type') live_id = cons.get('live') counter = CustomCounter(None, cname, value, minv, maxv, reset, display_type, live_id) consumables.append(counter.to_dict()) death_saves = { "successes": character.get('consumables', {}).get('deathsaves', {}).get('success', {}).get('value', 0), "fails": character.get('consumables', {}).get('deathsaves', {}).get('fail', {}).get('value', 0) } # spellcasting slots = {} max_slots = {} for l in range(1, 10): slots[str(l)] = character.get('consumables', {}).get('spellslots', {}).get(str(l), {}).get('value', 0) max_slots[str(l)] = character.get('spellbook', {}).get('spellslots', {}).get(str(l), 0) spells = [] for old_spell in character.get('spellbook', {}).get('spells', []): if isinstance(old_spell, dict): spl = SpellbookSpell(old_spell['name'], old_spell['strict']) else: spl = SpellbookSpell(old_spell, True) spells.append(spl.to_dict()) spellbook = { "slots": slots, "max_slots": max_slots, "spells": spells, "dc": character.get('spellbook', {}).get('dc'), "sab": character.get('spellbook', {}).get('attackBonus'), "caster_level": character['levels']['level'] } live = 'dicecloud' if character.get('live') else None race = character.get('race') background = character.get('background') char = Character(owner, upstream, active, sheet_type, import_version, name, description, image, stats, levels, attacks, skills, resistances, saves, ac, max_hp, hp, temp_hp, cvars, options, overrides, consumables, death_saves, spellbook, live, race, background) return char