Пример #1
0
    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
Пример #2
0
    async def attack_add(self, ctx, name, *args):
        """
        Adds an attack to the active character.
        __Arguments__
        -d [damage]: How much damage the attack should do.
        -b [to-hit]: The to-hit bonus of the attack.
        -desc [description]: A description of the attack.
        """
        character: Character = await Character.from_ctx(ctx)
        parsed = argparse(args)

        attack = Attack.new(character,
                            name,
                            bonus_calc=parsed.join('b', '+'),
                            damage=parsed.join('d', '+'),
                            details=parsed.join('desc', '\n'))

        conflict = next((a for a in character.overrides.attacks
                         if a.name.lower() == attack.name.lower()), None)
        if conflict:
            character.overrides.attacks.remove(conflict)
        character.overrides.attacks.append(attack)
        await character.commit(ctx)

        out = f"Created attack {attack.name}!"
        if conflict:
            out += f" Removed a duplicate attack."
        await ctx.send(out)
Пример #3
0
    def parse_attack(self, name_index, bonus_index, damage_index, sheet=None):
        """Calculates and returns a dict."""
        if self.character_data is None:
            raise Exception('You must call get_character() first.')

        wksht = sheet or self.character_data

        name = wksht.cell(name_index).value
        damage = wksht.cell(damage_index).value
        bonus = wksht.cell(bonus_index).value
        details = None

        if not name:
            return None

        if not damage:
            damage = None
        else:
            details = None
            if '|' in damage:
                damage, details = damage.split('|', 1)

            dice, comment = get_roll_comment(damage)
            if details:
                details = details.strip()

            if any(d in comment.lower() for d in DAMAGE_TYPES):
                damage = "{}[{}]".format(dice, comment)
            else:
                damage = dice
                if comment.strip() and not details:
                    damage = comment.strip()

        bonus_calc = None
        if bonus:
            try:
                bonus = int(bonus)
            except (TypeError, ValueError):
                bonus_calc = bonus
                bonus = None
        else:
            bonus = None

        attack = Attack(name, bonus, damage, details, bonus_calc)
        return attack
Пример #4
0
    def parse_attack(self, atk_dict) -> Attack:
        """Calculates and returns a dict."""
        if self.character_data is None:
            raise Exception('You must call get_character() first.')

        log.debug(f"Processing attack {atk_dict.get('name')}")

        # setup temporary local vars
        temp_names = {}
        if atk_dict.get('parent', {}).get('collection') == 'Spells':
            spellParentID = atk_dict.get('parent', {}).get('id')
            try:
                spellObj = next(s
                                for s in self.character_data.get('spells', [])
                                if s.get('_id') == spellParentID)
            except StopIteration:
                pass
            else:
                spellListParentID = spellObj.get('parent', {}).get('id')
                try:
                    spellListObj = next(
                        s for s in self.character_data.get('spellLists', [])
                        if s.get('_id') == spellListParentID)
                except StopIteration:
                    pass
                else:
                    try:
                        temp_names['attackBonus'] = int(
                            self.evaluator.eval(
                                spellListObj.get('attackBonus')))
                        temp_names['DC'] = int(
                            self.evaluator.eval(spellListObj.get('saveDC')))
                    except Exception as e:
                        log.debug(f"Exception parsing spellvars: {e}")

        temp_names['rageDamage'] = self.calculate_stat('rageDamage')
        old_names = self.evaluator.names.copy()
        self.evaluator.names.update(temp_names)
        log.debug(f"evaluator tempnames: {temp_names}")

        # attack bonus
        bonus_calc = atk_dict.get('attackBonus',
                                  '').replace('{', '').replace('}', '')
        if not bonus_calc:
            bonus = None
        else:
            try:
                bonus = int(self.evaluator.eval(bonus_calc))
            except:
                bonus = None

        # damage
        def damage_sub(match):
            out = match.group(1)
            try:
                log.debug(f"damage_sub: evaluating {out}")
                return str(self.evaluator.eval(out))
            except Exception as ex:
                log.debug(f"exception in damage_sub: {ex}")
                return match.group(0)

        damage = re.sub(r'{(.*?)}', damage_sub, atk_dict.get('damage', ''))
        damage = damage.replace('{', '').replace('}', '')
        if not damage:
            damage = None
        else:
            damage += ' [{}]'.format(atk_dict.get('damageType'))

        # details
        details = atk_dict.get('details', None)
        if details:
            details = re.sub(r'{([^{}]*)}', damage_sub, details)

        # build attack
        name = atk_dict['name']
        attack = Attack(name, bonus, damage, details, bonus_calc)

        self.evaluator.names = old_names

        return attack
