Beispiel #1
0
    def __init__(self,
                 gobject_template,
                 gobject_instance=None,
                 is_summon=False,
                 **kwargs):
        super().__init__(**kwargs)

        self.gobject_template = gobject_template
        self.gobject_instance = gobject_instance
        self.is_summon = is_summon

        self.entry = self.gobject_template.entry
        self.native_display_id = self.gobject_template.display_id
        self.current_display_id = self.native_display_id
        self.native_scale = self.gobject_template.scale
        self.current_scale = self.native_scale
        self.faction = self.gobject_template.faction

        if gobject_instance:
            if GameObjectManager.CURRENT_HIGHEST_GUID < gobject_instance.spawn_id:
                GameObjectManager.CURRENT_HIGHEST_GUID = gobject_instance.spawn_id

            self.guid = self.generate_object_guid(gobject_instance.spawn_id)
            self.state = self.gobject_instance.spawn_state
            self.location.x = self.gobject_instance.spawn_positionX
            self.location.y = self.gobject_instance.spawn_positionY
            self.location.z = self.gobject_instance.spawn_positionZ
            self.location.o = self.gobject_instance.spawn_orientation
            self.map_ = self.gobject_instance.spawn_map
            self.respawn_time = randint(
                self.gobject_instance.spawn_spawntimemin,
                self.gobject_instance.spawn_spawntimemax)

        self.object_type_mask |= ObjectTypeFlags.TYPE_GAMEOBJECT
        self.update_packet_factory.init_values(GameObjectFields.GAMEOBJECT_END)

        self.respawn_timer = 0
        self.loot_manager = None

        from game.world.managers.objects.spell.SpellManager import SpellManager  # Local due to circular imports.
        self.spell_manager = SpellManager(self)

        # Chest only initializations.
        if self.gobject_template.type == GameObjectTypes.TYPE_CHEST:
            self.loot_manager = GameObjectLootManager(self)

        # Ritual initializations.
        if self.gobject_template.type == GameObjectTypes.TYPE_RITUAL:
            self.ritual_caster = None
            self.ritual_participants = []

        # Trap collision initializations.
        if self.gobject_template.type == GameObjectTypes.TYPE_TRAP:
            self.trap_manager = TrapManager.generate(self)
Beispiel #2
0
    def __init__(
            self,
            channel_spell=0,
            channel_object=0,
            health=0,
            power_type=0,
            power_1=0,  # mana
            power_2=0,  # rage
            power_3=100,  # focus
            power_4=100,  # energy
            max_health=0,
            max_power_1=0,
            max_power_2=1000,
            max_power_3=100,
            max_power_4=100,
            level=0,
            gender=0,
            bytes_0=0,  # race, class, gender, power_type
            creature_type=0,
            stat_0=0,
            stat_1=0,
            stat_2=0,
            stat_3=0,
            stat_4=0,
            base_stat_0=0,
            base_stat_1=0,
            base_stat_2=0,
            base_stat_3=0,
            base_stat_4=0,
            flags=0,
            coinage=0,
            combat_reach=config.Unit.Defaults.combat_reach,
            weapon_reach=0,
            mount_display_id=0,
            resistance_buff_mods_positive_0=0,  # Armor
            resistance_buff_mods_positive_1=0,  # Holy
            resistance_buff_mods_positive_2=0,  # Fire
            resistance_buff_mods_positive_3=0,  # Nature
            resistance_buff_mods_positive_4=0,  # Frost
            resistance_buff_mods_positive_5=0,  # Shadow
            resistance_buff_mods_negative_0=0,
            resistance_buff_mods_negative_1=0,
            resistance_buff_mods_negative_2=0,
            resistance_buff_mods_negative_3=0,
            resistance_buff_mods_negative_4=0,
            resistance_buff_mods_negative_5=0,
            base_attack_time=config.Unit.Defaults.base_attack_time,
            offhand_attack_time=config.Unit.Defaults.offhand_attack_time,
            resistance_0=0,  # Armor
            resistance_1=0,
            resistance_2=0,
            resistance_3=0,
            resistance_4=0,
            resistance_5=0,
            stand_state=0,
            sheathe_state=WeaponMode.SHEATHEDMODE,
            shapeshift_form=0,
            bytes_1=0,  # stand state, shapeshift form, sheathstate
            mod_cast_speed=1,
            dynamic_flags=0,
            damage=0,  # current damage, max damage
            bytes_2=0,  # combo points, 0, 0, 0
            current_target=0,  # guid
            combat_target=None,  # victim
            **kwargs):
        super().__init__(**kwargs)

        self.combat_target = combat_target
        self.channel_spell = channel_spell
        self.channel_object = channel_object
        self.health = health
        self.power_type = power_type
        self.power_1 = power_1
        self.power_2 = power_2
        self.power_3 = power_3
        self.power_4 = power_4
        self.max_health = max_health
        self.max_power_1 = max_power_1
        self.max_power_2 = max_power_2
        self.max_power_3 = max_power_3
        self.max_power_4 = max_power_4
        self.level = level
        self.gender = gender
        self.bytes_0 = bytes_0  # race, class, gender, power_type
        self.creature_type = creature_type
        self.str = stat_0
        self.agi = stat_1
        self.sta = stat_2
        self.int = stat_3
        self.spi = stat_4
        self.base_str = base_stat_0
        self.base_agi = base_stat_1
        self.base_sta = base_stat_2
        self.base_int = base_stat_3
        self.base_spi = base_stat_4
        self.flags = flags
        self.coinage = coinage
        self.combat_reach = combat_reach
        self.weapon_reach = weapon_reach
        self.mount_display_id = mount_display_id
        self.resistance_buff_mods_positive_0 = resistance_buff_mods_positive_0
        self.resistance_buff_mods_positive_1 = resistance_buff_mods_positive_1
        self.resistance_buff_mods_positive_2 = resistance_buff_mods_positive_2
        self.resistance_buff_mods_positive_3 = resistance_buff_mods_positive_3
        self.resistance_buff_mods_positive_4 = resistance_buff_mods_positive_4
        self.resistance_buff_mods_positive_5 = resistance_buff_mods_positive_5
        self.resistance_buff_mods_negative_0 = resistance_buff_mods_negative_0
        self.resistance_buff_mods_negative_1 = resistance_buff_mods_negative_1
        self.resistance_buff_mods_negative_2 = resistance_buff_mods_negative_2
        self.resistance_buff_mods_negative_3 = resistance_buff_mods_negative_3
        self.resistance_buff_mods_negative_4 = resistance_buff_mods_negative_4
        self.resistance_buff_mods_negative_5 = resistance_buff_mods_negative_5
        self.base_attack_time = base_attack_time
        self.offhand_attack_time = offhand_attack_time
        self.resistance_0 = resistance_0  # Armor
        self.resistance_1 = resistance_1
        self.resistance_2 = resistance_2
        self.resistance_3 = resistance_3
        self.resistance_4 = resistance_4
        self.resistance_5 = resistance_5
        self.stand_state = stand_state
        self.sheath_state = sheathe_state
        self.shapeshift_form = shapeshift_form
        self.bytes_1 = bytes_1  # stand state, shapeshift form, sheathstate
        self.mod_cast_speed = mod_cast_speed
        self.dynamic_flags = dynamic_flags
        self.damage = damage  # current damage, max damage
        self.bytes_2 = bytes_2  # combo points, 0, 0, 0
        self.current_target = current_target

        self.object_type.append(ObjectTypes.TYPE_UNIT)
        self.update_packet_factory.init_values(UnitFields.UNIT_END)

        self.is_alive = True
        self.is_sitting = False
        self.in_combat = False
        self.swing_error = AttackSwingError.NONE
        self.extra_attacks = 0
        self.disarmed_mainhand = False
        self.disarmed_offhand = False
        self.attackers = {}
        self.attack_timers = {
            AttackTypes.BASE_ATTACK: 0,
            AttackTypes.OFFHAND_ATTACK: 0,
            AttackTypes.RANGED_ATTACK: 0
        }

        self.spell_manager = SpellManager(self)
        self.aura_manager = AuraManager(self)
        self.movement_manager = MovementManager(self)
