def test_refill_probabilities_map_hp_downtime(self) -> None: # given keeper = KnightPotionKeeper(total_hp=TOTAL_HP, client=Mock(), battle_config=make_battle_config()) # when target = keeper.gen_refill_probability_map() # then for mana_priority in RefillPriority: if mana_priority > RefillPriority.DOWNTIME: self.assertEqual( target[RefillPriorities( hp_priority=RefillPriority.DOWNTIME, mana_priority=mana_priority, )], 0.0, ) self.assertEqual( target[RefillPriorities( hp_priority=RefillPriority.DOWNTIME, mana_priority=RefillPriority.DOWNTIME, )], 0.25, ) self.assertEqual( target[RefillPriorities( hp_priority=RefillPriority.DOWNTIME, mana_priority=RefillPriority.NO_REFILL, )], 1.0, )
def init_mana_keeper( self, client, char_config: CharConfig, battle_config: BattleConfig, mana_keeper: Optional[Union[ManaKeeper, KnightPotionKeeper]] = None, ): self.mana_keeper: Union[ManaKeeper, KnightPotionKeeper] = None # type: ignore if mana_keeper is None: if char_config.vocation == "mage": self.mana_keeper = ManaKeeper( client, battle_config.mana_hi, battle_config.mana_lo, battle_config.critical_mana, battle_config.downtime_mana, char_config.total_mana, ) elif char_config.vocation == "knight": self.mana_keeper = KnightPotionKeeper( client=client, battle_config=battle_config, total_hp=char_config.total_hp, ) else: raise Exception(f"Unsupported vocation: {char_config.vocation}") else: self.mana_keeper = mana_keeper
def test_refill_probabilities_map_hp_critical(self) -> None: # given keeper = KnightPotionKeeper(total_hp=TOTAL_HP, client=Mock(), battle_config=make_battle_config()) # when target = keeper.gen_refill_probability_map() # then for mana_priority in RefillPriority: self.assertEqual( target[RefillPriorities(hp_priority=RefillPriority.CRITICAL, mana_priority=mana_priority)], 1.0, )
def check_gen_hp_refill_probability(self, input_hp: Union[int, float], hp_priority: RefillPriority, mana_priority: RefillPriority, expected: float) -> None: # given target = KnightPotionKeeper(total_hp=TOTAL_HP, client=Mock(), battle_config=make_battle_config()) # when actual = target.gen_hp_refill_probability( input_hp, RefillPriorities(hp_priority=hp_priority, mana_priority=mana_priority)) # then self.assertAlmostEqual(actual, expected, places=3)
def check_handle_status_change( self, char_status: CharStatus, assert_fn: Callable[[Mock], None], is_downtime: bool = True, ) -> None: # given mock_client = Mock() target = KnightPotionKeeper(total_hp=TOTAL_HP, client=mock_client, battle_config=make_battle_config()) # when target.handle_status_change(char_status=char_status, is_downtime=is_downtime) # then assert_fn(mock_client)
def test_refill_probabilities_map_mana_no_refill(self) -> None: # given keeper = KnightPotionKeeper(total_hp=TOTAL_HP, client=Mock(), battle_config=make_battle_config()) # when target = keeper.gen_refill_probability_map() # then for hp_priority in RefillPriority: if hp_priority is not RefillPriority.NO_REFILL: self.assertEqual( target[RefillPriorities( hp_priority=hp_priority, mana_priority=RefillPriority.NO_REFILL, )], 1.0, )
def check_get_threshold_ms(self, stat_value: Union[int, float], expected: int, critical: int = STAT_CRIT, lo: int = STAT_LO, hi: int = STAT_HI, downtime: int = STAT_DOWNTIME) -> None: # given target = KnightPotionKeeper(total_hp=TOTAL_HP, client=Mock(), battle_config=make_battle_config()) # when actual = target.get_threshold_ms(stat_value=stat_value, stat_config=StatConfig( critical=critical, lo=lo, hi=hi, downtime=downtime)) # then self.assertEqual(actual, expected)
def test_handle_status_change_critical_mana_then_hi_pri_hp(self) -> None: # given char_status_crit_mana = CharStatus( hp=TOTAL_HP, mana=0, magic_shield_level=0, equipment_status={}, speed=HASTED_SPEED, ) char_status_hi_pri_hp = CharStatus( hp=POTION_HP_LO + 1, mana=TOTAL_MANA, magic_shield_level=0, equipment_status={ "has_greater_heal_potions": True, "has_medium_heal_potions": True, }, speed=HASTED_SPEED, ) mock_client = Mock() target = KnightPotionKeeper(total_hp=TOTAL_HP, client=mock_client, battle_config=make_battle_config()) target.handle_status_change(char_status=char_status_crit_mana, is_downtime=True) # when target.handle_status_change(char_status=char_status_hi_pri_hp, is_downtime=True) # then mock_client.drink_medium_heal.assert_called_with(666)
def test_refill_probabilities_map_hp_hipri(self) -> None: # given keeper = KnightPotionKeeper(total_hp=TOTAL_HP, client=Mock(), battle_config=make_battle_config()) # when target = keeper.gen_refill_probability_map() # then self.assertEqual( target[RefillPriorities( hp_priority=RefillPriority.HIGH_PRIORITY, mana_priority=RefillPriority.HIGH_PRIORITY, )], 0.6, ) self.assertEqual( target[RefillPriorities( hp_priority=RefillPriority.HIGH_PRIORITY, mana_priority=RefillPriority.CRITICAL, )], 0.33, )
class CharKeeper: def __init__( self, client: ClientInterface, char_config: CharConfig, battle_config: BattleConfig, hotkeys_config: HotkeysConfig, emergency_reporter: Optional[EmergencyReporter] = None, tank_mode_reporter: Optional[SimpleModeReporter] = None, defensive_mode_reporter: Optional[SimpleModeReporter] = None, mana_keeper: Optional[Union[ManaKeeper, KnightPotionKeeper]] = None, hp_keeper: Optional[HpKeeper] = None, speed_keeper: Optional[SpeedKeeper] = None, equipment_keeper: Optional[EquipmentKeeper] = None, magic_shield_keeper: Optional[Union[MagicShieldKeeper, ProtectorKeeper]] = None, item_crosshair_macros: Optional[List[ItemCrosshairMacro]] = None, core_macros: Optional[List[Macro]] = None, ): self.item_crosshair_macros: List[ItemCrosshairMacro] = [] self.directional_macros: List[DirectionalMacro] = [] self.core_macros: List[Macro] = [] self.drag_macros: List[DragMacro] = [] self.client = client self.hotkeys_config = hotkeys_config # load the first battle config from the first char config self.init_emergency_reporter(char_config, battle_config, emergency_reporter) self.init_tank_mode_reporter(tank_mode_reporter) self.init_defensive_mode_reporter(defensive_mode_reporter) self.init_mana_keeper(client, char_config, battle_config, mana_keeper) self.init_hp_keeper(client, char_config, battle_config, hp_keeper) self.init_speed_keeper(client, char_config, battle_config, speed_keeper) self.init_equipment_keeper(client, battle_config, equipment_keeper) self.init_magic_shield_keeper( client, char_config, battle_config, magic_shield_keeper ) self.init_item_crosshair_macros( battle_config.item_crosshair_macros or [], self.hotkeys_config, item_crosshair_macros, ) self.init_core_macros(self.hotkeys_config, core_macros) self.init_directional_macros(battle_config.directional_macros or []) def load_char_config(self, char_config: CharConfig, battle_config: BattleConfig): self.init_emergency_reporter(char_config, battle_config) self.init_mana_keeper(self.client, char_config, battle_config) self.init_hp_keeper(self.client, char_config, battle_config) self.init_speed_keeper(self.client, char_config, battle_config) self.init_equipment_keeper(self.client, battle_config) self.init_magic_shield_keeper(self.client, char_config, battle_config) self.init_item_crosshair_macros( battle_config.item_crosshair_macros or [], self.hotkeys_config ) self.init_core_macros(self.hotkeys_config) self.init_directional_macros(battle_config.directional_macros or []) self.init_drag_macros(battle_config.drag_macros or []) def init_emergency_reporter( self, char_config: CharConfig, battle_config: BattleConfig, emergency_reporter: Optional[EmergencyReporter] = None, ): if emergency_reporter is None: self.emergency_reporter = EmergencyReporter( char_config.total_hp, battle_config.mana_lo, battle_config.emergency_hp_threshold, ) else: self.emergency_reporter = emergency_reporter def init_tank_mode_reporter( self, tank_mode_reporter: Optional[SimpleModeReporter] = None, ): if tank_mode_reporter is None: self.tank_mode_reporter = SimpleModeReporter() else: self.tank_mode_reporter = tank_mode_reporter def init_defensive_mode_reporter( self, defensive_mode_reporter: Optional[SimpleModeReporter] = None, ): if defensive_mode_reporter is None: self.defensive_mode_reporter = SimpleModeReporter() else: self.defensive_mode_reporter = defensive_mode_reporter def init_mana_keeper( self, client, char_config: CharConfig, battle_config: BattleConfig, mana_keeper: Optional[Union[ManaKeeper, KnightPotionKeeper]] = None, ): self.mana_keeper: Union[ManaKeeper, KnightPotionKeeper] = None # type: ignore if mana_keeper is None: if char_config.vocation == "mage": self.mana_keeper = ManaKeeper( client, battle_config.mana_hi, battle_config.mana_lo, battle_config.critical_mana, battle_config.downtime_mana, char_config.total_mana, ) elif char_config.vocation == "knight": self.mana_keeper = KnightPotionKeeper( client=client, battle_config=battle_config, total_hp=char_config.total_hp, ) else: raise Exception(f"Unsupported vocation: {char_config.vocation}") else: self.mana_keeper = mana_keeper def init_hp_keeper( self, client, char_config: CharConfig, battle_config: BattleConfig, hp_keeper: Optional[HpKeeper] = None, ): if hp_keeper is None: self.hp_keeper = HpKeeper( client, self.emergency_reporter, char_config.total_hp, battle_config.heal_at_missing, battle_config.minor_heal, battle_config.medium_heal, battle_config.greater_heal, battle_config.downtime_heal_at_missing, battle_config.emergency_hp_threshold, ) else: self.hp_keeper = hp_keeper def init_speed_keeper( self, client, char_config: CharConfig, battle_config: BattleConfig, speed_keeper: Optional[SpeedKeeper] = None, ): if speed_keeper is None: self.speed_keeper = SpeedKeeper( client, char_config.base_speed, battle_config.hasted_speed ) else: self.speed_keeper = speed_keeper def init_equipment_keeper( self, client, battle_config: BattleConfig, equipment_keeper: Optional[EquipmentKeeper] = None, ): if equipment_keeper is None: self.equipment_keeper = EquipmentKeeper( client, self.emergency_reporter, self.tank_mode_reporter, self.defensive_mode_reporter, battle_config.should_equip_amulet, battle_config.should_equip_ring, battle_config.should_eat_food, battle_config.equip_amulet_secs, battle_config.equip_ring_secs, ) else: self.equipment_keeper = equipment_keeper def init_magic_shield_keeper( self, client, char_config: CharConfig, battle_config: BattleConfig, magic_shield_keeper: Union[MagicShieldKeeper, ProtectorKeeper] = None, ): self.magic_shield_keeper: Union[ MagicShieldKeeper, ProtectorKeeper, NoopKeeper ] = None # type: ignore magic_shield_type = battle_config.magic_shield_type if magic_shield_keeper is not None: self.magic_shield_keeper = magic_shield_keeper if magic_shield_type == "permanent": if char_config.vocation is None or char_config.vocation == "mage": self.magic_shield_keeper = MagicShieldKeeper( client, char_config.total_hp, battle_config.magic_shield_threshold ) elif char_config.vocation == "knight": self.magic_shield_keeper = ProtectorKeeper(client) else: raise Exception(f"Unsupported vocation: {char_config.vocation}") elif magic_shield_type == "emergency": if char_config.vocation is None or char_config.vocation == "mage": self.magic_shield_keeper = EmergencyMagicShieldKeeper( client, self.emergency_reporter, self.tank_mode_reporter, char_config.total_hp, battle_config.magic_shield_threshold or 0, ) elif char_config.vocation == "knight": self.magic_shield_keeper = EmergencyProtectorKeeper( client, self.emergency_reporter, self.tank_mode_reporter ) else: raise Exception(f"Unsupported vocation: {char_config.vocation}") elif magic_shield_type: raise Exception(f"Unknown magic shield type {magic_shield_type}") else: # None or empty self.magic_shield_keeper = NoopKeeper() def init_item_crosshair_macros( self, macro_configs: List[ItemCrosshairMacroConfig], hotkeys_config: HotkeysConfig, item_crosshair_macros: Optional[List[ItemCrosshairMacro]] = None, ): self.unload_item_crosshair_macros() if item_crosshair_macros is not None: self.item_crosshair_macros = item_crosshair_macros else: for macro_config in macro_configs: self.item_crosshair_macros.append( ItemCrosshairMacro(self.client, macro_config, hotkeys_config) ) def unload_item_crosshair_macros(self): self.__unhook_macros(self.item_crosshair_macros) self.item_crosshair_macros = [] def init_drag_macros( self, macro_configs: List[DragMacroConfig] = [], drag_macros: Optional[List[DragMacro]] = None, ): self.unload_drag_macros() if drag_macros is not None: self.drag_macros = drag_macros else: for macro_config in macro_configs: self.drag_macros.append(DragMacro(self.client, macro_config)) def unload_drag_macros(self): self.__unhook_macros(self.drag_macros) self.drag_macros = [] def init_directional_macros( self, macro_configs: List[DirectionalMacroConfig], directional_macros: Optional[List[DirectionalMacro]] = None, ): self.unload_directional_macros() if directional_macros is not None: self.directional_macros = directional_macros else: for macro_config in macro_configs: self.directional_macros.append(DirectionalMacro(macro_config)) def unload_directional_macros(self): self.__unhook_macros(self.directional_macros) self.directional_macros = [] def init_core_macros( self, hotkeys_config: HotkeysConfig, core_macros: Optional[List[Macro]] = None ): self.unload_core_macros() if core_macros is not None: self.core_macros = core_macros else: emergency_override_reporter = ( self.emergency_reporter.gen_override_reporter() ) cancel_emergency_key = hotkeys_config.cancel_emergency if cancel_emergency_key: self.core_macros.append( CancelModeMacro( emergency_override_reporter, cancel_emergency_key, ) ) start_emergency_key = hotkeys_config.start_emergency if start_emergency_key: self.core_macros.append( StartModeMacro( mode_reporter=emergency_override_reporter, exclusive_mode_reporters=( self.tank_mode_reporter, self.defensive_mode_reporter, ), hotkey=start_emergency_key, ) ) cancel_tank_mode_key = hotkeys_config.cancel_tank_mode if cancel_tank_mode_key: self.core_macros.append( CancelModeMacro(self.tank_mode_reporter, cancel_tank_mode_key) ) start_tank_mode_key = hotkeys_config.start_tank_mode if start_tank_mode_key: self.core_macros.append( StartModeMacro( mode_reporter=self.tank_mode_reporter, exclusive_mode_reporters=( emergency_override_reporter, self.defensive_mode_reporter, ), hotkey=start_tank_mode_key, ) ) cancel_defensive_mode_key = hotkeys_config.cancel_defensive_mode if cancel_defensive_mode_key: self.core_macros.append( CancelModeMacro( self.defensive_mode_reporter, cancel_defensive_mode_key ) ) start_defensive_mode_key = hotkeys_config.start_defensive_mode if start_defensive_mode_key: self.core_macros.append( StartModeMacro( mode_reporter=self.defensive_mode_reporter, exclusive_mode_reporters=[ emergency_override_reporter, self.tank_mode_reporter, ], hotkey=start_defensive_mode_key, ) ) def unload_core_macros(self): self.__unhook_macros(self.core_macros) self.core_macros = [] def unhook_macros(self): self.__unhook_macros(self.item_crosshair_macros) self.__unhook_macros(self.core_macros) self.__unhook_macros(self.directional_macros) def __unhook_macros(self, macros: Optional[List[Macro]] = None): for macro in macros or []: macro.unhook_hotkey() def hook_macros(self): self.__hook_macros(self.item_crosshair_macros) self.__hook_macros(self.core_macros) self.__hook_macros(self.directional_macros) def hook_drag_macros(self): self.__hook_macros(self.drag_macros) def unhook_drag_macros(self): self.__unhook_macros(self.drag_macros) def __hook_macros(self, macros: Optional[List[Macro]] = None): for macro in macros or []: macro.hook_hotkey() def handle_char_status(self, char_status: CharStatus): # First set the emergency status, so all sub-keepers can change their # their behaviours accordingly. self.handle_emergency_status_change(char_status) # Note that we have to handle stats changes always, even if they # haven't actually changed, because a command to heal or drink mana # or haste could be ignored if the character is exhausted, therefore # we have to spam the action until the effect takes place. # # The order of these is important, since reading pixels off the screen # takes a few miliseconds, so addressing equipment before addresing # magic shield, it will make it so that magic shield will have to wait # for equipment pixels to fetched before magic shield status pixels. # # Additionally, casting spells that share a cooldown has an effect on # the subsequent char keeping. e.g. if we cast haste before casting # utamo, then we won't be able to cast utamo when needed since using # haste will put utamo on cooldown. self.handle_hp_change(char_status) self.handle_shield_change(char_status) self.handle_mana_change(char_status) self.handle_equipment(char_status) self.handle_speed_change(char_status) def handle_emergency_status_change(self, char_status: CharStatus): if self.emergency_reporter.is_mode_on(): if self.emergency_reporter.should_stop_emergency(char_status): self.emergency_reporter.stop_mode() elif self.emergency_reporter.is_emergency(char_status): self.emergency_reporter.start_mode() def handle_shield_change(self, char_status: CharStatus): self.magic_shield_keeper.handle_status_change(char_status) def handle_hp_change(self, char_status: CharStatus): is_downtime = self.speed_keeper.is_hasted( char_status.speed ) and self.mana_keeper.is_healthy(char_status) self.hp_keeper.handle_status_change(char_status, is_downtime) def handle_mana_change(self, char_status: CharStatus): is_downtime = self.hp_keeper.is_healthy( char_status ) and self.speed_keeper.is_hasted(char_status.speed) self.mana_keeper.handle_status_change(char_status, is_downtime) def handle_speed_change(self, char_status: CharStatus): if not self.should_skip_haste(char_status): self.speed_keeper.handle_status_change(char_status) def should_skip_haste(self, char_status: CharStatus) -> bool: # Do not issue order to haste if we're at critical HP levels, # since haste shared cooldown with magic shield. if self.hp_keeper.is_critical_hp( char_status.hp ) and not self.magic_shield_keeper.is_healthy(char_status): return True # Do not issue a haste order if we're not paralyzed and we're at # critical mana levels. if self.mana_keeper.is_critical_mana( char_status.mana ) and not self.speed_keeper.is_paralized(char_status.speed): return True # Do not issue haste, if we should be casting magic shield next, since # haste and magic shield share cooldowns. if ( char_status.magic_shield_status == MagicShieldStatus.OFF_COOLDOWN and self.magic_shield_keeper.should_cast(char_status) ): return True return False def handle_equipment(self, char_status: CharStatus): self.equipment_keeper.handle_status_change(char_status)