Пример #5
0
    def parse_attack(self, atkIn, atkType):
        """Calculates and returns a list of dicts."""
        if self.character_data is None:
            raise Exception('You must call get_character() first.')
        prof = self.get_stats().prof_bonus
        out = []

        if atkType == 'action':
            if atkIn['dice'] is None:
                return []  # thanks DDB
            isProf = atkIn['isProficient']
            atkBonus = None
            dmgBonus = ""
            if atkIn["abilityModifierStatId"]:
                atkBonus = self.stat_from_id(
                    atkIn['abilityModifierStatId']) + (prof if isProf else 0)
                dmgBonus = f"+{self.stat_from_id(atkIn['abilityModifierStatId'])}"
            attack = Attack(
                atkIn['name'], atkBonus,
                f"{atkIn['dice']['diceString']}{dmgBonus}[{parse_dmg_type(atkIn)}]",
                atkIn['snippet'])
            out.append(attack)
        elif atkType == 'customAction':
            isProf = atkIn['isProficient']
            dmgBonus = (atkIn['fixedValue'] or 0) + (atkIn['damageBonus'] or 0)
            atkBonus = None
            if atkIn['statId']:
                atkBonus = self.stat_from_id(atkIn['statId']) + (
                    prof if isProf else 0) + (atkIn['toHitBonus'] or 0)
                dmgBonus = (atkIn['fixedValue'] or 0) + self.stat_from_id(
                    atkIn['statId']) + (atkIn['damageBonus'] or 0)

            if atkIn['attackSubtype'] == 3:  # natural weapons
                if atkBonus is not None:
                    atkBonus += self.get_stat('natural-attacks')
                dmgBonus += self.get_stat('natural-attacks-damage')

            damage = f"{atkIn['diceCount']}d{atkIn['diceType']}+{dmgBonus}[{parse_dmg_type(atkIn)}]"
            attack = Attack(atkIn['name'], atkBonus, damage, atkIn['snippet'])
            out.append(attack)
        elif atkType == 'item':
            itemdef = atkIn['definition']
            weirdBonuses = self.get_specific_item_bonuses(atkIn['id'])
            isProf = self.get_prof(itemdef['type']) or weirdBonuses['isPact']
            magicBonus = sum(
                m['value'] for m in itemdef['grantedModifiers']
                if m['type'] == 'bonus' and m['subType'] == 'magic')
            modBonus = self.get_relevant_atkmod(
                itemdef) if not weirdBonuses['isHex'] else self.stat_from_id(6)

            dmgBonus = modBonus + magicBonus + weirdBonuses['damage']
            toHitBonus = (prof if isProf else
                          0) + magicBonus + weirdBonuses['attackBonus']

            is_melee = not 'Range' in [
                p['name'] for p in itemdef['properties']
            ]
            is_one_handed = not 'Two-Handed' in [
                p['name'] for p in itemdef['properties']
            ]
            is_weapon = itemdef['filterType'] == 'Weapon'

            if is_melee and is_one_handed:
                dmgBonus += self.get_stat('one-handed-melee-attacks-damage')
            if not is_melee and is_weapon:
                toHitBonus += self.get_stat('ranged-weapon-attacks')

            damage = None
            if itemdef['fixedDamage'] or itemdef['damage']:
                damage = f"{itemdef['fixedDamage'] or itemdef['damage']['diceString']}+{dmgBonus}" \
                    f"[{itemdef['damageType'].lower()}" \
                    f"{'^' if itemdef['magic'] or weirdBonuses['isPact'] else ''}]"

            atkBonus = weirdBonuses[
                'attackBonusOverride'] or modBonus + toHitBonus
            details = html2text.html2text(itemdef['description'],
                                          bodywidth=0).strip()
            attack = Attack(itemdef['name'], atkBonus, damage, details)
            out.append(attack)

            if 'Versatile' in [p['name'] for p in itemdef['properties']]:
                versDmg = next(p['notes'] for p in itemdef['properties']
                               if p['name'] == 'Versatile')
                damage = f"{versDmg}+{dmgBonus}[{itemdef['damageType'].lower()}" \
                    f"{'^' if itemdef['magic'] or weirdBonuses['isPact'] else ''}]"
                attack = Attack(f"2-Handed {itemdef['name']}", atkBonus,
                                damage, details)
                out.append(attack)
        elif atkType == 'unarmed':
            monk_level = self.get_levels().get('Monk')
            ability_mod = self.stat_from_id(1) if not monk_level else max(
                self.stat_from_id(1), self.stat_from_id(2))
            if not monk_level:
                dmg = 1 + ability_mod
            elif monk_level < 5:
                dmg = f"1d4+{ability_mod}"
            elif monk_level < 11:
                dmg = f"1d6+{ability_mod}"
            elif monk_level < 17:
                dmg = f"1d8+{ability_mod}"
            else:
                dmg = f"1d10+{ability_mod}"
            atkBonus = self.get_stats().prof_bonus

            atkBonus += self.get_stat('natural-attacks')
            natural_bonus = self.get_stat('natural-attacks-damage')
            if natural_bonus:
                dmg = f"{dmg}+{natural_bonus}"

            attack = Attack("Unarmed Strike", ability_mod + atkBonus,
                            f"{dmg}[bludgeoning]")
            out.append(attack)
        return [a.to_dict() for a in out]