Beispiel #3
0
class UnitManager(ObjectManager):
    def __init__(
            self,
            channel_spell=0,
            channel_object=0,
            health=0,
            power_type=0,
            power_1=0,  # mana
            power_2=0,  # rage
            power_3=100,  # focus
            power_4=100,  # energy
            max_health=0,
            max_power_1=0,
            max_power_2=1000,
            max_power_3=100,
            max_power_4=100,
            level=0,
            gender=0,
            bytes_0=0,  # race, class, gender, power_type
            creature_type=0,
            stat_0=0,
            stat_1=0,
            stat_2=0,
            stat_3=0,
            stat_4=0,
            base_stat_0=0,
            base_stat_1=0,
            base_stat_2=0,
            base_stat_3=0,
            base_stat_4=0,
            flags=0,
            coinage=0,
            combat_reach=config.Unit.Defaults.combat_reach,
            weapon_reach=0,
            mount_display_id=0,
            resistance_buff_mods_positive_0=0,  # Armor
            resistance_buff_mods_positive_1=0,  # Holy
            resistance_buff_mods_positive_2=0,  # Fire
            resistance_buff_mods_positive_3=0,  # Nature
            resistance_buff_mods_positive_4=0,  # Frost
            resistance_buff_mods_positive_5=0,  # Shadow
            resistance_buff_mods_negative_0=0,
            resistance_buff_mods_negative_1=0,
            resistance_buff_mods_negative_2=0,
            resistance_buff_mods_negative_3=0,
            resistance_buff_mods_negative_4=0,
            resistance_buff_mods_negative_5=0,
            base_attack_time=config.Unit.Defaults.base_attack_time,
            offhand_attack_time=config.Unit.Defaults.offhand_attack_time,
            resistance_0=0,  # Armor
            resistance_1=0,
            resistance_2=0,
            resistance_3=0,
            resistance_4=0,
            resistance_5=0,
            stand_state=0,
            sheathe_state=WeaponMode.SHEATHEDMODE,
            shapeshift_form=0,
            bytes_1=0,  # stand state, shapeshift form, sheathstate
            mod_cast_speed=1,
            dynamic_flags=0,
            damage=0,  # current damage, max damage
            bytes_2=0,  # combo points, 0, 0, 0
            current_target=0,  # guid
            combat_target=None,  # victim
            **kwargs):
        super().__init__(**kwargs)

        self.combat_target = combat_target
        self.channel_spell = channel_spell
        self.channel_object = channel_object
        self.health = health
        self.power_type = power_type
        self.power_1 = power_1
        self.power_2 = power_2
        self.power_3 = power_3
        self.power_4 = power_4
        self.max_health = max_health
        self.max_power_1 = max_power_1
        self.max_power_2 = max_power_2
        self.max_power_3 = max_power_3
        self.max_power_4 = max_power_4
        self.level = level
        self.gender = gender
        self.bytes_0 = bytes_0  # race, class, gender, power_type
        self.creature_type = creature_type
        self.str = stat_0
        self.agi = stat_1
        self.sta = stat_2
        self.int = stat_3
        self.spi = stat_4
        self.base_str = base_stat_0
        self.base_agi = base_stat_1
        self.base_sta = base_stat_2
        self.base_int = base_stat_3
        self.base_spi = base_stat_4
        self.flags = flags
        self.coinage = coinage
        self.combat_reach = combat_reach
        self.weapon_reach = weapon_reach
        self.mount_display_id = mount_display_id
        self.resistance_buff_mods_positive_0 = resistance_buff_mods_positive_0
        self.resistance_buff_mods_positive_1 = resistance_buff_mods_positive_1
        self.resistance_buff_mods_positive_2 = resistance_buff_mods_positive_2
        self.resistance_buff_mods_positive_3 = resistance_buff_mods_positive_3
        self.resistance_buff_mods_positive_4 = resistance_buff_mods_positive_4
        self.resistance_buff_mods_positive_5 = resistance_buff_mods_positive_5
        self.resistance_buff_mods_negative_0 = resistance_buff_mods_negative_0
        self.resistance_buff_mods_negative_1 = resistance_buff_mods_negative_1
        self.resistance_buff_mods_negative_2 = resistance_buff_mods_negative_2
        self.resistance_buff_mods_negative_3 = resistance_buff_mods_negative_3
        self.resistance_buff_mods_negative_4 = resistance_buff_mods_negative_4
        self.resistance_buff_mods_negative_5 = resistance_buff_mods_negative_5
        self.base_attack_time = base_attack_time
        self.offhand_attack_time = offhand_attack_time
        self.resistance_0 = resistance_0  # Armor
        self.resistance_1 = resistance_1
        self.resistance_2 = resistance_2
        self.resistance_3 = resistance_3
        self.resistance_4 = resistance_4
        self.resistance_5 = resistance_5
        self.stand_state = stand_state
        self.sheath_state = sheathe_state
        self.shapeshift_form = shapeshift_form
        self.bytes_1 = bytes_1  # stand state, shapeshift form, sheathstate
        self.mod_cast_speed = mod_cast_speed
        self.dynamic_flags = dynamic_flags
        self.damage = damage  # current damage, max damage
        self.bytes_2 = bytes_2  # combo points, 0, 0, 0
        self.current_target = current_target

        self.object_type.append(ObjectTypes.TYPE_UNIT)
        self.update_packet_factory.init_values(UnitFields.UNIT_END)

        self.is_alive = True
        self.is_sitting = False
        self.in_combat = False
        self.swing_error = AttackSwingError.NONE
        self.extra_attacks = 0
        self.disarmed_mainhand = False
        self.disarmed_offhand = False
        self.attackers = {}
        self.attack_timers = {
            AttackTypes.BASE_ATTACK: 0,
            AttackTypes.OFFHAND_ATTACK: 0,
            AttackTypes.RANGED_ATTACK: 0
        }

        self.spell_manager = SpellManager(self)
        self.aura_manager = AuraManager(self)
        self.movement_manager = MovementManager(self)

    def is_within_interactable_distance(self, victim):
        current_distance = self.location.distance(victim.location)
        return current_distance <= UnitFormulas.interactable_distance(
            self, victim)

    def attack(self, victim, is_melee=True):
        if not victim or victim == self:
            return False

        # Dead units can neither attack nor be attacked
        if not self.is_alive or not victim.is_alive:
            return False

        # Mounted players can't attack
        if self.get_type(
        ) == ObjectTypes.TYPE_PLAYER and self.mount_display_id > 0:
            return False

        # In fight already
        if self.combat_target:
            if self.combat_target == victim:
                if is_melee and self.is_within_interactable_distance(
                        self.combat_target):
                    self.send_attack_start(victim.guid)
                    return True
                return False

            self.attack_stop(target_switch=True)

        self.set_current_target(victim.guid)
        self.combat_target = victim
        victim.attackers[self.guid] = self

        # Reset offhand weapon attack
        if self.has_offhand_weapon():
            self.set_attack_timer(AttackTypes.OFFHAND_ATTACK,
                                  self.offhand_attack_time)

        self.send_attack_start(self.combat_target.guid)

        return True

    def attack_stop(self, target_switch=False):
        if self.combat_target and self.guid in self.combat_target.attackers:
            self.combat_target.attackers.pop(self.guid, None)

        # Clear target
        self.set_current_target(self.guid)
        victim = self.combat_target
        self.combat_target = None

        self.send_attack_stop(victim.guid if victim else self.guid)
        self.set_dirty()

    def send_attack_start(self, victim_guid):
        data = pack('<2Q', self.guid, victim_guid)
        MapManager.send_surrounding(
            PacketWriter.get_packet(OpCode.SMSG_ATTACKSTART, data), self)

    def send_attack_stop(self, victim_guid):
        # Last uint32 is "deceased"; can be either 1 (self is dead), or 0, (self is alive).
        # Forces the unit to face the corpse and disables clientside
        # turning (UnitFlags.DisableMovement) CGUnit_C::OnAttackStop
        data = pack('<2QI', self.guid, victim_guid, 0 if self.is_alive else 1)
        MapManager.send_surrounding(
            PacketWriter.get_packet(OpCode.SMSG_ATTACKSTOP, data), self)

    def attack_update(self, elapsed):
        if self.combat_target and not self.combat_target.is_alive:
            self.leave_combat()
            self.set_dirty()
            return

        self.update_attack_time(AttackTypes.BASE_ATTACK, elapsed * 1000.0)
        if self.has_offhand_weapon():
            self.update_attack_time(AttackTypes.OFFHAND_ATTACK,
                                    elapsed * 1000.0)

        self.update_melee_attacking_state()

    def update_melee_attacking_state(self):
        swing_error = AttackSwingError.NONE
        combat_angle = math.pi

        if not self.combat_target:
            if self.in_combat and len(self.attackers) == 0:
                self.leave_combat()
                self.set_dirty()
            return False

        if not self.is_attack_ready(
                AttackTypes.BASE_ATTACK) and not self.is_attack_ready(
                    AttackTypes.OFFHAND_ATTACK):
            return False

        current_angle = self.location.angle(self.combat_target.location)
        # Out of reach
        if not self.is_within_interactable_distance(self.combat_target):
            swing_error = AttackSwingError.NOTINRANGE
        # Not proper angle
        elif current_angle > combat_angle or current_angle < -combat_angle:
            swing_error = AttackSwingError.BADFACING
        # Moving
        elif self.movement_flags & MoveFlags.MOVEFLAG_MOTION_MASK:
            swing_error = AttackSwingError.MOVING
        # Not standing
        elif self.stand_state != StandState.UNIT_STANDING:
            swing_error = AttackSwingError.NOTSTANDING
        # Dead target
        elif not self.combat_target.is_alive:
            self.attackers.pop(self.combat_target.guid)
            swing_error = AttackSwingError.DEADTARGET
        else:
            # Main hand attack
            if self.is_attack_ready(AttackTypes.BASE_ATTACK):
                # Prevent both and attacks at the same time
                if self.has_offhand_weapon():
                    if self.attack_timers[AttackTypes.OFFHAND_ATTACK] < 500:
                        self.set_attack_timer(AttackTypes.OFFHAND_ATTACK, 500)

                self.attacker_state_update(self.combat_target,
                                           AttackTypes.BASE_ATTACK, False)
                self.set_attack_timer(AttackTypes.BASE_ATTACK,
                                      self.base_attack_time)

            # Off hand attack
            if self.has_offhand_weapon() and self.is_attack_ready(
                    AttackTypes.OFFHAND_ATTACK):
                # Prevent both and attacks at the same time
                if self.attack_timers[AttackTypes.BASE_ATTACK] < 500:
                    self.set_attack_timer(AttackTypes.BASE_ATTACK, 500)

                self.attacker_state_update(self.combat_target,
                                           AttackTypes.OFFHAND_ATTACK, False)
                self.set_attack_timer(AttackTypes.OFFHAND_ATTACK,
                                      self.offhand_attack_time)

        if self.object_type == ObjectTypes.TYPE_PLAYER:
            if swing_error != AttackSwingError.NONE:
                self.set_attack_timer(AttackTypes.BASE_ATTACK,
                                      self.base_attack_time)
                if self.has_offhand_weapon():
                    self.set_attack_timer(AttackTypes.OFFHAND_ATTACK,
                                          self.offhand_attack_time)

                if swing_error == AttackSwingError.NOTINRANGE:
                    self.send_attack_swing_not_in_range(self.combat_target)
                elif swing_error == AttackSwingError.BADFACING:
                    self.send_attack_swing_facing_wrong_way(self.combat_target)
                elif swing_error == AttackSwingError.DEADTARGET:
                    self.send_attack_swing_dead_target(self.combat_target)
                elif swing_error == AttackSwingError.NOTSTANDING:
                    self.send_attack_swing_not_standing(self.combat_target)

                self.send_attack_stop(self.combat_target.guid)

        self.swing_error = swing_error
        return swing_error == AttackSwingError.NONE

    def attacker_state_update(self, victim, attack_type, extra):
        if attack_type == AttackTypes.BASE_ATTACK:
            # TODO: Cast current melee spell

            # No recent extra attack only at any non extra attack
            if not extra and self.extra_attacks > 0:
                self.execute_extra_attacks()
                return

            if self.spell_manager.cast_queued_melee_ability(attack_type):
                return  # Melee ability replaces regular attack

        damage_info = self.calculate_melee_damage(victim, attack_type)
        if not damage_info:
            return

        self.send_attack_state_update(damage_info)

        # Extra attack only at any non extra attack
        if not extra and self.extra_attacks > 0:
            self.execute_extra_attacks()

    def execute_extra_attacks(self):
        while self.extra_attacks > 0:
            self.attacker_state_update(self.combat_target,
                                       AttackTypes.BASE_ATTACK, True)
            self.extra_attacks -= 1

    def calculate_melee_damage(self, victim, attack_type):
        damage_info = DamageInfoHolder()

        if not victim:
            return None

        if not self.is_alive or not victim.is_alive:
            return None

        damage_info.attacker = self
        damage_info.target = victim
        damage_info.damage += self.calculate_damage(attack_type)
        # Not taking "subdamages" into account
        damage_info.total_damage = damage_info.damage

        # Generate rage (if needed)
        self.generate_rage(
            damage_info, is_player=self.get_type() == ObjectTypes.TYPE_PLAYER)

        if attack_type == AttackTypes.BASE_ATTACK:
            damage_info.proc_attacker = ProcFlags.DEAL_COMBAT_DMG | ProcFlags.SWING
            damage_info.proc_victim = ProcFlags.TAKE_COMBAT_DMG
            damage_info.hit_info = HitInfo.SUCCESS
        elif attack_type == AttackTypes.OFFHAND_ATTACK:
            damage_info.proc_attacker = ProcFlags.DEAL_COMBAT_DMG | ProcFlags.SWING
            damage_info.proc_victim = ProcFlags.TAKE_COMBAT_DMG
            damage_info.hit_info = HitInfo.SUCCESS | HitInfo.OFFHAND
        elif attack_type == AttackTypes.RANGED_ATTACK:
            damage_info.proc_attacker = ProcFlags.DEAL_COMBAT_DMG
            damage_info.proc_victim = ProcFlags.TAKE_COMBAT_DMG
            damage_info.hit_info = HitInfo.DAMAGE  # ?

        # Prior to version 1.8, dual wield's miss chance had a hard cap of 19%,
        # meaning that all dual-wield auto-attacks had a minimum 19% miss chance
        # regardless of how much +hit% gear was equipped.
        # TODO FINISH IMPLEMENTING
        damage_info.target_state = VictimStates.VS_WOUND  # test remove later

        return damage_info

    def send_attack_state_update(self, damage_info):
        data = pack(
            '<I2QIBIf7I',
            damage_info.hit_info,
            damage_info.attacker.guid,
            damage_info.target.guid,
            damage_info.total_damage,
            1,  # Sub damage count
            damage_info.damage_school_mask,
            damage_info.total_damage,
            damage_info.damage,
            damage_info.absorb,
            damage_info.target_state,
            damage_info.resist,
            0,
            0,
            damage_info.blocked_amount)
        MapManager.send_surrounding(
            PacketWriter.get_packet(OpCode.SMSG_ATTACKERSTATEUPDATE, data),
            self,
            include_self=self.get_type() == ObjectTypes.TYPE_PLAYER)

        # Damage effects
        self.deal_damage(damage_info.target, damage_info.total_damage)

    def calculate_damage(self, attack_type):
        min_damage, max_damage = self.calculate_min_max_damage(attack_type)

        if min_damage > max_damage:
            tmp_min = min_damage
            min_damage = max_damage
            max_damage = tmp_min

        return random.randint(min_damage, max_damage)

    # Implemented by PlayerManager
    def generate_rage(self, damage_info, is_player=False):
        return

    # Implemented by PlayerManager and CreatureManager
    def calculate_min_max_damage(self, attack_type=0):
        return 0, 0

    def deal_damage(self, target, damage):
        if not target or not target.is_alive or damage < 1:
            return

        if self.guid not in target.attackers:
            target.attackers[self.guid] = self

        if not self.in_combat:
            self.enter_combat()
            self.set_dirty()

        if not target.in_combat:
            target.enter_combat()
            target.set_dirty()

        target.receive_damage(damage, source=self)

    def receive_damage(self, amount, source=None):
        is_player = self.get_type() == ObjectTypes.TYPE_PLAYER

        new_health = self.health - amount
        if new_health <= 0:
            self.die(killer=source)
        else:
            self.set_health(new_health)

        # If unit is a creature and it's being attacked by another unit, automatically set combat target.
        if not self.combat_target and not is_player and source and source.get_type(
        ) != ObjectTypes.TYPE_GAMEOBJECT:
            self.attack(source)

        update_packet = self.generate_proper_update_packet(is_self=is_player)
        MapManager.send_surrounding(update_packet,
                                    self,
                                    include_self=is_player)

    def deal_spell_damage(self, target, damage, school,
                          spell_id):  # TODO Spell hit damage visual?
        data = pack('<IQQIIfiii', 1, self.guid, target.guid, spell_id, damage,
                    damage, school, damage, 0)
        packet = PacketWriter.get_packet(
            OpCode.SMSG_ATTACKERSTATEUPDATEDEBUGINFOSPELL, data)
        MapManager.send_surrounding(
            packet,
            target,
            include_self=target.get_type() == ObjectTypes.TYPE_PLAYER)
        self.deal_damage(target, damage)

    def set_current_target(self, guid):
        self.current_target = guid
        self.set_uint64(UnitFields.UNIT_FIELD_TARGET, guid)

    # Implemented by PlayerManager
    def send_attack_swing_not_in_range(self, victim):
        pass

    # Implemented by PlayerManager
    def send_attack_swing_facing_wrong_way(self, victim):
        pass

    # Implemented by PlayerManager
    def send_attack_swing_cant_attack(self, victim):
        pass

    # Implemented by PlayerManager
    def send_attack_swing_dead_target(self, victim):
        pass

    # Implemented by PlayerManager
    def send_attack_swing_not_standing(self, victim):
        pass

    # Implemented by PlayerManager and CreatureManager
    def has_offhand_weapon(self):
        return False

    def enter_combat(self):
        self.in_combat = True
        self.unit_flags |= UnitFlags.UNIT_FLAG_IN_COMBAT
        self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

    def leave_combat(self, force=False):
        if not self.in_combat and not force:
            return

        # Remove self from attacker list of attackers
        for victim in list(self.attackers.values()):
            if self.guid in victim.attackers:
                victim.attackers.pop(self.guid)
        self.attackers.clear()

        self.send_attack_stop(
            self.combat_target.guid if self.combat_target else self.guid)
        self.swing_error = 0

        self.combat_target = None
        self.in_combat = False
        self.unit_flags &= ~UnitFlags.UNIT_FLAG_IN_COMBAT
        self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

    def can_use_attack_type(self, attack_type):
        if attack_type == AttackTypes.BASE_ATTACK:
            return self.disarmed_mainhand
        if attack_type == AttackTypes.OFFHAND_ATTACK:
            return self.disarmed_offhand
        return True

    def is_attack_ready(self, attack_type):
        return self.attack_timers[attack_type] <= 0

    def update_attack_time(self, attack_type, value):
        new_value = self.attack_timers[attack_type] - value
        if new_value < 0:
            new_value = 0
        if not self.is_attack_ready(attack_type):
            self.set_attack_timer(attack_type, new_value)

    def set_attack_timer(self, attack_type, value):
        self.attack_timers[attack_type] = value

    def play_emote(self, emote):
        if emote != 0:
            data = pack('<IQ', emote, self.guid)
            MapManager.send_surrounding_in_range(
                PacketWriter.get_packet(OpCode.SMSG_EMOTE, data), self,
                config.World.Chat.ChatRange.emote_range)

    def summon_mount(self, creature_entry):
        creature_template = WorldDatabaseManager.creature_get_by_entry(
            creature_entry)
        if not creature_template:
            return False

        self.mount(creature_template.display_id1)
        return True

    def mount(self, mount_display_id):
        if mount_display_id > 0 and DbcDatabaseManager.creature_display_info_get_by_id(
                mount_display_id):
            self.mount_display_id = mount_display_id
            self.unit_flags |= UnitFlags.UNIT_MASK_MOUNTED
            self.set_uint32(UnitFields.UNIT_FIELD_MOUNTDISPLAYID,
                            self.mount_display_id)
            self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

    def unmount(self):
        self.mount_display_id = 0
        self.unit_flags &= ~UnitFlags.UNIT_MASK_MOUNTED
        self.set_uint32(UnitFields.UNIT_FIELD_MOUNTDISPLAYID,
                        self.mount_display_id)
        self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

    def set_health(self, health):
        if health < 0:
            health = 0
        self.health = health
        self.set_uint32(UnitFields.UNIT_FIELD_HEALTH, health)

    def set_max_health(self, health):
        self.max_health = health
        self.set_uint32(UnitFields.UNIT_FIELD_MAXHEALTH, health)

    def set_mana(self, mana):
        if mana < 0:
            mana = 0
        self.power_1 = min(mana, self.max_power_1)
        self.set_uint32(UnitFields.UNIT_FIELD_POWER1, mana)

    def set_rage(self, rage):
        if rage < 0:
            rage = 0
        self.power_2 = min(rage, self.max_power_2)
        self.set_uint32(UnitFields.UNIT_FIELD_POWER2, rage)

    def set_focus(self, focus):
        if focus < 0:
            focus = 0
        self.power_3 = min(focus, self.max_power_3)
        self.set_uint32(UnitFields.UNIT_FIELD_POWER3, focus)

    def set_energy(self, energy):
        if energy < 0:
            energy = 0
        self.power_4 = min(energy, self.max_power_4)
        self.set_uint32(UnitFields.UNIT_FIELD_POWER4, energy)

    def set_max_mana(self, mana):
        self.max_power_1 = mana
        self.set_uint32(UnitFields.UNIT_FIELD_MAXPOWER1, mana)

    def set_armor(self, armor):
        self.resistance_0 = armor
        self.set_int64(UnitFields.UNIT_FIELD_RESISTANCES, self.resistance_0)

    def set_holy_res(self, holy_res):
        self.resistance_1 = holy_res
        self.set_int64(UnitFields.UNIT_FIELD_RESISTANCES + 1,
                       self.resistance_1)

    def set_fire_res(self, fire_res):
        self.resistance_2 = fire_res
        self.set_int64(UnitFields.UNIT_FIELD_RESISTANCES + 2,
                       self.resistance_2)

    def set_nature_res(self, nature_res):
        self.resistance_3 = nature_res
        self.set_int64(UnitFields.UNIT_FIELD_RESISTANCES + 3,
                       self.resistance_3)

    def set_frost_res(self, frost_res):
        self.resistance_4 = frost_res
        self.set_int64(UnitFields.UNIT_FIELD_RESISTANCES + 4,
                       self.resistance_4)

    def set_shadow_res(self, shadow_res):
        self.resistance_5 = shadow_res
        self.set_int64(UnitFields.UNIT_FIELD_RESISTANCES + 5,
                       self.resistance_5)

    def set_melee_damage(self, min_dmg, max_dmg):
        damages = unpack('<I', pack('<2H', min_dmg, max_dmg))[0]
        self.damage = damages
        self.set_uint32(UnitFields.UNIT_FIELD_DAMAGE, damages)

    def set_melee_attack_time(self, attack_time):
        self.base_attack_time = attack_time
        self.set_uint32(UnitFields.UNIT_FIELD_BASEATTACKTIME, attack_time)

    def set_offhand_attack_time(self, attack_time):
        self.offhand_attack_time = attack_time
        self.set_uint32(UnitFields.UNIT_FIELD_BASEATTACKTIME + 1, attack_time)

    def set_weapon_mode(self, weapon_mode):
        self.sheath_state = weapon_mode

        # TODO: Implement temp enchants updates.
        if WeaponMode.NORMALMODE:
            # Update main hand temp enchants
            # Update off hand temp enchants
            pass
        elif WeaponMode.RANGEDMODE:
            # Update ranged temp enchants
            pass

    def set_shapeshift_form(self, shapeshift_form):
        self.shapeshift_form = shapeshift_form

    def has_form(self, shapeshift_form):
        return self.shapeshift_form == shapeshift_form

    # Implemented by PlayerManager
    def add_combo_points_on_target(self, target, combo_points):
        pass

    # Implemented by PlayerManager
    def remove_combo_points(self):
        pass

    def set_stand_state(self, stand_state):
        self.stand_state = stand_state

    # override
    def set_display_id(self, display_id):
        super().set_display_id(display_id)
        if display_id <= 0 or not \
                DbcDatabaseManager.creature_display_info_get_by_id(display_id):
            return

        self.set_uint32(UnitFields.UNIT_FIELD_DISPLAYID,
                        self.current_display_id)

    def generate_proper_update_packet(self, is_self=False, create=False):
        update_packet = UpdatePacketFactory.compress_if_needed(
            PacketWriter.get_packet(
                OpCode.SMSG_UPDATE_OBJECT,
                self.get_full_update_packet(is_self=is_self)
                if create else self.get_partial_update_packet()))
        return update_packet

    def die(self, killer=None):
        if not self.is_alive:
            return False
        self.is_alive = False

        # Stop movement on death
        if len(self.movement_manager.pending_waypoints) > 0:
            self.movement_manager.send_move_to([self.location],
                                               self.running_speed,
                                               SplineFlags.SPLINEFLAG_NONE)

        self.set_health(0)
        self.set_stand_state(StandState.UNIT_DEAD)

        self.unit_flags = UnitFlags.UNIT_MASK_DEAD
        self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

        self.dynamic_flags |= UnitDynamicTypes.UNIT_DYNAMIC_DEAD
        self.set_uint32(UnitFields.UNIT_DYNAMIC_FLAGS, self.dynamic_flags)

        if killer and killer.get_type() == ObjectTypes.TYPE_PLAYER:
            if killer.current_selection == self.guid:
                killer.set_current_selection(killer.guid)
                killer.set_dirty()

            # Clear combo of killer if this unit was the target
            if killer.combo_target == self.guid:
                killer.remove_combo_points()
                killer.set_dirty()

        # Clear all pending waypoint movement
        self.movement_manager.reset()

        self.leave_combat()
        return True

    def respawn(self):
        # Force leave combat just in case.
        self.leave_combat(force=True)
        self.set_current_target(self.guid)
        self.is_alive = True

        self.unit_flags = UnitFlags.UNIT_FLAG_STANDARD
        self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

        self.dynamic_flags = UnitDynamicTypes.UNIT_DYNAMIC_NONE
        self.set_uint32(UnitFields.UNIT_DYNAMIC_FLAGS, self.dynamic_flags)

        self.set_stand_state(StandState.UNIT_STANDING)

    # override
    def on_cell_change(self):
        pass

    # override
    def get_type(self):
        return ObjectTypes.TYPE_UNIT

    # override
    def get_type_id(self):
        return ObjectTypeIds.ID_UNIT

    def _allegiance_status_checker(self, target, check_friendly=True):
        own_faction = DbcDatabaseManager.FactionTemplateHolder.faction_template_get_by_id(
            self.faction)
        target_faction = DbcDatabaseManager.FactionTemplateHolder.faction_template_get_by_id(
            target.faction)

        # Some units currently have a bugged faction, terminate the method if this is encountered
        if not target_faction:
            return False

        own_enemies = [
            own_faction.Enemies_1, own_faction.Enemies_2,
            own_faction.Enemies_3, own_faction.Enemies_4
        ]
        own_friends = [
            own_faction.Friend_1, own_faction.Friend_2, own_faction.Friend_3,
            own_faction.Friend_4
        ]
        if target_faction.Faction > 0:
            for enemy in own_enemies:
                if enemy == target_faction.Faction:
                    return not check_friendly
            for friend in own_friends:
                if friend == target_faction.Faction:
                    return check_friendly

        if check_friendly:
            return (
                (own_faction.FriendGroup & target_faction.FactionGroup) or
                (own_faction.FactionGroup & target_faction.FriendGroup)) != 0
        else:
            return (
                (own_faction.EnemyGroup & target_faction.FactionGroup) or
                (own_faction.FactionGroup & target_faction.EnemyGroup)) != 0

    def is_friendly_to(self, target):
        return self._allegiance_status_checker(target, True)

    def is_enemy_to(self, target):
        return self._allegiance_status_checker(target, False)
    def __init__(self,
                 gobject_template,
                 gobject_instance=None,
                 summoner=None,
                 **kwargs):
        super().__init__(**kwargs)

        self.gobject_template = gobject_template
        self.gobject_instance = gobject_instance
        self.summoner = summoner
        self.spell_id = 0  # Spell that summoned this object.
        self.known_players = {}

        self.entry = self.gobject_template.entry
        self.native_display_id = self.gobject_template.display_id
        self.current_display_id = self.native_display_id
        self.native_scale = self.gobject_template.scale
        self.current_scale = self.native_scale
        self.faction = self.gobject_template.faction
        self.lock = 0  # Unlocked.
        self.flags = self.gobject_template.flags

        if self.summoner:
            self.flags |= GameObjectFlags.TRIGGERED

        if gobject_instance:
            if GameObjectManager.CURRENT_HIGHEST_GUID < gobject_instance.spawn_id:
                GameObjectManager.CURRENT_HIGHEST_GUID = gobject_instance.spawn_id

            self.guid = self.generate_object_guid(gobject_instance.spawn_id)
            self.state = self.gobject_instance.spawn_state
            self.location.x = self.gobject_instance.spawn_positionX
            self.location.y = self.gobject_instance.spawn_positionY
            self.location.z = self.gobject_instance.spawn_positionZ
            self.location.o = self.gobject_instance.spawn_orientation
            # If spawned by another unit, use that unit map and zone.
            self.map_ = self.gobject_instance.spawn_map if not self.summoner else self.summoner.map_
            self.zone = self.summoner.zone if self.summoner else 0
            self.respawn_time = randint(self.gobject_instance.spawn_spawntimemin,
                                        self.gobject_instance.spawn_spawntimemax)

        self.object_type_mask |= ObjectTypeFlags.TYPE_GAMEOBJECT
        self.update_packet_factory.init_values(self.guid, GameObjectFields)

        self.respawn_timer = 0
        self.loot_manager = None  # Optional.
        self.trap_manager = None  # Optional.
        self.fishing_node_manager = None  # Optional.
        self.mining_node_manager = None  # Optional.

        from game.world.managers.objects.spell.SpellManager import SpellManager  # Local due to circular imports.
        self.spell_manager = SpellManager(self)

        # Chest initializations.
        if self.gobject_template.type == GameObjectTypes.TYPE_CHEST:
            self.lock = self.gobject_template.data0
            self.loot_manager = GameObjectLootManager(self)
            # Mining node.
            if self.gobject_template.data4 != 0 and self.gobject_template.data5 > self.gobject_template.data4:
                self.mining_node_manager = MiningNodeManager(self)

        # Fishing node initialization.
        if self.gobject_template.type == GameObjectTypes.TYPE_FISHINGNODE:
            self.loot_manager = GameObjectLootManager(self)
            self.fishing_node_manager = FishingNodeManager(self)

        # Ritual initializations.
        if self.gobject_template.type == GameObjectTypes.TYPE_RITUAL:
            self.ritual_participants = []

        # Set channeled object for fishing nodes or rituals.
        if summoner and summoner.get_type_id() == ObjectTypeIds.ID_PLAYER:
            if self.gobject_template.type == GameObjectTypes.TYPE_RITUAL or \
                    self.gobject_template.type == GameObjectTypes.TYPE_FISHINGNODE:
                summoner.set_channel_object(self.guid)

        # Trap collision initializations.
        self.lock = self.gobject_template.data0
        if self.gobject_template.type == GameObjectTypes.TYPE_TRAP:
            self.trap_manager = TrapManager.generate(self)

        # Lock initialization for button and door.
        if self.gobject_template.type == GameObjectTypes.TYPE_BUTTON or \
                self.gobject_template.type == GameObjectTypes.TYPE_DOOR:
            self.lock = self.gobject_template.data1

        # Lock initialization for quest giver, goober and camera.
        if self.gobject_template.type == GameObjectTypes.TYPE_QUESTGIVER or \
                self.gobject_template.type == GameObjectTypes.TYPE_GOOBER or \
                self.gobject_template.type == GameObjectTypes.TYPE_CAMERA:
            self.lock = gobject_template.data0
