693 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			693 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# pylint: disable=W0212
 | 
						|
from __future__ import annotations
 | 
						|
 | 
						|
import math
 | 
						|
from dataclasses import dataclass
 | 
						|
from functools import cached_property
 | 
						|
from typing import TYPE_CHECKING, Any, List, Optional, Set, Tuple, Union
 | 
						|
 | 
						|
from .cache import CacheDict
 | 
						|
from .constants import (
 | 
						|
    CAN_BE_ATTACKED,
 | 
						|
    IS_ARMORED,
 | 
						|
    IS_BIOLOGICAL,
 | 
						|
    IS_CLOAKED,
 | 
						|
    IS_ENEMY,
 | 
						|
    IS_LIGHT,
 | 
						|
    IS_MASSIVE,
 | 
						|
    IS_MECHANICAL,
 | 
						|
    IS_MINE,
 | 
						|
    IS_PLACEHOLDER,
 | 
						|
    IS_PSIONIC,
 | 
						|
    IS_REVEALED,
 | 
						|
    IS_SNAPSHOT,
 | 
						|
    IS_STRUCTURE,
 | 
						|
    IS_VISIBLE,
 | 
						|
)
 | 
						|
from .data import Alliance, Attribute, CloakState, Race
 | 
						|
from .position import Point2, Point3
 | 
						|
 | 
						|
if TYPE_CHECKING:
 | 
						|
    from .bot_ai import BotAI
 | 
						|
    from .game_data import AbilityData, UnitTypeData
 | 
						|
 | 
						|
 | 
						|
@dataclass
 | 
						|
