mirror of
				https://github.com/MarioSpore/Grinch-AP.git
				synced 2025-10-21 20:21:32 -06:00 
			
		
		
		
	
		
			
	
	
		
			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) |