Exemple #1
0
async def get_castable_spell(ctx, name, choices=None):
    if choices is None:
        choices = await get_spell_choices(ctx)

    result = search(choices, name, lambda sp: sp.name)
    if result and result[1]:
        return result[0]
    return None
Exemple #2
0
    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
Exemple #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
    def get_spellbook(self):
        if self.character is None:
            raise Exception('You must call get_character() first.')
        spellbook = {
            'spellslots': {},
            'spells': [],  # C96:AH143 - gah.
            'dc': 0,
            'attackBonus': 0
        }

        spellslots = {
            '1': int(self.character.cell('AK101').value or 0),
            '2': int(self.character.cell('E107').value or 0),
            '3': int(self.character.cell('AK113').value or 0),
            '4': int(self.character.cell('E119').value or 0),
            '5': int(self.character.cell('AK124').value or 0),
            '6': int(self.character.cell('E129').value or 0),
            '7': int(self.character.cell('AK134').value or 0),
            '8': int(self.character.cell('E138').value or 0),
            '9': int(self.character.cell('AK142').value or 0)
        }
        spellbook['spellslots'] = spellslots

        potential_spells = self.character.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({'name': result[0].name, 'strict': True})
                    elif len(value) > 2:
                        spells.append({'name': value, 'strict': False})

        spellbook['spells'] = spells

        try:
            spellbook['dc'] = int(self.character.cell('AB91').value or 0)
        except ValueError:
            pass

        try:
            spellbook['attackBonus'] = int(
                self.character.cell('AI91').value or 0)
        except ValueError:
            pass

        log.debug(f"Completed parsing spellbook: {spellbook}")
        return spellbook
Exemple #5
0
    async def spellbook(self, ctx):
        """Commands to display a character's known spells and metadata."""
        character = await Character.from_ctx(ctx)
        embed = EmbedWithCharacter(character)
        embed.description = f"{character.get_name()} knows {len(character.get_spell_list())} spells."
        embed.add_field(name="DC", value=str(character.get_save_dc()))
        embed.add_field(name="Spell Attack Bonus",
                        value=str(character.get_spell_ab()))
        embed.add_field(name="Spell Slots",
                        value=character.get_remaining_slots_str() or "None")
        spells_known = {}
        choices = await get_spell_choices(ctx)
        for spell_ in character.get_raw_spells():
            if isinstance(spell_, str):
                spell, strict = search(c.spells, spell_, lambda sp: sp.name)
                if spell is None or not strict:
                    continue
                spells_known[str(spell.level)] = spells_known.get(
                    str(spell.level), []) + [spell.name]
            else:
                spellname = spell_['name']
                strict = spell_['strict']
                spell = await get_castable_spell(ctx, spellname, choices)
                if spell is None and strict:
                    continue
                elif spell is None:
                    spells_known['unknown'] = spells_known.get(
                        'unknown', []) + [f"*{spellname}*"]
                else:
                    if spell.source == 'homebrew':
                        formatted = f"*{spell.name}*"
                    else:
                        formatted = spell.name
                    spells_known[str(spell.level)] = spells_known.get(
                        str(spell.level), []) + [formatted]

        level_name = {
            '0': 'Cantrips',
            '1': '1st Level',
            '2': '2nd Level',
            '3': '3rd Level',
            '4': '4th Level',
            '5': '5th Level',
            '6': '6th Level',
            '7': '7th Level',
            '8': '8th Level',
            '9': '9th Level'
        }
        for level, spells in sorted(list(spells_known.items()),
                                    key=lambda k: k[0]):
            if spells:
                spells.sort()
                embed.add_field(name=level_name.get(level, "Unknown"),
                                value=', '.join(spells))
        await ctx.send(embed=embed)
Exemple #6
0
    async def spellbook(self, ctx):
        """Commands to display a character's known spells and metadata."""
        await ctx.trigger_typing()

        character: Character = await Character.from_ctx(ctx)
        embed = EmbedWithCharacter(character)
        embed.description = f"{character.name} knows {len(character.spellbook.spells)} spells."
        embed.add_field(name="DC", value=str(character.spellbook.dc))
        embed.add_field(name="Spell Attack Bonus", value=str(character.spellbook.sab))
        embed.add_field(name="Spell Slots", value=character.spellbook.slots_str() or "None")

        # dynamic help flags
        flag_show_multiple_source_help = False
        flag_show_homebrew_help = False

        spells_known = collections.defaultdict(lambda: [])
        choices = await get_spell_choices(ctx)
        for spell_ in character.spellbook.spells:
            results, strict = search(choices, spell_.name, lambda sp: sp.name, strict=True)
            if not strict:
                if len(results) > 1:
                    spells_known['unknown'].append(f"*{spell_.name} ({'*' * len(results)})*")
                    flag_show_multiple_source_help = True
                else:
                    spells_known['unknown'].append(f"*{spell_.name}*")
                flag_show_homebrew_help = True
            else:
                spell = results
                if spell.homebrew:
                    formatted = f"*{spell.name}*"
                    flag_show_homebrew_help = True
                else:
                    formatted = spell.name
                spells_known[str(spell.level)].append(formatted)

        level_name = {'0': 'Cantrips', '1': '1st Level', '2': '2nd Level', '3': '3rd Level',
                      '4': '4th Level', '5': '5th Level', '6': '6th Level',
                      '7': '7th Level', '8': '8th Level', '9': '9th Level'}
        for level, spells in sorted(list(spells_known.items()), key=lambda k: k[0]):
            if spells:
                spells.sort()
                embed.add_field(name=level_name.get(level, "Unknown"), value=', '.join(spells), inline=False)

        # dynamic help
        footer_out = []
        if flag_show_homebrew_help:
            footer_out.append("An italicized spell indicates that the spell is homebrew.")
        if flag_show_multiple_source_help:
            footer_out.append("Asterisks after a spell indicates that the spell is being provided by multiple sources.")

        if footer_out:
            embed.set_footer(text=' '.join(footer_out))

        await ctx.send(embed=embed)