class RallyTarget:
 | 
						|
    point: Point2
 | 
						|
    tag: Optional[int] = None
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def from_proto(cls, proto: Any) -> RallyTarget:
 | 
						|
        return cls(
 | 
						|
            Point2.from_proto(proto.point),
 | 
						|
            proto.tag if proto.HasField("tag") else None,
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
@dataclass
 | 
						|
class UnitOrder:
 | 
						|
    ability: AbilityData  # TODO: Should this be AbilityId instead?
 | 
						|
    target: Optional[Union[int, Point2]] = None
 | 
						|
    progress: float = 0
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def from_proto(cls, proto: Any, bot_object: BotAI) -> UnitOrder:
 | 
						|
        target: Optional[Union[int, Point2]] = proto.target_unit_tag
 | 
						|
        if proto.HasField("target_world_space_pos"):
 | 
						|
            target = Point2.from_proto(proto.target_world_space_pos)
 | 
						|
        elif proto.HasField("target_unit_tag"):
 | 
						|
            target = proto.target_unit_tag
 | 
						|
        return cls(
 | 
						|
            ability=bot_object.game_data.abilities[proto.ability_id],
 | 
						|
            target=target,
 | 
						|
            progress=proto.progress,
 | 
						|
        )
 | 
						|
 | 
						|
    def __repr__(self) -> str:
 | 
						|
        return f"UnitOrder({self.ability}, {self.target}, {self.progress})"
 | 
						|
 | 
						|
 | 
						|
# pylint: disable=R0904
 | 
						|
class Unit:
 | 
						|
    class_cache = CacheDict()
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        proto_data,
 | 
						|
        bot_object: BotAI,
 | 
						|
        distance_calculation_index: int = -1,
 | 
						|
        base_build: int = -1,
 | 
						|
    ):
 | 
						|
        """
 | 
						|
        :param proto_data:
 | 
						|
        :param bot_object:
 | 
						|
        :param distance_calculation_index:
 | 
						|
        :param base_build:
 | 
						|
        """
 | 
						|
        self._proto = proto_data
 | 
						|
        self._bot_object: BotAI = bot_object
 | 
						|
        self.game_loop: int = bot_object.state.game_loop
 | 
						|
        self.base_build = base_build
 | 
						|
        # Index used in the 2D numpy array to access the 2D distance between two units
 | 
						|
        self.distance_calculation_index: int = distance_calculation_index
 | 
						|
 | 
						|
    def __repr__(self) -> str:
 | 
						|
        """ Returns string of this form: Unit(name='SCV', tag=4396941328). """
 | 
						|
        return f"Unit(name={self.name !r}, tag={self.tag})"
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def _type_data(self) -> UnitTypeData:
 | 
						|
        """ Provides the unit type data. """
 | 
						|
        return self._bot_object.game_data.units[self._proto.unit_type]
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def _creation_ability(self) -> AbilityData:
 | 
						|
        """ Provides the AbilityData of the creation ability of this unit. """
 | 
						|
        return self._type_data.creation_ability
 | 
						|
 | 
						|
    @property
 | 
						|
    def name(self) -> str:
 | 
						|
        """ Returns the name of the unit. """
 | 
						|
        return self._type_data.name
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def race(self) -> Race:
 | 
						|
        """ Returns the race of the unit """
 | 
						|
        return Race(self._type_data._proto.race)
 | 
						|
 | 
						|
    @property
 | 
						|
    def tag(self) -> int:
 | 
						|
        """ Returns the unique tag of the unit. """
 | 
						|
        return self._proto.tag
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_structure(self) -> bool:
 | 
						|
        """ Checks if the unit is a structure. """
 | 
						|
        return IS_STRUCTURE in self._type_data.attributes
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_light(self) -> bool:
 | 
						|
        """ Checks if the unit has the 'light' attribute. """
 | 
						|
        return IS_LIGHT in self._type_data.attributes
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_armored(self) -> bool:
 | 
						|
        """ Checks if the unit has the 'armored' attribute. """
 | 
						|
        return IS_ARMORED in self._type_data.attributes
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_biological(self) -> bool:
 | 
						|
        """ Checks if the unit has the 'biological' attribute. """
 | 
						|
        return IS_BIOLOGICAL in self._type_data.attributes
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_mechanical(self) -> bool:
 | 
						|
        """ Checks if the unit has the 'mechanical' attribute. """
 | 
						|
        return IS_MECHANICAL in self._type_data.attributes
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_massive(self) -> bool:
 | 
						|
        """ Checks if the unit has the 'massive' attribute. """
 | 
						|
        return IS_MASSIVE in self._type_data.attributes
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_psionic(self) -> bool:
 | 
						|
        """ Checks if the unit has the 'psionic' attribute. """
 | 
						|
        return IS_PSIONIC in self._type_data.attributes
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def _weapons(self):
 | 
						|
        """ Returns the weapons of the unit. """
 | 
						|
        return self._type_data._proto.weapons
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def bonus_damage(self) -> Optional[Tuple[int, str]]:
 | 
						|
        """Returns a tuple of form '(bonus damage, armor type)' if unit does 'bonus damage' against 'armor type'.
 | 
						|
        Possible armor typs are: 'Light', 'Armored', 'Biological', 'Mechanical', 'Psionic', 'Massive', 'Structure'."""
 | 
						|
        # TODO: Consider units with ability attacks (Oracle, Baneling) or multiple attacks (Thor).
 | 
						|
        if self._weapons:
 | 
						|
            for weapon in self._weapons:
 | 
						|
                if weapon.damage_bonus:
 | 
						|
                    b = weapon.damage_bonus[0]
 | 
						|
                    return b.bonus, Attribute(b.attribute).name
 | 
						|
        return None
 | 
						|
 | 
						|
    @property
 | 
						|
    def armor(self) -> float:
 | 
						|
        """ Returns the armor of the unit. Does not include upgrades """
 | 
						|
        return self._type_data._proto.armor
 | 
						|
 | 
						|
    @property
 | 
						|
    def sight_range(self) -> float:
 | 
						|
        """ Returns the sight range of the unit. """
 | 
						|
        return self._type_data._proto.sight_range
 | 
						|
 | 
						|
    @property
 | 
						|
    def movement_speed(self) -> float:
 | 
						|
        """Returns the movement speed of the unit.
 | 
						|
        This is the unit movement speed on game speed 'normal'. To convert it to 'faster' movement speed, multiply it by a factor of '1.4'. E.g. reaper movement speed is listed here as 3.75, but should actually be 5.25.
 | 
						|
        Does not include upgrades or buffs."""
 | 
						|
        return self._type_data._proto.movement_speed
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_mineral_field(self) -> bool:
 | 
						|
        """ Checks if the unit is a mineral field. """
 | 
						|
        return self._type_data.has_minerals
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_vespene_geyser(self) -> bool:
 | 
						|
        """ Checks if the unit is a non-empty vespene geyser or gas extraction building. """
 | 
						|
        return self._type_data.has_vespene
 | 
						|
 | 
						|
    @property
 | 
						|
    def health(self) -> float:
 | 
						|
        """ Returns the health of the unit. Does not include shields. """
 | 
						|
        return self._proto.health
 | 
						|
 | 
						|
    @property
 | 
						|
    def health_max(self) -> float:
 | 
						|
        """ Returns the maximum health of the unit. Does not include shields. """
 | 
						|
        return self._proto.health_max
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def health_percentage(self) -> float:
 | 
						|
        """ Returns the percentage of health the unit has. Does not include shields. """
 | 
						|
        if not self._proto.health_max:
 | 
						|
            return 0
 | 
						|
        return self._proto.health / self._proto.health_max
 | 
						|
 | 
						|
    @property
 | 
						|
    def shield(self) -> float:
 | 
						|
        """ Returns the shield points the unit has. Returns 0 for non-protoss units. """
 | 
						|
        return self._proto.shield
 | 
						|
 | 
						|
    @property
 | 
						|
    def shield_max(self) -> float:
 | 
						|
        """ Returns the maximum shield points the unit can have. Returns 0 for non-protoss units. """
 | 
						|
        return self._proto.shield_max
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def shield_percentage(self) -> float:
 | 
						|
        """ Returns the percentage of shield points the unit has. Returns 0 for non-protoss units. """
 | 
						|
        if not self._proto.shield_max:
 | 
						|
            return 0
 | 
						|
        return self._proto.shield / self._proto.shield_max
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def shield_health_percentage(self) -> float:
 | 
						|
        """Returns the percentage of combined shield + hp points the unit has.
 | 
						|
        Also takes build progress into account."""
 | 
						|
        max_ = (self._proto.shield_max + self._proto.health_max) * self.build_progress
 | 
						|
        if max_ == 0:
 | 
						|
            return 0
 | 
						|
        return (self._proto.shield + self._proto.health) / max_
 | 
						|
 | 
						|
    @property
 | 
						|
    def energy(self) -> float:
 | 
						|
        """ Returns the amount of energy the unit has. Returns 0 for units without energy. """
 | 
						|
        return self._proto.energy
 | 
						|
 | 
						|
    @property
 | 
						|
    def energy_max(self) -> float:
 | 
						|
        """ Returns the maximum amount of energy the unit can have. Returns 0 for units without energy. """
 | 
						|
        return self._proto.energy_max
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def energy_percentage(self) -> float:
 | 
						|
        """ Returns the percentage of amount of energy the unit has. Returns 0 for units without energy. """
 | 
						|
        if not self._proto.energy_max:
 | 
						|
            return 0
 | 
						|
        return self._proto.energy / self._proto.energy_max
 | 
						|
 | 
						|
    @property
 | 
						|
    def age_in_frames(self) -> int:
 | 
						|
        """ Returns how old the unit object data is (in game frames). This age does not reflect the unit was created / trained / morphed! """
 | 
						|
        return self._bot_object.state.game_loop - self.game_loop
 | 
						|
 | 
						|
    @property
 | 
						|
    def age(self) -> float:
 | 
						|
        """ Returns how old the unit object data is (in game seconds). This age does not reflect when the unit was created / trained / morphed! """
 | 
						|
        return (self._bot_object.state.game_loop - self.game_loop) / 22.4
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_memory(self) -> bool:
 | 
						|
        """ Returns True if this Unit object is referenced from the future and is outdated. """
 | 
						|
        return self.game_loop != self._bot_object.state.game_loop
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def is_snapshot(self) -> bool:
 | 
						|
        """Checks if the unit is only available as a snapshot for the bot.
 | 
						|
        Enemy buildings that have been scouted and are in the fog of war or
 | 
						|
        attacking enemy units on higher, not visible ground appear this way."""
 | 
						|
        if self.base_build >= 82457:
 | 
						|
            return self._proto.display_type == IS_SNAPSHOT
 | 
						|
        # TODO: Fixed in version 5.0.4, remove if a new linux binary is released: https://github.com/Blizzard/s2client-proto/issues/167
 | 
						|
        position = self.position.rounded
 | 
						|
        return self._bot_object.state.visibility.data_numpy[position[1], position[0]] != 2
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def is_visible(self) -> bool:
 | 
						|
        """Checks if the unit is visible for the bot.
 | 
						|
        NOTE: This means the bot has vision of the position of the unit!
 | 
						|
        It does not give any information about the cloak status of the unit."""
 | 
						|
        if self.base_build >= 82457:
 | 
						|
            return self._proto.display_type == IS_VISIBLE
 | 
						|
        # TODO: Remove when a new linux binary (5.0.4 or newer) is released
 | 
						|
        return self._proto.display_type == IS_VISIBLE and not self.is_snapshot
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_placeholder(self) -> bool:
 | 
						|
        """Checks if the unit is a placerholder for the bot.
 | 
						|
        Raw information about placeholders:
 | 
						|
            display_type: Placeholder
 | 
						|
            alliance: Self
 | 
						|
            unit_type: 86
 | 
						|
            owner: 1
 | 
						|
            pos {
 | 
						|
              x: 29.5
 | 
						|
              y: 53.5
 | 
						|
              z: 7.98828125
 | 
						|
            }
 | 
						|
            radius: 2.75
 | 
						|
            is_on_screen: false
 | 
						|
        """
 | 
						|
        return self._proto.display_type == IS_PLACEHOLDER
 | 
						|
 | 
						|
    @property
 | 
						|
    def alliance(self) -> Alliance:
 | 
						|
        """ Returns the team the unit belongs to. """
 | 
						|
        return self._proto.alliance
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_mine(self) -> bool:
 | 
						|
        """ Checks if the unit is controlled by the bot. """
 | 
						|
        return self._proto.alliance == IS_MINE
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_enemy(self) -> bool:
 | 
						|
        """ Checks if the unit is hostile. """
 | 
						|
        return self._proto.alliance == IS_ENEMY
 | 
						|
 | 
						|
    @property
 | 
						|
    def owner_id(self) -> int:
 | 
						|
        """ Returns the owner of the unit. This is a value of 1 or 2 in a two player game. """
 | 
						|
        return self._proto.owner
 | 
						|
 | 
						|
    @property
 | 
						|
    def position_tuple(self) -> Tuple[float, float]:
 | 
						|
        """ Returns the 2d position of the unit as tuple without conversion to Point2. """
 | 
						|
        return self._proto.pos.x, self._proto.pos.y
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def position(self) -> Point2:
 | 
						|
        """ Returns the 2d position of the unit. """
 | 
						|
        return Point2.from_proto(self._proto.pos)
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def position3d(self) -> Point3:
 | 
						|
        """ Returns the 3d position of the unit. """
 | 
						|
        return Point3.from_proto(self._proto.pos)
 | 
						|
 | 
						|
    def distance_to(self, p: Union[Unit, Point2]) -> float:
 | 
						|
        """Using the 2d distance between self and p.
 | 
						|
        To calculate the 3d distance, use unit.position3d.distance_to(p)
 | 
						|
 | 
						|
        :param p:
 | 
						|
        """
 | 
						|
        if isinstance(p, Unit):
 | 
						|
            return self._bot_object._distance_squared_unit_to_unit(self, p)**0.5
 | 
						|
        return self._bot_object.distance_math_hypot(self.position_tuple, p)
 | 
						|
 | 
						|
    def distance_to_squared(self, p: Union[Unit, Point2]) -> float:
 | 
						|
        """Using the 2d distance squared between self and p. Slightly faster than distance_to, so when filtering a lot of units, this function is recommended to be used.
 | 
						|
        To calculate the 3d distance, use unit.position3d.distance_to(p)
 | 
						|
 | 
						|
        :param p:
 | 
						|
        """
 | 
						|
        if isinstance(p, Unit):
 | 
						|
            return self._bot_object._distance_squared_unit_to_unit(self, p)
 | 
						|
        return self._bot_object.distance_math_hypot_squared(self.position_tuple, p)
 | 
						|
 | 
						|
    @property
 | 
						|
    def facing(self) -> float:
 | 
						|
        """Returns direction the unit is facing as a float in range [0,2π). 0 is in direction of x axis."""
 | 
						|
        return self._proto.facing
 | 
						|
 | 
						|
    def is_facing(self, other_unit: Unit, angle_error: float = 0.05) -> bool:
 | 
						|
        """Check if this unit is facing the target unit. If you make angle_error too small, there might be rounding errors. If you make angle_error too big, this function might return false positives.
 | 
						|
 | 
						|
        :param other_unit:
 | 
						|
        :param angle_error:
 | 
						|
        """
 | 
						|
        # TODO perhaps return default True for units that cannot 'face' another unit? e.g. structures (planetary fortress, bunker, missile turret, photon cannon, spine, spore) or sieged tanks
 | 
						|
        angle = math.atan2(
 | 
						|
            other_unit.position_tuple[1] - self.position_tuple[1], other_unit.position_tuple[0] - self.position_tuple[0]
 | 
						|
        )
 | 
						|
        if angle < 0:
 | 
						|
            angle += math.pi * 2
 | 
						|
        angle_difference = math.fabs(angle - self.facing)
 | 
						|
        return angle_difference < angle_error
 | 
						|
 | 
						|
    @property
 | 
						|
    def footprint_radius(self) -> Optional[float]:
 | 
						|
        """For structures only.
 | 
						|
        For townhalls this returns 2.5
 | 
						|
        For barracks, spawning pool, gateway, this returns 1.5
 | 
						|
        For supply depot, this returns 1
 | 
						|
        For sensor tower, creep tumor, this return 0.5
 | 
						|
 | 
						|
        NOTE: This can be None if a building doesn't have a creation ability.
 | 
						|
        For rich vespene buildings, flying terran buildings, this returns None"""
 | 
						|
        return self._type_data.footprint_radius
 | 
						|
 | 
						|
    @property
 | 
						|
    def radius(self) -> float:
 | 
						|
        """ Half of unit size. See https://liquipedia.net/starcraft2/Unit_Statistics_(Legacy_of_the_Void) """
 | 
						|
        return self._proto.radius
 | 
						|
 | 
						|
    @property
 | 
						|
    def build_progress(self) -> float:
 | 
						|
        """ Returns completion in range [0,1]."""
 | 
						|
        return self._proto.build_progress
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_ready(self) -> bool:
 | 
						|
        """ Checks if the unit is completed. """
 | 
						|
        return self.build_progress == 1
 | 
						|
 | 
						|
    @property
 | 
						|
    def cloak(self) -> CloakState:
 | 
						|
        """Returns cloak state.
 | 
						|
        See https://github.com/Blizzard/s2client-api/blob/d9ba0a33d6ce9d233c2a4ee988360c188fbe9dbf/include/sc2api/sc2_unit.h#L95
 | 
						|
        """
 | 
						|
        return CloakState(self._proto.cloak)
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_cloaked(self) -> bool:
 | 
						|
        """ Checks if the unit is cloaked. """
 | 
						|
        return self._proto.cloak in IS_CLOAKED
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_revealed(self) -> bool:
 | 
						|
        """ Checks if the unit is revealed. """
 | 
						|
        return self._proto.cloak == IS_REVEALED
 | 
						|
 | 
						|
    @property
 | 
						|
    def can_be_attacked(self) -> bool:
 | 
						|
        """ Checks if the unit is revealed or not cloaked and therefore can be attacked. """
 | 
						|
        return self._proto.cloak in CAN_BE_ATTACKED
 | 
						|
 | 
						|
    @property
 | 
						|
    def detect_range(self) -> float:
 | 
						|
        """ Returns the detection distance of the unit. """
 | 
						|
        return self._proto.detect_range
 | 
						|
 | 
						|
    @property
 | 
						|
    def radar_range(self) -> float:
 | 
						|
        return self._proto.radar_range
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_selected(self) -> bool:
 | 
						|
        """ Checks if the unit is currently selected. """
 | 
						|
        return self._proto.is_selected
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_on_screen(self) -> bool:
 | 
						|
        """ Checks if the unit is on the screen. """
 | 
						|
        return self._proto.is_on_screen
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_blip(self) -> bool:
 | 
						|
        """ Checks if the unit is detected by a sensor tower. """
 | 
						|
        return self._proto.is_blip
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_powered(self) -> bool:
 | 
						|
        """ Checks if the unit is powered by a pylon or warppism. """
 | 
						|
        return self._proto.is_powered
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_active(self) -> bool:
 | 
						|
        """ Checks if the unit has an order (e.g. unit is currently moving or attacking, structure is currently training or researching). """
 | 
						|
        return self._proto.is_active
 | 
						|
 | 
						|
    # PROPERTIES BELOW THIS COMMENT ARE NOT POPULATED FOR SNAPSHOTS
 | 
						|
 | 
						|
    @property
 | 
						|
    def mineral_contents(self) -> int:
 | 
						|
        """ Returns the amount of minerals remaining in a mineral field. """
 | 
						|
        return self._proto.mineral_contents
 | 
						|
 | 
						|
    @property
 | 
						|
    def vespene_contents(self) -> int:
 | 
						|
        """ Returns the amount of gas remaining in a geyser. """
 | 
						|
        return self._proto.vespene_contents
 | 
						|
 | 
						|
    @property
 | 
						|
    def has_vespene(self) -> bool:
 | 
						|
        """Checks if a geyser has any gas remaining.
 | 
						|
        You can't build extractors on empty geysers."""
 | 
						|
        return bool(self._proto.vespene_contents)
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_burrowed(self) -> bool:
 | 
						|
        """ Checks if the unit is burrowed. """
 | 
						|
        return self._proto.is_burrowed
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_hallucination(self) -> bool:
 | 
						|
        """ Returns True if the unit is your own hallucination or detected. """
 | 
						|
        return self._proto.is_hallucination
 | 
						|
 | 
						|
    @property
 | 
						|
    def attack_upgrade_level(self) -> int:
 | 
						|
        """Returns the upgrade level of the units attack.
 | 
						|
        # NOTE: Returns 0 for units without a weapon."""
 | 
						|
        return self._proto.attack_upgrade_level
 | 
						|
 | 
						|
    @property
 | 
						|
    def armor_upgrade_level(self) -> int:
 | 
						|
        """ Returns the upgrade level of the units armor. """
 | 
						|
        return self._proto.armor_upgrade_level
 | 
						|
 | 
						|
    @property
 | 
						|
    def shield_upgrade_level(self) -> int:
 | 
						|
        """Returns the upgrade level of the units shield.
 | 
						|
        # NOTE: Returns 0 for units without a shield."""
 | 
						|
        return self._proto.shield_upgrade_level
 | 
						|
 | 
						|
    @property
 | 
						|
    def buff_duration_remain(self) -> int:
 | 
						|
        """Returns the amount of remaining frames of the visible timer bar.
 | 
						|
        # NOTE: Returns 0 for units without a timer bar."""
 | 
						|
        return self._proto.buff_duration_remain
 | 
						|
 | 
						|
    @property
 | 
						|
    def buff_duration_max(self) -> int:
 | 
						|
        """Returns the maximum amount of frames of the visible timer bar.
 | 
						|
        # NOTE: Returns 0 for units without a timer bar."""
 | 
						|
        return self._proto.buff_duration_max
 | 
						|
 | 
						|
    # PROPERTIES BELOW THIS COMMENT ARE NOT POPULATED FOR ENEMIES
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def orders(self) -> List[UnitOrder]:
 | 
						|
        """ Returns the a list of the current orders. """
 | 
						|
        # TODO: add examples on how to use unit orders
 | 
						|
        return [UnitOrder.from_proto(order, self._bot_object) for order in self._proto.orders]
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def order_target(self) -> Optional[Union[int, Point2]]:
 | 
						|
        """Returns the target tag (if it is a Unit) or Point2 (if it is a Position)
 | 
						|
        from the first order, returns None if the unit is idle"""
 | 
						|
        if self.orders:
 | 
						|
            target = self.orders[0].target
 | 
						|
            if isinstance(target, int):
 | 
						|
                return target
 | 
						|
            return Point2.from_proto(target)
 | 
						|
        return None
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_idle(self) -> bool:
 | 
						|
        """ Checks if unit is idle. """
 | 
						|
        return not self._proto.orders
 | 
						|
 | 
						|
    @property
 | 
						|
    def add_on_tag(self) -> int:
 | 
						|
        """Returns the tag of the addon of unit. If the unit has no addon, returns 0."""
 | 
						|
        return self._proto.add_on_tag
 | 
						|
 | 
						|
    @property
 | 
						|
    def has_add_on(self) -> bool:
 | 
						|
        """ Checks if unit has an addon attached. """
 | 
						|
        return bool(self._proto.add_on_tag)
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def has_techlab(self) -> bool:
 | 
						|
        """Check if a structure is connected to a techlab addon. This should only ever return True for BARRACKS, FACTORY, STARPORT. """
 | 
						|
        return self.add_on_tag in self._bot_object.techlab_tags
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def has_reactor(self) -> bool:
 | 
						|
        """Check if a structure is connected to a reactor addon. This should only ever return True for BARRACKS, FACTORY, STARPORT. """
 | 
						|
        return self.add_on_tag in self._bot_object.reactor_tags
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def add_on_land_position(self) -> Point2:
 | 
						|
        """If this unit is an addon (techlab, reactor), returns the position
 | 
						|
        where a terran building (BARRACKS, FACTORY, STARPORT) has to land to connect to this addon.
 | 
						|
 | 
						|
        Why offset (-2.5, 0.5)? See description in 'add_on_position'
 | 
						|
        """
 | 
						|
        return self.position.offset(Point2((-2.5, 0.5)))
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def add_on_position(self) -> Point2:
 | 
						|
        """If this unit is a terran production building (BARRACKS, FACTORY, STARPORT),
 | 
						|
        this property returns the position of where the addon should be, if it should build one or has one attached.
 | 
						|
 | 
						|
        Why offset (2.5, -0.5)?
 | 
						|
        A barracks is of size 3x3. The distance from the center to the edge is 1.5.
 | 
						|
        An addon is 2x2 and the distance from the edge to center is 1.
 | 
						|
        The total distance from center to center on the x-axis is 2.5.
 | 
						|
        The distance from center to center on the y-axis is -0.5.
 | 
						|
        """
 | 
						|
        return self.position.offset(Point2((2.5, -0.5)))
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def passengers(self) -> Set[Unit]:
 | 
						|
        """ Returns the units inside a Bunker, CommandCenter, PlanetaryFortress, Medivac, Nydus, Overlord or WarpPrism. """
 | 
						|
        return {Unit(unit, self._bot_object) for unit in self._proto.passengers}
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def passengers_tags(self) -> Set[int]:
 | 
						|
        """ Returns the tags of the units inside a Bunker, CommandCenter, PlanetaryFortress, Medivac, Nydus, Overlord or WarpPrism. """
 | 
						|
        return {unit.tag for unit in self._proto.passengers}
 | 
						|
 | 
						|
    @property
 | 
						|
    def cargo_used(self) -> int:
 | 
						|
        """Returns how much cargo space is currently used in the unit.
 | 
						|
        Note that some units take up more than one space."""
 | 
						|
        return self._proto.cargo_space_taken
 | 
						|
 | 
						|
    @property
 | 
						|
    def has_cargo(self) -> bool:
 | 
						|
        """ Checks if this unit has any units loaded. """
 | 
						|
        return bool(self._proto.cargo_space_taken)
 | 
						|
 | 
						|
    @property
 | 
						|
    def cargo_size(self) -> int:
 | 
						|
        """ Returns the amount of cargo space the unit needs. """
 | 
						|
        return self._type_data.cargo_size
 | 
						|
 | 
						|
    @property
 | 
						|
    def cargo_max(self) -> int:
 | 
						|
        """ How much cargo space is available at maximum. """
 | 
						|
        return self._proto.cargo_space_max
 | 
						|
 | 
						|
    @property
 | 
						|
    def cargo_left(self) -> int:
 | 
						|
        """ Returns how much cargo space is currently left in the unit. """
 | 
						|
        return self._proto.cargo_space_max - self._proto.cargo_space_taken
 | 
						|
 | 
						|
    @property
 | 
						|
    def assigned_harvesters(self) -> int:
 | 
						|
        """ Returns the number of workers currently gathering resources at a geyser or mining base."""
 | 
						|
        return self._proto.assigned_harvesters
 | 
						|
 | 
						|
    @property
 | 
						|
    def ideal_harvesters(self) -> int:
 | 
						|
        """Returns the ideal harverster count for unit.
 | 
						|
        3 for gas buildings, 2*n for n mineral patches on that base."""
 | 
						|
        return self._proto.ideal_harvesters
 | 
						|
 | 
						|
    @property
 | 
						|
    def surplus_harvesters(self) -> int:
 | 
						|
        """Returns a positive int if unit has too many harvesters mining,
 | 
						|
        a negative int if it has too few mining.
 | 
						|
        Will only works on townhalls, and gas buildings.
 | 
						|
        """
 | 
						|
        return self._proto.assigned_harvesters - self._proto.ideal_harvesters
 | 
						|
 | 
						|
    @property
 | 
						|
    def weapon_cooldown(self) -> float:
 | 
						|
        """Returns the time until the unit can fire again,
 | 
						|
        returns -1 for units that can't attack.
 | 
						|
        Usage:
 | 
						|
        if unit.weapon_cooldown == 0:
 | 
						|
            unit.attack(target)
 | 
						|
        elif unit.weapon_cooldown < 0:
 | 
						|
            unit.move(closest_allied_unit_because_cant_attack)
 | 
						|
        else:
 | 
						|
            unit.move(retreatPosition)"""
 | 
						|
        if self.can_attack:
 | 
						|
            return self._proto.weapon_cooldown
 | 
						|
        return -1
 | 
						|
 | 
						|
    @property
 | 
						|
    def weapon_ready(self) -> bool:
 | 
						|
        """Checks if the weapon is ready to be fired."""
 | 
						|
        return self.weapon_cooldown == 0
 | 
						|
 | 
						|
    @property
 | 
						|
    def engaged_target_tag(self) -> int:
 | 
						|
        # TODO What does this do?
 | 
						|
        return self._proto.engaged_target_tag
 | 
						|
 | 
						|
    @cached_property
 | 
						|
    def rally_targets(self) -> List[RallyTarget]:
 | 
						|
        """ Returns the queue of rallytargets of the structure. """
 | 
						|
        return [RallyTarget.from_proto(rally_target) for rally_target in self._proto.rally_targets]
 | 
						|
 | 
						|
    # Unit functions
 | 
						|
 | 
						|
    def __hash__(self) -> int:
 | 
						|
        return self.tag
 | 
						|
 | 
						|
    def __eq__(self, other: Union[Unit, Any]) -> bool:
 | 
						|
        """
 | 
						|
        :param other:
 | 
						|
        """
 | 
						|
        return self.tag == getattr(other, "tag", -1)
 |