class GameObjectManager(ObjectManager):
    CURRENT_HIGHEST_GUID = 0

    def __init__(self,
                 gobject_template,
                 gobject_instance=None,
                 summoner=None,
                 **kwargs):
        super().__init__(**kwargs)

        self.gobject_template = gobject_template
        self.gobject_instance = gobject_instance
        self.summoner = summoner
        self.spell_id = 0  # Spell that summoned this object.
        self.known_players = {}

        self.entry = self.gobject_template.entry
        self.native_display_id = self.gobject_template.display_id
        self.current_display_id = self.native_display_id
        self.native_scale = self.gobject_template.scale
        self.current_scale = self.native_scale
        self.faction = self.gobject_template.faction
        self.lock = 0  # Unlocked.
        self.flags = self.gobject_template.flags

        if self.summoner:
            self.flags |= GameObjectFlags.TRIGGERED

        if gobject_instance:
            if GameObjectManager.CURRENT_HIGHEST_GUID < gobject_instance.spawn_id:
                GameObjectManager.CURRENT_HIGHEST_GUID = gobject_instance.spawn_id

            self.guid = self.generate_object_guid(gobject_instance.spawn_id)
            self.state = self.gobject_instance.spawn_state
            self.location.x = self.gobject_instance.spawn_positionX
            self.location.y = self.gobject_instance.spawn_positionY
            self.location.z = self.gobject_instance.spawn_positionZ
            self.location.o = self.gobject_instance.spawn_orientation
            # If spawned by another unit, use that unit map and zone.
            self.map_ = self.gobject_instance.spawn_map if not self.summoner else self.summoner.map_
            self.zone = self.summoner.zone if self.summoner else 0
            self.respawn_time = randint(self.gobject_instance.spawn_spawntimemin,
                                        self.gobject_instance.spawn_spawntimemax)

        self.object_type_mask |= ObjectTypeFlags.TYPE_GAMEOBJECT
        self.update_packet_factory.init_values(self.guid, GameObjectFields)

        self.respawn_timer = 0
        self.loot_manager = None  # Optional.
        self.trap_manager = None  # Optional.
        self.fishing_node_manager = None  # Optional.
        self.mining_node_manager = None  # Optional.

        from game.world.managers.objects.spell.SpellManager import SpellManager  # Local due to circular imports.
        self.spell_manager = SpellManager(self)

        # Chest initializations.
        if self.gobject_template.type == GameObjectTypes.TYPE_CHEST:
            self.lock = self.gobject_template.data0
            self.loot_manager = GameObjectLootManager(self)
            # Mining node.
            if self.gobject_template.data4 != 0 and self.gobject_template.data5 > self.gobject_template.data4:
                self.mining_node_manager = MiningNodeManager(self)

        # Fishing node initialization.
        if self.gobject_template.type == GameObjectTypes.TYPE_FISHINGNODE:
            self.loot_manager = GameObjectLootManager(self)
            self.fishing_node_manager = FishingNodeManager(self)

        # Ritual initializations.
        if self.gobject_template.type == GameObjectTypes.TYPE_RITUAL:
            self.ritual_participants = []

        # Set channeled object for fishing nodes or rituals.
        if summoner and summoner.get_type_id() == ObjectTypeIds.ID_PLAYER:
            if self.gobject_template.type == GameObjectTypes.TYPE_RITUAL or \
                    self.gobject_template.type == GameObjectTypes.TYPE_FISHINGNODE:
                summoner.set_channel_object(self.guid)

        # Trap collision initializations.
        self.lock = self.gobject_template.data0
        if self.gobject_template.type == GameObjectTypes.TYPE_TRAP:
            self.trap_manager = TrapManager.generate(self)

        # Lock initialization for button and door.
        if self.gobject_template.type == GameObjectTypes.TYPE_BUTTON or \
                self.gobject_template.type == GameObjectTypes.TYPE_DOOR:
            self.lock = self.gobject_template.data1

        # Lock initialization for quest giver, goober and camera.
        if self.gobject_template.type == GameObjectTypes.TYPE_QUESTGIVER or \
                self.gobject_template.type == GameObjectTypes.TYPE_GOOBER or \
                self.gobject_template.type == GameObjectTypes.TYPE_CAMERA:
            self.lock = gobject_template.data0

    def load(self):
        MapManager.update_object(self)

    @staticmethod
    def spawn(entry, location, map_id, summoner=None, spell_id=0, override_faction=0, despawn_time=1):
        go_template = WorldDatabaseManager.gameobject_template_get_by_entry(entry)

        if not go_template:
            return None

        instance = SpawnsGameobjects()
        instance.spawn_id = GameObjectManager.CURRENT_HIGHEST_GUID + 1
        instance.spawn_entry = entry
        instance.spawn_map = map_id
        instance.spawn_rotation0 = 0
        instance.spawn_rotation2 = 0
        instance.spawn_rotation1 = 0
        instance.spawn_rotation3 = 0
        instance.spawn_positionX = location.x
        instance.spawn_positionY = location.y
        instance.spawn_positionZ = location.z
        instance.spawn_orientation = location.o
        if despawn_time < 1:
            despawn_time = 1
        instance.spawn_spawntimemin = despawn_time
        instance.spawn_spawntimemax = despawn_time
        instance.spawn_state = GameObjectStates.GO_STATE_READY

        gameobject = GameObjectManager(
            gobject_template=go_template,
            gobject_instance=instance,
            summoner=summoner
        )

        if spell_id:
            gameobject.spell_id = spell_id

        if override_faction:
            gameobject.faction = override_faction
            gameobject.set_uint32(GameObjectFields.GAMEOBJECT_FACTION, override_faction)

        gameobject.load()
        return gameobject

    def handle_loot_release(self, player):
        # On loot release, always despawn the fishing bobber regardless of it still having loot or not.
        if self.gobject_template.type == GameObjectTypes.TYPE_FISHINGNODE:
            self.despawn(True)
            return

        if self.loot_manager:
            # Normal chest.
            if not self.mining_node_manager:
                # Chest still has loot.
                if self.loot_manager.has_loot():
                    self.set_ready()
                else:  # Despawn or destroy.
                    self.despawn(True if self.summoner else False)
            # Mining node.
            else:
                self.mining_node_manager.handle_looted(player)

    def _handle_use_door(self, player):
        # TODO: Check locks etc.
        self.set_active()

    def _handle_use_button(self, player):
        # TODO: Trigger scripts / events on cooldown restart.
        self.set_active()

    def _handle_use_camera(self, player):
        cinematic_id = self.gobject_template.data1
        if DbcDatabaseManager.cinematic_sequences_get_by_id(cinematic_id):
            data = pack('<I', cinematic_id)
            player.enqueue_packet(PacketWriter.get_packet(OpCode.SMSG_TRIGGER_CINEMATIC, data))

    def _handle_use_chair(self, player):
        slots = self.gobject_template.data0
        height = self.gobject_template.data1

        lowest_distance = 90.0
        x_lowest = self.location.x
        y_lowest = self.location.y

        if slots > 0:
            orthogonal_orientation = self.location.o + pi * 0.5
            for x in range(slots):
                relative_distance = (self.current_scale * x) - (self.current_scale * (slots - 1) / 2.0)
                x_i = self.location.x + relative_distance * cos(orthogonal_orientation)
                y_i = self.location.y + relative_distance * sin(orthogonal_orientation)

                player_slot_distance = player.location.distance(Vector(x_i, y_i, player.location.z))
                if player_slot_distance <= lowest_distance:
                    lowest_distance = player_slot_distance
                    x_lowest = x_i
                    y_lowest = y_i
            player.teleport(player.map_, Vector(x_lowest, y_lowest, self.location.z, self.location.o), is_instant=True)
            player.set_stand_state(StandState.UNIT_SITTINGCHAIRLOW.value + height)

    # noinspection PyMethodMayBeStatic
    def _handle_use_quest_giver(self, player, target):
        if target:
            player.quest_manager.handle_quest_giver_hello(target, target.guid)

    def _handle_fishing_node(self, player):
        # Generate loot if it's empty.
        if not self.loot_manager.has_loot():
            self.loot_manager.generate_loot(player)

        if self.fishing_node_manager.try_hook_attempt(player):
            player.send_loot(self.loot_manager)

        # Remove cast.
        player.spell_manager.remove_cast_by_id(self.spell_id)

    def _handle_use_chest(self, player):
        # Activate chest open animation, while active, it won't let any other player loot.
        if self.state == GameObjectStates.GO_STATE_READY:
            self.set_state(GameObjectStates.GO_STATE_ACTIVE)

        # Player kneel loot.
        player.unit_flags |= UnitFlags.UNIT_FLAG_LOOTING
        player.set_uint32(UnitFields.UNIT_FIELD_FLAGS, player.unit_flags)

        # Generate loot if it's empty.
        if not self.loot_manager.has_loot():
            self.loot_manager.generate_loot(player)

        player.send_loot(self.loot_manager)

    def _handle_use_ritual(self, player):
        # Group check.
        if not self.summoner.group_manager or not self.summoner.group_manager.is_party_member(player.guid):
            player.spell_manager.send_cast_result(self.spell_id, SpellCheckCastResult.SPELL_FAILED_TARGET_NOT_IN_PARTY)
            return

        ritual_channel_spell_id = self.gobject_template.data2
        if player is self.summoner or player in self.ritual_participants:
            return  # No action needed for this player.

        # Make the player channel for summoning.
        channel_spell_entry = DbcDatabaseManager.SpellHolder.spell_get_by_id(ritual_channel_spell_id)
        spell = player.spell_manager.try_initialize_spell(channel_spell_entry, self, SpellTargetMask.GAMEOBJECT,
                                                          validate=False)

        # Note: these triggered casts will skip the actual effects of the summon spell, only starting the channel.
        player.spell_manager.remove_colliding_casts(spell)
        player.spell_manager.casting_spells.append(spell)
        player.spell_manager.handle_channel_start(spell)
        self.ritual_participants.append(player)

        # Check if the ritual can be completed with the current participants.
        required_participants = self.gobject_template.data0 - 1  # -1 to include caster.
        if len(self.ritual_participants) >= required_participants:
            ritual_finish_spell_id = self.gobject_template.data1

            # Cast the finishing spell.
            spell_entry = DbcDatabaseManager.SpellHolder.spell_get_by_id(ritual_finish_spell_id)
            spell_cast = self.summoner.spell_manager.try_initialize_spell(spell_entry, self.summoner,
                                                                          SpellTargetMask.SELF,
                                                                          triggered=True, validate=False)
            if spell_cast:
                self.summoner.spell_manager.start_spell_cast(initialized_spell=spell_cast)
            else:
                # Interrupt ritual channel if the summon fails.
                self.summoner.spell_manager.remove_cast_by_id(ritual_channel_spell_id)

    def has_observers(self):
        return any(self.known_players)

    def apply_spell_damage(self, target, damage, casting_spell, is_periodic=False):
        damage_info = casting_spell.get_cast_damage_info(self, target, damage, 0)
        miss_info = casting_spell.object_target_results[target.guid].result

        target.send_spell_cast_debug_info(damage_info, miss_info, casting_spell.spell_entry.ID, is_periodic=is_periodic)
        target.receive_damage(damage, self, is_periodic)

        # Send environmental damage log packet to the affected player.
        if self.gobject_template.type == GameObjectTypes.TYPE_TRAP and target.get_type_id() == ObjectTypeIds.ID_PLAYER:
            data = pack(
                '<Q2I',
                target.guid,
                casting_spell.spell_entry.School,
                damage
            )
            target.enqueue_packet(PacketWriter.get_packet(OpCode.SMSG_ENVIRONMENTALDAMAGELOG, data))

    def apply_spell_healing(self, target, healing, casting_spell, is_periodic=False):
        miss_info = casting_spell.object_target_results[target.guid].result
        damage_info = casting_spell.get_cast_damage_info(self, target, healing, 0)

        target.send_spell_cast_debug_info(damage_info, miss_info, casting_spell.spell_entry.ID, healing=True, is_periodic=is_periodic)
        target.receive_healing(healing, self)

    def _handle_use_goober(self, player):
        # Deadmines IronClad door (After triggering cannon)
        # TODO: This should be moved somewhere else to avoid hardcoding entries in the core GameObject manager.
        #  Future instance/map scripts?
        if self.entry == 16398:  # Cannon.
            # TODO, scripting, instancing, etc.
            iron_clad_doors = [go for go in MapManager.get_surrounding_gameobjects(self).values() if go.entry == 16397]
            if len(iron_clad_doors) > 0:
                self.send_custom_animation(0)
                iron_clad_doors[0].set_active()
        else:
            Logger.warning(f'Unimplemented gameobject use for type Goober entry {self.entry} name {self.gobject_template.name}')
        pass

    def use(self, player, target=None):
        if self.gobject_template.type == GameObjectTypes.TYPE_DOOR:
            self._handle_use_door(player)
        if self.gobject_template.type == GameObjectTypes.TYPE_BUTTON:
            self._handle_use_button(player)
        elif self.gobject_template.type == GameObjectTypes.TYPE_CAMERA:
            self._handle_use_camera(player)
        elif self.gobject_template.type == GameObjectTypes.TYPE_CHAIR:
            self._handle_use_chair(player)
        elif self.gobject_template.type == GameObjectTypes.TYPE_CHEST:
            self._handle_use_chest(player)
        elif self.gobject_template.type == GameObjectTypes.TYPE_RITUAL:
            self._handle_use_ritual(player)
        elif self.gobject_template.type == GameObjectTypes.TYPE_GOOBER:
            self._handle_use_goober(player)
        elif self.gobject_template.type == GameObjectTypes.TYPE_QUESTGIVER:
            self._handle_use_quest_giver(player, target)
        elif self.gobject_template.type == GameObjectTypes.TYPE_FISHINGNODE:
            self._handle_fishing_node(player)

    def set_state(self, state):
        self.state = state
        self.set_uint32(GameObjectFields.GAMEOBJECT_STATE, self.state)

        # If not a fishing node, set this go in_use flag.
        if not self.fishing_node_manager:
            if state == GameObjectStates.GO_STATE_ACTIVE:
                self.flags |= GameObjectFlags.IN_USE
                self.set_uint32(GameObjectFields.GAMEOBJECT_FLAGS, self.flags)
            else:
                self.flags &= ~GameObjectFlags.IN_USE
                self.set_uint32(GameObjectFields.GAMEOBJECT_FLAGS, self.flags)

    def has_flag(self, flag: GameObjectFlags):
        return self.flags & flag

    def set_active(self):
        if self.state == GameObjectStates.GO_STATE_READY:
            self.set_state(GameObjectStates.GO_STATE_ACTIVE)
            return True
        return False

    def is_active(self):
        return self.state == GameObjectStates.GO_STATE_ACTIVE

    def set_ready(self):
        if self.state != GameObjectStates.GO_STATE_READY:
            self.set_state(GameObjectStates.GO_STATE_READY)
            return True
        return False

    # override
    def set_display_id(self, display_id):
        super().set_display_id(display_id)
        if display_id <= 0 or not \
                DbcDatabaseManager.gameobject_display_info_get_by_id(display_id):
            return False

        self.set_uint32(GameObjectFields.GAMEOBJECT_DISPLAYID, self.current_display_id)
        return True

    # override
    def _get_fields_update(self, is_create, requester):
        data = b''
        mask = self.update_packet_factory.update_mask.copy()
        for field_index in range(self.update_packet_factory.update_mask.field_count):
            # Partial packets only care for fields that had changes.
            if not is_create and mask[field_index] == 0 and not self.update_packet_factory.is_dynamic_field(field_index):
                continue
            # Check for encapsulation, turn off the bit if requester has no read access.
            if not self.update_packet_factory.has_read_rights_for_field(field_index, requester):
                mask[field_index] = 0
                continue
            # Handle dynamic field, turn on this extra bit.
            if self.update_packet_factory.is_dynamic_field(field_index):
                data += pack('<I', self.generate_dynamic_field_value(requester))
                mask[field_index] = 1
            else:
                # Append field value and turn on bit on mask.
                data += self.update_packet_factory.update_values_bytes[field_index]
                mask[field_index] = 1
        return pack('<B', self.update_packet_factory.update_mask.block_count) + mask.tobytes() + data

    # There are only 3 possible animations that can be used here.
    # Effect might depend on the gameobject type, apparently. e.g. Fishing bobber does its animation by sending 0.
    # TODO: See if we can retrieve the animation names.
    def send_custom_animation(self, animation):
        data = pack('<QI', self.guid, animation)
        packet = PacketWriter.get_packet(OpCode.SMSG_GAMEOBJECT_CUSTOM_ANIM, data)
        MapManager.send_surrounding(packet, self, include_self=False)

    def generate_dynamic_field_value(self, requester):
        # TODO: Handle more dynamic cases.
        # QUESTGIVERS and CHESTS (This includes other interactive game objects).
        if self.gobject_template.type == GameObjectTypes.TYPE_CHEST or \
                self.gobject_template.type == GameObjectTypes.TYPE_QUESTGIVER:
            if requester.quest_manager.should_interact_with_go(self):
                return 1
        return 0

    # override
    def initialize_field_values(self):
        # Initial field values, after this, fields must be modified by setters or directly writing values to them.
        if not self.initialized and self.gobject_template and self.gobject_instance:
            # Object fields.
            self.set_uint64(ObjectFields.OBJECT_FIELD_GUID, self.guid)
            self.set_uint32(ObjectFields.OBJECT_FIELD_TYPE, self.object_type_mask)
            self.set_uint32(ObjectFields.OBJECT_FIELD_ENTRY, self.entry)
            self.set_float(ObjectFields.OBJECT_FIELD_SCALE_X, self.current_scale)
            self.set_uint32(ObjectFields.OBJECT_FIELD_PADDING, 0)

            # Gameobject fields.
            self.set_uint32(GameObjectFields.GAMEOBJECT_DISPLAYID, self.current_display_id)
            self.set_uint32(GameObjectFields.GAMEOBJECT_FLAGS, self.flags)
            self.set_uint32(GameObjectFields.GAMEOBJECT_FACTION, self.faction)
            self.set_uint32(GameObjectFields.GAMEOBJECT_STATE, self.state)
            self.set_float(GameObjectFields.GAMEOBJECT_ROTATION, self.gobject_instance.spawn_rotation0)
            self.set_float(GameObjectFields.GAMEOBJECT_ROTATION + 1, self.gobject_instance.spawn_rotation1)

            if self.gobject_instance.spawn_rotation2 == 0 and self.gobject_instance.spawn_rotation3 == 0:
                f_rot1 = math.sin(self.location.o / 2.0)
                f_rot2 = math.cos(self.location.o / 2.0)
            else:
                f_rot1 = self.gobject_instance.spawn_rotation2
                f_rot2 = self.gobject_instance.spawn_rotation3

            self.set_float(GameObjectFields.GAMEOBJECT_ROTATION + 2, f_rot1)
            self.set_float(GameObjectFields.GAMEOBJECT_ROTATION + 3, f_rot2)
            self.set_float(GameObjectFields.GAMEOBJECT_POS_X, self.location.x)
            self.set_float(GameObjectFields.GAMEOBJECT_POS_Y, self.location.y)
            self.set_float(GameObjectFields.GAMEOBJECT_POS_Z, self.location.z)
            self.set_float(GameObjectFields.GAMEOBJECT_FACING, self.location.o)

            self.initialized = True

    def query_details(self):
        name_bytes = PacketWriter.string_to_bytes(self.gobject_template.name)
        data = pack(
            f'<3I{len(name_bytes)}ssss10I',
            self.gobject_template.entry,
            self.gobject_template.type,
            self.current_display_id,
            name_bytes, b'\x00', b'\x00', b'\x00',
            self.gobject_template.data0,
            self.gobject_template.data1,
            self.gobject_template.data2,
            self.gobject_template.data3,
            self.gobject_template.data4,
            self.gobject_template.data5,
            self.gobject_template.data6,
            self.gobject_template.data7,
            self.gobject_template.data8,
            self.gobject_template.data9
        )
        return PacketWriter.get_packet(OpCode.SMSG_GAMEOBJECT_QUERY_RESPONSE, data)

    # override
    def respawn(self):
        # Set properties before making it visible.
        self.set_state(GameObjectStates.GO_STATE_READY)
        self.respawn_timer = 0
        self.respawn_time = randint(self.gobject_instance.spawn_spawntimemin,
                                    self.gobject_instance.spawn_spawntimemin)

        if self.gobject_template.type == GameObjectTypes.TYPE_TRAP:
            self.trap_manager.reset()

        MapManager.respawn_object(self)

    # override
    def update(self, now):
        if now > self.last_tick > 0:
            elapsed = now - self.last_tick

            if self.is_spawned and self.initialized:
                # Logic for Trap GameObjects (type 6).
                if self.has_observers() and self.gobject_template.type == GameObjectTypes.TYPE_TRAP:
                    self.trap_manager.update(elapsed)
                # Logic for Fishing node.
                if self.has_observers() and self.gobject_template.type == GameObjectTypes.TYPE_FISHINGNODE:
                    self.fishing_node_manager.update(elapsed)

                # SpellManager update.
                self.spell_manager.update(now)

                # Check if this game object should be updated yet or not.
                if self.has_pending_updates():
                    MapManager.update_object(self, has_changes=True)
                    self.reset_fields_older_than(now)
            # Not spawned but initialized.
            elif self.initialized:
                self.respawn_timer += elapsed
                if self.respawn_timer >= self.respawn_time:
                    if self.summoner:
                        self.despawn(destroy=True)
                    else:
                        self.respawn()

        self.last_tick = now

    # override
    def on_cell_change(self):
        pass

    # override
    def get_type_id(self):
        return ObjectTypeIds.ID_GAMEOBJECT

    # override
    def generate_object_guid(self, low_guid):
        return low_guid | HighGuid.HIGHGUID_GAMEOBJECT