Exemple #7
0
    def get_spellbook(self):
        if self.character is None:
            raise Exception('You must call get_character() first.')
        spellbook = {
            'spellslots': {},
            'spells': [],
            'dc':
            0,
            'attackBonus':
            0,
            'dicecloud_id':
            next((sl['_id'] for sl in self.character.get('spellLists', [])
                  if not sl.get('removed')), None)
        }

        spells = self.character.get('spells', [])
        spellnames = [
            s.get('name', '') for s in spells if not s.get('removed', False)
        ]

        for lvl in range(1, 10):
            numSlots = self.calculate_stat(f"level{lvl}SpellSlots")
            spellbook['spellslots'][str(lvl)] = numSlots

        for spell in spellnames:
            result = search(c.spells, spell.strip(), lambda sp: sp.name)
            if result and result[0] and result[1]:
                spellbook['spells'].append({
                    'name': result[0].name,
                    'strict': True
                })
            else:
                spellbook['spells'].append({
                    'name': spell,
                    'strict': False
                })  # non-strict spell

        sls = [(0, 0)]  # ab, dc
        for sl in self.character.get('spellLists', []):
            try:
                ab = int(self.evaluator.eval(sl.get('attackBonus')))
                dc = int(self.evaluator.eval(sl.get('saveDC')))
                sls.append((ab, dc))
            except:
                pass
        sl = sorted(sls, key=lambda k: k[0], reverse=True)[0]
        spellbook['attackBonus'] = sl[0]
        spellbook['dc'] = sl[1]

        log.debug(f"Completed parsing spellbook: {spellbook}")

        return spellbook
Exemple #8
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.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
Exemple #9
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
Exemple #10
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
Exemple #11
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
Exemple #12
0
    async def spellbook(self, ctx, *args):
        """
        Commands to display a character's known spells and metadata.
        __Valid Arguments__
        all - Display all of a character's known spells, including unprepared ones.
        """
        await ctx.trigger_typing()

        character: Character = await Character.from_ctx(ctx)
        ep = embeds.EmbedPaginator(EmbedWithCharacter(character))
        ep.add_field(name="DC", value=str(character.spellbook.dc), inline=True)
        ep.add_field(name="Spell Attack Bonus", value=str(character.spellbook.sab), inline=True)
        ep.add_field(name="Spell Slots", value=character.spellbook.slots_str() or "None", inline=True)

        show_unprepared = 'all' in args
        known_count = len(character.spellbook.spells)
        prepared_count = sum(1 for spell in character.spellbook.spells if spell.prepared)

        if known_count == prepared_count:
            ep.add_description(f"{character.name} knows {known_count} spells.")
        else:
            ep.add_description(f"{character.name} has {prepared_count} spells prepared and knows {known_count} spells.")

        # dynamic help flags
        flag_show_multiple_source_help = False
        flag_show_homebrew_help = False
        flag_show_prepared_help = False
        flag_show_prepared_underline_help = False

        spells_known = collections.defaultdict(lambda: [])
        choices = await get_spell_choices(ctx)
        for sb_spell in character.spellbook.spells:
            if not (sb_spell.prepared or show_unprepared):
                flag_show_prepared_help = True
                continue

            # homebrew / multisource formatting
            results, strict = search(choices, sb_spell.name, lambda sp: sp.name, strict=True)
            if not strict:
                known_level = 'unknown'
                if len(results) > 1:
                    formatted = f"*{sb_spell.name} ({'*' * len(results)})*"
                    flag_show_multiple_source_help = True
                else:
                    formatted = f"*{sb_spell.name}*"
                flag_show_homebrew_help = True
            else:
                spell = results
                known_level = str(spell.level)
                if spell.homebrew:
                    formatted = f"*{spell.name}*"
                    flag_show_homebrew_help = True
                else:
                    formatted = spell.name

            # prepared formatting
            if show_unprepared and sb_spell.prepared:
                formatted = f"__{formatted}__"
                flag_show_prepared_underline_help = True

            spells_known[known_level].append(formatted)

        level_name = {
            '0': 'Cantrips', '1': '1st Level', '2': '2nd Level', '3': '3rd Level', '4': '4th Level', '5': '5th Level',
            '6': '6th Level', '7': '7th Level', '8': '8th Level', '9': '9th Level'
        }
        for level, spells in sorted(list(spells_known.items()), key=lambda k: k[0]):
            if spells:
                spells.sort(key=lambda s: s.lstrip('*_'))
                ep.add_field(name=level_name.get(level, "Unknown"), value=', '.join(spells), inline=False)

        # dynamic help
        footer_out = []
        if flag_show_homebrew_help:
            footer_out.append("An italicized spell indicates that the spell is homebrew.")
        if flag_show_multiple_source_help:
            footer_out.append("Asterisks after a spell indicates that the spell is being provided by multiple sources.")
        if flag_show_prepared_help:
            footer_out.append(f"Unprepared spells were not shown. Use \"{ctx.prefix}spellbook all\" to view them!")
        if flag_show_prepared_underline_help:
            footer_out.append("Prepared spells are marked with an underline.")

        if footer_out:
            ep.set_footer(value=' '.join(footer_out))
        await ep.send_to(ctx)