Пример #6
0
    async def monster_atk(self, ctx, monster_name, atk_name=None, *, args=''):
        """Rolls a monster's attack.
        __Valid Arguments__
        adv/dis
        -ac [target ac]
        -b [to hit bonus]
        -d [damage bonus]
        -d# [applies damage to the first # hits]
        -rr [times to reroll]
        -t [target]
        -phrase [flavor text]
        crit (automatically crit)
        -h (hides monster name, image, and attack details)"""
        if atk_name is None or atk_name == 'list':
            return await ctx.invoke(self.monster_atk_list, monster_name)

        try:
            await ctx.message.delete()
        except:
            pass

        monster = await select_monster_full(ctx, monster_name)
        attacks = monster.attacks
        monster_name = monster.get_title_name()

        attack = await search_and_select(ctx, attacks, atk_name,
                                         lambda a: a['name'])
        args = await scripting.parse_snippets(args, ctx)
        args = argparse(args)
        if not args.last('h', type_=bool):
            name = monster_name
            image = args.get('image') or monster.get_image_url()
        else:
            name = "An unknown creature"
            image = None

        attack = Attack.from_old(attack)

        embed = discord.Embed()
        if args.last('title') is not None:
            embed.title = args.last('title') \
                .replace('[name]', name) \
                .replace('[aname]', attack.name)
        else:
            embed.title = '{} attacks with {}!'.format(name,
                                                       a_or_an(attack.name))

        if image:
            embed.set_thumbnail(url=image)

        caster, targets, combat = await targetutils.maybe_combat(
            ctx, monster, args.get('t'))
        await Automation.from_attack(attack).run(ctx,
                                                 embed,
                                                 caster,
                                                 targets,
                                                 args,
                                                 combat=combat,
                                                 title=embed.title)
        if combat:
            await combat.final()

        _fields = args.get('f')
        embeds.add_fields_from_args(embed, _fields)
        embed.colour = random.randint(0, 0xffffff)

        if monster.source == 'homebrew':
            embeds.add_homebrew_footer(embed)

        await ctx.send(embed=embed)
