matteoferla/DnD-battler

View on GitHub
DnD_battler/creature/_level.py

Summary

Maintainability
B
5 hrs
Test Coverage
from ._base import CreatureBase
from ..actions import Action, MeleeAttack, Multiattack, equip_standard_weapon, weapons
from typing import *
import json
from ..dice import AttackRoll


class CreatureLevel(CreatureBase):
    def set_level(self, level: int, hp: Optional[int] = None, **other):
        """
        Alter the level of the creature.
        :param level: opt. int, the level. if absent it will set it to the stored level.
        :return: nothing. changes self.
        """
        level = int(level)
        old_level = self.level
        if hp is not None:
            self.hp = int(hp)
        elif old_level == 0:  # zero???
            self.recalculate_hp()
        else:
            for x in range(level - old_level):
                self.hp += self.hit_die.roll() + self.con.bonus
        self.level = level
        self.starting_hp = self.hp
        self.proficiency.level = level

    def recalculate_hp(self, max_level_one=True):
        self.hp = 0
        if max_level_one:
            self.hit_die.crit = 1  # Not-RAW: first level is always max for PCs, but not monsters.
        for x in range(self.level):
            self.hp += self.hit_die.roll()

    def set_ac(self,
               ac: Optional[int] = None,
               armor_bonus: Optional[int] = None,
               armour_name: Optional[str] = None,
               armor_ability_name: Optional[str] = None, **kwargs):
        if armour_name:
            self.armor.name = armour_name
        if armor_ability_name:
            # so for monk armor_ability_name='dex+str'
            self.armor.ability_dice = [self[ability_name] for ability_name in armor_ability_name.split('+')]
        if ac:
            self.armor.ac = int(ac)
        elif armor_bonus:
            self.armor.bonus = int(armor_bonus)
        else:
            pass
        return None

    # TODO Make self.actions
    # TODO parse_attacks(attack_parameters=) NO

    def parse_attack(self, attack: Union[None, Action, dict, list]) -> Action:
        if attack is None:  # nothing to be done.
            return None
        elif isinstance(attack, Action):
            return attack  # already an action.
        elif isinstance(attack, dict):
            roll = AttackRoll.parse_attack(**{'ability_die': self.str, **attack})
            return MeleeAttack(creature=self, name=roll.name, attack_roll=roll)
        elif isinstance(attack, list) and len(attack) > 1:
            # this is the ancient legacy input ['club', 2, 0, 4]
            roll = AttackRoll.parse_list_attack(attack, self.str)
            return MeleeAttack(creature=self, name=roll.name, attack_roll=roll)
        elif isinstance(attack, list) and len(attack) == 1 and attack in weapons:  # single weapon
            return equip_standard_weapon(self, weapon_name=attack)
        else:
            raise TypeError(f'attack is of type {type(attack)}')

    def parse_attacks(self,
                      attacks: Optional[List[Union[dict, Action, AttackRoll]]] = None,
                      **others) -> Action:
        """
        A multiattack is a curious case where a creature can do multiple attacks as a single action.
        Although technically it could do a single action –these are not added.
        The values of ``attacks`` if more than one
        are interpreted as a multiattack if multiple, regular if one.

        One could fill Creature.actions manually with the Action directly.
        Or use this.
        Due to legacy reasons it is heavily overloaded.
        attacks is a list of "attacks" or a single "attack".
        This being an action, attackroll, dict or list
        """
        # -----------it is not a list ----------------------------------------
        if attacks is None:
            return None  # What?
        elif len(attacks) == 0:
            return None  # ... ?
        elif isinstance(attacks, str) and attacks in weapons:  # single weapon
            return equip_standard_weapon(self, weapon_name=attacks)
        elif isinstance(attacks, str):  # json
            # go round again after de-json-ing.
            return self.parse_attacks(json.loads(attacks))
        elif isinstance(attacks, (dict, AttackRoll, Action)):
            # single attack. (list form solved below)
            return self.parse_attack(attacks)
        elif not isinstance(attacks, (list, tuple)):
            raise TypeError(f'Attacks is not list or tuple, but {type(attacks)}')
        # -----------it is a list ----------------------------------------
        elif isinstance(attacks[0], str):
            # attacks[0] is a name. single attack in list form.
            return self.parse_attack(attacks)
        elif not isinstance(attacks[0], Action):
            # attacks need converting first
            attacks = [self.parse_attack(attack) for attack in attacks]
            return self.parse_attacks(attacks)
        elif len(attacks) > 1:
            # attacks is a list of actions
            multi = Multiattack(creature=self, name='multiattack', actions=attacks)
            return multi
        else:
            # single attack
            return attacks[0]