Beispiel #6
0
class UnitManager(ObjectManager):
    def __init__(
            self,
            channel_spell=0,
            channel_object=0,
            health=0,
            power_type=0,
            power_1=0,  # mana
            power_2=0,  # rage
            power_3=100,  # focus
            power_4=100,  # energy
            max_health=0,
            max_power_1=0,
            max_power_2=1000,
            max_power_3=100,
            max_power_4=100,
            level=0,
            gender=0,
            bytes_0=0,  # race, class, gender, power_type
            creature_type=0,
            stat_0=0,
            stat_1=0,
            stat_2=0,
            stat_3=0,
            stat_4=0,
            base_stat_0=0,
            base_stat_1=0,
            base_stat_2=0,
            base_stat_3=0,
            base_stat_4=0,
            flags=0,
            coinage=0,
            combat_reach=config.Unit.Defaults.combat_reach,
            weapon_reach=0,
            mount_display_id=0,
            resistance_buff_mods_positive_0=0,  # Armor
            resistance_buff_mods_positive_1=0,  # Holy
            resistance_buff_mods_positive_2=0,  # Fire
            resistance_buff_mods_positive_3=0,  # Nature
            resistance_buff_mods_positive_4=0,  # Frost
            resistance_buff_mods_positive_5=0,  # Shadow
            resistance_buff_mods_negative_0=0,
            resistance_buff_mods_negative_1=0,
            resistance_buff_mods_negative_2=0,
            resistance_buff_mods_negative_3=0,
            resistance_buff_mods_negative_4=0,
            resistance_buff_mods_negative_5=0,
            base_attack_time=config.Unit.Defaults.base_attack_time,
            offhand_attack_time=config.Unit.Defaults.offhand_attack_time,
            resistance_0=0,  # Armor
            resistance_1=0,
            resistance_2=0,
            resistance_3=0,
            resistance_4=0,
            resistance_5=0,
            stand_state=0,
            sheathe_state=WeaponMode.SHEATHEDMODE,
            shapeshift_form=0,
            bytes_1=0,  # stand state, shapeshift form, sheathstate
            mod_cast_speed=1,
            dynamic_flags=0,
            damage=0,  # current damage, max damage
            bytes_2=0,  # combo points, 0, 0, 0
            current_target=0,  # guid
            combat_target=None,  # victim
            **kwargs):
        super().__init__(**kwargs)

        self.combat_target = combat_target
        self.channel_spell = channel_spell
        self.channel_object = channel_object
        self.health = health
        self.power_type = power_type
        self.power_1 = power_1
        self.power_2 = power_2
        self.power_3 = power_3
        self.power_4 = power_4
        self.max_health = max_health
        self.max_power_1 = max_power_1
        self.max_power_2 = max_power_2
        self.max_power_3 = max_power_3
        self.max_power_4 = max_power_4
        self.level = level
        self.gender = gender
        self.bytes_0 = bytes_0  # race, class, gender, power_type
        self.creature_type = creature_type
        self.str = stat_0
        self.agi = stat_1
        self.sta = stat_2
        self.int = stat_3
        self.spi = stat_4
        self.base_str = base_stat_0
        self.base_agi = base_stat_1
        self.base_sta = base_stat_2
        self.base_int = base_stat_3
        self.base_spi = base_stat_4
        self.flags = flags
        self.coinage = coinage
        self.combat_reach = combat_reach
        self.weapon_reach = weapon_reach
        self.mount_display_id = mount_display_id
        self.resistance_buff_mods_positive_0 = resistance_buff_mods_positive_0
        self.resistance_buff_mods_positive_1 = resistance_buff_mods_positive_1
        self.resistance_buff_mods_positive_2 = resistance_buff_mods_positive_2
        self.resistance_buff_mods_positive_3 = resistance_buff_mods_positive_3
        self.resistance_buff_mods_positive_4 = resistance_buff_mods_positive_4
        self.resistance_buff_mods_positive_5 = resistance_buff_mods_positive_5
        self.resistance_buff_mods_negative_0 = resistance_buff_mods_negative_0
        self.resistance_buff_mods_negative_1 = resistance_buff_mods_negative_1
        self.resistance_buff_mods_negative_2 = resistance_buff_mods_negative_2
        self.resistance_buff_mods_negative_3 = resistance_buff_mods_negative_3
        self.resistance_buff_mods_negative_4 = resistance_buff_mods_negative_4
        self.resistance_buff_mods_negative_5 = resistance_buff_mods_negative_5
        self.base_attack_time = base_attack_time
        self.offhand_attack_time = offhand_attack_time
        self.resistance_0 = resistance_0  # Armor
        self.resistance_1 = resistance_1
        self.resistance_2 = resistance_2
        self.resistance_3 = resistance_3
        self.resistance_4 = resistance_4
        self.resistance_5 = resistance_5
        self.stand_state = stand_state
        self.sheath_state = sheathe_state
        self.shapeshift_form = shapeshift_form
        self.bytes_1 = bytes_1  # stand state, shapeshift form, sheathstate
        self.mod_cast_speed = mod_cast_speed
        self.dynamic_flags = dynamic_flags
        self.damage = damage  # current damage, max damage
        self.bytes_2 = bytes_2  # combo points, 0, 0, 0
        self.current_target = current_target

        self.object_type.append(ObjectTypes.TYPE_UNIT)
        self.update_packet_factory.init_values(UnitFields.UNIT_END)

        self.is_alive = True
        self.is_sitting = False
        self.in_combat = False
        self.swing_error = AttackSwingError.NONE
        self.extra_attacks = 0
        self.disarmed_mainhand = False
        self.disarmed_offhand = False
        self.attackers = {}
        self.attack_timers = {
            AttackTypes.BASE_ATTACK: 0,
            AttackTypes.OFFHAND_ATTACK: 0,
            AttackTypes.RANGED_ATTACK: 0
        }

        # Used to determine the current state of the unit (internal usage).
        self.unit_state = UnitStates.NONE

        # Defensive passive spells are not handled through the aura system.
        # The effects will instead flag the unit with these fields.
        self.has_block_passive = False
        self.has_parry_passive = False
        self.has_dodge_passive = False

        self.stat_manager = StatManager(self)
        self.spell_manager = SpellManager(self)
        self.aura_manager = AuraManager(self)
        self.movement_manager = MovementManager(self)

    def is_within_interactable_distance(self, victim):
        current_distance = self.location.distance(victim.location)
        return current_distance <= UnitFormulas.interactable_distance(
            self, victim)

    def attack(self, victim, is_melee=True):
        if not victim or victim == self:
            return False

        # Dead units can neither attack nor be attacked
        if not self.is_alive or not victim.is_alive:
            return False

        # Mounted players can't attack
        if self.get_type(
        ) == ObjectTypes.TYPE_PLAYER and self.mount_display_id > 0:
            return False

        # In fight already
        if self.combat_target:
            if self.combat_target == victim:
                if is_melee and self.is_within_interactable_distance(
                        self.combat_target):
                    self.send_attack_start(victim.guid)
                    return True
                return False

            self.attack_stop(target_switch=True)

        self.set_current_target(victim.guid)
        self.combat_target = victim
        victim.attackers[self.guid] = self

        # Reset offhand weapon attack
        if self.has_offhand_weapon():
            self.set_attack_timer(AttackTypes.OFFHAND_ATTACK,
                                  self.offhand_attack_time)

        self.send_attack_start(self.combat_target.guid)

        return True

    def attack_stop(self, target_switch=False):
        # Clear target
        self.set_current_target(0)
        victim = self.combat_target
        self.combat_target = None

        self.send_attack_stop(victim.guid if victim else self.guid)
        self.set_dirty()

    def send_attack_start(self, victim_guid):
        data = pack('<2Q', self.guid, victim_guid)
        MapManager.send_surrounding(
            PacketWriter.get_packet(OpCode.SMSG_ATTACKSTART, data), self)

    def send_attack_stop(self, victim_guid):
        # Last uint32 is "deceased"; can be either 1 (self is dead), or 0, (self is alive).
        # Forces the unit to face the corpse and disables clientside
        # turning (UnitFlags.DisableMovement) CGUnit_C::OnAttackStop
        data = pack('<2QI', self.guid, victim_guid, 0 if self.is_alive else 1)
        MapManager.send_surrounding(
            PacketWriter.get_packet(OpCode.SMSG_ATTACKSTOP, data), self)

    def attack_update(self, elapsed):
        if self.combat_target and not self.combat_target.is_alive:
            self.leave_combat()
            self.set_dirty()
            return

        self.update_attack_time(AttackTypes.BASE_ATTACK, elapsed * 1000.0)
        if self.has_offhand_weapon():
            self.update_attack_time(AttackTypes.OFFHAND_ATTACK,
                                    elapsed * 1000.0)

        self.update_melee_attacking_state()

    def update_melee_attacking_state(self):
        if self.unit_state & UnitStates.STUNNED:
            return

        swing_error = AttackSwingError.NONE
        combat_angle = math.pi

        if not self.combat_target:
            if self.in_combat and len(self.attackers) == 0:
                self.leave_combat()
                self.set_dirty()
            return False

        if not self.is_attack_ready(
                AttackTypes.BASE_ATTACK) and not self.is_attack_ready(
                    AttackTypes.OFFHAND_ATTACK
                ) or self.spell_manager.is_casting():
            return False

        # Out of reach
        if not self.is_within_interactable_distance(self.combat_target):
            swing_error = AttackSwingError.NOTINRANGE
        # Not proper angle
        elif not self.location.has_in_arc(self.combat_target.location,
                                          combat_angle):
            swing_error = AttackSwingError.BADFACING
        # Moving
        elif self.movement_flags & MoveFlags.MOVEFLAG_MOTION_MASK:
            swing_error = AttackSwingError.MOVING
        # Not standing
        elif self.stand_state != StandState.UNIT_STANDING:
            swing_error = AttackSwingError.NOTSTANDING
        # Dead target
        elif not self.combat_target.is_alive:
            self.attackers.pop(self.combat_target.guid)
            swing_error = AttackSwingError.DEADTARGET
        else:
            # Main hand attack
            if self.is_attack_ready(AttackTypes.BASE_ATTACK):
                # Prevent both and attacks at the same time
                if self.has_offhand_weapon():
                    if self.attack_timers[AttackTypes.OFFHAND_ATTACK] < 500:
                        self.set_attack_timer(AttackTypes.OFFHAND_ATTACK, 500)

                self.attacker_state_update(self.combat_target,
                                           AttackTypes.BASE_ATTACK, False)
                self.set_attack_timer(AttackTypes.BASE_ATTACK,
                                      self.base_attack_time)

            # Off hand attack
            if self.has_offhand_weapon() and self.is_attack_ready(
                    AttackTypes.OFFHAND_ATTACK):
                # Prevent both and attacks at the same time
                if self.attack_timers[AttackTypes.BASE_ATTACK] < 500:
                    self.set_attack_timer(AttackTypes.BASE_ATTACK, 500)

                self.attacker_state_update(self.combat_target,
                                           AttackTypes.OFFHAND_ATTACK, False)
                self.set_attack_timer(AttackTypes.OFFHAND_ATTACK,
                                      self.offhand_attack_time)

        if self.object_type == ObjectTypes.TYPE_PLAYER:
            if swing_error != AttackSwingError.NONE:
                self.set_attack_timer(AttackTypes.BASE_ATTACK,
                                      self.base_attack_time)
                if self.has_offhand_weapon():
                    self.set_attack_timer(AttackTypes.OFFHAND_ATTACK,
                                          self.offhand_attack_time)

                if swing_error == AttackSwingError.NOTINRANGE:
                    self.send_attack_swing_not_in_range(self.combat_target)
                elif swing_error == AttackSwingError.BADFACING:
                    self.send_attack_swing_facing_wrong_way(self.combat_target)
                elif swing_error == AttackSwingError.DEADTARGET:
                    self.send_attack_swing_dead_target(self.combat_target)
                elif swing_error == AttackSwingError.NOTSTANDING:
                    self.send_attack_swing_not_standing(self.combat_target)

                self.send_attack_stop(self.combat_target.guid)

        self.swing_error = swing_error
        return swing_error == AttackSwingError.NONE

    def attacker_state_update(self, victim, attack_type, extra):
        if attack_type == AttackTypes.BASE_ATTACK:
            # No recent extra attack only at any non extra attack
            if not extra and self.extra_attacks > 0:
                self.execute_extra_attacks()
                return

            if self.spell_manager.cast_queued_melee_ability(attack_type):
                return  # Melee ability replaces regular attack

        damage_info = self.calculate_melee_damage(victim, attack_type)
        if not damage_info:
            return

        if damage_info.total_damage > 0:
            victim.spell_manager.check_spell_interrupts(
                received_auto_attack=True, hit_info=damage_info.hit_info)

        victim.aura_manager.check_aura_procs(damage_info=damage_info,
                                             is_melee_swing=True)
        self.aura_manager.check_aura_procs(damage_info=damage_info,
                                           is_melee_swing=True)

        self.send_attack_state_update(damage_info)

        # Extra attack only at any non extra attack
        if not extra and self.extra_attacks > 0:
            self.execute_extra_attacks()

    def execute_extra_attacks(self):
        while self.extra_attacks > 0:
            self.attacker_state_update(self.combat_target,
                                       AttackTypes.BASE_ATTACK, True)
            self.extra_attacks -= 1

    def calculate_melee_damage(self, victim, attack_type):
        damage_info = DamageInfoHolder()

        if not victim:
            return None

        if not self.is_alive or not victim.is_alive:
            return None

        damage_info.attacker = self
        damage_info.target = victim
        damage_info.attack_type = attack_type

        hit_info = victim.stat_manager.get_attack_result_against_self(
            self, attack_type,
            0.19 if self.has_offhand_weapon() else 0)  # Dual wield penalty.

        damage_info.damage = self.calculate_base_attack_damage(
            attack_type, SpellSchools.SPELL_SCHOOL_NORMAL, victim)
        damage_info.clean_damage = damage_info.total_damage = damage_info.damage
        damage_info.hit_info = hit_info
        damage_info.target_state = VictimStates.VS_WOUND  # Default state on successful attack.

        if hit_info != HitInfo.SUCCESS:
            damage_info.hit_info = HitInfo.MISS
            damage_info.total_damage = 0
            if hit_info == HitInfo.DODGE:
                damage_info.target_state = VictimStates.VS_DODGE
                damage_info.proc_victim |= ProcFlags.DODGE
            elif hit_info == HitInfo.PARRY:
                damage_info.target_state = VictimStates.VS_PARRY
                damage_info.proc_victim |= ProcFlags.PARRY
            elif hit_info == HitInfo.BLOCK:
                # 0.6 patch notes: "Blocking an attack no longer avoids all of the damage of an attack."
                # Completely mitigate damage on block.
                damage_info.target_state = VictimStates.VS_BLOCK
                damage_info.proc_victim |= ProcFlags.BLOCK

        # Generate rage (if needed)
        self.generate_rage(
            damage_info, is_player=self.get_type() == ObjectTypes.TYPE_PLAYER)

        # Note: 1.1.0 patch: "Skills will not increase from use while dueling or engaged in PvP."
        self.handle_combat_skill_gain(damage_info)
        victim.handle_combat_skill_gain(damage_info)

        if damage_info.total_damage > 0:
            damage_info.proc_victim |= ProcFlags.TAKE_COMBAT_DMG
            damage_info.proc_attacker |= ProcFlags.DEAL_COMBAT_DMG

        if attack_type == AttackTypes.BASE_ATTACK:
            damage_info.proc_attacker |= ProcFlags.SWING
        elif attack_type == AttackTypes.OFFHAND_ATTACK:
            damage_info.proc_attacker |= ProcFlags.SWING
            damage_info.hit_info |= HitInfo.OFFHAND

        return damage_info

    def send_attack_state_update(self, damage_info):
        data = pack(
            '<I2QIBIf7I',
            damage_info.hit_info,
            damage_info.attacker.guid,
            damage_info.target.guid,
            damage_info.total_damage,
            1,  # Sub damage count
            damage_info.damage_school_mask,
            damage_info.total_damage,
            damage_info.damage,
            damage_info.absorb,
            damage_info.target_state,
            damage_info.resist,
            0,
            0,
            damage_info.blocked_amount)
        MapManager.send_surrounding(
            PacketWriter.get_packet(OpCode.SMSG_ATTACKERSTATEUPDATE, data),
            self,
            include_self=self.get_type() == ObjectTypes.TYPE_PLAYER)

        # Damage effects
        self.deal_damage(damage_info.target, damage_info.total_damage)

    def calculate_base_attack_damage(self,
                                     attack_type: AttackTypes,
                                     attack_school: SpellSchools,
                                     target,
                                     apply_bonuses=True):
        min_damage, max_damage = self.calculate_min_max_damage(
            attack_type, attack_school, target)

        if min_damage > max_damage:
            tmp_min = min_damage
            min_damage = max_damage
            max_damage = tmp_min

        return random.randint(min_damage, max_damage)

    # Implemented by PlayerManager
    def generate_rage(self, damage_info, is_player=False):
        return

    # Implemented by PlayerManager
    def handle_combat_skill_gain(self, damage_info):
        return

    def calculate_min_max_damage(self, attack_type: AttackTypes,
                                 attack_school: SpellSchools, target):
        return self.stat_manager.get_base_attack_base_min_max_damage(
            AttackTypes(attack_type))

    # Implemented by PlayerManager
    def calculate_spell_damage(self,
                               base_damage,
                               spell_school: SpellSchools,
                               target,
                               spell_attack_type: AttackTypes = -1):
        return base_damage

    def deal_damage(self, target, damage, is_periodic=False):
        if not target or not target.is_alive:
            return

        if target is not self:
            if self.guid not in target.attackers:
                target.attackers[self.guid] = self

            if not self.in_combat:
                self.enter_combat()
                self.set_dirty()

            if not target.in_combat:
                target.enter_combat()
                target.set_dirty()

        target.receive_damage(damage, source=self, is_periodic=False)

    def receive_damage(self, amount, source=None, is_periodic=False):
        is_player = self.get_type() == ObjectTypes.TYPE_PLAYER

        if source is not self and not is_periodic and amount > 0:
            self.aura_manager.check_aura_interrupts(received_damage=True)
            self.spell_manager.check_spell_interrupts(received_damage=True)

        new_health = self.health - amount
        if new_health <= 0:
            self.die(killer=source)
        else:
            self.set_health(new_health)

        # If unit is a creature and it's being attacked by another unit, automatically set combat target.
        if not self.combat_target and not is_player and source and source.get_type(
        ) != ObjectTypes.TYPE_GAMEOBJECT:
            # Make sure to first stop any movement right away.
            if len(self.movement_manager.pending_waypoints) > 0:
                self.movement_manager.send_move_stop()
            # Attack.
            self.attack(source)

        self.set_dirty()

    def receive_healing(self, amount, source=None):
        new_health = self.health + amount
        if new_health > self.max_health:
            self.set_health(self.max_health)
        else:
            self.set_health(new_health)

        self.set_dirty()

    def receive_power(self, amount, power_type, source=None):
        if self.power_type != power_type:
            return

        new_power = self.get_power_type_value() + amount
        if power_type == PowerTypes.TYPE_MANA:
            self.set_mana(new_power)
        elif power_type == PowerTypes.TYPE_RAGE:
            self.set_rage(new_power)
        elif power_type == PowerTypes.TYPE_FOCUS:
            self.set_focus(new_power)
        elif power_type == PowerTypes.TYPE_ENERGY:
            self.set_energy(new_power)

        self.set_dirty()

    def apply_spell_damage(self,
                           target,
                           damage,
                           casting_spell,
                           is_periodic=False):
        if target.guid in casting_spell.object_target_results:
            miss_reason = casting_spell.object_target_results[
                target.guid].result
        else:  # TODO Proc damage effects (SPELL_AURA_PROC_TRIGGER_DAMAGE) can't fill target results - should they be able to miss?
            miss_reason = SpellMissReason.MISS_REASON_NONE

        damage = self.calculate_spell_damage(damage,
                                             casting_spell.spell_entry.School,
                                             target,
                                             casting_spell.spell_attack_type)

        # TODO Handle misses, absorbs etc. for spells.
        damage_info = self.get_spell_cast_damage_info(target, casting_spell,
                                                      damage, 0)

        if casting_spell.casts_on_swing(
        ):  # TODO Should other spells give skill too?
            self.handle_combat_skill_gain(damage_info)
            target.handle_combat_skill_gain(damage_info)

        self.send_spell_cast_debug_info(damage_info,
                                        miss_reason,
                                        casting_spell.spell_entry.ID,
                                        is_periodic=is_periodic)

        self.deal_damage(target, damage, is_periodic)

    def apply_spell_healing(self,
                            target,
                            healing,
                            casting_spell,
                            is_periodic=False):
        miss_info = casting_spell.object_target_results[target.guid].result
        damage_info = self.get_spell_cast_damage_info(target, casting_spell,
                                                      healing, 0)
        self.send_spell_cast_debug_info(damage_info,
                                        miss_info,
                                        casting_spell.spell_entry.ID,
                                        healing=True,
                                        is_periodic=is_periodic)
        target.receive_healing(healing, self)

    def get_spell_cast_damage_info(self, victim, casting_spell, damage,
                                   absorb):
        damage_info = DamageInfoHolder()

        if not victim:
            return None

        damage_info.attacker = self
        damage_info.target = victim
        damage_info.attack_type = casting_spell.spell_attack_type if casting_spell.spell_attack_type != -1 else 0

        damage_info.damage += damage
        damage_info.damage_school_mask = casting_spell.spell_entry.School
        # Not taking "subdamages" into account
        damage_info.total_damage = max(0, damage - absorb)
        damage_info.absorb = absorb
        damage_info.hit_info = HitInfo.DAMAGE

        return damage_info

    def send_spell_cast_debug_info(self,
                                   damage_info,
                                   miss_reason,
                                   spell_id,
                                   healing=False,
                                   is_periodic=False):
        flags = SpellHitFlags.HIT_FLAG_HEALED if healing else SpellHitFlags.HIT_FLAG_DAMAGE
        if is_periodic:  # Periodic damage/healing does not show in combat log - only on character frame.
            flags |= SpellHitFlags.HIT_FLAG_PERIODIC

        if miss_reason != SpellMissReason.MISS_REASON_NONE:
            combat_log_data = pack('<i2Q2i', flags, damage_info.attacker.guid,
                                   damage_info.target.guid, spell_id,
                                   miss_reason)
            combat_log_opcode = OpCode.SMSG_ATTACKERSTATEUPDATEDEBUGINFOSPELLMISS
        else:

            combat_log_data = pack(
                '<I2Q2If3I', flags, damage_info.attacker.guid,
                damage_info.target.guid, spell_id, damage_info.total_damage,
                damage_info.damage, damage_info.damage_school_mask,
                damage_info.damage, damage_info.absorb)
            combat_log_opcode = OpCode.SMSG_ATTACKERSTATEUPDATEDEBUGINFOSPELL

        MapManager.send_surrounding(
            PacketWriter.get_packet(combat_log_opcode, combat_log_data),
            self,
            include_self=self.get_type() == ObjectTypes.TYPE_PLAYER)

        if not healing:
            damage_data = pack('<Q2i2IQ', damage_info.target.guid,
                               damage_info.total_damage, damage_info.damage,
                               miss_reason, spell_id,
                               damage_info.attacker.guid)
            MapManager.send_surrounding(
                PacketWriter.get_packet(OpCode.SMSG_DAMAGE_DONE, damage_data),
                self,
                include_self=self.get_type() == ObjectTypes.TYPE_PLAYER)

    def set_current_target(self, guid):
        self.current_target = guid
        self.set_uint64(UnitFields.UNIT_FIELD_TARGET, guid)

    # Implemented by PlayerManager
    def send_attack_swing_not_in_range(self, victim):
        pass

    # Implemented by PlayerManager
    def send_attack_swing_facing_wrong_way(self, victim):
        pass

    # Implemented by PlayerManager
    def send_attack_swing_cant_attack(self, victim):
        pass

    # Implemented by PlayerManager
    def send_attack_swing_dead_target(self, victim):
        pass

    # Implemented by PlayerManager
    def send_attack_swing_not_standing(self, victim):
        pass

    # Implemented by PlayerManager and CreatureManager
    def has_offhand_weapon(self):
        return False

    # Implemented by PlayerManager and CreatureManager
    def has_ranged_weapon(self):
        return False

    # Location is used by PlayerManager if provided.
    # According to 1.3.0 notes, creatures were able to block/parry from behind.
    def can_block(self, attacker_location=None):
        return self.has_block_passive  # TODO Stunned/casting checks

    def can_parry(self, attacker_location=None):
        return self.has_parry_passive  # TODO Stunned/casting checks

    def can_dodge(self, attacker_location=None):
        return self.has_dodge_passive  # TODO Stunned/casting checks

    def enter_combat(self):
        self.in_combat = True
        self.unit_flags |= UnitFlags.UNIT_FLAG_IN_COMBAT
        self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

    def leave_combat(self, force=False):
        if not self.in_combat and not force:
            return

        # Remove self from attacker list of attackers
        for victim in list(self.attackers.values()):
            if self.guid in victim.attackers:
                victim.attackers.pop(self.guid)
        self.attackers.clear()

        self.send_attack_stop(
            self.combat_target.guid if self.combat_target else self.guid)
        self.swing_error = 0

        self.combat_target = None
        self.in_combat = False
        self.unit_flags &= ~UnitFlags.UNIT_FLAG_IN_COMBAT
        self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

    def can_use_attack_type(self, attack_type):
        if attack_type == AttackTypes.BASE_ATTACK:
            return self.disarmed_mainhand
        if attack_type == AttackTypes.OFFHAND_ATTACK:
            return self.disarmed_offhand
        return True

    def is_attack_ready(self, attack_type):
        return self.attack_timers[attack_type] <= 0

    def update_attack_time(self, attack_type, value):
        new_value = self.attack_timers[attack_type] - value
        if new_value < 0:
            new_value = 0
        if not self.is_attack_ready(attack_type):
            self.set_attack_timer(attack_type, new_value)

    def set_attack_timer(self, attack_type, value):
        self.attack_timers[attack_type] = value

    # override
    def change_speed(self, speed=0):
        # Assign new base speed.
        self.stat_manager.base_stats[
            UnitStats.
            SPEED_RUNNING] = speed if speed > 0 else config.Unit.Defaults.run_speed
        # Get new total speed.
        speed = self.stat_manager.get_total_stat(UnitStats.SPEED_RUNNING)
        # Limit to 0-56 and assign object field.
        return super().change_speed(speed)

    def set_root(self, active):
        if active:
            self.movement_flags |= MoveFlags.MOVEFLAG_ROOTED
            self.unit_state |= UnitStates.ROOTED
        else:
            self.movement_flags &= ~MoveFlags.MOVEFLAG_ROOTED
            self.unit_state &= ~UnitStates.ROOTED

    def play_emote(self, emote):
        if emote != 0:
            data = pack('<IQ', emote, self.guid)
            MapManager.send_surrounding_in_range(
                PacketWriter.get_packet(OpCode.SMSG_EMOTE, data), self,
                config.World.Chat.ChatRange.emote_range)

    def summon_mount(self, creature_entry):
        creature_template = WorldDatabaseManager.creature_get_by_entry(
            creature_entry)
        if not creature_template:
            return False

        self.mount(creature_template.display_id1)
        return True

    def mount(self, mount_display_id):
        if mount_display_id > 0 and DbcDatabaseManager.CreatureDisplayInfoHolder.creature_display_info_get_by_id(
                mount_display_id):
            self.mount_display_id = mount_display_id
            self.unit_flags |= UnitFlags.UNIT_MASK_MOUNTED
            self.set_uint32(UnitFields.UNIT_FIELD_MOUNTDISPLAYID,
                            self.mount_display_id)
            self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

    def unmount(self):
        self.mount_display_id = 0
        self.unit_flags &= ~UnitFlags.UNIT_MASK_MOUNTED
        self.set_uint32(UnitFields.UNIT_FIELD_MOUNTDISPLAYID,
                        self.mount_display_id)
        self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

    def get_power_type_value(self):
        if self.power_type == PowerTypes.TYPE_MANA:
            return self.power_1
        elif self.power_type == PowerTypes.TYPE_RAGE:
            return self.power_2
        elif self.power_type == PowerTypes.TYPE_FOCUS:
            return self.power_3
        elif self.power_type == PowerTypes.TYPE_ENERGY:
            return self.power_4
        return 0

    def get_max_power_value(self):
        if self.power_type == PowerTypes.TYPE_MANA:
            return self.max_power_1
        elif self.power_type == PowerTypes.TYPE_RAGE:
            return self.max_power_2
        elif self.power_type == PowerTypes.TYPE_FOCUS:
            return self.max_power_3
        elif self.power_type == PowerTypes.TYPE_ENERGY:
            return self.max_power_4
        return 0

    def recharge_power(self):
        max_power = self.get_max_power_value()
        if self.power_type == PowerTypes.TYPE_MANA:
            self.set_mana(max_power)
        elif self.power_type == PowerTypes.TYPE_RAGE:
            self.set_rage(max_power)
        elif self.power_type == PowerTypes.TYPE_FOCUS:
            self.set_focus(max_power)
        elif self.power_type == PowerTypes.TYPE_ENERGY:
            self.set_energy(max_power)

    def set_health(self, health):
        if health < 0:
            health = 0
        self.health = health
        self.set_uint32(UnitFields.UNIT_FIELD_HEALTH, health)

    def set_max_health(self, health):
        self.max_health = health
        self.set_uint32(UnitFields.UNIT_FIELD_MAXHEALTH, health)

    def set_mana(self, mana):
        if mana < 0:
            mana = 0
        self.power_1 = min(mana, self.max_power_1)
        self.set_uint32(UnitFields.UNIT_FIELD_POWER1, mana)

    def set_rage(self, rage):
        if rage < 0:
            rage = 0
        self.power_2 = min(rage, self.max_power_2)
        self.set_uint32(UnitFields.UNIT_FIELD_POWER2, rage)

    def set_focus(self, focus):
        if focus < 0:
            focus = 0
        self.power_3 = min(focus, self.max_power_3)
        self.set_uint32(UnitFields.UNIT_FIELD_POWER3, focus)

    def set_energy(self, energy):
        if energy < 0:
            energy = 0
        self.power_4 = min(energy, self.max_power_4)
        self.set_uint32(UnitFields.UNIT_FIELD_POWER4, energy)

    def set_max_mana(self, mana):
        self.max_power_1 = mana
        self.set_uint32(UnitFields.UNIT_FIELD_MAXPOWER1, mana)

    def set_armor(self, total_armor, item_armor):
        self.resistance_0 = total_armor
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES, self.resistance_0)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEITEMMODS, item_armor)

    def set_holy_res(self, total_holy_res, item_holy_res):
        self.resistance_1 = total_holy_res
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 1,
                       self.resistance_1)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEITEMMODS + 1,
                       item_holy_res)

    def set_fire_res(self, total_fire_res, item_fire_res):
        self.resistance_2 = total_fire_res
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 2,
                       self.resistance_2)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEITEMMODS + 2,
                       item_fire_res)

    def set_nature_res(self, total_nature_res, item_nature_res):
        self.resistance_3 = total_nature_res
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 3,
                       self.resistance_3)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEITEMMODS + 3,
                       item_nature_res)

    def set_frost_res(self, total_frost_res, item_frost_res):
        self.resistance_4 = total_frost_res
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 4,
                       self.resistance_4)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEITEMMODS + 4,
                       item_frost_res)

    def set_shadow_res(self, total_shadow_res, item_shadow_res):
        self.resistance_5 = total_shadow_res
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 5,
                       self.resistance_5)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEITEMMODS + 5,
                       item_shadow_res)

    def set_bonus_armor(self, negative_mods, positive_mods):
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE,
                       positive_mods)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE,
                       negative_mods)

    def set_bonus_holy_res(self, negative_mods, positive_mods):
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE + 1,
                       positive_mods)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE + 1,
                       negative_mods)

    def set_bonus_fire_res(self, negative_mods, positive_mods):
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE + 2,
                       positive_mods)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE + 2,
                       negative_mods)

    def set_bonus_nature_res(self, negative_mods, positive_mods):
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE + 3,
                       positive_mods)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE + 3,
                       negative_mods)

    def set_bonus_frost_res(self, negative_mods, positive_mods):
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE + 4,
                       positive_mods)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE + 4,
                       negative_mods)

    def set_bonus_shadow_res(self, negative_mods, positive_mods):
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE + 5,
                       positive_mods)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE + 5,
                       negative_mods)

    def set_bonus_damage_done_for_school(self, value, school):  # TODO Fields
        self.set_int32(UnitFields.UNIT_FIELD_MOD_DAMAGE_DONE + school, value)

    def set_melee_damage(self, min_dmg, max_dmg):
        damages = unpack('<I', pack('<2H', min_dmg, max_dmg))[0]
        self.damage = damages
        self.set_uint32(UnitFields.UNIT_FIELD_DAMAGE, damages)

    def set_melee_attack_time(self, attack_time):
        self.base_attack_time = attack_time
        self.set_uint32(UnitFields.UNIT_FIELD_BASEATTACKTIME, attack_time)

    def set_offhand_attack_time(self, attack_time):
        self.offhand_attack_time = attack_time
        self.set_uint32(UnitFields.UNIT_FIELD_BASEATTACKTIME + 1, attack_time)

    def set_weapon_mode(self, weapon_mode):
        self.sheath_state = weapon_mode

        # TODO: Implement temp enchants updates.
        if WeaponMode.NORMALMODE:
            # Update main hand temp enchants
            # Update off hand temp enchants
            pass
        elif WeaponMode.RANGEDMODE:
            # Update ranged temp enchants
            pass

    def set_shapeshift_form(self, shapeshift_form):
        self.shapeshift_form = shapeshift_form

    def has_form(self, shapeshift_form):
        return self.shapeshift_form == shapeshift_form

    # Implemented by PlayerManager
    def add_combo_points_on_target(self, target, combo_points):
        pass

    # Implemented by PlayerManager
    def remove_combo_points(self):
        pass

    def set_stand_state(self, stand_state):
        self.stand_state = stand_state

    def set_taxi_flying_state(self,
                              is_flying,
                              mount_display_id=0,
                              set_dirty=False):
        if is_flying:
            self.mount(mount_display_id)
            self.unit_flags |= (UnitFlags.UNIT_FLAG_FROZEN
                                | UnitFlags.UNIT_FLAG_TAXI_FLIGHT)
        else:
            self.unmount()
            self.unit_flags &= ~(UnitFlags.UNIT_FLAG_FROZEN
                                 | UnitFlags.UNIT_FLAG_TAXI_FLIGHT)

        self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

        if set_dirty:
            self.set_dirty()

    # override
    def set_display_id(self, display_id):
        super().set_display_id(display_id)
        if display_id <= 0 or not \
                DbcDatabaseManager.CreatureDisplayInfoHolder.creature_display_info_get_by_id(display_id):
            return False

        self.set_uint32(UnitFields.UNIT_FIELD_DISPLAYID,
                        self.current_display_id)
        return True

    def set_channel_object(self, guid):
        self.channel_object = guid
        self.set_uint64(UnitFields.UNIT_FIELD_CHANNEL_OBJECT, guid)

    def set_channel_spell(self, spell_id):
        self.channel_spell = spell_id
        self.set_uint64(UnitFields.UNIT_CHANNEL_SPELL, spell_id)

    def die(self, killer=None):
        if not self.is_alive:
            return False
        self.is_alive = False

        # Reset movement and unit state flags.
        self.movement_flags = MoveFlags.MOVEFLAG_NONE
        self.unit_state = UnitStates.NONE

        # Stop movement on death.
        if len(self.movement_manager.pending_waypoints) > 0:
            self.movement_manager.send_move_stop()

        self.set_health(0)
        self.set_stand_state(StandState.UNIT_DEAD)

        self.unit_flags = UnitFlags.UNIT_MASK_DEAD
        self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

        self.dynamic_flags |= UnitDynamicTypes.UNIT_DYNAMIC_DEAD
        self.set_uint32(UnitFields.UNIT_DYNAMIC_FLAGS, self.dynamic_flags)

        if killer and killer.get_type() == ObjectTypes.TYPE_PLAYER:
            if killer.current_selection == self.guid:
                killer.set_current_selection(killer.guid)
                killer.set_dirty()

            # Clear combo of killer if this unit was the target
            if killer.combo_target == self.guid:
                killer.remove_combo_points()
                killer.set_dirty()

        if killer:
            killer.spell_manager.remove_unit_from_all_cast_targets(
                self.guid)  # Interrupt casting on target death
            killer.aura_manager.check_aura_procs(killed_unit=True)

        self.spell_manager.remove_all_casts()
        self.aura_manager.handle_death()

        self.leave_combat()
        return True

    def respawn(self):
        super().respawn()

        # Force leave combat just in case.
        self.leave_combat(force=True)
        self.set_current_target(0)
        self.is_alive = True

        self.unit_flags = UnitFlags.UNIT_FLAG_STANDARD
        self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

        self.dynamic_flags = UnitDynamicTypes.UNIT_DYNAMIC_NONE
        self.set_uint32(UnitFields.UNIT_DYNAMIC_FLAGS, self.dynamic_flags)

        self.set_stand_state(StandState.UNIT_STANDING)

    # override
    def on_cell_change(self):
        pass

    # override
    def get_type(self):
        return ObjectTypes.TYPE_UNIT

    # override
    def get_type_id(self):
        return ObjectTypeIds.ID_UNIT

    # override
    def generate_object_guid(self, low_guid):
        return low_guid | HighGuid.HIGHGUID_UNIT

    def _allegiance_status_checker(self, target, check_friendly=True):
        own_faction = DbcDatabaseManager.FactionTemplateHolder.faction_template_get_by_id(
            self.faction)
        target_faction = DbcDatabaseManager.FactionTemplateHolder.faction_template_get_by_id(
            target.faction)

        if not own_faction:
            Logger.error(f'Invalid faction template: {self.faction}.')
            return not check_friendly

        if not target_faction:
            Logger.error(f'Invalid faction template: {target.faction}.')
            return not check_friendly

        own_enemies = [
            own_faction.Enemies_1, own_faction.Enemies_2,
            own_faction.Enemies_3, own_faction.Enemies_4
        ]
        own_friends = [
            own_faction.Friend_1, own_faction.Friend_2, own_faction.Friend_3,
            own_faction.Friend_4
        ]
        if target_faction.Faction > 0:
            for enemy in own_enemies:
                if enemy == target_faction.Faction:
                    return not check_friendly
            for friend in own_friends:
                if friend == target_faction.Faction:
                    return check_friendly

        if check_friendly:
            return (
                (own_faction.FriendGroup & target_faction.FactionGroup) or
                (own_faction.FactionGroup & target_faction.FriendGroup)) != 0
        else:
            return (
                (own_faction.EnemyGroup & target_faction.FactionGroup) or
                (own_faction.FactionGroup & target_faction.EnemyGroup)) != 0

    # noinspection PyUnresolvedReferences
    def can_attack_target(self, target):
        if target is self:
            return False

        is_enemy = self.is_enemy_to(target)
        if is_enemy or self.get_type(
        ) != ObjectTypes.TYPE_PLAYER or target.get_type(
        ) != ObjectTypes.TYPE_PLAYER:
            return is_enemy

        if not self.duel_manager:
            return is_enemy

        # Return true if the players are friendly but dueling
        return self.duel_manager.player_involved(
            target
        ) and self.duel_manager.duel_state == DuelState.DUEL_STATE_STARTED

    def is_friendly_to(self, target):
        return self._allegiance_status_checker(target, True)

    def is_enemy_to(self, target):
        return self._allegiance_status_checker(target, False)