Пример #7
0
    def parse_attack(self, atkIn, atkType):
        """Calculates and returns a list of dicts."""
        if self.character_data is None: raise Exception('You must call get_character() first.')
        prof = self.get_stats().prof_bonus
        out = []

        def monk_scale():
            monk_level = self.get_levels().get('Monk')
            if not monk_level:
                monk_dice_size = 0
            elif monk_level < 5:
                monk_dice_size = 4
            elif monk_level < 11:
                monk_dice_size = 6
            elif monk_level < 17:
                monk_dice_size = 8
            else:
                monk_dice_size = 10
            return monk_dice_size

        if atkType == 'action':
            if atkIn['dice'] is None:
                return []  # thanks DDB
            isProf = atkIn['isProficient']
            atkBonus = None
            dmgBonus = None

            dice_size = max(monk_scale(), atkIn['dice']['diceValue'])
            base_dice = f"{atkIn['dice']['diceCount']}d{dice_size}"

            if atkIn["abilityModifierStatId"]:
                atkBonus = self.stat_from_id(atkIn['abilityModifierStatId'])
                dmgBonus = self.stat_from_id(atkIn['abilityModifierStatId'])

            if atkIn["isMartialArts"] and self.get_levels().get("Monk"):
                atkBonus = max(atkBonus, self.stat_from_id(2))  # allow using dex
                dmgBonus = max(dmgBonus, self.stat_from_id(2))

            if isProf:
                atkBonus += prof

            if dmgBonus:
                damage = f"{base_dice}+{dmgBonus}[{parse_dmg_type(atkIn)}]"
            else:
                damage = f"{base_dice}[{parse_dmg_type(atkIn)}]"
            attack = Attack(
                atkIn['name'], atkBonus, damage,
                atkIn['snippet']
            )
            out.append(attack)
        elif atkType == 'customAction':
            isProf = atkIn['isProficient']
            dmgBonus = (atkIn['fixedValue'] or 0) + (atkIn['damageBonus'] or 0)
            atkBonus = None
            if atkIn['statId']:
                atkBonus = self.stat_from_id(atkIn['statId']) + (prof if isProf else 0) + (atkIn['toHitBonus'] or 0)
                dmgBonus = (atkIn['fixedValue'] or 0) + self.stat_from_id(atkIn['statId']) + (atkIn['damageBonus'] or 0)

            if atkIn['attackSubtype'] == 3:  # natural weapons
                if atkBonus is not None:
                    atkBonus += self.get_stat('natural-attacks')
                dmgBonus += self.get_stat('natural-attacks-damage')

            damage = f"{atkIn['diceCount']}d{atkIn['diceType']}+{dmgBonus}[{parse_dmg_type(atkIn)}]"
            attack = Attack(
                atkIn['name'], atkBonus, damage, atkIn['snippet']
            )
            out.append(attack)
        elif atkType == 'item':
            itemdef = atkIn['definition']
            weirdBonuses = self.get_specific_item_bonuses(atkIn['id'])
            isProf = self.get_prof(itemdef['type']) or weirdBonuses['isPact']
            magicBonus = self._item_magic_bonus(itemdef)
            modBonus = self.get_relevant_atkmod(itemdef) if not weirdBonuses['isHex'] else self.stat_from_id(6)
            item_dmg_bonus = self.get_stat(f"{itemdef['type'].lower()}-damage")

            dmgBonus = modBonus + magicBonus + weirdBonuses['damage'] + item_dmg_bonus
            toHitBonus = (prof if isProf else 0) + magicBonus + weirdBonuses['attackBonus']

            is_melee = not 'Range' in [p['name'] for p in itemdef['properties']]
            is_one_handed = not 'Two-Handed' in [p['name'] for p in itemdef['properties']]
            is_weapon = itemdef['filterType'] == 'Weapon'

            if is_melee and is_one_handed:
                dmgBonus += self.get_stat('one-handed-melee-attacks-damage')

            if not is_melee and is_weapon:
                toHitBonus += self.get_stat('ranged-weapon-attacks')

            if weirdBonuses['isPact'] and self._improved_pact_weapon_applies(itemdef):
                dmgBonus += 1
                toHitBonus += 1

            base_dice = None
            if itemdef['fixedDamage']:
                base_dice = itemdef['fixedDamage']
            elif itemdef['damage']:
                if not itemdef['isMonkWeapon']:
                    base_dice = f"{itemdef['damage']['diceCount']}d{itemdef['damage']['diceValue']}"
                else:
                    dice_size = max(monk_scale(), itemdef['damage']['diceValue'])
                    base_dice = f"{itemdef['damage']['diceCount']}d{dice_size}"

            if base_dice:
                damage = f"{base_dice}+{dmgBonus}" \
                         f"[{itemdef['damageType'].lower()}" \
                         f"{'^' if itemdef['magic'] or weirdBonuses['isPact'] else ''}]"
            else:
                damage = None

            atkBonus = weirdBonuses['attackBonusOverride'] or modBonus + toHitBonus
            details = html2text.html2text(itemdef['description'], bodywidth=0).strip()
            attack = Attack(
                itemdef['name'], atkBonus, damage, details
            )
            out.append(attack)

            if 'Versatile' in [p['name'] for p in itemdef['properties']]:
                versDmg = next(p['notes'] for p in itemdef['properties'] if p['name'] == 'Versatile')
                damage = f"{versDmg}+{dmgBonus}[{itemdef['damageType'].lower()}" \
                         f"{'^' if itemdef['magic'] or weirdBonuses['isPact'] else ''}]"
                attack = Attack(
                    f"2-Handed {itemdef['name']}", atkBonus, damage, details
                )
                out.append(attack)
        elif atkType == 'unarmed':
            dice_size = monk_scale()
            ability_mod = self.stat_from_id(1) if not self.get_levels().get('Monk') else max(self.stat_from_id(1),
                                                                                             self.stat_from_id(2))
            atkBonus = prof
            if dice_size:
                dmg = f"1d{dice_size}+{ability_mod}"
            else:
                dmg = 1 + ability_mod

            atkBonus += self.get_stat('natural-attacks')
            natural_bonus = self.get_stat('natural-attacks-damage')
            if natural_bonus:
                dmg = f"{dmg}+{natural_bonus}"

            attack = Attack(
                "Unarmed Strike", ability_mod + atkBonus, f"{dmg}[bludgeoning]"
            )
            out.append(attack)
        return [a.to_dict() for a in out]