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)
|