Example #1
0
 def copy(cls, other: Spellbook):
     """Makes a copy of a MonsterSpellbook (for adding to init)."""
     new = other.to_dict()
     if 'daily' in new:
         new['daily'] = new['daily'].copy()
     new['slots'] = new['slots'].copy()
     return cls.from_dict(new)
Example #2
0
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
Example #3
0
    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
Example #4
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 #5
0
 def from_dict(cls, raw, ctx, combat):
     if raw['saves']:
         saves = Saves.from_dict(raw['saves'])
     else:
         saves = None
     inst = cls(raw['name'], raw['controller'], raw['init'], raw['mod'], raw['hpMax'], raw['hp'], raw['ac'],
                raw['private'], raw['resists'], raw['attacks'], saves, ctx, combat,
                index=raw['index'], notes=raw['notes'], effects=[], group=raw['group'],  # begin backwards compatibility
                temphp=raw.get('temphp'), spellbook=Spellbook.from_dict(raw.get('spellbook', {})))
     inst._effects = [Effect.from_dict(e, combat, inst) for e in raw['effects']]
     return inst
Example #6
0
 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)
Example #7
0
    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
Example #8
0
    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
Example #9
0
    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)
Example #10
0
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
Example #11
0
    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)
Example #12
0
    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
Example #13
0
    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)
Example #14
0
    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