Beispiel #7
0
    def __init__(
            self,
            channel_spell=0,
            channel_object=0,
            health=0,
            power_type=0,
            power_1=0,  # mana
            power_2=0,  # rage
            power_3=100,  # focus
            power_4=100,  # energy
            max_health=0,
            max_power_1=0,
            max_power_2=1000,
            max_power_3=100,
            max_power_4=100,
            level=0,
            gender=0,
            bytes_0=0,  # race, class, gender, power_type
            creature_type=0,
            stat_0=0,
            stat_1=0,
            stat_2=0,
            stat_3=0,
            stat_4=0,
            base_stat_0=0,
            base_stat_1=0,
            base_stat_2=0,
            base_stat_3=0,
            base_stat_4=0,
            base_hp=0,
            base_mana=0,
            flags=0,
            coinage=0,
            combat_reach=config.Unit.Defaults.combat_reach,
            weapon_reach=0,
            mount_display_id=0,
            resistance_buff_mods_positive_0=0,  # Armor
            resistance_buff_mods_positive_1=0,  # Holy
            resistance_buff_mods_positive_2=0,  # Fire
            resistance_buff_mods_positive_3=0,  # Nature
            resistance_buff_mods_positive_4=0,  # Frost
            resistance_buff_mods_positive_5=0,  # Shadow
            resistance_buff_mods_negative_0=0,
            resistance_buff_mods_negative_1=0,
            resistance_buff_mods_negative_2=0,
            resistance_buff_mods_negative_3=0,
            resistance_buff_mods_negative_4=0,
            resistance_buff_mods_negative_5=0,
            base_attack_time=config.Unit.Defaults.base_attack_time,
            offhand_attack_time=config.Unit.Defaults.offhand_attack_time,
            resistance_0=0,  # Armor
            resistance_1=0,
            resistance_2=0,
            resistance_3=0,
            resistance_4=0,
            resistance_5=0,
            stand_state=0,
            sheathe_state=WeaponMode.SHEATHEDMODE,
            shapeshift_form=0,
            bytes_1=0,  # stand state, shapeshift form, sheathstate
            mod_cast_speed=1,
            dynamic_flags=0,
            damage=0,  # current damage, max damage
            bytes_2=0,  # combo points, 0, 0, 0
            current_target=0,  # guid
            combat_target=None,  # victim
            **kwargs):
        super().__init__(**kwargs)

        self.combat_target = combat_target
        self.channel_spell = channel_spell
        self.channel_object = channel_object
        self.health = health
        self.power_type = power_type
        self.power_1 = power_1
        self.power_2 = power_2
        self.power_3 = power_3
        self.power_4 = power_4
        self.max_health = max_health
        self.max_power_1 = max_power_1
        self.max_power_2 = max_power_2
        self.max_power_3 = max_power_3
        self.max_power_4 = max_power_4
        self.level = level
        self.race = 0
        self.class_ = 0
        self.gender = gender
        self.bytes_0 = bytes_0  # race, class, gender, power_type
        self.creature_type = creature_type
        self.str = stat_0
        self.agi = stat_1
        self.sta = stat_2
        self.int = stat_3
        self.spi = stat_4
        self.base_str = base_stat_0
        self.base_agi = base_stat_1
        self.base_sta = base_stat_2
        self.base_int = base_stat_3
        self.base_spi = base_stat_4
        self.base_hp = base_hp
        self.base_mana = base_mana
        self.flags = flags
        self.coinage = coinage
        self.combat_reach = combat_reach
        self.weapon_reach = weapon_reach
        self.mount_display_id = mount_display_id
        self.resistance_buff_mods_positive_0 = resistance_buff_mods_positive_0
        self.resistance_buff_mods_positive_1 = resistance_buff_mods_positive_1
        self.resistance_buff_mods_positive_2 = resistance_buff_mods_positive_2
        self.resistance_buff_mods_positive_3 = resistance_buff_mods_positive_3
        self.resistance_buff_mods_positive_4 = resistance_buff_mods_positive_4
        self.resistance_buff_mods_positive_5 = resistance_buff_mods_positive_5
        self.resistance_buff_mods_negative_0 = resistance_buff_mods_negative_0
        self.resistance_buff_mods_negative_1 = resistance_buff_mods_negative_1
        self.resistance_buff_mods_negative_2 = resistance_buff_mods_negative_2
        self.resistance_buff_mods_negative_3 = resistance_buff_mods_negative_3
        self.resistance_buff_mods_negative_4 = resistance_buff_mods_negative_4
        self.resistance_buff_mods_negative_5 = resistance_buff_mods_negative_5
        self.base_attack_time = base_attack_time
        self.offhand_attack_time = offhand_attack_time
        self.resistance_0 = resistance_0  # Armor
        self.resistance_1 = resistance_1
        self.resistance_2 = resistance_2
        self.resistance_3 = resistance_3
        self.resistance_4 = resistance_4
        self.resistance_5 = resistance_5
        self.stand_state = stand_state
        self.sheath_state = sheathe_state
        self.shapeshift_form = shapeshift_form
        self.bytes_1 = bytes_1  # stand state, shapeshift form, sheathstate
        self.mod_cast_speed = mod_cast_speed
        self.dynamic_flags = dynamic_flags
        self.damage = damage  # current damage, max damage
        self.bytes_2 = bytes_2  # combo points, 0, 0, 0
        self.current_target = current_target

        self.object_type_mask |= ObjectTypeFlags.TYPE_UNIT
        self.update_packet_factory.init_values(UnitFields.UNIT_END)

        self.is_alive = True
        self.in_combat = False
        self.is_evading = False
        self.evading_waypoints = []
        self.swing_error = AttackSwingError.NONE
        self.extra_attacks = 0
        self.disarmed_mainhand = False
        self.disarmed_offhand = False
        self.last_regen = 0
        self.regen_flags = RegenStatsFlags.NO_REGENERATION
        self.attackers = {}
        self.attack_timers = {
            AttackTypes.BASE_ATTACK: 0,
            AttackTypes.OFFHAND_ATTACK: 0,
            AttackTypes.RANGED_ATTACK: 0
        }

        # Used to determine the current state of the unit (internal usage).
        self.unit_state = UnitStates.NONE

        # Defensive passive spells are not handled through the aura system.
        # The effects will instead flag the unit with these fields.
        self.has_block_passive = False
        self.has_parry_passive = False
        self.has_dodge_passive = False

        self.stat_manager = StatManager(self)
        self.spell_manager = SpellManager(self)
        self.aura_manager = AuraManager(self)
        self.movement_manager = MovementManager(self)
Beispiel #8
0
class UnitManager(ObjectManager):
    def __init__(
            self,
            channel_spell=0,
            channel_object=0,
            health=0,
            power_type=0,
            power_1=0,  # mana
            power_2=0,  # rage
            power_3=100,  # focus
            power_4=100,  # energy
            max_health=0,
            max_power_1=0,
            max_power_2=1000,
            max_power_3=100,
            max_power_4=100,
            level=0,
            gender=0,
            bytes_0=0,  # race, class, gender, power_type
            creature_type=0,
            stat_0=0,
            stat_1=0,
            stat_2=0,
            stat_3=0,
            stat_4=0,
            base_stat_0=0,
            base_stat_1=0,
            base_stat_2=0,
            base_stat_3=0,
            base_stat_4=0,
            base_hp=0,
            base_mana=0,
            flags=0,
            coinage=0,
            combat_reach=config.Unit.Defaults.combat_reach,
            weapon_reach=0,
            mount_display_id=0,
            resistance_buff_mods_positive_0=0,  # Armor
            resistance_buff_mods_positive_1=0,  # Holy
            resistance_buff_mods_positive_2=0,  # Fire
            resistance_buff_mods_positive_3=0,  # Nature
            resistance_buff_mods_positive_4=0,  # Frost
            resistance_buff_mods_positive_5=0,  # Shadow
            resistance_buff_mods_negative_0=0,
            resistance_buff_mods_negative_1=0,
            resistance_buff_mods_negative_2=0,
            resistance_buff_mods_negative_3=0,
            resistance_buff_mods_negative_4=0,
            resistance_buff_mods_negative_5=0,
            base_attack_time=config.Unit.Defaults.base_attack_time,
            offhand_attack_time=config.Unit.Defaults.offhand_attack_time,
            resistance_0=0,  # Armor
            resistance_1=0,
            resistance_2=0,
            resistance_3=0,
            resistance_4=0,
            resistance_5=0,
            stand_state=0,
            sheathe_state=WeaponMode.SHEATHEDMODE,
            shapeshift_form=0,
            bytes_1=0,  # stand state, shapeshift form, sheathstate
            mod_cast_speed=1,
            dynamic_flags=0,
            damage=0,  # current damage, max damage
            bytes_2=0,  # combo points, 0, 0, 0
            current_target=0,  # guid
            combat_target=None,  # victim
            **kwargs):
        super().__init__(**kwargs)

        self.combat_target = combat_target
        self.channel_spell = channel_spell
        self.channel_object = channel_object
        self.health = health
        self.power_type = power_type
        self.power_1 = power_1
        self.power_2 = power_2
        self.power_3 = power_3
        self.power_4 = power_4
        self.max_health = max_health
        self.max_power_1 = max_power_1
        self.max_power_2 = max_power_2
        self.max_power_3 = max_power_3
        self.max_power_4 = max_power_4
        self.level = level
        self.race = 0
        self.class_ = 0
        self.gender = gender
        self.bytes_0 = bytes_0  # race, class, gender, power_type
        self.creature_type = creature_type
        self.str = stat_0
        self.agi = stat_1
        self.sta = stat_2
        self.int = stat_3
        self.spi = stat_4
        self.base_str = base_stat_0
        self.base_agi = base_stat_1
        self.base_sta = base_stat_2
        self.base_int = base_stat_3
        self.base_spi = base_stat_4
        self.base_hp = base_hp
        self.base_mana = base_mana
        self.flags = flags
        self.coinage = coinage
        self.combat_reach = combat_reach
        self.weapon_reach = weapon_reach
        self.mount_display_id = mount_display_id
        self.resistance_buff_mods_positive_0 = resistance_buff_mods_positive_0
        self.resistance_buff_mods_positive_1 = resistance_buff_mods_positive_1
        self.resistance_buff_mods_positive_2 = resistance_buff_mods_positive_2
        self.resistance_buff_mods_positive_3 = resistance_buff_mods_positive_3
        self.resistance_buff_mods_positive_4 = resistance_buff_mods_positive_4
        self.resistance_buff_mods_positive_5 = resistance_buff_mods_positive_5
        self.resistance_buff_mods_negative_0 = resistance_buff_mods_negative_0
        self.resistance_buff_mods_negative_1 = resistance_buff_mods_negative_1
        self.resistance_buff_mods_negative_2 = resistance_buff_mods_negative_2
        self.resistance_buff_mods_negative_3 = resistance_buff_mods_negative_3
        self.resistance_buff_mods_negative_4 = resistance_buff_mods_negative_4
        self.resistance_buff_mods_negative_5 = resistance_buff_mods_negative_5
        self.base_attack_time = base_attack_time
        self.offhand_attack_time = offhand_attack_time
        self.resistance_0 = resistance_0  # Armor
        self.resistance_1 = resistance_1
        self.resistance_2 = resistance_2
        self.resistance_3 = resistance_3
        self.resistance_4 = resistance_4
        self.resistance_5 = resistance_5
        self.stand_state = stand_state
        self.sheath_state = sheathe_state
        self.shapeshift_form = shapeshift_form
        self.bytes_1 = bytes_1  # stand state, shapeshift form, sheathstate
        self.mod_cast_speed = mod_cast_speed
        self.dynamic_flags = dynamic_flags
        self.damage = damage  # current damage, max damage
        self.bytes_2 = bytes_2  # combo points, 0, 0, 0
        self.current_target = current_target

        self.object_type_mask |= ObjectTypeFlags.TYPE_UNIT
        self.update_packet_factory.init_values(UnitFields.UNIT_END)

        self.is_alive = True
        self.in_combat = False
        self.is_evading = False
        self.evading_waypoints = []
        self.swing_error = AttackSwingError.NONE
        self.extra_attacks = 0
        self.disarmed_mainhand = False
        self.disarmed_offhand = False
        self.last_regen = 0
        self.regen_flags = RegenStatsFlags.NO_REGENERATION
        self.attackers = {}
        self.attack_timers = {
            AttackTypes.BASE_ATTACK: 0,
            AttackTypes.OFFHAND_ATTACK: 0,
            AttackTypes.RANGED_ATTACK: 0
        }

        # Used to determine the current state of the unit (internal usage).
        self.unit_state = UnitStates.NONE

        # Defensive passive spells are not handled through the aura system.
        # The effects will instead flag the unit with these fields.
        self.has_block_passive = False
        self.has_parry_passive = False
        self.has_dodge_passive = False

        self.stat_manager = StatManager(self)
        self.spell_manager = SpellManager(self)
        self.aura_manager = AuraManager(self)
        self.movement_manager = MovementManager(self)

    def is_within_interactable_distance(self, victim):
        current_distance = self.location.distance(victim.location)
        return current_distance <= UnitFormulas.interactable_distance(
            self, victim)

    def attack(self, victim, is_melee=True):
        if not victim or victim == self:
            return False

        # Dead units can neither attack nor be attacked
        if not self.is_alive or not victim.is_alive:
            return False

        # Mounted players can't attack
        if self.get_type_id(
        ) == ObjectTypeIds.ID_PLAYER and self.mount_display_id > 0:
            return False

        # In fight already
        if self.combat_target:
            if self.combat_target == victim:
                if is_melee and self.is_within_interactable_distance(
                        self.combat_target):
                    self.send_attack_start(victim.guid)
                    return True
                return False

            self.attack_stop(target_switch=True)

        self.set_current_target(victim.guid)
        self.combat_target = victim
        victim.attackers[self.guid] = self

        # Reset offhand weapon attack
        if self.has_offhand_weapon():
            self.set_attack_timer(AttackTypes.OFFHAND_ATTACK,
                                  self.offhand_attack_time)

        self.send_attack_start(self.combat_target.guid)

        return True

    def attack_stop(self, target_switch=False):
        # Clear target
        self.set_current_target(0)
        victim = self.combat_target
        self.combat_target = None

        self.send_attack_stop(victim.guid if victim else self.guid)

    def send_attack_start(self, victim_guid):
        data = pack('<2Q', self.guid, victim_guid)
        MapManager.send_surrounding(
            PacketWriter.get_packet(OpCode.SMSG_ATTACKSTART, data), self)

    def send_attack_stop(self, victim_guid):
        # Last uint32 is "deceased"; can be either 1 (self is dead), or 0, (self is alive).
        # Forces the unit to face the corpse and disables clientside
        # turning (UnitFlags.DisableMovement) CGUnit_C::OnAttackStop
        data = pack('<2QI', self.guid, victim_guid, 0 if self.is_alive else 1)
        MapManager.send_surrounding(
            PacketWriter.get_packet(OpCode.SMSG_ATTACKSTOP, data), self)

    def attack_update(self, elapsed):
        self.update_attack_time(AttackTypes.BASE_ATTACK, elapsed * 1000.0)
        if self.has_offhand_weapon():
            self.update_attack_time(AttackTypes.OFFHAND_ATTACK,
                                    elapsed * 1000.0)

        self.update_melee_attacking_state()

    def update_melee_attacking_state(self):
        if self.unit_state & UnitStates.STUNNED:
            return

        swing_error = AttackSwingError.NONE
        combat_angle = math.pi

        if not self.combat_target:
            if self.in_combat and len(self.attackers) == 0:
                self.leave_combat()
            return False

        if not self.is_attack_ready(
                AttackTypes.BASE_ATTACK) and not self.is_attack_ready(
                    AttackTypes.OFFHAND_ATTACK
                ) or self.spell_manager.is_casting():
            return False

        # Out of reach
        if not self.is_within_interactable_distance(self.combat_target):
            swing_error = AttackSwingError.NOTINRANGE
        # Not proper angle
        elif not self.location.has_in_arc(self.combat_target.location,
                                          combat_angle):
            swing_error = AttackSwingError.BADFACING
        # Moving
        elif self.movement_flags & MoveFlags.MOVEFLAG_MOTION_MASK:
            swing_error = AttackSwingError.MOVING
        # Not standing
        elif self.stand_state != StandState.UNIT_STANDING:
            swing_error = AttackSwingError.NOTSTANDING
        # Dead target
        elif not self.combat_target.is_alive:
            self.attackers.pop(self.combat_target.guid)
            swing_error = AttackSwingError.DEADTARGET
        else:
            # Main hand attack
            if self.is_attack_ready(AttackTypes.BASE_ATTACK):
                # Prevent both and attacks at the same time
                if self.has_offhand_weapon():
                    if self.attack_timers[AttackTypes.OFFHAND_ATTACK] < 500:
                        self.set_attack_timer(AttackTypes.OFFHAND_ATTACK, 500)

                self.attacker_state_update(self.combat_target,
                                           AttackTypes.BASE_ATTACK, False)
                self.set_attack_timer(AttackTypes.BASE_ATTACK,
                                      self.base_attack_time)

            # Off hand attack
            if self.has_offhand_weapon() and self.is_attack_ready(
                    AttackTypes.OFFHAND_ATTACK):
                # Prevent both and attacks at the same time
                if self.attack_timers[AttackTypes.BASE_ATTACK] < 500:
                    self.set_attack_timer(AttackTypes.BASE_ATTACK, 500)

                self.attacker_state_update(self.combat_target,
                                           AttackTypes.OFFHAND_ATTACK, False)
                self.set_attack_timer(AttackTypes.OFFHAND_ATTACK,
                                      self.offhand_attack_time)

        if self.get_type_id() == ObjectTypeIds.ID_PLAYER:
            if swing_error != AttackSwingError.NONE:
                self.set_attack_timer(AttackTypes.BASE_ATTACK,
                                      self.base_attack_time)
                if self.has_offhand_weapon():
                    self.set_attack_timer(AttackTypes.OFFHAND_ATTACK,
                                          self.offhand_attack_time)

                if swing_error == AttackSwingError.NOTINRANGE:
                    self.send_attack_swing_not_in_range(self.combat_target)
                elif swing_error == AttackSwingError.BADFACING:
                    self.send_attack_swing_facing_wrong_way(self.combat_target)
                elif swing_error == AttackSwingError.DEADTARGET:
                    self.send_attack_swing_dead_target(self.combat_target)
                elif swing_error == AttackSwingError.NOTSTANDING:
                    self.send_attack_swing_not_standing(self.combat_target)

                self.send_attack_stop(self.combat_target.guid)

        self.swing_error = swing_error
        return swing_error == AttackSwingError.NONE

    def attacker_state_update(self, victim, attack_type, extra):
        if attack_type == AttackTypes.BASE_ATTACK:
            # No recent extra attack only at any non extra attack
            if not extra and self.extra_attacks > 0:
                self.execute_extra_attacks()
                return

            if self.spell_manager.cast_queued_melee_ability(attack_type):
                return  # Melee ability replaces regular attack

        damage_info = self.calculate_melee_damage(victim, attack_type)
        if not damage_info:
            return

        if damage_info.total_damage > 0:
            victim.spell_manager.check_spell_interrupts(
                received_auto_attack=True, hit_info=damage_info.hit_info)

        victim.aura_manager.check_aura_procs(damage_info=damage_info,
                                             is_melee_swing=True)
        self.aura_manager.check_aura_procs(damage_info=damage_info,
                                           is_melee_swing=True)

        self.send_attack_state_update(damage_info)

        # Extra attack only at any non extra attack
        if not extra and self.extra_attacks > 0:
            self.execute_extra_attacks()

    def execute_extra_attacks(self):
        while self.extra_attacks > 0:
            self.attacker_state_update(self.combat_target,
                                       AttackTypes.BASE_ATTACK, True)
            self.extra_attacks -= 1

    def calculate_melee_damage(self, victim, attack_type):
        damage_info = DamageInfoHolder()

        if not victim:
            return None

        if not self.is_alive or not victim.is_alive:
            return None

        damage_info.attacker = self
        damage_info.target = victim
        damage_info.attack_type = attack_type

        hit_info = victim.stat_manager.get_attack_result_against_self(
            self, attack_type,
            0.19 if self.has_offhand_weapon() else 0)  # Dual wield penalty.

        damage_info.damage = self.calculate_base_attack_damage(
            attack_type, SpellSchools.SPELL_SCHOOL_NORMAL, victim)
        damage_info.clean_damage = damage_info.total_damage = damage_info.damage
        damage_info.hit_info = hit_info
        damage_info.target_state = VictimStates.VS_WOUND  # Default state on successful attack.

        if hit_info == HitInfo.CRITICAL_HIT:
            damage_info.total_damage *= 2
            damage_info.proc_ex = ProcFlagsExLegacy.CRITICAL_HIT

        elif hit_info != HitInfo.SUCCESS:
            damage_info.hit_info = HitInfo.MISS
            damage_info.total_damage = 0
            # Check evade, there is no HitInfo flag for this.
            if victim.is_evading:
                damage_info.target_state = VictimStates.VS_EVADE
                damage_info.proc_victim |= ProcFlags.NONE
            elif hit_info == HitInfo.DODGE:
                damage_info.target_state = VictimStates.VS_DODGE
                damage_info.proc_victim |= ProcFlags.DODGE
            elif hit_info == HitInfo.PARRY:
                damage_info.target_state = VictimStates.VS_PARRY
                damage_info.proc_victim |= ProcFlags.PARRY
            elif hit_info == HitInfo.BLOCK:
                # 0.6 patch notes: "Blocking an attack no longer avoids all of the damage of an attack."
                # Completely mitigate damage on block.
                damage_info.target_state = VictimStates.VS_BLOCK
                damage_info.proc_victim |= ProcFlags.BLOCK

        # Generate rage (if needed).
        self.generate_rage(damage_info, is_attacking=True)

        # Note: 1.1.0 patch: "Skills will not increase from use while dueling or engaged in PvP."
        self.handle_combat_skill_gain(damage_info)
        victim.handle_combat_skill_gain(damage_info)

        if damage_info.total_damage > 0:
            damage_info.proc_victim |= ProcFlags.TAKE_COMBAT_DMG
            damage_info.proc_attacker |= ProcFlags.DEAL_COMBAT_DMG

        if attack_type == AttackTypes.BASE_ATTACK:
            damage_info.proc_attacker |= ProcFlags.SWING
        elif attack_type == AttackTypes.OFFHAND_ATTACK:
            damage_info.proc_attacker |= ProcFlags.SWING
            damage_info.hit_info |= HitInfo.OFFHAND

        return damage_info

    def send_attack_state_update(self, damage_info):
        data = pack(
            '<I2QIBIf7I',
            damage_info.hit_info,
            damage_info.attacker.guid,
            damage_info.target.guid,
            damage_info.total_damage,
            1,  # Sub damage count
            damage_info.damage_school_mask,
            damage_info.total_damage,
            damage_info.damage,
            damage_info.absorb,
            damage_info.target_state,
            damage_info.resist,
            0,
            0,
            damage_info.blocked_amount)
        MapManager.send_surrounding(
            PacketWriter.get_packet(OpCode.SMSG_ATTACKERSTATEUPDATE, data),
            self,
            include_self=self.get_type_id() == ObjectTypeIds.ID_PLAYER)

        # Damage effects
        self.deal_damage(damage_info.target, damage_info.total_damage)

    def calculate_base_attack_damage(self,
                                     attack_type: AttackTypes,
                                     attack_school: SpellSchools,
                                     target,
                                     apply_bonuses=True):
        min_damage, max_damage = self.calculate_min_max_damage(
            attack_type, attack_school, target)

        if min_damage > max_damage:
            tmp_min = min_damage
            min_damage = max_damage
            max_damage = tmp_min

        return random.randint(min_damage, max_damage)

    def regenerate(self, elapsed):
        if not self.is_alive or self.health == 0:
            return

        self.last_regen += elapsed
        # Every 2 seconds
        if self.last_regen >= 2:
            self.last_regen = 0

            # Healing aura increases regeneration "by 2 every second", and base points equal to 10.
            # Calculate 2/5 of hp5/mp5.
            health_regen = self.stat_manager.get_total_stat(
                UnitStats.HEALTH_REGENERATION_PER_5) * 0.4
            mana_regen = self.stat_manager.get_total_stat(
                UnitStats.POWER_REGENERATION_PER_5) * 0.4

            # Health
            if self.regen_flags & RegenStatsFlags.REGEN_FLAG_HEALTH:
                if self.health < self.max_health and not self.in_combat:
                    if health_regen < 1:
                        health_regen = 1

                    # Apply bonus if sitting.
                    if self.is_sitting():
                        health_regen += health_regen * 0.33

                    if self.health + health_regen >= self.max_health:
                        self.set_health(self.max_health)
                    elif self.health < self.max_health:
                        self.set_health(self.health + int(health_regen))

            # Powers
            # Check if this unit should regenerate its powers.
            if self.regen_flags & RegenStatsFlags.REGEN_FLAG_POWER:
                # Mana
                if self.power_type == PowerTypes.TYPE_MANA:
                    if self.power_1 < self.max_power_1:
                        if self.in_combat:
                            # 1% per second (5% per 5 seconds)
                            mana_regen = self.base_mana * 0.02

                        if mana_regen < 1:
                            mana_regen = 1

                        if self.power_1 + mana_regen >= self.max_power_1:
                            self.set_mana(self.max_power_1)
                        elif self.power_1 < self.max_power_1:
                            self.set_mana(self.power_1 + int(mana_regen))
                # Focus
                elif self.power_type == PowerTypes.TYPE_FOCUS:
                    # Apparently focus didn't regenerate while moving.
                    # Note: Needs source, not 100% confirmed.
                    if self.power_3 < self.max_power_3 or not (
                            self.movement_flags
                            & MoveFlags.MOVEFLAG_MOTION_MASK):
                        if self.power_3 + 5 >= self.max_power_3:
                            self.set_focus(self.max_power_3)
                        elif self.power_3 < self.max_power_3:
                            self.set_focus(self.power_3 + 5)
                # Energy
                elif self.power_type == PowerTypes.TYPE_ENERGY:
                    if self.power_4 < self.max_power_4:
                        if self.power_4 + 20 >= self.max_power_4:
                            self.set_energy(self.max_power_4)
                        elif self.power_4 < self.max_power_4:
                            self.set_energy(self.power_4 + 20)

            # Rage decay
            if self.power_type == PowerTypes.TYPE_RAGE:
                if self.power_2 > 0:
                    if not self.in_combat:
                        # Defensive Stance (71) description says:
                        #     "A defensive stance that reduces rage decay when out of combat. [...]."
                        # We assume the rage decay value is reduced by 50% when on Defensive Stance. We don't really
                        # know how much it should be reduced, but 50% seemed reasonable (1 point instead of 2).
                        rage_decay_value = 10 if self.has_form(
                            ShapeshiftForms.SHAPESHIFT_FORM_DEFENSIVESTANCE
                        ) else 20
                        self.set_rage(self.power_2 - rage_decay_value)

    def generate_rage(self, damage_info, is_attacking=True):
        # Warrior Stances and Bear Form.
        # Defensive Stance (71): "A defensive stance that reduces rage decay when out of combat.
        # Generate rage when you are hit."
        # Battle Stance (2457): "A balanced combat stance. Generate rage when hit and when you strike an opponent."
        # Berserker Stance (2458): "An aggressive stance. Generate rage when you strike an opponent."
        if not is_attacking and self.has_form(ShapeshiftForms.SHAPESHIFT_FORM_DEFENSIVESTANCE) \
                or is_attacking and self.has_form(ShapeshiftForms.SHAPESHIFT_FORM_BERSERKERSTANCE) \
                or self.has_form(ShapeshiftForms.SHAPESHIFT_FORM_BATTLESTANCE) \
                or self.has_form(ShapeshiftForms.SHAPESHIFT_FORM_BEAR):
            self.set_rage(self.power_2 + UnitFormulas.calculate_rage_regen(
                damage_info, is_attacking=is_attacking))

    # Implemented by PlayerManager
    def handle_combat_skill_gain(self, damage_info):
        return

    def calculate_min_max_damage(self, attack_type: AttackTypes,
                                 attack_school: SpellSchools, target):
        return self.stat_manager.get_base_attack_base_min_max_damage(
            AttackTypes(attack_type))

    # Implemented by PlayerManager
    def calculate_spell_damage(self,
                               base_damage,
                               spell_school: SpellSchools,
                               target,
                               spell_attack_type: AttackTypes = -1):
        return base_damage

    def deal_damage(self, target, damage, is_periodic=False):
        if not target or not target.is_alive:
            return

        if target.is_evading:
            return

        if target is not self:
            if self.guid not in target.attackers:
                target.attackers[self.guid] = self

            if not self.in_combat:
                self.enter_combat()

            if not target.in_combat:
                target.enter_combat()

        target.receive_damage(damage, source=self, is_periodic=False)

    def receive_damage(self, amount, source=None, is_periodic=False):
        if source is not self and not is_periodic and amount > 0:
            self.aura_manager.check_aura_interrupts(received_damage=True)
            self.spell_manager.check_spell_interrupts(received_damage=True)

        new_health = self.health - amount
        if new_health <= 0:
            self.die(killer=source)
        else:
            damage_info = DamageInfoHolder()
            damage_info.damage = amount
            damage_info.victim = self
            self.set_health(new_health)
            self.generate_rage(damage_info, is_attacking=False)

    def receive_healing(self, amount, source=None):
        new_health = self.health + amount
        if new_health > self.max_health:
            self.set_health(self.max_health)
        else:
            self.set_health(new_health)

    def receive_power(self, amount, power_type, source=None):
        if self.power_type != power_type:
            return

        new_power = self.get_power_type_value() + amount
        if power_type == PowerTypes.TYPE_MANA:
            self.set_mana(new_power)
        elif power_type == PowerTypes.TYPE_RAGE:
            self.set_rage(new_power)
        elif power_type == PowerTypes.TYPE_FOCUS:
            self.set_focus(new_power)
        elif power_type == PowerTypes.TYPE_ENERGY:
            self.set_energy(new_power)

    def apply_spell_damage(self,
                           target,
                           damage,
                           casting_spell,
                           is_periodic=False):
        if target.guid in casting_spell.object_target_results:
            miss_reason = casting_spell.object_target_results[
                target.guid].result
        else:  # TODO Proc damage effects (SPELL_AURA_PROC_TRIGGER_DAMAGE) can't fill target results - should they be able to miss?
            miss_reason = SpellMissReason.MISS_REASON_NONE

        # Overwrite if evading.
        if target.is_evading:
            miss_reason = SpellMissReason.MISS_REASON_EVADED

        damage = self.calculate_spell_damage(damage,
                                             casting_spell.spell_entry.School,
                                             target,
                                             casting_spell.spell_attack_type)

        # TODO Handle misses, absorbs etc. for spells.
        damage_info = casting_spell.get_cast_damage_info(
            self, target, damage, 0)

        if miss_reason == SpellMissReason.MISS_REASON_EVADED:
            damage_info.total_damage = 0
            damage_info.hit_info = HitInfo.MISS
            damage_info.proc_victim |= ProcFlags.NONE

        if casting_spell.casts_on_swing(
        ) or casting_spell.is_ranged_weapon_attack(
        ):  # TODO Should other spells give skill too?
            self.handle_combat_skill_gain(damage_info)
            target.handle_combat_skill_gain(damage_info)

        self.send_spell_cast_debug_info(damage_info,
                                        miss_reason,
                                        casting_spell.spell_entry.ID,
                                        is_periodic=is_periodic)

        self.deal_damage(target, damage, is_periodic)

    def apply_spell_healing(self,
                            target,
                            healing,
                            casting_spell,
                            is_periodic=False):
        miss_info = casting_spell.object_target_results[target.guid].result
        damage_info = casting_spell.get_cast_damage_info(
            self, target, healing, 0)
        self.send_spell_cast_debug_info(damage_info,
                                        miss_info,
                                        casting_spell.spell_entry.ID,
                                        healing=True,
                                        is_periodic=is_periodic)
        target.receive_healing(healing, self)

    def send_spell_cast_debug_info(self,
                                   damage_info,
                                   miss_reason,
                                   spell_id,
                                   healing=False,
                                   is_periodic=False):
        flags = SpellHitFlags.HIT_FLAG_HEALED if healing else SpellHitFlags.HIT_FLAG_DAMAGE
        if is_periodic:  # Periodic damage/healing does not show in combat log - only on character frame.
            flags |= SpellHitFlags.HIT_FLAG_PERIODIC

        if miss_reason != SpellMissReason.MISS_REASON_NONE:
            combat_log_data = pack('<i2Q2i', flags, damage_info.attacker.guid,
                                   damage_info.target.guid, spell_id,
                                   miss_reason)
            combat_log_opcode = OpCode.SMSG_ATTACKERSTATEUPDATEDEBUGINFOSPELLMISS
        else:

            combat_log_data = pack(
                '<I2Q2If3I', flags, damage_info.attacker.guid,
                damage_info.target.guid, spell_id, damage_info.total_damage,
                damage_info.damage, damage_info.damage_school_mask,
                damage_info.damage, damage_info.absorb)
            combat_log_opcode = OpCode.SMSG_ATTACKERSTATEUPDATEDEBUGINFOSPELL

        MapManager.send_surrounding(
            PacketWriter.get_packet(combat_log_opcode, combat_log_data),
            self,
            include_self=self.get_type_id() == ObjectTypeIds.ID_PLAYER)

        if not healing:
            damage_data = pack('<Q2i2IQ', damage_info.target.guid,
                               damage_info.total_damage, damage_info.damage,
                               miss_reason, spell_id,
                               damage_info.attacker.guid)
            MapManager.send_surrounding(
                PacketWriter.get_packet(OpCode.SMSG_DAMAGE_DONE, damage_data),
                self,
                include_self=self.get_type_id() == ObjectTypeIds.ID_PLAYER)

    def set_current_target(self, guid):
        self.current_target = guid
        self.set_uint64(UnitFields.UNIT_FIELD_TARGET, guid)

    # Implemented by PlayerManager
    def send_attack_swing_not_in_range(self, victim):
        pass

    # Implemented by PlayerManager
    def send_attack_swing_facing_wrong_way(self, victim):
        pass

    # Implemented by PlayerManager
    def send_attack_swing_cant_attack(self, victim):
        pass

    # Implemented by PlayerManager
    def send_attack_swing_dead_target(self, victim):
        pass

    # Implemented by PlayerManager
    def send_attack_swing_not_standing(self, victim):
        pass

    # Implemented by PlayerManager and CreatureManager
    def has_offhand_weapon(self):
        return False

    # Implemented by PlayerManager and CreatureManager
    def has_ranged_weapon(self):
        return False

    # Location is used by PlayerManager if provided.
    # According to 1.3.0 notes, creatures were able to block/parry from behind.
    def can_block(self, attacker_location=None):
        return self.has_block_passive  # TODO Stunned/casting checks

    def can_parry(self, attacker_location=None):
        return self.has_parry_passive  # TODO Stunned/casting checks

    def can_dodge(self, attacker_location=None):
        return self.has_dodge_passive  # TODO Stunned/casting checks

    def enter_combat(self):
        self.in_combat = True
        self.unit_flags |= UnitFlags.UNIT_FLAG_IN_COMBAT
        self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

    def leave_combat(self, force=False):
        if not self.in_combat and not force:
            return

        # Remove self from attacker list of attackers
        for victim in list(self.attackers.values()):
            if self.guid in victim.attackers:
                victim.attackers.pop(self.guid)
        self.attackers.clear()

        self.send_attack_stop(
            self.combat_target.guid if self.combat_target else self.guid)
        self.swing_error = 0

        self.evading_waypoints.clear()
        self.combat_target = None
        self.in_combat = False
        self.unit_flags &= ~UnitFlags.UNIT_FLAG_IN_COMBAT
        self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

    def can_use_attack_type(self, attack_type):
        if attack_type == AttackTypes.BASE_ATTACK:
            return self.disarmed_mainhand
        if attack_type == AttackTypes.OFFHAND_ATTACK:
            return self.disarmed_offhand
        return True

    def is_attack_ready(self, attack_type):
        return self.attack_timers[attack_type] <= 0

    def update_attack_time(self, attack_type, value):
        new_value = self.attack_timers[attack_type] - value
        if new_value < 0:
            new_value = 0
        if not self.is_attack_ready(attack_type):
            self.set_attack_timer(attack_type, new_value)

    def set_attack_timer(self, attack_type, value):
        self.attack_timers[attack_type] = value

    def is_sitting(self):
        return self.stand_state == StandState.UNIT_SITTING

    def set_stand_state(self, stand_state):
        if stand_state == self.stand_state:
            return

        self.stand_state = stand_state
        self.aura_manager.check_aura_interrupts(changed_stand_state=True)

    def is_stealthed(self):
        return self.unit_flags & UnitFlags.UNIT_FLAG_SNEAK == UnitFlags.UNIT_FLAG_SNEAK

    def set_stealthed(self, stealthed):
        if stealthed:
            self.unit_flags |= UnitFlags.UNIT_FLAG_SNEAK
        else:
            self.unit_flags &= ~UnitFlags.UNIT_FLAG_SNEAK
        self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

    # override
    def change_speed(self, speed=0):
        # Assign new base speed.
        self.stat_manager.base_stats[
            UnitStats.
            SPEED_RUNNING] = speed if speed > 0 else config.Unit.Defaults.run_speed
        # Get new total speed.
        speed = self.stat_manager.get_total_stat(UnitStats.SPEED_RUNNING)
        # Limit to 0-56 and assign object field.
        return super().change_speed(speed)

    def set_root(self, active):
        if active:
            # Stop movement if the unit has pending waypoints.
            if len(self.movement_manager.pending_waypoints) > 0:
                self.movement_manager.send_move_stop()

            self.movement_flags |= MoveFlags.MOVEFLAG_ROOTED
            self.unit_state |= UnitStates.ROOTED
        else:
            self.movement_flags &= ~MoveFlags.MOVEFLAG_ROOTED
            self.unit_state &= ~UnitStates.ROOTED

    def play_emote(self, emote):
        if emote != 0:
            data = pack('<IQ', emote, self.guid)
            MapManager.send_surrounding_in_range(
                PacketWriter.get_packet(OpCode.SMSG_EMOTE, data), self,
                config.World.Chat.ChatRange.emote_range)

    def summon_mount(self, creature_entry):
        creature_template = WorldDatabaseManager.creature_get_by_entry(
            creature_entry)
        if not creature_template:
            return False

        self.mount(creature_template.display_id1)
        return True

    def mount(self, mount_display_id):
        if mount_display_id > 0 and DbcDatabaseManager.CreatureDisplayInfoHolder.creature_display_info_get_by_id(
                mount_display_id):
            self.mount_display_id = mount_display_id
            self.unit_flags |= UnitFlags.UNIT_MASK_MOUNTED
            self.set_uint32(UnitFields.UNIT_FIELD_MOUNTDISPLAYID,
                            self.mount_display_id)
            self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

    def unmount(self):
        self.mount_display_id = 0
        self.unit_flags &= ~UnitFlags.UNIT_MASK_MOUNTED
        self.set_uint32(UnitFields.UNIT_FIELD_MOUNTDISPLAYID,
                        self.mount_display_id)
        self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

    # Implemented by Creature/PlayerManager.
    def update_power_type(self):
        pass

    def get_power_type_value(self, power_type=-1):
        if power_type == -1:
            power_type = self.power_type

        if power_type == PowerTypes.TYPE_MANA:
            return self.power_1
        elif power_type == PowerTypes.TYPE_RAGE:
            return self.power_2
        elif power_type == PowerTypes.TYPE_FOCUS:
            return self.power_3
        elif power_type == PowerTypes.TYPE_ENERGY:
            return self.power_4
        return 0

    def get_max_power_value(self):
        if self.power_type == PowerTypes.TYPE_MANA:
            return self.max_power_1
        elif self.power_type == PowerTypes.TYPE_RAGE:
            return self.max_power_2
        elif self.power_type == PowerTypes.TYPE_FOCUS:
            return self.max_power_3
        elif self.power_type == PowerTypes.TYPE_ENERGY:
            return self.max_power_4
        return 0

    def recharge_power(self):
        max_power = self.get_max_power_value()
        if self.power_type == PowerTypes.TYPE_MANA:
            self.set_mana(max_power)
        elif self.power_type == PowerTypes.TYPE_RAGE:
            self.set_rage(max_power)
        elif self.power_type == PowerTypes.TYPE_FOCUS:
            self.set_focus(max_power)
        elif self.power_type == PowerTypes.TYPE_ENERGY:
            self.set_energy(max_power)

    def set_health(self, health):
        if health < 0:
            health = 0
        self.health = min(health, self.max_health)
        self.set_uint32(UnitFields.UNIT_FIELD_HEALTH, self.health)

    def set_max_health(self, health):
        self.max_health = health
        self.set_uint32(UnitFields.UNIT_FIELD_MAXHEALTH, health)

    def set_mana(self, mana):
        if mana < 0:
            mana = 0
        self.power_1 = min(mana, self.max_power_1)
        self.set_uint32(UnitFields.UNIT_FIELD_POWER1, self.power_1)

    def set_rage(self, rage):
        if rage < 0:
            rage = 0
        self.power_2 = min(rage, self.max_power_2)
        self.set_uint32(UnitFields.UNIT_FIELD_POWER2, self.power_2)

    def set_focus(self, focus):
        if focus < 0:
            focus = 0
        self.power_3 = min(focus, self.max_power_3)
        self.set_uint32(UnitFields.UNIT_FIELD_POWER3, self.power_3)

    def set_energy(self, energy):
        if energy < 0:
            energy = 0
        self.power_4 = min(energy, self.max_power_4)
        self.set_uint32(UnitFields.UNIT_FIELD_POWER4, self.power_4)

    def set_max_mana(self, mana):
        self.max_power_1 = mana
        self.set_uint32(UnitFields.UNIT_FIELD_MAXPOWER1, mana)

    def set_armor(self, total_armor, item_armor):
        self.resistance_0 = total_armor
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES, self.resistance_0)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEITEMMODS, item_armor)

    def set_holy_res(self, total_holy_res, item_holy_res):
        self.resistance_1 = total_holy_res
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 1,
                       self.resistance_1)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEITEMMODS + 1,
                       item_holy_res)

    def set_fire_res(self, total_fire_res, item_fire_res):
        self.resistance_2 = total_fire_res
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 2,
                       self.resistance_2)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEITEMMODS + 2,
                       item_fire_res)

    def set_nature_res(self, total_nature_res, item_nature_res):
        self.resistance_3 = total_nature_res
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 3,
                       self.resistance_3)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEITEMMODS + 3,
                       item_nature_res)

    def set_frost_res(self, total_frost_res, item_frost_res):
        self.resistance_4 = total_frost_res
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 4,
                       self.resistance_4)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEITEMMODS + 4,
                       item_frost_res)

    def set_shadow_res(self, total_shadow_res, item_shadow_res):
        self.resistance_5 = total_shadow_res
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 5,
                       self.resistance_5)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEITEMMODS + 5,
                       item_shadow_res)

    def set_bonus_armor(self, negative_mods, positive_mods):
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE,
                       positive_mods)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE,
                       negative_mods)

    def set_bonus_holy_res(self, negative_mods, positive_mods):
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE + 1,
                       positive_mods)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE + 1,
                       negative_mods)

    def set_bonus_fire_res(self, negative_mods, positive_mods):
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE + 2,
                       positive_mods)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE + 2,
                       negative_mods)

    def set_bonus_nature_res(self, negative_mods, positive_mods):
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE + 3,
                       positive_mods)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE + 3,
                       negative_mods)

    def set_bonus_frost_res(self, negative_mods, positive_mods):
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE + 4,
                       positive_mods)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE + 4,
                       negative_mods)

    def set_bonus_shadow_res(self, negative_mods, positive_mods):
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSPOSITIVE + 5,
                       positive_mods)
        self.set_int32(UnitFields.UNIT_FIELD_RESISTANCEBUFFMODSNEGATIVE + 5,
                       negative_mods)

    def set_bonus_damage_done_for_school(self, value, school):  # TODO Fields
        self.set_int32(UnitFields.UNIT_FIELD_MOD_DAMAGE_DONE + school, value)

    def set_melee_damage(self, min_dmg, max_dmg):
        damages = ByteUtils.shorts_to_int(max_dmg, min_dmg)
        self.damage = damages
        self.set_uint32(UnitFields.UNIT_FIELD_DAMAGE, damages)

    def set_melee_attack_time(self, attack_time):
        self.base_attack_time = attack_time
        self.set_uint32(UnitFields.UNIT_FIELD_BASEATTACKTIME, attack_time)

    def set_offhand_attack_time(self, attack_time):
        self.offhand_attack_time = attack_time
        self.set_uint32(UnitFields.UNIT_FIELD_BASEATTACKTIME + 1, attack_time)

    def set_weapon_mode(self, weapon_mode):
        self.sheath_state = weapon_mode

        # TODO: Implement temp enchants updates.
        if WeaponMode.NORMALMODE:
            # Update main hand temp enchants
            # Update off hand temp enchants
            pass
        elif WeaponMode.RANGEDMODE:
            # Update ranged temp enchants
            pass

    def set_shapeshift_form(self, shapeshift_form):
        self.shapeshift_form = shapeshift_form

    def has_form(self, shapeshift_form):
        return self.shapeshift_form == shapeshift_form

    def is_in_feral_form(self):
        return self.has_form(
            ShapeshiftForms.SHAPESHIFT_FORM_BEAR) or self.has_form(
                ShapeshiftForms.SHAPESHIFT_FORM_CAT)

    # Implemented by PlayerManager
    def add_combo_points_on_target(self, target, combo_points):
        pass

    # Implemented by PlayerManager
    def remove_combo_points(self):
        pass

    def set_taxi_flying_state(self, is_flying, mount_display_id=0):
        if is_flying:
            self.mount(mount_display_id)
            self.unit_flags |= (UnitFlags.UNIT_FLAG_FROZEN
                                | UnitFlags.UNIT_FLAG_TAXI_FLIGHT)
        else:
            self.unmount()
            self.unit_flags &= ~(UnitFlags.UNIT_FLAG_FROZEN
                                 | UnitFlags.UNIT_FLAG_TAXI_FLIGHT)

        self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

    # override
    def set_display_id(self, display_id):
        super().set_display_id(display_id)
        if display_id <= 0 or not \
                DbcDatabaseManager.CreatureDisplayInfoHolder.creature_display_info_get_by_id(display_id):
            return False

        self.set_uint32(UnitFields.UNIT_FIELD_DISPLAYID,
                        self.current_display_id)
        return True

    def set_channel_object(self, guid):
        self.channel_object = guid
        self.set_uint64(UnitFields.UNIT_FIELD_CHANNEL_OBJECT, guid)

    def set_channel_spell(self, spell_id):
        self.channel_spell = spell_id
        self.set_uint64(UnitFields.UNIT_CHANNEL_SPELL, spell_id)

    def die(self, killer=None):
        if not self.is_alive:
            return False
        self.is_alive = False

        # Reset movement and unit state flags.
        self.movement_flags = MoveFlags.MOVEFLAG_NONE
        self.unit_state = UnitStates.NONE

        # Stop movement on death.
        if len(self.movement_manager.pending_waypoints) > 0:
            self.movement_manager.send_move_stop()

        self.set_health(0)
        self.set_stand_state(StandState.UNIT_DEAD)

        self.unit_flags = UnitFlags.UNIT_MASK_DEAD
        self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

        self.dynamic_flags |= UnitDynamicTypes.UNIT_DYNAMIC_DEAD
        self.set_uint32(UnitFields.UNIT_DYNAMIC_FLAGS, self.dynamic_flags)

        if killer and killer.get_type_id() == ObjectTypeIds.ID_PLAYER:
            if killer.current_selection == self.guid:
                killer.set_current_selection(killer.guid)

            # Clear combo of killer if this unit was the target
            if killer.combo_target == self.guid:
                killer.remove_combo_points()

        if killer and killer.object_type_mask & ObjectTypeFlags.TYPE_UNIT:
            killer.spell_manager.remove_unit_from_all_cast_targets(
                self.guid)  # Interrupt casting on target death
            killer.aura_manager.check_aura_procs(killed_unit=True)

        self.spell_manager.remove_all_casts()
        self.aura_manager.handle_death()

        self.leave_combat()
        return True

    def respawn(self):
        super().respawn()

        # Force leave combat just in case.
        self.leave_combat(force=True)
        self.set_current_target(0)
        self.is_alive = True

        self.unit_flags = UnitFlags.UNIT_FLAG_STANDARD
        self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags)

        self.dynamic_flags = UnitDynamicTypes.UNIT_DYNAMIC_NONE
        self.set_uint32(UnitFields.UNIT_DYNAMIC_FLAGS, self.dynamic_flags)

        self.set_stand_state(StandState.UNIT_STANDING)

    # Implemented by CreatureManager and PlayerManager
    def get_bytes_0(self):
        pass

    # Implemented by CreatureManager and PlayerManager
    def get_bytes_1(self):
        pass

    # Implemented by CreatureManager and PlayerManager
    def get_bytes_2(self):
        pass

    # Implemented by CreatureManager and PlayerManager
    def get_damages(self):
        pass

    # override
    def on_cell_change(self):
        pass

    # override
    def get_type_id(self):
        return ObjectTypeIds.ID_UNIT

    # override
    def generate_object_guid(self, low_guid):
        return low_guid | HighGuid.HIGHGUID_UNIT
Beispiel #9
0
class GameObjectManager(ObjectManager):
    CURRENT_HIGHEST_GUID = 0

    def __init__(self,
                 gobject_template,
                 gobject_instance=None,
                 is_summon=False,
                 **kwargs):
        super().__init__(**kwargs)

        self.gobject_template = gobject_template
        self.gobject_instance = gobject_instance
        self.is_summon = is_summon

        self.entry = self.gobject_template.entry
        self.native_display_id = self.gobject_template.display_id
        self.current_display_id = self.native_display_id
        self.native_scale = self.gobject_template.scale
        self.current_scale = self.native_scale
        self.faction = self.gobject_template.faction

        if gobject_instance:
            if GameObjectManager.CURRENT_HIGHEST_GUID < gobject_instance.spawn_id:
                GameObjectManager.CURRENT_HIGHEST_GUID = gobject_instance.spawn_id

            self.guid = self.generate_object_guid(gobject_instance.spawn_id)
            self.state = self.gobject_instance.spawn_state
            self.location.x = self.gobject_instance.spawn_positionX
            self.location.y = self.gobject_instance.spawn_positionY
            self.location.z = self.gobject_instance.spawn_positionZ
            self.location.o = self.gobject_instance.spawn_orientation
            self.map_ = self.gobject_instance.spawn_map
            self.respawn_time = randint(
                self.gobject_instance.spawn_spawntimemin,
                self.gobject_instance.spawn_spawntimemax)

        self.object_type_mask |= ObjectTypeFlags.TYPE_GAMEOBJECT
        self.update_packet_factory.init_values(GameObjectFields.GAMEOBJECT_END)

        self.respawn_timer = 0
        self.loot_manager = None

        from game.world.managers.objects.spell.SpellManager import SpellManager  # Local due to circular imports.
        self.spell_manager = SpellManager(self)

        # Chest only initializations.
        if self.gobject_template.type == GameObjectTypes.TYPE_CHEST:
            self.loot_manager = GameObjectLootManager(self)

        # Ritual initializations.
        if self.gobject_template.type == GameObjectTypes.TYPE_RITUAL:
            self.ritual_caster = None
            self.ritual_participants = []

        # Trap collision initializations.
        if self.gobject_template.type == GameObjectTypes.TYPE_TRAP:
            self.trap_manager = TrapManager.generate(self)

    def load(self):
        MapManager.update_object(self)

    @staticmethod
    def spawn(entry, location, map_id, override_faction=0, despawn_time=1):
        go_template = WorldDatabaseManager.gameobject_template_get_by_entry(
            entry)

        if not go_template:
            return None

        instance = SpawnsGameobjects()
        instance.spawn_id = GameObjectManager.CURRENT_HIGHEST_GUID + 1
        instance.spawn_entry = entry
        instance.spawn_map = map_id
        instance.spawn_rotation0 = 0
        instance.spawn_rotation2 = 0
        instance.spawn_rotation1 = 0
        instance.spawn_rotation3 = 0
        instance.spawn_positionX = location.x
        instance.spawn_positionY = location.y
        instance.spawn_positionZ = location.z
        instance.spawn_orientation = location.o
        if despawn_time < 1:
            despawn_time = 1
        instance.spawn_spawntimemin = despawn_time
        instance.spawn_spawntimemax = despawn_time
        instance.spawn_state = GameObjectStates.GO_STATE_READY

        gameobject = GameObjectManager(gobject_template=go_template,
                                       gobject_instance=instance,
                                       is_summon=True)
        if override_faction > 0:
            gameobject.faction = override_faction

        gameobject.load()
        return gameobject

    def _handle_use_door(self, player):
        # TODO: Check locks etc.
        self.set_active()

    def _handle_use_button(self, player):
        # TODO: Trigger scripts / events on cooldown restart.
        self.set_active()

    def _handle_use_camera(self, player):
        cinematic_id = self.gobject_template.data1
        if DbcDatabaseManager.cinematic_sequences_get_by_id(cinematic_id):
            data = pack('<I', cinematic_id)
            player.enqueue_packet(
                PacketWriter.get_packet(OpCode.SMSG_TRIGGER_CINEMATIC, data))

    def _handle_use_chair(self, player):
        slots = self.gobject_template.data0
        height = self.gobject_template.data1

        lowest_distance = 90.0
        x_lowest = self.location.x
        y_lowest = self.location.y

        if slots > 0:
            orthogonal_orientation = self.location.o + pi * 0.5
            for x in range(0, slots):
                relative_distance = (self.current_scale *
                                     x) - (self.current_scale *
                                           (slots - 1) / 2.0)
                x_i = self.location.x + relative_distance * cos(
                    orthogonal_orientation)
                y_i = self.location.y + relative_distance * sin(
                    orthogonal_orientation)

                player_slot_distance = player.location.distance(
                    Vector(x_i, y_i, player.location.z))
                if player_slot_distance <= lowest_distance:
                    lowest_distance = player_slot_distance
                    x_lowest = x_i
                    y_lowest = y_i
            player.teleport(player.map_,
                            Vector(x_lowest, y_lowest, self.location.z,
                                   self.location.o),
                            is_instant=True)
            player.set_stand_state(StandState.UNIT_SITTINGCHAIRLOW.value +
                                   height)

    # noinspection PyMethodMayBeStatic
    def _handle_use_quest_giver(self, player, target):
        if target:
            player.quest_manager.handle_quest_giver_hello(target, target.guid)

    def _handle_use_chest(self, player):
        # Activate chest open animation, while active, it won't let any other player loot.
        if self.state == GameObjectStates.GO_STATE_READY:
            self.set_state(GameObjectStates.GO_STATE_ACTIVE)

        # Generate loot if it's empty.
        if not self.loot_manager.has_loot():
            self.loot_manager.generate_loot(player)

        player.send_loot(self)

    def _handle_use_ritual(self, player):
        # Participant group limitations.
        if not self.ritual_caster.group_manager or not self.ritual_caster.group_manager.is_party_member(
                player.guid):
            return

        ritual_channel_spell_id = self.gobject_template.data2
        if player is self.ritual_caster or player in self.ritual_participants:
            return  # No action needed for this player.

        # Make the player channel for summoning.
        channel_spell_entry = DbcDatabaseManager.SpellHolder.spell_get_by_id(
            ritual_channel_spell_id)
        spell = player.spell_manager.try_initialize_spell(
            channel_spell_entry,
            self,
            SpellTargetMask.GAMEOBJECT,
            validate=False)

        # Note: these triggered casts will skip the actual effects of the summon spell, only starting the channel.
        player.spell_manager.remove_colliding_casts(spell)
        player.spell_manager.casting_spells.append(spell)
        player.spell_manager.handle_channel_start(spell)
        self.ritual_participants.append(player)

        # Check if the ritual can be completed with the current participants.
        required_participants = self.gobject_template.data0 - 1  # -1 to include caster.
        if len(self.ritual_participants) >= required_participants:
            ritual_finish_spell_id = self.gobject_template.data1

            # Cast the finishing spell.
            spell_entry = DbcDatabaseManager.SpellHolder.spell_get_by_id(
                ritual_finish_spell_id)
            spell_cast = self.ritual_caster.spell_manager.try_initialize_spell(
                spell_entry,
                self.ritual_caster,
                SpellTargetMask.SELF,
                triggered=True,
                validate=False)
            if spell_cast:
                self.ritual_caster.spell_manager.start_spell_cast(
                    initialized_spell=spell_cast)
            else:
                self.ritual_caster.spell_manager.remove_cast_by_id(
                    ritual_channel_spell_id
                )  # Interrupt ritual channel if the summon fails.

    def apply_spell_damage(self,
                           target,
                           damage,
                           casting_spell,
                           is_periodic=False):
        damage_info = casting_spell.get_cast_damage_info(
            self, target, damage, 0)
        miss_info = casting_spell.object_target_results[target.guid].result

        target.send_spell_cast_debug_info(damage_info,
                                          miss_info,
                                          casting_spell.spell_entry.ID,
                                          is_periodic=is_periodic)
        target.receive_damage(damage, self, is_periodic)

        # Send environmental damage log packet to the affected player.
        if self.gobject_template.type == GameObjectTypes.TYPE_TRAP and target.get_type_id(
        ) == ObjectTypeIds.ID_PLAYER:
            data = pack('<Q2I', target.guid, casting_spell.spell_entry.School,
                        damage)
            target.enqueue_packet(
                PacketWriter.get_packet(OpCode.SMSG_ENVIRONMENTALDAMAGELOG,
                                        data))

    def apply_spell_healing(self,
                            target,
                            healing,
                            casting_spell,
                            is_periodic=False):
        miss_info = casting_spell.object_target_results[target.guid].result
        damage_info = casting_spell.get_cast_damage_info(
            self, target, healing, 0)

        target.send_spell_cast_debug_info(damage_info,
                                          miss_info,
                                          casting_spell.spell_entry.ID,
                                          healing=True,
                                          is_periodic=is_periodic)
        target.receive_healing(healing, self)

    def _handle_use_goober(self, player):
        pass

    def use(self, player, target=None):
        if self.gobject_template.type == GameObjectTypes.TYPE_DOOR:
            self._handle_use_door(player)
        if self.gobject_template.type == GameObjectTypes.TYPE_BUTTON:
            self._handle_use_button(player)
        elif self.gobject_template.type == GameObjectTypes.TYPE_CAMERA:
            self._handle_use_camera(player)
        elif self.gobject_template.type == GameObjectTypes.TYPE_CHAIR:
            self._handle_use_chair(player)
        elif self.gobject_template.type == GameObjectTypes.TYPE_CHEST:
            self._handle_use_chest(player)
        elif self.gobject_template.type == GameObjectTypes.TYPE_RITUAL:
            self._handle_use_ritual(player)
        elif self.gobject_template.type == GameObjectTypes.TYPE_GOOBER:
            self._handle_use_goober(player)
        elif self.gobject_template.type == GameObjectTypes.TYPE_QUESTGIVER:
            self._handle_use_quest_giver(player, target)

    def set_state(self, state):
        self.state = state
        self.set_uint32(GameObjectFields.GAMEOBJECT_STATE, self.state)

    def set_active(self):
        if self.state == GameObjectStates.GO_STATE_READY:
            self.set_state(GameObjectStates.GO_STATE_ACTIVE)
            return True
        return False

    def set_ready(self):
        if self.state != GameObjectStates.GO_STATE_READY:
            self.set_state(GameObjectStates.GO_STATE_READY)
            return True
        return False

    # override
    def set_display_id(self, display_id):
        super().set_display_id(display_id)
        if display_id <= 0 or not \
                DbcDatabaseManager.gameobject_display_info_get_by_id(display_id):
            return False

        self.set_uint32(GameObjectFields.GAMEOBJECT_DISPLAYID,
                        self.current_display_id)
        return True

    # override
    def _get_fields_update(self, requester):
        data = pack('<B', self.update_packet_factory.update_mask.block_count)

        # Use a temporary bit mask in case we need to set more bits.
        temporal_mask = self.update_packet_factory.update_mask.copy()
        fields_data = b''
        for index in range(0,
                           self.update_packet_factory.update_mask.field_count):
            if self.is_dynamic_field(index):
                fields_data += self.generate_dynamic_field_value(requester)
                temporal_mask[index] = 1
            elif self.update_packet_factory.update_mask.is_set(index):
                fields_data += self.update_packet_factory.update_values[index]

        data += temporal_mask.tobytes()
        data += fields_data

        return data

    def is_dynamic_field(self, index):
        # TODO: Check more fields?
        return index == GameObjectFields.GAMEOBJECT_DYN_FLAGS

    def generate_dynamic_field_value(self, requester):
        # TODO: Handle more dynamic cases.
        # QUESTGIVERS and CHESTS (This includes other interactive game objects).
        if self.gobject_template.type == GameObjectTypes.TYPE_CHEST or \
                self.gobject_template.type == GameObjectTypes.TYPE_QUESTGIVER:
            if requester.quest_manager.should_interact_with_go(self):
                return pack('<I', 1)
        return pack('<I', 0)

    # override
    def get_full_update_packet(self, requester):
        if self.gobject_template and self.gobject_instance:
            # Object fields
            self.set_uint64(ObjectFields.OBJECT_FIELD_GUID, self.guid)
            self.set_uint32(ObjectFields.OBJECT_FIELD_TYPE,
                            self.object_type_mask)
            self.set_uint32(ObjectFields.OBJECT_FIELD_ENTRY, self.entry)
            self.set_float(ObjectFields.OBJECT_FIELD_SCALE_X,
                           self.current_scale)
            self.set_uint32(ObjectFields.OBJECT_FIELD_PADDING, 0)

            # Gameobject fields
            self.set_uint32(GameObjectFields.GAMEOBJECT_DISPLAYID,
                            self.current_display_id)
            self.set_uint32(GameObjectFields.GAMEOBJECT_FLAGS,
                            self.gobject_template.flags)
            self.set_uint32(GameObjectFields.GAMEOBJECT_FACTION, self.faction)
            self.set_uint32(GameObjectFields.GAMEOBJECT_STATE, self.state)
            self.set_float(GameObjectFields.GAMEOBJECT_ROTATION,
                           self.gobject_instance.spawn_rotation0)
            self.set_float(GameObjectFields.GAMEOBJECT_ROTATION + 1,
                           self.gobject_instance.spawn_rotation1)

            if self.gobject_instance.spawn_rotation2 == 0 and self.gobject_instance.spawn_rotation3 == 0:
                f_rot1 = math.sin(self.location.o / 2.0)
                f_rot2 = math.cos(self.location.o / 2.0)
            else:
                f_rot1 = self.gobject_instance.spawn_rotation2
                f_rot2 = self.gobject_instance.spawn_rotation3

            self.set_float(GameObjectFields.GAMEOBJECT_ROTATION + 2, f_rot1)
            self.set_float(GameObjectFields.GAMEOBJECT_ROTATION + 3, f_rot2)
            self.set_float(GameObjectFields.GAMEOBJECT_POS_X, self.location.x)
            self.set_float(GameObjectFields.GAMEOBJECT_POS_Y, self.location.y)
            self.set_float(GameObjectFields.GAMEOBJECT_POS_Z, self.location.z)
            self.set_float(GameObjectFields.GAMEOBJECT_FACING, self.location.o)

            return self.get_object_create_packet(requester)

    def query_details(self):
        name_bytes = PacketWriter.string_to_bytes(self.gobject_template.name)
        data = pack(f'<3I{len(name_bytes)}ssss10I',
                    self.gobject_template.entry, self.gobject_template.type,
                    self.current_display_id, name_bytes, b'\x00', b'\x00',
                    b'\x00', self.gobject_template.data0,
                    self.gobject_template.data1, self.gobject_template.data2,
                    self.gobject_template.data3, self.gobject_template.data4,
                    self.gobject_template.data5, self.gobject_template.data6,
                    self.gobject_template.data7, self.gobject_template.data8,
                    self.gobject_template.data9)
        return PacketWriter.get_packet(OpCode.SMSG_GAMEOBJECT_QUERY_RESPONSE,
                                       data)

    # override
    def respawn(self):
        # Set properties before making it visible.
        self.state = GameObjectStates.GO_STATE_READY
        self.respawn_timer = 0
        self.respawn_time = randint(self.gobject_instance.spawn_spawntimemin,
                                    self.gobject_instance.spawn_spawntimemin)

        if self.gobject_template.type == GameObjectTypes.TYPE_TRAP:
            self.trap_manager.reset()

        MapManager.respawn_object(self)

    # override
    def update(self, now):
        if now > self.last_tick > 0:
            elapsed = now - self.last_tick

            if self.is_spawned:
                # Logic for Trap GameObjects (type 6).
                if self.gobject_template.type == GameObjectTypes.TYPE_TRAP:
                    self.trap_manager.update(elapsed)

                # Check if this game object should be updated yet or not.
                if self.has_pending_updates():
                    MapManager.update_object(self, check_pending_changes=True)
                    self.reset_fields_older_than(now)

                # SpellManager update.
                self.spell_manager.update(now)
            # Not spawned.
            else:
                self.respawn_timer += elapsed
                if self.respawn_timer >= self.respawn_time:
                    if self.is_summon:
                        self.despawn(destroy=True)
                    else:
                        self.respawn()

        self.last_tick = now

    # override
    def on_cell_change(self):
        pass

    # override
    def get_type_id(self):
        return ObjectTypeIds.ID_GAMEOBJECT

    # override
    def generate_object_guid(self, low_guid):
        return low_guid | HighGuid.HIGHGUID_GAMEOBJECT