class RewardTacticalClass(UI, InfoHandler): tactical_animation_timer = Timer(2, count=3) def _tactical_animation_running(self): """ Returns: bool: If showing skill points increasing animation. """ color_height = np.mean(self.device.image.crop((922, 0, 1036, 720)).convert('L'), axis=1) parameters = {'height': 200} peaks, _ = signal.find_peaks(color_height, **parameters) peaks = [y for y in peaks if y > 67 + 243] if not len(peaks): logger.warning('No student card found.') for y in peaks: student_area = (447, y - 243, 1244, y) area = area_offset((677, 172, 761, 183), student_area[0:2]) # Normal: 160, In skill-increasing animation: 109 if np.mean(get_color(self.device.image, area)) < 135: return True return False def _tactical_books_choose(self): """ Choose tactical book according to config. """ books = BookGroup([Book(self.device.image, button) for button in BOOKS_GRID.buttons()]).select(valid=True) logger.attr('Book_count', len(books)) for index in range(1, 4): logger.info(f'Book_T{index}: {books.select(tier=index)}') if not books: logger.warning('No book found.') raise ScriptError('No book found.') if not time_range_active(self.config.TACTICAL_NIGHT_RANGE): tier = self.config.TACTICAL_BOOK_TIER exp = self.config.TACTICAL_EXP_FIRST else: tier = self.config.TACTICAL_BOOK_TIER_NIGHT exp = self.config.TACTICAL_EXP_FIRST_NIGHT book = books.choose(tier=tier, exp=exp) self.device.click(book.button) self.device.sleep((0.3, 0.5)) def _tactical_class_receive(self, skip_first_screenshot=True): """Remember to make sure current page is page_reward before calls. Args: skip_first_screenshot (bool): Returns: bool: If rewarded. """ if not self.appear(REWARD_2): logger.info('No tactical class reward.') return False logger.hr('Tactical class receive') while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() if self.appear_then_click(REWARD_2, interval=1): continue if self.handle_popup_confirm(): continue if self.handle_urgent_commission(save_get_items=False): # Only one button in the middle, when skill reach max level. continue if self.appear(TACTICAL_CLASS_CANCEL, offset=(30, 30), interval=2) \ and self.appear(TACTICAL_CLASS_START, offset=(30, 30)): self.device.sleep(0.3) self.device.screenshot() self._tactical_books_choose() self.device.click(TACTICAL_CLASS_START) self.interval_reset(TACTICAL_CLASS_CANCEL) continue # End if self.appear(TACTICAL_CHECK, offset=(20, 20)): self.ui_current = page_tactical if not self._tactical_animation_running(): if self.tactical_animation_timer.reached(): logger.info('Tactical reward end.') break else: self.tactical_animation_timer.reset() self.ui_goto(page_reward, skip_first_screenshot=True) return True def handle_tactical_class(self): """ Returns: bool: If rewarded. """ if not self.config.ENABLE_TACTICAL_REWARD: return False self._tactical_class_receive() return True
def _submarine_goto(self, location): """ Move submarine to given location. Args: location (tuple, str, GridInfo): Destination. Returns: bool: If submarine moved. Pages: in: SUBMARINE_MOVE_CONFIRM out: SUBMARINE_MOVE_CONFIRM """ location = location_ensure(location) moved = True while 1: self.in_sight(location, sight=self._walk_sight) self.focus_to_grid_center() grid = self.convert_global_to_local(location) grid.__str__ = location self.device.click(grid) arrived = False # Usually no need to wait arrive_timer = Timer(0.1, count=0) # If nothing happens, click again. walk_timeout = Timer(2, count=6).start() while 1: self.device.screenshot() self.view.update(image=self.device.image) # Arrive arrive_checker = grid.predict_submarine_move() if grid.predict_submarine() or (walk_timeout.reached() and grid.predict_fleet()): arrive_checker = True moved = False if arrive_checker: if not arrive_timer.started(): logger.info(f'Arrive {location2node(location)}') arrive_timer.start() if not arrive_timer.reached(): continue logger.info(f'Submarine arrive {location2node(location)} confirm.') if not moved: logger.info(f'Submarine already at {location2node(location)}') arrived = True break # End if walk_timeout.reached(): logger.warning('Walk timeout. Retrying.') self.predict() self.ensure_edge_insight(skip_first_update=False) break # End if arrived: break return moved
def _tactical_class_receive(self, skip_first_screenshot=True): """ Receive tactical rewards and fill books. Args: skip_first_screenshot (bool): Returns: bool: If rewarded. Pages: in: page_reward, TACTICAL_CLASS_START out: page_tactical """ logger.hr('Tactical class receive', level=1) tactical_class_timout = Timer(10, count=10).start() tactical_animation_timer = Timer(2, count=3).start() while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() if self.appear_then_click(REWARD_2, interval=1): tactical_class_timout.reset() tactical_animation_timer.reset() continue if self.appear_then_click(REWARD_GOTO_TACTICAL, offset=(20, 20), interval=1): tactical_class_timout.reset() tactical_animation_timer.reset() continue if self.handle_popup_confirm('TACTICAL'): tactical_class_timout.reset() tactical_animation_timer.reset() continue if self.handle_urgent_commission(): # Only one button in the middle, when skill reach max level. tactical_class_timout.reset() tactical_animation_timer.reset() continue if self.appear(TACTICAL_CLASS_CANCEL, offset=(30, 30), interval=2) \ and self.appear(TACTICAL_CLASS_START, offset=(30, 30)): self.device.sleep(0.3) self._tactical_books_choose() self.interval_reset(TACTICAL_CLASS_CANCEL) tactical_class_timout.reset() tactical_animation_timer.reset() continue # End if self.appear(TACTICAL_CHECK, offset=(20, 20)): self.ui_current = page_tactical if not self._tactical_animation_running(): if tactical_animation_timer.reached(): logger.info('Tactical reward end.') break else: tactical_animation_timer.reset() if tactical_class_timout.reached(): logger.info('Tactical reward timeout.') break return True
class InfoHandler(ModuleBase): """ Class to handle all kinds of message. """ """ Info bar """ def info_bar_count(self): if self.appear(INFO_BAR_3): return 3 elif self.appear(INFO_BAR_2): return 2 elif self.appear(INFO_BAR_1): return 1 else: return 0 def handle_info_bar(self): if self.info_bar_count(): self.wait_until_disappear(INFO_BAR_1) return True else: return False def ensure_no_info_bar(self, timeout=0.6): timeout = Timer(timeout) timeout.start() while 1: self.device.screenshot() self.handle_info_bar() if timeout.reached(): break """ Popup info """ _popup_offset = (3, 30) def handle_popup_confirm(self, name=''): if self.appear(POPUP_CANCEL, offset=self._popup_offset) \ and self.appear(POPUP_CONFIRM, offset=self._popup_offset, interval=2): POPUP_CONFIRM.name = POPUP_CONFIRM.name + '_' + name self.device.click(POPUP_CONFIRM) POPUP_CONFIRM.name = POPUP_CONFIRM.name[:-len(name) - 1] return True else: return False def handle_popup_cancel(self, name=''): if self.appear(POPUP_CONFIRM, offset=self._popup_offset) \ and self.appear(POPUP_CANCEL, offset=self._popup_offset, interval=2): POPUP_CANCEL.name = POPUP_CANCEL.name + '_' + name self.device.click(POPUP_CANCEL) POPUP_CANCEL.name = POPUP_CANCEL.name[:-len(name) - 1] return True else: return False def handle_urgent_commission(self, save_get_items=None): """ Args: save_get_items (bool): Returns: bool: """ if save_get_items is None: save_get_items = self.config.ENABLE_SAVE_GET_ITEMS appear = self.appear(GET_MISSION, offset=True, interval=2) if appear: logger.info('Get urgent commission') if save_get_items: self.handle_info_bar() self.device.save_screenshot('get_mission') self.device.click(GET_MISSION) return appear def handle_combat_low_emotion(self): if not self.config.IGNORE_LOW_EMOTION_WARN: return False return self.handle_popup_confirm('IGNORE_LOW_EMOTION') """ Story """ story_popup_timout = Timer(10, count=20) def story_skip(self): if self.story_popup_timout.started( ) and not self.story_popup_timout.reached(): if self.handle_popup_confirm('STORY_SKIP'): self.story_popup_timout = Timer(10) self.interval_reset(STORY_SKIP) self.interval_reset(STORY_LETTERS_ONLY) return True if self.appear_then_click(STORY_SKIP, offset=True, interval=2): self.story_popup_timout.reset() return True if self.appear(STORY_LETTER_BLACK) and self.appear_then_click( STORY_LETTERS_ONLY, offset=True, interval=2): self.story_popup_timout.reset() return True if self.appear_then_click(STORY_CHOOSE, offset=True, interval=2): self.story_popup_timout.reset() return True if self.appear_then_click(STORY_CHOOSE_2, offset=True, interval=2): self.story_popup_timout.reset() return True return False def handle_story_skip(self): if not self.config.ENABLE_MAP_CLEAR_MODE: return False if self.config.ENABLE_FAST_FORWARD: return False return self.story_skip() def ensure_no_story(self, skip_first_screenshot=True): logger.info('Ensure no story') story_timer = Timer(5, count=10).start() while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() if self.story_skip(): story_timer.reset() if story_timer.reached(): break def handle_map_after_combat_story(self): if not self.config.MAP_HAS_MAP_STORY: return False self.ensure_no_story()
def _reward_mission_collect(self, interval=1): """ Streamline handling of mission rewards for both 'all' and 'weekly' pages Args: interval (int): Configure the interval for assets involved Returns: bool, if encountered at least 1 GET_ITEMS_* """ # Reset any existing interval for the following assets [ self.interval_clear(asset) for asset in [ GET_ITEMS_1, GET_ITEMS_2, MISSION_MULTI, MISSION_SINGLE, GET_SHIP ] ] # Basic timers for certain scenarios exit_timer = Timer(2) click_timer = Timer(1) timeout = Timer(10) exit_timer.start() timeout.start() reward = False while 1: self.device.screenshot() for button in [GET_ITEMS_1, GET_ITEMS_2]: if self.appear_then_click(button, offset=(30, 30), interval=interval): exit_timer.reset() timeout.reset() reward = True continue for button in [MISSION_MULTI, MISSION_SINGLE]: if not click_timer.reached(): continue if self.appear(button, offset=(0, 200), interval=interval) \ and button.match_appear_on(self.device.image): self.device.click(button) exit_timer.reset() click_timer.reset() timeout.reset() continue if not self.appear(MISSION_CHECK): if self.appear_then_click(GET_SHIP, interval=interval): exit_timer.reset() click_timer.reset() timeout.reset() continue if self.handle_mission_popup_ack(): exit_timer.reset() click_timer.reset() timeout.reset() continue # Story if self.handle_vote_popup(): exit_timer.reset() click_timer.reset() timeout.reset() continue if self.story_skip(): exit_timer.reset() click_timer.reset() timeout.reset() continue if self.handle_popup_confirm('MISSION_REWARD'): exit_timer.reset() click_timer.reset() timeout.reset() continue # End if reward and exit_timer.reached(): break if timeout.reached(): logger.warning('Wait get items timeout.') break return reward
class AutoSearchCombat(MapOperation, Combat): _auto_search_in_stage_timer = Timer(3, count=6) _auto_search_confirm_low_emotion = False auto_search_oil_limit_triggered = False def _handle_auto_search_menu_missing(self): """ Sometimes game is bugged, auto search menu is not shown. After BOSS battle, it enters campaign directly. To handle this, if game in campaign for a certain time, it means auto search ends. Returns: bool: If triggered """ if self.is_in_stage(): if self._auto_search_in_stage_timer.reached(): logger.info('Catch auto search menu missing') return True else: self._auto_search_in_stage_timer.reset() return False def auto_search_watch_fleet(self, checked=False): """ Watch fleet index and ship level. Args: checked (bool): Watchers are only executed or logged once during fleet moving. Set True to skip executing again. Returns: bool: If executed. """ prev = self.fleet_current_index self.get_fleet_show_index() self.get_fleet_current_index() if self.fleet_current_index == prev: # Same as current, only print once if not checked: logger.info( f'Fleet: {self.fleet_show_index}, fleet_current_index: {self.fleet_current_index}' ) checked = True self.lv_get(after_battle=True) else: # Fleet changed logger.info( f'Fleet: {self.fleet_show_index}, fleet_current_index: {self.fleet_current_index}' ) checked = True self.lv_get(after_battle=False) return checked def auto_search_watch_oil(self, checked=False): """ Watch oil. This will set auto_search_oil_limit_triggered. """ if not checked: oil = OCR_OIL.ocr(self.device.image) if oil == 0: logger.warning('Oil not found') else: if oil < self.config.StopCondition_OilLimit: logger.info('Reach oil limit') self.auto_search_oil_limit_triggered = True checked = True return checked def auto_search_moving(self, skip_first_screenshot=True): """ Pages: in: map out: is_combat_loading() """ logger.info('Auto search moving') self.device.stuck_record_clear() checked_fleet = False checked_oil = False while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() if self.is_auto_search_running(): checked_fleet = self.auto_search_watch_fleet(checked_fleet) checked_oil = self.auto_search_watch_oil(checked_oil) if self.handle_retirement(): continue if self.handle_auto_search_map_option(): continue if self.handle_combat_low_emotion(): self._auto_search_confirm_low_emotion = True continue if self.handle_story_skip(): continue if self.handle_map_cat_attack(): continue if self.handle_vote_popup(): continue # End if self.is_combat_loading(): break if self.is_in_auto_search_menu( ) or self._handle_auto_search_menu_missing(): raise CampaignEnd def auto_search_combat_execute(self, emotion_reduce, fleet_index): """ Args: emotion_reduce (bool): fleet_index (int): Pages: in: is_combat_loading() out: combat status """ logger.info('Auto search combat loading') self.device.screenshot_interval_set( self.config.Optimization_CombatScreenshotInterval) while 1: self.device.screenshot() if self.handle_combat_automation_confirm(): continue if self.handle_story_skip(): continue if self.handle_vote_popup(): continue # End if self.is_in_auto_search_menu( ) or self._handle_auto_search_menu_missing(): raise CampaignEnd if self.is_combat_executing(): break logger.info('Auto Search combat execute') self.submarine_call_reset() submarine_mode = 'do_not_use' if self.config.Submarine_Fleet: submarine_mode = self.config.Submarine_Mode self.combat_auto_reset() self.combat_manual_reset() if emotion_reduce: self.emotion.reduce(fleet_index) auto = self.config.Fleet_Fleet1Mode if fleet_index == 1 else self.config.Fleet_Fleet2Mode while 1: self.device.screenshot() if self.handle_submarine_call(submarine_mode): continue if self.handle_combat_auto(auto): continue if self.handle_combat_manual(auto): continue if auto != 'combat_auto' and self.auto_mode_checked and self.is_combat_executing( ): if self.handle_combat_weapon_release(): continue if self.handle_popup_confirm('AUTO_SEARCH_COMBAT_EXECUTE'): continue if self.handle_story_skip(): continue if self.handle_vote_popup(): continue # End if self.is_in_auto_search_menu( ) or self._handle_auto_search_menu_missing(): self.device.screenshot_interval_set(0) raise CampaignEnd if self.is_combat_executing(): continue if self.appear(BATTLE_STATUS_S) or self.appear(BATTLE_STATUS_A) or self.appear(BATTLE_STATUS_B) \ or self.appear(EXP_INFO_S) or self.appear(EXP_INFO_A) or self.appear(EXP_INFO_B) \ or self.is_auto_search_running(): self.device.screenshot_interval_set(0) break def auto_search_combat_status(self, skip_first_screenshot=True): """ Pages: in: any out: is_auto_search_running() """ logger.info('Auto Search combat status') exp_info = False # This is for the white screen bug in game while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() # End if self.is_auto_search_running(): self._auto_search_confirm_low_emotion = False break if self.is_in_auto_search_menu( ) or self._handle_auto_search_menu_missing(): raise CampaignEnd # Combat status if self.handle_get_ship(): continue if self.handle_popup_confirm('AUTO_SEARCH_COMBAT_STATUS'): continue if self.handle_auto_search_map_option(): self._auto_search_confirm_low_emotion = False continue if self.handle_urgent_commission(): continue if self.handle_story_skip(): continue if self.handle_guild_popup_cancel(): continue if self.handle_vote_popup(): continue # Handle low emotion combat # Combat status if self._auto_search_confirm_low_emotion: if not exp_info and self.handle_get_ship(): continue if self.handle_get_items(): continue if self.handle_battle_status(): continue if self.handle_popup_confirm('combat_status'): continue if self.handle_exp_info(): exp_info = True continue def auto_search_combat(self, emotion_reduce=None, fleet_index=1): """ Execute a combat. Note that fleet index == 1 is mob fleet, 2 is boss fleet. It's not the fleet index in fleet preparation or auto search setting. """ emotion_reduce = emotion_reduce if emotion_reduce is not None else self.config.Emotion_CalculateEmotion self.device.stuck_record_clear() self.auto_search_combat_execute(emotion_reduce=emotion_reduce, fleet_index=fleet_index) self.auto_search_combat_status() logger.info('Combat end.')
class MapEventHandler(EnemySearchingHandler): def handle_map_get_items(self, interval=2): if self.is_in_map(): return False if self.appear(GET_ITEMS_1, interval=interval) \ or self.appear(GET_ITEMS_2, interval=interval) \ or self.appear(GET_ITEMS_3, interval=interval): self.device.click(CLICK_SAFE_AREA) return True if self.appear(GET_ADAPTABILITY, interval=interval): self.device.click(CLICK_SAFE_AREA) return True if self.appear(GET_MEOWFFICER_ITEMS_1, interval=interval): self.device.click(CLICK_SAFE_AREA) return True if self.appear(GET_MEOWFFICER_ITEMS_2, interval=interval): self.device.click(CLICK_SAFE_AREA) return True return False def handle_map_archives(self): if self.appear(MAP_ARCHIVES, interval=5): self.device.click(CLICK_SAFE_AREA) return True if self.appear_then_click(MAP_WORLD, offset=(20, 20), interval=5): return True return False def handle_ash_popup(self): name = 'ASH' if self.appear(POPUP_CONFIRM, offset=self._popup_offset) \ and self.appear(POPUP_CANCEL, offset=self._popup_offset, interval=2) \ and self.image_color_count(ASH_POPUP_CHECK, color=(255, 93, 90), threshold=221, count=100): POPUP_CANCEL.name = POPUP_CANCEL.name + '_' + name self.device.click(POPUP_CANCEL) POPUP_CANCEL.name = POPUP_CANCEL.name[:-len(name) - 1] return True else: return False def handle_siren_platform(self): """ Handle siren platform notice after entering map Returns: bool: If handled """ if not self.handle_story_skip(): return False logger.info('Handle siren platform') timeout = Timer(self.MAP_ENEMY_SEARCHING_TIMEOUT_SECOND).start() appeared = False while 1: self.device.screenshot() if self.is_in_map(): timeout.start() else: timeout.reset() if self.handle_story_skip(): timeout.reset() continue # End if self.enemy_searching_appear(): appeared = True else: if appeared: self.handle_enemy_flashing() self.device.sleep(1) logger.info('Enemy searching appeared.') break self.enemy_searching_color_initial() if timeout.reached(): logger.info('Enemy searching timeout.') break return True def handle_map_event(self): """ Returns: bool: If clicked to handle any map event. """ if self.handle_map_get_items(): return True if self.handle_map_archives(): return True if self.handle_guild_popup_cancel(): return True if self.handle_ash_popup(): return True if self.handle_urgent_commission(save_get_items=False): return True if self.handle_story_skip(): return True return False _os_in_map_confirm_timer = Timer(1.5, count=3) def handle_os_in_map(self): """ Returns: bool: If is in map and confirmed. """ if self.is_in_map(): if self._os_in_map_confirm_timer.reached(): return True else: return False else: self._os_in_map_confirm_timer.reset() return False def ensure_no_map_event(self): self._os_in_map_confirm_timer.reset() while 1: self.device.screenshot() if self.handle_map_event(): continue # End if self.handle_os_in_map(): break def os_auto_search_quit(self): confirm_timer = Timer(1, count=2).start() while 1: self.device.screenshot() if self.appear_then_click(AUTO_SEARCH_REWARD, offset=(20, 20), interval=1): confirm_timer.reset() continue if self.handle_map_event(): confirm_timer.reset() continue # End if self.is_in_map(): if confirm_timer.reached(): break else: confirm_timer.reset() def handle_os_auto_search_map_option(self): """ Returns: bool: If clicked. """ if self.appear(AUTO_SEARCH_OS_MAP_OPTION_OFF, offset=(5, 50)) \ and AUTO_SEARCH_OS_MAP_OPTION_OFF.match_appear_on(self.device.image) \ and self.info_bar_count() >= 2: raise CampaignEnd if self.appear(AUTO_SEARCH_REWARD, offset=(20, 50)): self.os_auto_search_quit() raise CampaignEnd if self.appear(AUTO_SEARCH_OS_MAP_OPTION_OFF, offset=(5, 50), interval=3) \ and AUTO_SEARCH_OS_MAP_OPTION_OFF.match_appear_on(self.device.image): self.device.click(AUTO_SEARCH_OS_MAP_OPTION_OFF) return True
def enter_map(self, button, mode='normal', skip_first_screenshot=True): """Enter a campaign. Args: button: Campaign to enter. mode (str): 'normal' or 'hard' or 'cd' skip_first_screenshot (bool): """ logger.hr('Enter map') campaign_timer = Timer(5) map_timer = Timer(5) fleet_timer = Timer(5) campaign_click = 0 map_click = 0 fleet_click = 0 checked_in_map = False self.stage_entrance = button with self.stat.new(genre=self.config.campaign_name, method=self.config.DropRecord_CombatRecord) as drop: while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() # Check errors if campaign_click > 5: logger.critical( f"Failed to enter {button}, too many click on {button}" ) logger.critical( "Possible reason #1: You haven't reached the commander level to unlock this stage." ) raise RequestHumanTakeover if fleet_click > 5: logger.critical( f"Failed to enter {button}, too many click on FLEET_PREPARATION" ) logger.critical( "Possible reason #1: " "Your fleets haven't satisfied the stat restrictions of this stage." ) logger.critical( "Possible reason #2: " "This stage can only be farmed once a day, " "but it's the second time that you are entering") raise RequestHumanTakeover # Already in map if not checked_in_map and self.is_in_map(): logger.info('Already in map, skip enter_map.') return False else: checked_in_map = True # Map preparation if map_timer.reached() and self.handle_map_preparation(): self.map_get_info() self.handle_fast_forward() self.handle_auto_search() if self.triggered_map_stop(): self.enter_map_cancel() self.handle_map_stop() raise ScriptEnd( f'Reach condition: {self.config.StopCondition_MapAchievement}' ) self.device.click(MAP_PREPARATION) map_click += 1 map_timer.reset() campaign_timer.reset() continue # Fleet preparation if fleet_timer.reached() and self.appear(FLEET_PREPARATION, offset=(20, 20)): if mode == 'normal' or mode == 'hard': self.handle_2x_book_setting(mode='prep') self.fleet_preparation() self.handle_auto_submarine_call_disable() self.handle_auto_search_setting() self.map_fleet_checked = True self.device.click(FLEET_PREPARATION) fleet_click += 1 fleet_timer.reset() campaign_timer.reset() continue # Auto search continue if self.handle_auto_search_continue(): campaign_timer.reset() continue # Retire if self.handle_retirement(): continue # Use Data Key if self.handle_use_data_key(): continue # Emotion if self.handle_combat_low_emotion(): continue # Urgent commission if self.handle_urgent_commission(drop=drop): continue # 2X book popup if self.handle_2x_book_popup(): continue # Story skip if self.handle_story_skip(): campaign_timer.reset() continue # Enter campaign if campaign_timer.reached() and self.appear_then_click(button): campaign_click += 1 campaign_timer.reset() continue # End if self.map_is_auto_search: if self.is_auto_search_running(): break else: if self.handle_in_map_with_enemy_searching(): # self.handle_map_after_combat_story() break return True
def set(self, main, left=None, right=None, upper=None, bottom=None, skip_first_screenshot=True): """ Set nav bar from 1 direction. Args: main (ModuleBase): left (int): Index of nav item counted from left. Start from 1. right (int): Index of nav item counted from right. Start from 1. upper (int): Index of nav item counted from upper. Start from 1. bottom (int): Index of nav item counted from bottom. Start from 1. skip_first_screenshot (bool): Returns: bool: If success """ if left is None and right is None and upper is None and bottom is None: logger.warning( 'Invalid index to set, must set an index from 1 direction') return False text = '' if left is None and upper is not None: left = upper if right is None and bottom is not None: right = bottom for k in ['left', 'right', 'upper', 'bottom']: if locals().get(k, None) is not None: text += f'{k}={locals().get(k, None)} ' logger.info(f'{self.name} set to {text.strip()}') interval = Timer(2, count=4) timeout = Timer(10, count=20).start() while 1: if skip_first_screenshot: skip_first_screenshot = False else: main.device.screenshot() if timeout.reached(): logger.warning(f'{self.name} failed to set {text.strip()}') return False active, minimum, maximum = self.get_info(main=main) logger.info( f'Nav item active: {active} from range ({minimum}, {maximum})') # if active is None: # continue index = minimum + left - 1 if left is not None else maximum - right + 1 if not minimum <= index <= maximum: logger.warning( f'Index to set ({index}) is not within the nav items that appears ({minimum}, {maximum})' ) continue # End if active == index: return True if interval.reached(): main.device.click(self.grids.buttons[index]) main.device.sleep((0.1, 0.2)) interval.reset()
def ui_goto(self, destination, offset=(20, 20), confirm_wait=0, skip_first_screenshot=True): """ Args: destination (Page): offset: confirm_wait: skip_first_screenshot: """ # Reset connection for page in self.ui_pages: page.parent = None # Create connection visited = [destination] visited = set(visited) while 1: new = visited.copy() for page in visited: for link in self.ui_pages: if link in visited: continue if page in link.links: link.parent = page new.add(link) if len(new) == len(visited): break visited = new logger.hr(f'UI goto {destination}') confirm_timer = Timer(confirm_wait, count=int(confirm_wait // 0.5)).start() while 1: GOTO_MAIN.clear_offset() if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() # Destination page if self.appear(destination.check_button, offset=offset): if confirm_timer.reached(): break else: confirm_timer.reset() # Other pages for page in visited: if page.parent is None or page.check_button is None: continue if self.appear(page.check_button, offset=offset, interval=5): self.device.click(page.links[page.parent]) confirm_timer.reset() break # Additional if self.ui_additional(): continue # Reset connection for page in self.ui_pages: page.parent = None
class MapOperation(MysteryHandler, FleetPreparation, Retirement, FastForwardHandler): map_cat_attack_timer = Timer(2) map_clear_percentage_prev = -1 map_clear_percentage_timer = Timer(0.3, count=1) # Fleet that shows on screen. fleet_show_index = 1 # Note that this is different from get_fleet_current_index() # In fleet_current_index, 1 means mob fleet, 2 means boss fleet. fleet_current_index = 1 def get_fleet_show_index(self): """ Get the fleet that shows on screen. Returns: int: 1 or 2 Pages: in: in_map """ if self.appear(FLEET_NUM_1, offset=(20, 20)): self.fleet_show_index = 1 return 1 elif self.appear(FLEET_NUM_2, offset=(20, 20)): self.fleet_show_index = 2 return 2 else: logger.warning('Unknown fleet current index, use 1 by default') self.fleet_show_index = 1 return 1 def get_fleet_current_index(self): """ Returns: int: 1 or 2 """ if self.fleets_reversed: self.fleet_current_index = 3 - self.fleet_show_index return self.fleet_current_index else: self.fleet_current_index = self.fleet_show_index return self.fleet_current_index def fleet_set(self, index=None, skip_first_screenshot=True): """ Args: index (int): Target fleet_current_index skip_first_screenshot (bool): Returns: bool: If switched. """ logger.info(f'Fleet set to {index}') timeout = Timer(5, count=10).start() count = 0 while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() if timeout.reached(): logger.warning( 'Fleet set timeout, assume current fleet is correct') break if self.handle_story_skip(): timeout.reset() continue if self.handle_in_stage(): timeout.reset() continue self.get_fleet_show_index() self.get_fleet_current_index() logger.info( f'Fleet: {self.fleet_show_index}, fleet_current_index: {self.fleet_current_index}' ) if self.fleet_current_index == index: break elif self.appear_then_click(SWITCH_OVER): count += 1 self.device.sleep((1, 1.5)) timeout.reset() continue else: logger.warning('SWITCH_OVER not found') continue return count > 0 def enter_map(self, button, mode='normal', skip_first_screenshot=True): """Enter a campaign. Args: button: Campaign to enter. mode (str): 'normal' or 'hard' or 'cd' skip_first_screenshot (bool): """ logger.hr('Enter map') campaign_timer = Timer(5) map_timer = Timer(5) fleet_timer = Timer(5) campaign_click = 0 map_click = 0 fleet_click = 0 checked_in_map = False self.stage_entrance = button with self.stat.new(genre=self.config.campaign_name, method=self.config.DropRecord_CombatRecord) as drop: while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() # Check errors if campaign_click > 5: logger.critical( f"Failed to enter {button}, too many click on {button}" ) logger.critical( "Possible reason #1: You haven't reached the commander level to unlock this stage." ) raise RequestHumanTakeover if fleet_click > 5: logger.critical( f"Failed to enter {button}, too many click on FLEET_PREPARATION" ) logger.critical( "Possible reason #1: " "Your fleets haven't satisfied the stat restrictions of this stage." ) logger.critical( "Possible reason #2: " "This stage can only be farmed once a day, " "but it's the second time that you are entering") raise RequestHumanTakeover # Already in map if not checked_in_map and self.is_in_map(): logger.info('Already in map, skip enter_map.') return False else: checked_in_map = True # Map preparation if map_timer.reached() and self.handle_map_preparation(): self.map_get_info() self.handle_fast_forward() self.handle_auto_search() if self.triggered_map_stop(): self.enter_map_cancel() self.handle_map_stop() raise ScriptEnd( f'Reach condition: {self.config.StopCondition_MapAchievement}' ) self.device.click(MAP_PREPARATION) map_click += 1 map_timer.reset() campaign_timer.reset() continue # Fleet preparation if fleet_timer.reached() and self.appear(FLEET_PREPARATION, offset=(20, 20)): if mode == 'normal' or mode == 'hard': self.handle_2x_book_setting(mode='prep') self.fleet_preparation() self.handle_auto_submarine_call_disable() self.handle_auto_search_setting() self.map_fleet_checked = True self.device.click(FLEET_PREPARATION) fleet_click += 1 fleet_timer.reset() campaign_timer.reset() continue # Auto search continue if self.handle_auto_search_continue(): campaign_timer.reset() continue # Retire if self.handle_retirement(): continue # Use Data Key if self.handle_use_data_key(): continue # Emotion if self.handle_combat_low_emotion(): continue # Urgent commission if self.handle_urgent_commission(drop=drop): continue # 2X book popup if self.handle_2x_book_popup(): continue # Story skip if self.handle_story_skip(): campaign_timer.reset() continue # Enter campaign if campaign_timer.reached() and self.appear_then_click(button): campaign_click += 1 campaign_timer.reset() continue # End if self.map_is_auto_search: if self.is_auto_search_running(): break else: if self.handle_in_map_with_enemy_searching(): # self.handle_map_after_combat_story() break return True def enter_map_cancel(self, skip_first_screenshot=True): logger.hr('Enter map cancel') while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() if self.appear(MAP_PREPARATION, offset=(20, 20)) or self.appear( FLEET_PREPARATION, offset=(20, 20)): self.device.click(MAP_PREPARATION_CANCEL) continue if self.is_in_stage(): break return True def handle_map_preparation(self): """ Returns: bool: If MAP_PREPARATION and tha animation of map information finished """ if not self.appear(MAP_PREPARATION, offset=(20, 20)): self.map_clear_percentage_prev = -1 self.map_clear_percentage_timer.reset() return False percent = self.get_map_clear_percentage() logger.attr('Map_clear_percentage', percent) # Comment this because percentage starts from 100% and increase from 0% to actual value # if percent > 0.95: # # map clear percentage 100%, exit directly # return True if abs(percent - self.map_clear_percentage_prev) < 0.02: self.map_clear_percentage_prev = percent if self.map_clear_percentage_timer.reached(): return True else: return False else: self.map_clear_percentage_prev = percent self.map_clear_percentage_timer.reset() return False def withdraw(self, skip_first_screenshot=True): """ Withdraw campaign. """ logger.hr('Map withdraw') while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() if self.handle_popup_confirm('WITHDRAW'): continue if self.appear_then_click(WITHDRAW, interval=5): continue if self.handle_auto_search_exit(): continue # End if self.handle_in_stage(): raise CampaignEnd('Withdraw') def handle_map_cat_attack(self): """ Click to skip the animation when cat attacks. """ if not self.map_cat_attack_timer.reached(): return False if self.image_color_count(MAP_CAT_ATTACK, color=(255, 231, 123), threshold=221, count=100): logger.info('Skip map cat attack') self.device.click(MAP_CAT_ATTACK) self.map_cat_attack_timer.reset() return True if not self.map_is_clear_mode: # Threat: Med has 106 pixels count, MAP_CAT_ATTACK_MIRROR has 290. if self.image_color_count(MAP_CAT_ATTACK_MIRROR, color=(255, 231, 123), threshold=221, count=200): logger.info('Skip map being attack') self.device.click(MAP_CAT_ATTACK) self.map_cat_attack_timer.reset() return True return False @property def fleets_reversed(self): if not self.config.FLEET_2: return False return self.config.Fleet_FleetOrder in [ 'fleet1_boss_fleet2_mob', 'fleet1_standby_fleet2_all' ] def handle_fleet_reverse(self): """ The game chooses the fleet with a smaller index to be the first fleet, no matter what we choose in fleet preparation. After the update of auto-search, the game no longer ignore user settings. Returns: bool: Fleet changed """ if not self.map_is_hard_mode \ and self.config.Fleet_FleetOrder in ['fleet1_boss_fleet2_mob', 'fleet1_standby_fleet2_all']: logger.warning( f"You shouldn't use a reversed fleet order ({self.config.Fleet_FleetOrder}) in normal mode." ) logger.warning( 'Please reverse your Fleet 1 and Fleet 2, ' 'use "fleet1_mob_fleet2_boss" or "fleet1_all_fleet2_standby"') # raise RequestHumanTakeover if not self.fleets_reversed: return False return self.fleet_set(index=2)
def wait_until_walk_stable(self, confirm_timer=None, skip_first_screenshot=False, walk_out_of_step=True, drop=None): """ Wait until homo_loca stabled. DETECTION_BACKEND must be 'homography'. Args: confirm_timer (Timer): skip_first_screenshot (bool): walk_out_of_step (bool): If catch walk_out_of_step error. Default to True, use False in abyssal zones. drop (DropImage): Raises: MapWalkError: If unable to goto such grid. """ logger.hr('Wait until walk stable') record = None enemy_searching_appear = False self.device.screenshot_interval_set(0.35) if confirm_timer is None: confirm_timer = Timer(0.8, count=2) confirm_timer.reset() while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() # Map event if self.handle_map_event(drop=drop): confirm_timer.reset() continue if self.handle_retirement(): confirm_timer.reset() continue if self.handle_walk_out_of_step(): if walk_out_of_step: raise MapWalkError('walk_out_of_step') else: continue # Enemy searching if not enemy_searching_appear and self.enemy_searching_appear(): enemy_searching_appear = True confirm_timer.reset() continue else: if enemy_searching_appear: self.handle_enemy_flashing() self.device.sleep(0.3) logger.info('Enemy searching appeared.') enemy_searching_appear = False if self.is_in_map(): self.enemy_searching_color_initial() # Combat if self.combat_appear(): # Use ui_back() for testing, because there are too few abyssal loggers every month. # self.ui_back(check_button=self.is_in_map) self.combat(expected_end=self.is_in_map, fleet_index=self.fleet_show_index, save_get_items=drop) confirm_timer.reset() continue # Arrive # Check colors, because screen goes black when something is unlocking. if self.is_in_map() and IN_MAP.match_appear_on(self.device.image): self.update_os() current = self.view.backend.homo_loca logger.attr('homo_loca', current) if record is None or (current is not None and np.linalg.norm( np.subtract(current, record)) < 3): if confirm_timer.reached(): break else: confirm_timer.reset() record = current else: confirm_timer.reset() logger.info('Walk stabled') self.device.screenshot_interval_set()
class Combat(HPBalancer, EnemySearchingHandler, Retirement, SubmarineCall, CombatAuto, CombatManual): _automation_set_timer = Timer(1) _emotion: Emotion battle_status_click_interval = 0 @property def emotion(self): if not hasattr(self, '_emotion'): self._emotion = Emotion(config=self.config) return self._emotion def combat_appear(self): """ Returns: bool: If enter combat. """ if self.config.ENABLE_MAP_FLEET_LOCK and not self.is_in_map(): if self.is_combat_loading(): return True if self.appear(BATTLE_PREPARATION): return True if self.appear(BATTLE_PREPARATION_WITH_OVERLAY) and self.handle_combat_automation_confirm(): return True return False def map_offensive(self): while 1: self.device.screenshot() if self.appear_then_click(MAP_OFFENSIVE, interval=1): continue if self.handle_combat_low_emotion(): continue if self.handle_retirement(): continue # Break if self.combat_appear(): break def is_combat_loading(self): """ Returns: bool: """ left = color_bar_percentage(self.device.image, area=LOADING_BAR.area, prev_color=(99, 150, 255)) right = color_bar_percentage(self.device.image, area=LOADING_BAR.area, prev_color=(225, 225, 225), reverse=True) if 0.15 < left < 0.95 and right > 0.15 and left + right <= 1.2: logger.attr('Loading', f'{int(left * 100)}%({int(right * 100)}%)') return True return False def is_combat_executing(self): """ Returns: bool: """ return self.appear(PAUSE) and np.max(self.device.image.crop(PAUSE_DOUBLE_CHECK.area)) < 153 def handle_combat_automation_confirm(self): if self.appear(AUTOMATION_CONFIRM_CHECK, interval=1): self.appear_then_click(AUTOMATION_CONFIRM, offset=True) return True return False def combat_preparation(self, balance_hp=False, emotion_reduce=False, auto=True, fleet_index=1): """ Args: balance_hp (bool): emotion_reduce (bool): auto (bool): fleet_index (int): """ logger.info('Combat preparation.') if emotion_reduce: self.emotion.wait(fleet=fleet_index) if balance_hp: self.hp_balance() while 1: self.device.screenshot() if self.appear(BATTLE_PREPARATION): if self.handle_combat_automation_set(auto=auto): continue if self.handle_retirement(): if self.config.ENABLE_HP_BALANCE: self.wait_until_appear(BATTLE_PREPARATION) # When re-entering battle_preparation page, the emergency icon is active by default, even if # nothing to use. After a short animation, everything shows as usual. self.device.sleep(0.5) # Wait animation. continue if self.handle_combat_low_emotion(): continue if self.handle_emergency_repair_use(): continue if self.appear_then_click(BATTLE_PREPARATION, interval=2): continue if self.handle_combat_automation_confirm(): continue if self.handle_story_skip(): continue # End if self.is_combat_executing(): if emotion_reduce: self.emotion.reduce(fleet_index) break def handle_combat_automation_set(self, auto): """ Args: auto (bool): If use auto. Returns: bool: """ if not self._automation_set_timer.reached(): return False if self.appear(AUTOMATION_ON): logger.info('[Automation] ON') if not auto: self.device.click(AUTOMATION_SWITCH) self.device.sleep(1) self._automation_set_timer.reset() return True if self.appear(AUTOMATION_OFF): logger.info('[Automation] OFF') if auto: self.device.click(AUTOMATION_SWITCH) self.device.sleep(1) self._automation_set_timer.reset() return True if self.appear_then_click(AUTOMATION_CONFIRM, offset=True): self._automation_set_timer.reset() return True return False def handle_emergency_repair_use(self): if not self.config.ENABLE_HP_BALANCE: return False if self.appear_then_click(EMERGENCY_REPAIR_CONFIRM, offset=True): self.device.sleep(0.5) # Animation: hp increase and emergency_repair amount decrease. return True if self.appear(BATTLE_PREPARATION) and self.appear(EMERGENCY_REPAIR_AVAILABLE): logger.info('EMERGENCY_REPAIR_AVAILABLE') if not len(self.hp): return False if np.min(np.array(self.hp)[np.array(self.hp) > 0.001]) < self.config.EMERGENCY_REPAIR_SINGLE_THRESHOLD \ or np.max(self.hp[:3]) < self.config.EMERGENCY_REPAIR_HOLE_THRESHOLD \ or np.max(self.hp[3:]) < self.config.EMERGENCY_REPAIR_HOLE_THRESHOLD: logger.info('Use emergency repair') self.device.click(EMERGENCY_REPAIR_AVAILABLE) return True return False def combat_execute(self, auto=True, call_submarine_at_boss=False, save_get_items=False): """ Args: auto (bool): call_submarine_at_boss (bool): save_get_items (bool) """ logger.info('Combat execute') self.submarine_call_reset() self.combat_auto_reset() self.combat_manual_reset() confirm_timer = Timer(10) confirm_timer.start() self.device.screenshot_interval_set(self.config.COMBAT_SCREENSHOT_INTERVAL) while 1: self.device.screenshot() if not confirm_timer.reached() and self.appear_then_click(AUTOMATION_CONFIRM, offset=True): continue if self.handle_story_skip(): continue if self.handle_combat_auto(): continue if self.handle_combat_manual(): continue if not auto and self.is_combat_executing(): if self.handle_combat_weapon_release(): continue if call_submarine_at_boss: pass else: if self.handle_submarine_call(): continue # End if self.handle_battle_status(save_get_items=save_get_items): self.device.screenshot_interval_set(0) break def handle_battle_status(self, save_get_items=False): """ Args: save_get_items (bool): Returns: bool: """ if self.is_combat_executing(): return False if self.appear_then_click(BATTLE_STATUS_S, screenshot=save_get_items, genre='status', interval=self.battle_status_click_interval): if not save_get_items: self.device.sleep((0.25, 0.5)) return True if self.appear_then_click(BATTLE_STATUS_A, screenshot=save_get_items, genre='status', interval=self.battle_status_click_interval): logger.warning('Battle status: A') if not save_get_items: self.device.sleep((0.25, 0.5)) return True if self.appear_then_click(BATTLE_STATUS_B, screenshot=save_get_items, genre='status', interval=self.battle_status_click_interval): logger.warning('Battle Status B') if not save_get_items: self.device.sleep((0.25, 0.5)) return True return False def handle_get_items(self, save_get_items=False): """ Args: save_get_items (bool): Returns: bool: """ if self.appear_then_click(GET_ITEMS_1, screenshot=save_get_items, genre='get_items', offset=5, interval=self.battle_status_click_interval): self.interval_reset(BATTLE_STATUS_S) self.interval_reset(BATTLE_STATUS_A) self.interval_reset(BATTLE_STATUS_B) return True if self.appear_then_click(GET_ITEMS_2, screenshot=save_get_items, genre='get_items', offset=5, interval=self.battle_status_click_interval): self.interval_reset(BATTLE_STATUS_S) self.interval_reset(BATTLE_STATUS_A) self.interval_reset(BATTLE_STATUS_B) return True return False def handle_exp_info(self): """ Returns: bool: """ if self.appear_then_click(EXP_INFO_S): self.device.sleep((0.25, 0.5)) return True if self.appear_then_click(EXP_INFO_A): self.device.sleep((0.25, 0.5)) return True if self.appear_then_click(EXP_INFO_B): self.device.sleep((0.25, 0.5)) return True return False def handle_get_ship(self, save_get_items=False): """ Args: save_get_items (bool): Returns: bool: """ if self.appear_then_click(GET_SHIP, screenshot=save_get_items, genre='get_ship'): return True return False def combat_status(self, save_get_items=False, expected_end=None): """ Args: save_get_items (bool): expected_end (str): with_searching, no_searching, in_stage. """ logger.info('Combat status') logger.attr('expected_end', expected_end) exp_info = False # This is for the white screen bug in game while 1: self.device.screenshot() # Combat status if not exp_info and self.handle_get_ship(save_get_items=save_get_items): continue if self.handle_get_items(save_get_items=save_get_items): continue if self.handle_battle_status(save_get_items=save_get_items): continue if self.handle_popup_confirm(): continue if self.handle_exp_info(): exp_info = True continue if self.handle_urgent_commission(save_get_items=save_get_items): continue if self.handle_story_skip(): continue # End if self.handle_in_stage(): break if expected_end is None: if self.handle_in_map_with_enemy_searching(): break if isinstance(expected_end, str): if expected_end == 'in_stage' and self.handle_in_stage(): break if expected_end == 'with_searching' and self.handle_in_map_with_enemy_searching(): break if expected_end == 'no_searching' and self.handle_in_map_no_enemy_searching(): break if expected_end == 'in_ui' and self.appear(BACK_ARROW, offset=(20, 20)): break if callable(expected_end): if expected_end(): break def combat(self, balance_hp=None, emotion_reduce=None, func=None, call_submarine_at_boss=None, save_get_items=None, expected_end=None, fleet_index=1): """ Execute a combat. """ balance_hp = balance_hp if balance_hp is not None else self.config.ENABLE_HP_BALANCE emotion_reduce = emotion_reduce if emotion_reduce is not None else self.config.ENABLE_EMOTION_REDUCE auto = self.config.COMBAT_AUTO_MODE == 'combat_auto' call_submarine_at_boss = call_submarine_at_boss if call_submarine_at_boss is not None else self.config.SUBMARINE_CALL_AT_BOSS save_get_items = save_get_items if save_get_items is not None else self.config.ENABLE_SAVE_GET_ITEMS self.battle_status_click_interval = 3 if save_get_items else 0 # if not hasattr(self, 'emotion'): # self.emotion = Emotion(config=self.config) self.combat_preparation( balance_hp=balance_hp, emotion_reduce=emotion_reduce, auto=auto, fleet_index=fleet_index) self.combat_execute( auto=auto, call_submarine_at_boss=call_submarine_at_boss, save_get_items=save_get_items) self.combat_status( save_get_items=save_get_items, expected_end=expected_end) self.handle_map_after_combat_story()
class Combat(HPBalancer, EnemySearchingHandler, Retirement, SubmarineCall, CombatAuto, CombatManual): _automation_set_timer = Timer(1) _emotion: Emotion battle_status_click_interval = 0 @property def emotion(self): if not hasattr(self, '_emotion'): self._emotion = Emotion(config=self.config) return self._emotion def combat_appear(self): """ Returns: bool: If enter combat. """ if self.config.ENABLE_MAP_FLEET_LOCK and not self.is_in_map(): if self.is_combat_loading(): return True if self.appear(BATTLE_PREPARATION): return True if self.appear(BATTLE_PREPARATION_WITH_OVERLAY ) and self.handle_combat_automation_confirm(): return True return False def map_offensive(self): while 1: self.device.screenshot() if self.appear_then_click(MAP_OFFENSIVE, interval=1): continue if self.handle_combat_low_emotion(): continue if self.handle_retirement(): continue # Break if self.combat_appear(): break def is_combat_loading(self): """ Returns: bool: """ similarity, location = TEMPLATE_COMBAT_LOADING.match_result( self.device.image.crop((0, 620, 1280, 720))) if similarity > 0.85: loading = (location[0] + 38 - LOADING_BAR.area[0]) / ( LOADING_BAR.area[2] - LOADING_BAR.area[0]) logger.attr('Loading', f'{int(loading * 100)}%') return True else: return False def is_combat_executing(self): """ Returns: bool: """ return self.appear(PAUSE) and np.max( self.device.image.crop(PAUSE_DOUBLE_CHECK.area)) < 153 def ensure_combat_oil_loaded(self): self.wait_until_stable(COMBAT_OIL_LOADING) def handle_combat_automation_confirm(self): if self.appear(AUTOMATION_CONFIRM_CHECK, interval=1): self.appear_then_click(AUTOMATION_CONFIRM, offset=True) return True return False def combat_preparation(self, balance_hp=False, emotion_reduce=False, auto='combat_auto', fleet_index=1): """ Args: balance_hp (bool): emotion_reduce (bool): auto (str): fleet_index (int): """ logger.info('Combat preparation.') if emotion_reduce: self.emotion.wait(fleet=fleet_index) if balance_hp: self.hp_balance() while 1: self.device.screenshot() if self.appear(BATTLE_PREPARATION): if self.handle_combat_automation_set( auto=auto == 'combat_auto'): continue if self.handle_retirement(): continue if self.handle_combat_low_emotion(): continue if balance_hp and self.handle_emergency_repair_use(): continue if self.appear_then_click(BATTLE_PREPARATION, interval=2): continue if self.handle_combat_automation_confirm(): continue if self.handle_story_skip(): continue # End if self.is_combat_executing(): if emotion_reduce: self.emotion.reduce(fleet_index) break def handle_combat_automation_set(self, auto): """ Args: auto (bool): If use auto. Returns: bool: """ if not self._automation_set_timer.reached(): return False if self.appear(AUTOMATION_ON): logger.info('[Automation] ON') if not auto: self.device.click(AUTOMATION_SWITCH) self.device.sleep(1) self._automation_set_timer.reset() return True if self.appear(AUTOMATION_OFF): logger.info('[Automation] OFF') if auto: self.device.click(AUTOMATION_SWITCH) self.device.sleep(1) self._automation_set_timer.reset() return True if self.appear_then_click(AUTOMATION_CONFIRM, offset=True): self._automation_set_timer.reset() return True return False def handle_emergency_repair_use(self): if self.appear_then_click(EMERGENCY_REPAIR_CONFIRM, offset=True): return True if self.appear(BATTLE_PREPARATION) and self.appear( EMERGENCY_REPAIR_AVAILABLE): # When entering battle_preparation page (or after emergency repairing), the emergency icon is active by default, # even if nothing to use. After a short animation, everything shows as usual. Using fleet power number as a # stable checker. First wait for it to be non-zero, then wait for it to be stable. self.wait_until_disappear(MAIN_FLEET_POWER_ZERO, offset=(20, 20)) stable_checker = Button(area=MAIN_FLEET_POWER_ZERO.area, color=(), button=MAIN_FLEET_POWER_ZERO.button, name='STABLE_CHECKER') self.wait_until_stable(stable_checker) if not self.appear(EMERGENCY_REPAIR_AVAILABLE): return False logger.info('EMERGENCY_REPAIR_AVAILABLE') if not len(self.hp): return False if np.min(np.array(self.hp)[np.array(self.hp) > 0.001]) < self.config.EMERGENCY_REPAIR_SINGLE_THRESHOLD \ or np.max(self.hp[:3]) < self.config.EMERGENCY_REPAIR_HOLE_THRESHOLD \ or np.max(self.hp[3:]) < self.config.EMERGENCY_REPAIR_HOLE_THRESHOLD: logger.info('Use emergency repair') self.device.click(EMERGENCY_REPAIR_AVAILABLE) return True return False def combat_execute(self, auto='combat_auto', call_submarine_at_boss=False, save_get_items=False): """ Args: auto (str): Combat auto mode. call_submarine_at_boss (bool): save_get_items (bool) """ logger.info('Combat execute') self.submarine_call_reset() self.combat_auto_reset() self.combat_manual_reset() confirm_timer = Timer(10) confirm_timer.start() self.device.screenshot_interval_set( self.config.COMBAT_SCREENSHOT_INTERVAL) while 1: self.device.screenshot() if not confirm_timer.reached() and self.appear_then_click( AUTOMATION_CONFIRM, offset=True): continue if self.handle_story_skip(): continue if self.handle_combat_auto(auto): continue if self.handle_combat_manual(auto): continue if auto != 'combat_auto' and self.auto_mode_checked and self.is_combat_executing( ): if self.handle_combat_weapon_release(): continue if call_submarine_at_boss: pass else: if self.handle_submarine_call(): continue # End if self.handle_battle_status( save_get_items=save_get_items) or self.handle_get_items( save_get_items=save_get_items): self.device.screenshot_interval_set(0) break def handle_battle_status(self, save_get_items=False): """ Args: save_get_items (bool): Returns: bool: """ if self.is_combat_executing(): return False if self.appear_then_click(BATTLE_STATUS_S, screenshot=save_get_items, genre='status', interval=self.battle_status_click_interval): if not save_get_items: self.device.sleep((0.25, 0.5)) return True if self.appear_then_click(BATTLE_STATUS_A, screenshot=save_get_items, genre='status', interval=self.battle_status_click_interval): logger.warning('Battle status: A') if not save_get_items: self.device.sleep((0.25, 0.5)) return True if self.appear_then_click(BATTLE_STATUS_B, screenshot=save_get_items, genre='status', interval=self.battle_status_click_interval): logger.warning('Battle Status B') if not save_get_items: self.device.sleep((0.25, 0.5)) return True return False def handle_get_items(self, save_get_items=False): """ Args: save_get_items (bool): Returns: bool: """ if self.appear_then_click(GET_ITEMS_1, screenshot=save_get_items, genre='get_items', offset=5, interval=self.battle_status_click_interval): self.interval_reset(BATTLE_STATUS_S) self.interval_reset(BATTLE_STATUS_A) self.interval_reset(BATTLE_STATUS_B) return True if self.appear_then_click(GET_ITEMS_2, screenshot=save_get_items, genre='get_items', offset=5, interval=self.battle_status_click_interval): self.interval_reset(BATTLE_STATUS_S) self.interval_reset(BATTLE_STATUS_A) self.interval_reset(BATTLE_STATUS_B) return True return False def handle_exp_info(self): """ Returns: bool: """ if self.is_combat_executing(): return False if self.appear_then_click(EXP_INFO_S): self.device.sleep((0.25, 0.5)) return True if self.appear_then_click(EXP_INFO_A): self.device.sleep((0.25, 0.5)) return True if self.appear_then_click(EXP_INFO_B): self.device.sleep((0.25, 0.5)) return True return False def handle_get_ship(self, save_get_items=False): """ Args: save_get_items (bool): Returns: bool: """ if self.appear_then_click(GET_SHIP, screenshot=save_get_items, genre='get_ship'): return True return False def combat_status(self, save_get_items=False, expected_end=None): """ Args: save_get_items (bool): expected_end (str): with_searching, no_searching, in_stage. """ logger.info('Combat status') logger.attr( 'expected_end', expected_end.__name__ if callable(expected_end) else expected_end) exp_info = False # This is for the white screen bug in game while 1: self.device.screenshot() # Combat status if not exp_info and self.handle_get_ship( save_get_items=save_get_items): continue if self.handle_get_items(save_get_items=save_get_items): continue if self.handle_battle_status(save_get_items=save_get_items): continue if self.handle_popup_confirm(): continue if self.handle_exp_info(): exp_info = True continue if self.handle_urgent_commission(save_get_items=save_get_items): continue if self.handle_story_skip(): continue # End if self.handle_in_stage(): break if expected_end is None: if self.handle_in_map_with_enemy_searching(): break if isinstance(expected_end, str): if expected_end == 'in_stage' and self.handle_in_stage(): break if expected_end == 'with_searching' and self.handle_in_map_with_enemy_searching( ): break if expected_end == 'no_searching' and self.handle_in_map_no_enemy_searching( ): break if expected_end == 'in_ui' and self.appear(BACK_ARROW, offset=(20, 20)): break if callable(expected_end): if expected_end(): break def combat(self, balance_hp=None, emotion_reduce=None, auto_mode=None, call_submarine_at_boss=None, save_get_items=None, expected_end=None, fleet_index=1): """ Execute a combat. """ balance_hp = balance_hp if balance_hp is not None else self.config.ENABLE_HP_BALANCE emotion_reduce = emotion_reduce if emotion_reduce is not None else self.config.ENABLE_EMOTION_REDUCE if auto_mode is None: auto_mode = self.config.FLEET_1_AUTO_MODE if fleet_index == 1 else self.config.FLEET_2_AUTO_MODE call_submarine_at_boss = call_submarine_at_boss if call_submarine_at_boss is not None else self.config.SUBMARINE_CALL_AT_BOSS save_get_items = save_get_items if save_get_items is not None else self.config.ENABLE_SAVE_GET_ITEMS self.battle_status_click_interval = 7 if save_get_items else 0 # if not hasattr(self, 'emotion'): # self.emotion = Emotion(config=self.config) self.combat_preparation(balance_hp=balance_hp, emotion_reduce=emotion_reduce, auto=auto_mode, fleet_index=fleet_index) self.combat_execute(auto=auto_mode, call_submarine_at_boss=call_submarine_at_boss, save_get_items=save_get_items) self.combat_status(save_get_items=save_get_items, expected_end=expected_end) self.handle_map_after_combat_story()
class UI(InfoHandler): ui_pages = [page_main, page_campaign, page_fleet, page_exercise, page_daily, page_event, page_sp, page_mission, page_raid, page_reward, page_reshmenu, page_research, page_dormmenu, page_meowfficer] ui_pages_all = [page_main, page_campaign, page_fleet, page_exercise, page_daily, page_event, page_sp, page_mission, page_raid, page_commission, page_event_list, page_tactical, page_reward, page_unknown, page_reshmenu, page_research, page_dormmenu, page_meowfficer] ui_current: Page def ui_page_appear(self, page): """ Args: page (Page): """ return self.appear(page.check_button, offset=(20, 20)) def ensure_button_execute(self, button, offset=0): if isinstance(button, Button) and self.appear(button, offset=offset): return True elif callable(button) and button(): return True else: return False def ui_click(self, click_button, check_button, appear_button=None, additional=None, confirm_wait=1, offset=(20, 20), retry_wait=10, skip_first_screenshot=False): """ Args: click_button (Button): check_button (Button, callable): appear_button (Button, callable): additional (callable): confirm_wait (int, float): offset (bool, int, tuple): retry_wait (int, float): skip_first_screenshot (bool): """ logger.hr('UI click') if appear_button is None: appear_button = click_button click_timer = Timer(retry_wait, count=retry_wait // 0.5) confirm_wait = confirm_wait if additional is not None else 0 confirm_timer = Timer(confirm_wait, count=confirm_wait // 0.5).start() while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() if (isinstance(check_button, Button) and self.appear(check_button, offset=offset)) \ or (callable(check_button) and check_button()): if confirm_timer.reached(): break else: confirm_timer.reset() if click_timer.reached(): if (isinstance(appear_button, Button) and self.appear(appear_button, offset=offset)) \ or (callable(appear_button) and appear_button()): self.device.click(click_button) click_timer.reset() continue if additional is not None: if additional(): continue def ui_get_current_page(self): self.device.screenshot() for page in self.ui_pages: if self.ui_page_appear(page=page): logger.attr('UI', page.name) self.ui_current = page return page logger.info('Unknown ui page') if self.appear_then_click(GOTO_MAIN, offset=(20, 20)): logger.info('Goto page_main') self.ui_current = page_unknown self.ui_goto(page_main, skip_first_screenshot=True) if hasattr(self, 'ui_current'): logger.warning(f'Unrecognized ui_current, using previous: {self.ui_current}') else: logger.info('Unable to goto page_main') logger.warning('Starting from current page is not supported') logger.warning(f'Supported page: {[str(page) for page in self.ui_pages]}') logger.warning(f'Supported page: Any page with a "HOME" button on the upper-right') if not self.device.app_is_running(): raise GameNotRunningError('Game not running') else: exit(1) def ui_goto(self, destination, skip_first_screenshot=False): """ Args: destination (Page): skip_first_screenshot (bool): """ for page in self.ui_pages_all: page.parent = None # Iter visited = [self.ui_current] visited = set(visited) while 1: new = visited.copy() for page in visited: for link in page.links.keys(): if link in visited: continue link.parent = page new.add(link) if len(new) == len(visited): break visited = new # Find path if destination.parent is None: return [] route = [destination] while 1: destination = destination.parent if destination is not None: route.append(destination) else: break if len(route) > 30: logger.warning('UI route too long') logger.warning(str(route)) exit(1) route.reverse() if len(route) < 2: logger.warning('No page route found.') logger.attr('UI route', ' - '.join([p.name for p in route])) # Click for p1, p2 in zip(route[:-1], route[1:]): additional = f'ui_additional_{str(p2)}' self.ui_click( click_button=p1.links[p2], check_button=p2.check_button, additional=self.__getattribute__(additional) if hasattr(self, additional) else None, offset=(20, 20), skip_first_screenshot=skip_first_screenshot) self.ui_current = p2 skip_first_screenshot = True # Reset for page in visited: page.parent = None def ui_ensure(self, destination): """ Args: destination (Page): """ logger.hr('UI ensure') self.ui_get_current_page() if self.ui_current == destination: logger.info('Already at %s' % destination) return False else: logger.info('Goto %s' % destination) self.ui_goto(destination) return True def ui_goto_main(self): return self.ui_ensure(destination=page_main) def handle_stage_icon_spawn(self): self.device.sleep((1, 1.2)) self.device.screenshot() def ui_weigh_anchor(self): if self.ui_ensure(destination=page_campaign): self.handle_stage_icon_spawn() return True else: return False def ui_goto_event(self): if self.ui_ensure(destination=page_event): self.handle_stage_icon_spawn() return True else: return False def ui_goto_sp(self): if self.ui_ensure(destination=page_sp): self.handle_stage_icon_spawn() return True else: return False def ui_ensure_index(self, index, letter, next_button, prev_button, skip_first_screenshot=False, fast=True, interval=(0.2, 0.3), step_sleep=(0.2, 0.3), finish_sleep=(0.5, 0.8)): """ Args: index (int): letter (Ocr, callable): OCR button. next_button (Button): prev_button (Button): skip_first_screenshot (bool): fast (bool): Default true. False when index is not continuous. interval (tuple, int, float): Seconds between two click. step_sleep (tuple, int, float): Seconds between two step. finish_sleep (tuple, int, float): Second to wait when arrive. """ logger.hr('UI ensure index') while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() if isinstance(letter, Ocr): current = letter.ocr(self.device.image) else: current = letter(self.device.image) logger.attr('Index', current) diff = index - current if diff == 0: break button = next_button if diff > 0 else prev_button if fast: self.device.multi_click(button, n=abs(diff), interval=interval) else: self.device.click(button) self.device.sleep(step_sleep) self.device.sleep(finish_sleep) def ui_back(self, check_button, appear_button=None, offset=(20, 20), retry_wait=10, skip_first_screenshot=False): return self.ui_click(click_button=BACK_ARROW, check_button=check_button, appear_button=appear_button, offset=offset, retry_wait=retry_wait, skip_first_screenshot=skip_first_screenshot) def ui_additional_page_main(self): # Research popup, lost connection popup if self.handle_popup_confirm('PAGE_MAIN'): return True # Daily reset if self.appear_then_click(LOGIN_ANNOUNCE, offset=(30, 30), interval=5): return True if self.appear_then_click(GET_ITEMS_1, offset=(30, 30), interval=5): return True if self.appear_then_click(GET_SHIP, interval=5): return True if self.appear_then_click(LOGIN_RETURN_SIGN, offset=(30, 30), interval=5): return True if self.appear_then_click(GOTO_MAIN, offset=(30, 30), interval=5): return True return False _ui_additional_reward_goto_main_timer = Timer(5, count=5) def ui_additional_page_reward(self): # Research popup, lost connection popup if self.handle_popup_confirm('PAGE_REWARD'): return True # Daily reset if self.appear_then_click(LOGIN_ANNOUNCE, offset=(30, 30), interval=5): return True if self.appear_then_click(GET_ITEMS_1, offset=(30, 30), interval=5): return True if self.appear_then_click(GET_SHIP, interval=5): return True if self.appear_then_click(LOGIN_RETURN_SIGN, offset=(30, 30), interval=5): return True if self.appear(EVENT_LIST_CHECK, offset=(30, 30), interval=5) \ or self.appear(RESHMENU_CHECK, offset=(30, 30), interval=5) \ or self.appear(RESEARCH_CHECK, offset=(30, 30), interval=5): self.device.click(GOTO_MAIN) self._ui_additional_reward_goto_main_timer.reset() return True if not self._ui_additional_reward_goto_main_timer.reached(): if self.appear(MAIN_CHECK, offset=(30, 30), interval=5): self.device.click(MAIN_GOTO_REWARD) return True return False
class Device(Screenshot, Control, AppControl): _screen_size_checked = False stuck_record = set() stuck_timer = Timer(60, count=60).start() stuck_timer_long = Timer(300, count=300).start() stuck_long_wait_list = ['BATTLE_STATUS_S', 'PAUSE'] def handle_night_commission(self, hour=21, threshold=30): """ Args: hour (int): Hour that night commission refresh. threshold (int): Seconds around refresh time. Returns: bool: If handled. """ update = self.config.get_server_last_update(since=(hour, )) now = datetime.now().time() if now < (update - timedelta(seconds=threshold)).time(): return False if now > (update + timedelta(seconds=threshold)).time(): return False if GET_MISSION.match(self.image, offset=True): logger.info('Night commission appear.') self.click(GET_MISSION) return True return False def screenshot(self): """ Returns: PIL.Image.Image: """ self.stuck_record_check() super().screenshot() if self.handle_night_commission(): super().screenshot() if not self._screen_size_checked: self.check_screen() self._screen_size_checked = True return self.image def click(self, button, record_check=True): self.stuck_record_clear() return super().click(button, record_check=record_check) def check_screen(self): """ Screen size must be 1280x720. Take a screenshot before call. """ # Check screen size width, height = self.image.size logger.attr('Screen_size', f'{width}x{height}') if width == 1280 and height == 720: return True else: logger.warning(f'Not supported screen size: {width}x{height}') logger.warning('Alas requires 1280x720') logger.hr('Script end') exit(1) # Check screen color # May get a pure black screenshot on some emulators. color = get_color(self.image, area=(0, 0, 1280, 720)) if sum(color) < 1: logger.warning('Received a pure black screenshot') logger.warning(f'Color: {color}') exit(1) def stuck_record_add(self, button): self.stuck_record.add(str(button)) def stuck_record_clear(self): self.stuck_record = set() self.stuck_timer.reset() self.stuck_timer_long.reset() def stuck_record_check(self): reached = self.stuck_timer.reached() reached_long = self.stuck_timer_long.reached() if not reached: return False if not reached_long: for button in self.stuck_long_wait_list: if button in self.stuck_record: return False logger.warning('Wait too long') logger.warning(f'Waiting for {self.stuck_record}') self.stuck_record_clear() if self.config.ENABLE_GAME_STUCK_HANDLER: raise GameStuckError(f'Wait too long')
def _guild_operations_ensure(self, skip_first_screenshot=True): """ Ensure guild operation is loaded After entering guild operation, background loaded first, then dispatch/boss """ logger.attr('Guild master/official', self.config.GuildOperation_SelectNewOperation) confirm_timer = Timer(1.5, count=3).start() click_count = 0 while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() # End if click_count > 5: # Info bar showing `none4302`. # Probably because guild operation has been started by another guild officer already. # Enter guild page again should fix the issue. logger.warning( 'Unable to start/join guild operation, ' 'probably because guild operation has been started by another guild officer already' ) raise GameBugError('Unable to start/join guild operation') if self._handle_guild_operations_start(): confirm_timer.reset() continue if self.appear(GUILD_OPERATIONS_JOIN, interval=3): if self.image_color_count(GUILD_OPERATIONS_MONTHLY_COUNT, color=(255, 93, 90), threshold=221, count=20): logger.info( 'Unable to join operation, no more monthly attempts left' ) self.device.click(GUILD_OPERATIONS_CLICK_SAFE_AREA) else: current, remain, total = GUILD_OPERATIONS_PROGRESS.ocr( self.device.image) threshold = total * self.config.GuildOperation_JoinThreshold if current <= threshold: logger.info( 'Joining Operation, current progress less than ' f'threshold ({threshold:.2f})') self.device.click(GUILD_OPERATIONS_JOIN) else: logger.info( 'Refrain from joining operation, current progress exceeds ' f'threshold ({threshold:.2f})') self.device.click(GUILD_OPERATIONS_CLICK_SAFE_AREA) confirm_timer.reset() continue if self.handle_popup_confirm('JOIN_OPERATION'): click_count += 1 confirm_timer.reset() continue if self.handle_popup_single('FLEET_UPDATED'): logger.info( 'Fleet composition altered, may still be dispatch-able. However ' 'fellow guild members have updated their support line up. ' 'Suggestion: Enable Boss Recommend') confirm_timer.reset() continue # End if self.appear(GUILD_BOSS_ENTER) or self.appear( GUILD_OPERATIONS_ACTIVE_CHECK, offset=(20, 20)): if not self.info_bar_count() and confirm_timer.reached(): break
def wait_until_walk_stable(self, confirm_timer=None, skip_first_screenshot=False, walk_out_of_step=True, drop=None): """ Wait until homo_loca stabled. DETECTION_BACKEND must be 'homography'. Args: confirm_timer (Timer): skip_first_screenshot (bool): walk_out_of_step (bool): If catch walk_out_of_step error. Default to True, use False in abyssal zones. drop (DropImage): Returns: str: Things that fleet met on its way, 'event', 'search', 'akashi', 'combat', or their combinations like 'event_akashi', 'event_combat', or an empty string '' if nothing met. Raises: MapWalkError: If unable to goto such grid. """ logger.hr('Wait until walk stable') record = None enemy_searching_appear = False self.device.screenshot_interval_set(0.35) if confirm_timer is None: confirm_timer = Timer(0.8, count=2) result = set() confirm_timer.reset() while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() # Map event if self.handle_map_event(drop=drop): confirm_timer.reset() result.add('event') continue if self.handle_retirement(): confirm_timer.reset() continue if self.handle_walk_out_of_step(): if walk_out_of_step: raise MapWalkError('walk_out_of_step') else: continue # Accident click if self.is_in_globe(): self.os_globe_goto_map() confirm_timer.reset() continue if self.is_in_storage(): self.storage_quit() confirm_timer.reset() continue if self.is_in_os_mission(): self.os_mission_quit() confirm_timer.reset() continue if self.handle_os_game_tips(): confirm_timer.reset() continue # Enemy searching if not enemy_searching_appear and self.enemy_searching_appear(): enemy_searching_appear = True confirm_timer.reset() continue else: if enemy_searching_appear: self.handle_enemy_flashing() self.device.sleep(0.3) logger.info('Enemy searching appeared.') enemy_searching_appear = False result.add('search') if self.is_in_map(): self.enemy_searching_color_initial() # Combat if self.combat_appear(): # Use ui_back() for testing, because there are too few abyssal loggers every month. # self.ui_back(check_button=self.is_in_map) self.combat(expected_end=self.is_in_map, fleet_index=self.fleet_show_index, save_get_items=drop) confirm_timer.reset() result.add('event') continue # Akashi shop if self.appear(PORT_SUPPLY_CHECK, offset=(20, 20)): self.interval_clear(PORT_SUPPLY_CHECK) self.handle_akashi_supply_buy(CLICK_SAFE_AREA) confirm_timer.reset() result.add('akashi') continue # Arrive # Check colors, because screen goes black when something is unlocking. if self.is_in_map() and IN_MAP.match_appear_on(self.device.image): self.update_os() current = self.view.backend.homo_loca logger.attr('homo_loca', current) if record is None or (current is not None and np.linalg.norm( np.subtract(current, record)) < 3): if confirm_timer.reached(): break else: confirm_timer.reset() record = current else: confirm_timer.reset() result = '_'.join(result) logger.info(f'Walk stabled, result: {result}') self.device.screenshot_interval_set() return result
def _shipyard_buy_confirm(self, text, skip_first_screenshot=True): """ Handles screen transitions to use/buy BPs Args: text (str): for handle_popup_confirm skip_first_screenshot (bool): """ success = False append = self._shipyard_get_append() button = globals()[f'SHIPYARD_CONFIRM_{append}'] ocr_timer = Timer(10, count=10).start() confirm_timer = Timer(1, count=2).start() self.interval_clear(button) while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() if ocr_timer.reached(): logger.warning( 'Failed to detect for normal exit routine, resort to OCR check' ) _, _, current = self._shipyard_get_total() if not current: logger.info( 'Confirm action has completed, setting flag for exit') self.interval_reset(button) success = True ocr_timer.reset() continue if self.appear_then_click(button, offset=(20, 20), interval=3): continue if self.handle_popup_confirm(text): self.interval_reset(button) ocr_timer.reset() confirm_timer.reset() continue if self.story_skip(): self.interval_reset(button) success = True ocr_timer.reset() confirm_timer.reset() continue if self.handle_info_bar(): self.interval_reset(button) success = True ocr_timer.reset() confirm_timer.reset() continue # End if success and \ self._shipyard_in_ui(): if confirm_timer.reached(): break else: confirm_timer.reset()
class OSFleet(OSCamera, Combat, Fleet, OSAsh): def _goto(self, location, expected=''): super()._goto(location, expected) self.predict_radar() self.map.show() if self.handle_ash_beacon_attack(): # After ash attack, camera refocus to current fleet. self.camera = location self.update() def map_data_init(self, map_=None): """ Create new map object, and use the shape of current zone """ map_ = OSCampaignMap() map_.shape = self.zone.shape super().map_data_init(map_) def map_control_init(self): """ Remove non-exist things like strategy, round. """ # self.handle_strategy(index=1 if not self.fleets_reversed() else 2) self.update() # if self.handle_fleet_reverse(): # self.handle_strategy(index=1) self.hp_reset() self.hp_get() self.lv_reset() self.lv_get() self.ensure_edge_insight(preset=self.map.in_map_swipe_preset_data, swipe_limit=(6, 5)) # self.full_scan(must_scan=self.map.camera_data_spawn_point) # self.find_current_fleet() # self.find_path_initial() # self.map.show_cost() # self.round_reset() # self.round_battle() def find_current_fleet(self): self.fleet_1 = self.camera @property def _walk_sight(self): sight = (-4, -1, 3, 2) return sight _os_map_event_handled = False def ambush_color_initial(self): self._os_map_event_handled = False def handle_ambush(self): """ Treat map events as ambush, to trigger walk retrying """ if self.handle_map_get_items(): self._os_map_event_handled = True self.device.sleep(0.3) self.device.screenshot() return True elif self.handle_map_event(): self.ensure_no_map_event() self._os_map_event_handled = True return True else: return False def handle_mystery(self, button=None): """ After handle_ambush, if fleet has arrived, treat it as mystery, otherwise just ambush. """ if self._os_map_event_handled and button.predict_fleet( ) and button.predict_current_fleet(): return 'get_item' else: return False @staticmethod def _get_goto_expected(grid): """ Argument `expected` used in _goto() """ if grid.is_enemy: return 'combat' elif grid.is_resource or grid.is_meowfficer or grid.is_exclamation: return 'mystery' else: return '' def _hp_grid(self): hp_grid = super()._hp_grid() # Location of six HP bar, according to respective server for os if self.config.SERVER == 'en': hp_grid = ButtonGrid(origin=(35, 205), delta=(0, 100), button_shape=(66, 3), grid_shape=(1, 6)) elif self.config.SERVER == 'jp': pass else: pass return hp_grid def hp_retreat_triggered(self): return False need_repair = [False, False, False, False, False, False] def hp_get(self): """ Calculate current HP, also detects the wrench (Ship died, need to repair) """ super().hp_get() ship_icon = self._hp_grid().crop((0, -67, 67, 0)) need_repair = [ TEMPLATE_EMPTY_HP.match(self.image_crop(button)) for button in ship_icon.buttons ] self.need_repair = need_repair logger.attr('Repair icon', need_repair) if any(need_repair): for index, repair in enumerate(need_repair): if repair: self._hp_has_ship[self.fleet_current_index][index] = True self._hp[self.fleet_current_index][index] = 0 logger.attr( 'HP', ' '.join([ str(int(data * 100)).rjust(3) + '%' if use else '____' for data, use in zip(self.hp, self.hp_has_ship) ])) return self.hp def lv_get(self, after_battle=False): pass def get_sea_grids(self): """ Get sea grids on current view Returns: SelectedGrids: """ sea = [] for local in self.view: if not local.predict_sea() or local.predict_current_fleet(): continue # local = np.array(location) - self.camera + self.view.center_loca location = np.array( local.location) + self.camera - self.view.center_loca location = tuple(location.tolist()) if location == self.fleet_current or location not in self.map: continue sea.append(self.map[location]) if len(self.fleet_current): center = self.fleet_current else: center = self.camera return SelectedGrids(sea).sort_by_camera_distance(center) def wait_until_camera_stable(self, skip_first_screenshot=True): """ Wait until homo_loca stabled. DETECTION_BACKEND must be 'homography'. """ logger.hr('Wait until camera stable') record = None confirm_timer = Timer(0.6, count=2).start() while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() self.update_os() current = self.view.backend.homo_loca logger.attr('homo_loca', current) if record is None or (current is not None and np.linalg.norm( np.subtract(current, record)) < 3): if confirm_timer.reached(): break else: confirm_timer.reset() record = current logger.info('Camera stabled') def wait_until_walk_stable(self, confirm_timer=None, skip_first_screenshot=False, walk_out_of_step=True, drop=None): """ Wait until homo_loca stabled. DETECTION_BACKEND must be 'homography'. Args: confirm_timer (Timer): skip_first_screenshot (bool): walk_out_of_step (bool): If catch walk_out_of_step error. Default to True, use False in abyssal zones. drop (DropImage): Returns: str: Things that fleet met on its way, 'event', 'search', 'akashi', 'combat', or their combinations like 'event_akashi', 'event_combat', or an empty string '' if nothing met. Raises: MapWalkError: If unable to goto such grid. """ logger.hr('Wait until walk stable') record = None enemy_searching_appear = False self.device.screenshot_interval_set(0.35) if confirm_timer is None: confirm_timer = Timer(0.8, count=2) result = set() confirm_timer.reset() while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() # Map event if self.handle_map_event(drop=drop): confirm_timer.reset() result.add('event') continue if self.handle_retirement(): confirm_timer.reset() continue if self.handle_walk_out_of_step(): if walk_out_of_step: raise MapWalkError('walk_out_of_step') else: continue # Accident click if self.is_in_globe(): self.os_globe_goto_map() confirm_timer.reset() continue if self.is_in_storage(): self.storage_quit() confirm_timer.reset() continue if self.is_in_os_mission(): self.os_mission_quit() confirm_timer.reset() continue if self.handle_os_game_tips(): confirm_timer.reset() continue # Enemy searching if not enemy_searching_appear and self.enemy_searching_appear(): enemy_searching_appear = True confirm_timer.reset() continue else: if enemy_searching_appear: self.handle_enemy_flashing() self.device.sleep(0.3) logger.info('Enemy searching appeared.') enemy_searching_appear = False result.add('search') if self.is_in_map(): self.enemy_searching_color_initial() # Combat if self.combat_appear(): # Use ui_back() for testing, because there are too few abyssal loggers every month. # self.ui_back(check_button=self.is_in_map) self.combat(expected_end=self.is_in_map, fleet_index=self.fleet_show_index, save_get_items=drop) confirm_timer.reset() result.add('event') continue # Akashi shop if self.appear(PORT_SUPPLY_CHECK, offset=(20, 20)): self.interval_clear(PORT_SUPPLY_CHECK) self.handle_akashi_supply_buy(CLICK_SAFE_AREA) confirm_timer.reset() result.add('akashi') continue # Arrive # Check colors, because screen goes black when something is unlocking. if self.is_in_map() and IN_MAP.match_appear_on(self.device.image): self.update_os() current = self.view.backend.homo_loca logger.attr('homo_loca', current) if record is None or (current is not None and np.linalg.norm( np.subtract(current, record)) < 3): if confirm_timer.reached(): break else: confirm_timer.reset() record = current else: confirm_timer.reset() result = '_'.join(result) logger.info(f'Walk stabled, result: {result}') self.device.screenshot_interval_set() return result def port_goto(self): """ A simple and poor implement to goto port. Searching port on radar. In OpSi, camera always focus to fleet when fleet is moving which mess up `self.goto()`. In most situation, we use auto search to clear a map in OpSi, and classic methods are deprecated. But we still need to move fleet toward port, this method is for this situation. Raises: MapWalkError: If unable to goto such grid. Probably clicking at land, center of port, or fleet itself. """ confirm_timer = Timer(3, count=6).start() while 1: # Calculate destination grid = self.radar.port_predict(self.device.image) logger.info(f'Port route at {grid}') radar_arrive = np.linalg.norm(grid) == 0 port_arrive = self.appear(PORT_ENTER, offset=(20, 20)) if port_arrive: logger.info('Arrive port') break elif not port_arrive and radar_arrive: if confirm_timer.reached(): logger.warning( 'Arrive port on radar but port entrance not appear') raise MapWalkError else: logger.info( 'Arrive port on radar but port entrance not appear, confirming' ) self.device.screenshot() continue else: confirm_timer.reset() # Update local view self.update_os() self.predict() # Click way point grid = point_limit(grid, area=(-4, -2, 3, 2)) grid = self.convert_radar_to_local(grid) self.device.click(grid) # Wait until arrived self.wait_until_walk_stable() def fleet_set(self, index=1, skip_first_screenshot=True): """ Args: index (int): Target fleet_current_index skip_first_screenshot (bool): Returns: bool: If switched. """ logger.hr(f'Fleet set to {index}') if self.fleet_selector.ensure_to_be(index): self.wait_until_camera_stable() return True else: return False def parse_fleet_filter(self): """ Returns: list: List of BossFleet or str. Such as [Fleet-4, 'CallSubmarine', Fleet-2, Fleet-3, Fleet-1]. """ FLEET_FILTER.load(self.config.OpsiFleetFilter_Filter) fleets = FLEET_FILTER.apply([BossFleet(f) for f in [1, 2, 3, 4]]) # Set standby location standby_list = [(-1, -1), (0, -1), (1, -1)] index = 0 for fleet in fleets: if isinstance(fleet, BossFleet) and index < len(standby_list): fleet.standby_loca = standby_list[index] index += 1 return fleets def question_goto(self, has_fleet_step=False): logger.hr('Question goto') while 1: # Update local view # Not screenshots taking, reuse the old one self.update_os() self.predict() self.predict_radar() # Calculate destination grids = self.radar.select(is_question=True) if grids: # Click way point grid = location_ensure(grids[0]) grid = point_limit(grid, area=(-4, -2, 3, 2)) if has_fleet_step: grid = limit_walk(grid) grid = self.convert_radar_to_local(grid) self.device.click(grid) else: logger.info('No question mark to goto, stop') break # Wait until arrived # Having new screenshots self.wait_until_walk_stable(confirm_timer=Timer(1.5, count=4), walk_out_of_step=False) def boss_goto(self, location=(0, 0), has_fleet_step=False, drop=None): logger.hr('BOSS goto') while 1: # Update local view # Not screenshots taking, reuse the old one self.update_os() self.predict() self.predict_radar() # Calculate destination grids = self.radar.select(is_enemy=True) if grids: # Click way point grid = np.add(location_ensure(grids[0]), location) grid = point_limit(grid, area=(-4, -2, 3, 2)) if has_fleet_step: grid = limit_walk(grid) if grid == (0, 0): logger.info(f'Arrive destination: boss {location}') break grid = self.convert_radar_to_local(grid) self.device.click(grid) else: logger.info('No boss to goto, stop') break # Wait until arrived # Having new screenshots self.wait_until_walk_stable(confirm_timer=Timer(1.5, count=4), walk_out_of_step=False, drop=drop) def get_boss_leave_button(self): for grid in self.view: if grid.predict_current_fleet(): return None grids = [grid for grid in self.view if grid.predict_caught_by_siren()] if len(grids) == 1: center = grids[0] elif len(grids) > 1: logger.warning( f'Found multiple fleets in boss ({grids}), use the center one') center = SelectedGrids(grids).sort_by_camera_distance( self.view.center_loca)[0] else: logger.warning('No fleet in boss, use camera center instead') center = self.view[self.view.center_loca] logger.info(f'Fleet in boss: {center}') # The left half grid next to the center grid. area = corner2inner( center.grid2screen(area2corner((1, 0.25, 1.5, 0.75)))) button = Button(area=area, color=(), button=area, name='BOSS_LEAVE') return button def boss_leave(self, skip_first_screenshot=True): """ Pages: in: is_in_map(), or combat_appear() out: is_in_map(), fleet not in boss. """ logger.hr('BOSS leave') # Update local view self.update_os() click_timer = Timer(3) while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() # End if self.is_in_map(): self.predict_radar() if self.radar.select(is_enemy=True): logger.info('Fleet left boss, boss found') break # Re-enter boss accidentally if self.combat_appear(): self.ui_back(check_button=self.is_in_map) # Click leave button if self.is_in_map() and click_timer.reached(): button = self.get_boss_leave_button() if button is not None: self.device.click(button) click_timer.reset() continue else: logger.info('Fleet left boss, current fleet found') break def boss_clear(self, has_fleet_step=True): """ All fleets take turns in attacking the boss. Args: has_fleet_step (bool): Returns: bool: If success to clear. Pages: in: Siren logger (abyssal), boss appeared. out: If success, dangerous or safe zone. If failed, still in abyssal. """ logger.hr(f'BOSS clear', level=1) fleets = self.parse_fleet_filter() with self.stat.new(genre=inflection.underscore( self.config.task.command), method=self.config.DropRecord_OpsiRecord) as drop: for fleet in fleets: logger.hr(f'Turn: {fleet}', level=2) if not isinstance(fleet, BossFleet): self.os_order_execute(recon_scan=False, submarine_call=True) continue # Attack self.fleet_set(fleet.fleet_index) self.handle_os_map_fleet_lock(enable=False) self.boss_goto(location=(0, 0), has_fleet_step=has_fleet_step, drop=drop) # End self.predict_radar() if self.radar.select(is_question=True): logger.info('BOSS clear') if drop.count: drop.add(self.device.image) self.map_exit() return True # Standby self.boss_leave() if fleet.standby_loca != (0, 0): self.boss_goto(location=fleet.standby_loca, has_fleet_step=has_fleet_step, drop=drop) else: if drop.count: drop.add(self.device.image) break logger.critical('Unable to clear boss, fleets exhausted') return False def run_abyssal(self): """ Handle double confirms and attack abyssal (siren logger) boss. Returns: bool: If success to clear. Pages: in: Siren logger (abyssal). out: If success, in a dangerous or safe zone. If failed, still in abyssal. """ self.handle_os_map_fleet_lock(enable=False) def is_at_front(grid): # Grid location is usually to be (0, -2) x, y = grid.location return (abs(x) <= abs(y)) and (y < 0) while 1: self.device.screenshot() self.question_goto(has_fleet_step=True) if self.radar.select(is_enemy=True).filter(is_at_front): logger.info('Found boss at front') break else: logger.info('No boss at front, retry question_goto') continue result = self.boss_clear(has_fleet_step=True) return result def get_stronghold_percentage(self): """ Get the clear status in siren stronghold. Returns: str: Usually in ['100', '80', '60', '40', '20', '0'] """ ocr = PercentageOcr(STRONGHOLD_PERCENTAGE, letter=(255, 255, 255), threshold=128, name='STRONGHOLD_PERCENTAGE') result = ocr.ocr(self.device.image) result = result.rstrip('7Kk') for starter in ['100', '80', '60', '40', '20', '0']: if result.startswith(starter): result = starter logger.attr('STRONGHOLD_PERCENTAGE', result) return result logger.warning(f'Unexpected STRONGHOLD_PERCENTAGE: {result}') return result def get_second_fleet(self): """ Get a second fleet to unlock fleet mechanism that requires 2 fleets. Returns: int: """ current = self.fleet_selector.get() if current == 1: second = 2 else: second = 1 logger.attr('Second_fleet', second) return second @staticmethod def fleet_walk_limit(outside, step=3): if np.linalg.norm(outside) <= 3: return outside if step == 1: grids = np.array([ (0, -1), (0, 1), (-1, 0), (1, 0), ]) else: grids = np.array([ (0, -3), (0, 3), (-3, 0), (3, 0), (2, -2), (2, 2), (-2, 2), (2, 2), ]) degree = np.sum(grids * outside, axis=1) / np.linalg.norm( grids, axis=1) / np.linalg.norm(outside) return grids[np.argmax(degree)] _nearest_object_click_timer = Timer(2) def click_nearest_object(self): if not self._nearest_object_click_timer.reached(): return False if not self.appear(MAP_GOTO_GLOBE, offset=(200, 20)): return False if self.appear(PORT_ENTER, offset=(20, 20)): return False self.update_os() self.view.predict() self.radar.predict(self.device.image) self.radar.show() nearest = self.radar.nearest_object() if nearest is None: self._nearest_object_click_timer.reset() return False step = 1 if self.appear(FLEET_EMP_DEBUFF, offset=(50, 20)) else 3 nearest = self.fleet_walk_limit(nearest.location, step=step) try: nearest = self.convert_radar_to_local(nearest) except KeyError: logger.info('Radar grid not on local map') self._nearest_object_click_timer.reset() return False self.device.click(nearest) self._nearest_object_click_timer.reset()
def _goto(self, location, expected=''): """Goto a grid directly and handle ambush, air raid, mystery picked up, combat. Args: location (tuple, str, GridInfo): Destination. """ location = location_ensure(location) result_mystery = '' while 1: sight = self.map.camera_sight self.in_sight(location, sight=(sight[0], 0, sight[2], sight[3])) self.focus_to_grid_center() grid = self.convert_map_to_grid(location) self.ambush_color_initial() self.enemy_searching_color_initial() grid.__str__ = location result = 'nothing' self.device.click(grid) arrived = False # Wait to confirm fleet arrived. It does't appear immediately if fleet in combat . arrive_timer = Timer(0.5 + self.round_wait, count=2) arrive_unexpected_timer = Timer(1.5 + self.round_wait, count=6) # Wait after ambushed. ambushed_retry = Timer(0.5) # If nothing happens, click again. walk_timeout = Timer(20) walk_timeout.start() while 1: self.device.screenshot() grid.image = np.array(self.device.image) # Ambush if self.handle_ambush(): ambushed_retry.start() walk_timeout.reset() # Mystery mystery = self.handle_mystery(button=grid) if mystery: self.mystery_count += 1 result = 'mystery' result_mystery = mystery # Combat if self.config.ENABLE_MAP_FLEET_LOCK and not self.is_in_map(): if self.handle_retirement(): self.map_offensive() walk_timeout.reset() if self.handle_combat_low_emotion(): walk_timeout.reset() if self.combat_appear(): self.combat( expected_end=self._expected_combat_end(expected), fleet_index=self.fleet_current_index) self.hp_get() if self.hp_withdraw_triggered(): self.withdraw() arrived = True if not self.config.MAP_HAS_MOVABLE_ENEMY else False result = 'combat' self.battle_count += 1 self.fleet_ammo -= 1 if 'siren' in expected or ( self.config.MAP_HAS_MOVABLE_ENEMY and not expected): self.siren_count += 1 elif self.map[location].may_enemy: self.map[location].is_cleared = True self.handle_boss_appear_refocus() grid = self.convert_map_to_grid(location) walk_timeout.reset() # Cat attack animation if self.handle_map_cat_attack(): walk_timeout.reset() continue if self.handle_walk_out_of_step(): raise MapWalkError('walk_out_of_step') # Arrive if self.is_in_map() and \ (grid.predict_fleet() or (walk_timeout.reached() and grid.predict_current_fleet())): if not arrive_timer.started(): logger.info(f'Arrive {location2node(location)}') arrive_timer.start() arrive_unexpected_timer.start() if not arrive_timer.reached(): continue if expected and result not in expected: if arrive_unexpected_timer.reached(): logger.warning('Arrive with unexpected result') else: continue logger.info( f'Arrive {location2node(location)} confirm. Result: {result}. Expected: {expected}' ) arrived = True break # End if ambushed_retry.started() and ambushed_retry.reached(): break if walk_timeout.reached(): logger.warning('Walk timeout. Retrying.') self.ensure_edge_insight() break # End if arrived: # Ammo grid needs to click again, otherwise the next click doesn't work. if self.map[location].may_ammo: self.device.click(grid) break self.map[self.fleet_current].is_fleet = False self.map[location].wipe_out() self.map[location].is_fleet = True self.__setattr__('fleet_%s_location' % self.fleet_current_index, location) if result_mystery == 'get_carrier': self.full_scan_carrier() if result == 'combat': self.round_battle() self.round_next() if self.round_is_new: self.full_scan_movable() self.find_path_initial() raise MapEnemyMoved self.find_path_initial()
def os_map_goto_globe(self, unpin=True, skip_first_screenshot=True): """ Args: unpin (bool): skip_first_screenshot (bool): Pages: in: is_in_map out: is_in_globe """ click_count = 0 while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() if self.appear_then_click(MAP_GOTO_GLOBE, offset=(200, 5), interval=5): click_count += 1 if click_count >= 5: # When there's zone exploration reward, AL just don't let you go. logger.warning( 'Unable to goto globe, ' 'there might be uncollected zone exploration rewards preventing exit' ) raise GameTooManyClickError( f'Too many click for a button: {MAP_GOTO_GLOBE}') continue if self.handle_map_event(): continue # Popup: AUTO_SEARCH_REWARD appears slowly if self.appear_then_click(AUTO_SEARCH_REWARD, offset=(50, 50), interval=5): continue # Popup: Leaving current zone will terminate meowfficer searching. # Popup: Leaving current zone will retreat submarines # Searching reward will be shown after entering another zone. if self.handle_popup_confirm('GOTO_GLOBE'): continue # End if self.is_in_globe(): break skip_first_screenshot = True confirm_timer = Timer(1, count=2).start() unpinned = 0 while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() if unpin: if self.handle_zone_pinned(): unpinned += 1 confirm_timer.reset() else: if unpinned and confirm_timer.reached(): break else: if self.is_zone_pinned(): break
def os_mission_overview_accept(self): """ Accept all missions in mission overview. Returns: bool: True if all missions accepted or no mission found. False if unable to accept more missions. Pages: in: is_in_map out: is_in_map """ logger.hr('OS mission overview accept', level=1) # is_in_map self.os_map_goto_globe(unpin=False) # is_in_globe self.ui_click(MISSION_OVERVIEW_ENTER, check_button=MISSION_OVERVIEW_CHECK, offset=(200, 20), retry_wait=3, skip_first_screenshot=True) # MISSION_OVERVIEW_CHECK confirm_timer = Timer(1, count=3).start() skip_first_screenshot = True success = True while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() if self.info_bar_count(): logger.info( 'Unable to accept missions, because reached the maximum number of missions' ) success = False break if self.appear_then_click(MISSION_OVERVIEW_ACCEPT, offset=(20, 20), interval=0.2): confirm_timer.reset() continue else: # End if confirm_timer.reached(): success = True break if self.appear_then_click(MISSION_OVERVIEW_ACCEPT_SINGLE, offset=(20, 20), interval=0.2): confirm_timer.reset() continue # is_in_globe self.ui_back(appear_button=MISSION_OVERVIEW_CHECK, check_button=self.is_in_globe, skip_first_screenshot=True) # is_in_map self.os_globe_goto_map() return success
def meow_get(self, skip_first_screenshot=True): """ Transition through all the necessary screens to acquire each trained meowfficer Animation is waited for as the amount can vary Only gold variant meowfficer will prompt for confirmation Args: skip_first_screenshot (bool): Skip first screen shot or not Pages: in: MEOWFFICER_GET_CHECK out: MEOWFFICER_TRAIN """ # Loop through possible screen transitions confirm_timer = Timer(1.5, count=3).start() count = 0 while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() if self.handle_meow_popup_dismiss(): confirm_timer.reset() continue if self.appear(MEOWFFICER_GET_CHECK, offset=(40, 40), interval=3): count += 1 logger.attr('Meow_get', count) with self.stat.new(genre="meowfficer_talent", method=self.config. DropRecord_MeowfficerTalent) as drop: drop.add(self.device.image) list_talent_btn, special_talent = self._get_meow_talent_grid( ) if self.config.DropRecord_MeowfficerTalent != 'do_not': self._meow_talent_cap_handle(list_talent_btn, drop) if self.appear(MEOWFFICER_GOLD_CHECK, offset=(40, 40)): if not self.config.MeowfficerTrain_RetainTalentedGold or not special_talent: self._meow_skip_lock() skip_first_screenshot = True confirm_timer.reset() continue self._meow_apply_lock() if self.appear(MEOWFFICER_PURPLE_CHECK, offset=(40, 40)): if self.config.MeowfficerTrain_RetainTalentedPurple and special_talent: self._meow_apply_lock() # Susceptible to exception when collecting multiple # Mitigate by popping click_record self.device.click(MEOWFFICER_TRAIN_CLICK_SAFE_AREA) self.device.click_record.pop() confirm_timer.reset() self.interval_reset(MEOWFFICER_GET_CHECK) continue # End if self.appear(MEOWFFICER_TRAIN_START, offset=(20, 20)): if confirm_timer.reached(): break else: confirm_timer.reset()
class EnemySearchingHandler(InfoHandler): MAP_ENEMY_SEARCHING_OVERLAY_TRANSPARENCY_THRESHOLD = 0.5 # Usually (0.70, 0.80). MAP_ENEMY_SEARCHING_TIMEOUT_SECOND = 5 in_stage_timer = Timer(0.5, count=2) stage_entrance = None map_is_clear = False # Will be override in fast_forward.py def enemy_searching_color_initial(self): MAP_ENEMY_SEARCHING.load_color(self.device.image) def enemy_searching_appear(self): return red_overlay_transparency( MAP_ENEMY_SEARCHING.color, get_color(self.device.image, MAP_ENEMY_SEARCHING.area) ) > self.MAP_ENEMY_SEARCHING_OVERLAY_TRANSPARENCY_THRESHOLD def handle_enemy_flashing(self): self.device.sleep(1.2) def handle_in_stage(self): if self.is_in_stage(): if self.in_stage_timer.reached(): logger.info('In stage.') self.device.send_notification('AzurLaneAutoScript', 'Map cleared') self.ensure_no_info_bar(timeout=1.2) raise CampaignEnd('In stage.') else: return False else: if self.appear(MAP_PREPARATION) or self.appear(FLEET_PREPARATION): self.device.click(MAP_PREPARATION_CANCEL) self.in_stage_timer.reset() return False def is_in_stage(self): appear = [self.appear(check, offset=(20, 20)) for check in [CAMPAIGN_CHECK, EVENT_CHECK, SP_CHECK]] if not any(appear): return False # campaign_extract_name_image in CampaignOcr. try: if hasattr(self, 'campaign_extract_name_image') \ and not len(self.campaign_extract_name_image(self.device.image)): return False except IndexError: return False return True def is_in_map(self): return self.appear(IN_MAP) def is_event_animation(self): """ Animation in events after cleared an enemy. Returns: bool: If animation appearing. """ return False def handle_in_map_with_enemy_searching(self): if not self.is_in_map(): return False timeout = Timer(self.MAP_ENEMY_SEARCHING_TIMEOUT_SECOND) appeared = False while 1: self.device.screenshot() if self.is_event_animation(): continue if self.is_in_map(): timeout.start() else: timeout.reset() if self.handle_in_stage(): return True if self.handle_story_skip(): self.ensure_no_story() timeout.limit = 10 timeout.reset() if self.handle_guild_popup_cancel(): self.config.GUILD_POPUP_TRIGGERED = True timeout.limit = 10 timeout.reset() continue # End if self.enemy_searching_appear(): appeared = True else: if appeared: self.handle_enemy_flashing() self.device.sleep(1) logger.info('Enemy searching appeared.') break self.enemy_searching_color_initial() if timeout.reached(): logger.info('Enemy searching timeout.') break self.device.screenshot() return True def handle_in_map_no_enemy_searching(self): if not self.is_in_map(): return False self.device.sleep((1, 1.2)) return True
class Device(Screenshot, Control, AppControl): _screen_size_checked = False detect_record = set() click_record = deque(maxlen=15) stuck_timer = Timer(60, count=60).start() stuck_timer_long = Timer(180, count=180).start() stuck_long_wait_list = ['BATTLE_STATUS_S', 'PAUSE', 'LOGIN_CHECK'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.screenshot_interval_set() def handle_night_commission(self, daily_trigger='21:00', threshold=30): """ Args: daily_trigger (int): Time for commission refresh. threshold (int): Seconds around refresh time. Returns: bool: If handled. """ update = get_server_next_update(daily_trigger=daily_trigger) now = datetime.now() diff = (update.timestamp() - now.timestamp()) % 86400 if threshold < diff < 86400 - threshold: return False if GET_MISSION.match(self.image, offset=True): logger.info('Night commission appear.') self.click(GET_MISSION) return True return False def screenshot(self): """ Returns: np.ndarray: """ self.stuck_record_check() super().screenshot() if self.handle_night_commission(): super().screenshot() return self.image def stuck_record_add(self, button): self.detect_record.add(str(button)) def stuck_record_clear(self): self.detect_record = set() self.stuck_timer.reset() self.stuck_timer_long.reset() def stuck_record_check(self): """ Raises: GameStuckError: """ reached = self.stuck_timer.reached() reached_long = self.stuck_timer_long.reached() if not reached: return False if not reached_long: for button in self.stuck_long_wait_list: if button in self.detect_record: return False logger.warning('Wait too long') logger.warning(f'Waiting for {self.detect_record}') self.stuck_record_clear() raise GameStuckError(f'Wait too long') def handle_control_check(self, button): self.stuck_record_clear() self.click_record_add(button) self.click_record_check() def click_record_add(self, button): self.click_record.append(str(button)) def click_record_clear(self): self.click_record.clear() def click_record_check(self): """ Raises: GameTooManyClickError: """ count = {} for key in self.click_record: count[key] = count.get(key, 0) + 1 count = sorted(count.items(), key=lambda item: item[1]) if count[0][1] >= 12: logger.warning(f'Too many click for a button: {count[0][0]}') logger.warning( f'History click: {[str(prev) for prev in self.click_record]}') self.click_record_clear() raise GameTooManyClickError( f'Too many click for a button: {count[0][0]}') if len(count) >= 2 and count[0][1] >= 6 and count[1][1] >= 6: logger.warning( f'Too many click between 2 buttons: {count[0][0]}, {count[1][0]}' ) logger.warning( f'History click: {[str(prev) for prev in self.click_record]}') self.click_record_clear() raise GameTooManyClickError( f'Too many click between 2 buttons: {count[0][0]}, {count[1][0]}' ) def disable_stuck_detection(self): """ Disable stuck detection and its handler. Usually uses in semi auto and debugging. """ logger.info('Disable stuck detection') def empty_function(*arg, **kwargs): return False self.click_record_check = empty_function self.stuck_record_check = empty_function def app_start(self): if not self.config.Error_HandleError: logger.critical('No app stop/start, because HandleError disabled') logger.critical( 'Please enable Alas.Error.HandleError or manually login to AzurLane' ) raise RequestHumanTakeover super().app_start() self.stuck_record_clear() self.click_record_clear() def app_stop(self): if not self.config.Error_HandleError: logger.critical('No app stop/start, because HandleError disabled') logger.critical( 'Please enable Alas.Error.HandleError or manually login to AzurLane' ) raise RequestHumanTakeover super().app_stop() self.stuck_record_clear() self.click_record_clear()
def _goto(self, location, expected=''): """Goto a grid directly and handle ambush, air raid, mystery picked up, combat. Args: location (tuple, str, GridInfo): Destination. expected (str): Expected result on destination grid, such as 'combat', 'combat_siren', 'mystery'. Will give a waring if arrive with unexpected result. """ location = location_ensure(location) result_mystery = '' self.movable_before = self.map.select(is_siren=True) self.movable_before_normal = self.map.select(is_enemy=True) if self.hp_retreat_triggered(): self.withdraw() is_portal = self.map[location].is_portal # The upper grid is submarine, may mess up predict_fleet() may_submarine_icon = self.map.grid_covered(self.map[location], location=[(0, -1)]) may_submarine_icon = may_submarine_icon and self.fleet_submarine_location == may_submarine_icon[0].location while 1: self.in_sight(location, sight=self._walk_sight) self.focus_to_grid_center() grid = self.convert_global_to_local(location) self.ambush_color_initial() self.enemy_searching_color_initial() grid.__str__ = location result = 'nothing' self.device.click(grid) arrived = False # Wait to confirm fleet arrived. It does't appear immediately if fleet in combat. extra = 0 if self.config.Submarine_Mode == 'hunt_only': extra += 4.5 if self.config.MAP_HAS_LAND_BASED and grid.is_mechanism_trigger: extra += grid.mechanism_wait arrive_timer = Timer(0.5 + self.round_wait + extra, count=2) arrive_unexpected_timer = Timer(1.5 + self.round_wait + extra, count=6) # Wait after ambushed. ambushed_retry = Timer(0.5) # If nothing happens, click again. walk_timeout = Timer(20) walk_timeout.start() while 1: self.device.screenshot() self.view.update(image=self.device.image) if is_portal: self.update() grid = self.view[self.view.center_loca] # Combat if self.config.Campaign_UseFleetLock and not self.is_in_map(): if self.handle_retirement(): self.map_offensive() walk_timeout.reset() if self.handle_combat_low_emotion(): walk_timeout.reset() if self.combat_appear(): self.combat( expected_end=self._expected_end(expected), fleet_index=self.fleet_show_index, submarine_mode=self._submarine_mode(expected) ) self.hp_get() self.lv_get(after_battle=True) arrived = True if not self.config.MAP_HAS_MOVABLE_ENEMY else False result = 'combat' self.battle_count += 1 self.fleet_ammo -= 1 if 'siren' in expected or (self.config.MAP_HAS_MOVABLE_ENEMY and not expected): self.siren_count += 1 elif self.map[location].may_enemy: self.map[location].is_cleared = True if self.catch_camera_repositioning(self.map[location]): self.handle_boss_appear_refocus() if self.config.MAP_FOCUS_ENEMY_AFTER_BATTLE: self.camera = location self.update() grid = self.convert_global_to_local(location) arrive_timer = Timer(0.5 + extra, count=2) arrive_unexpected_timer = Timer(1.5 + extra, count=6) walk_timeout.reset() # Ambush if self.handle_ambush(): self.hp_get() self.lv_get(after_battle=True) walk_timeout.reset() self.view.update(image=self.device.image) if not (grid.predict_fleet() and grid.predict_current_fleet()): ambushed_retry.start() # Mystery mystery = self.handle_mystery(button=grid) if mystery: self.mystery_count += 1 result = 'mystery' result_mystery = mystery # Cat attack animation if self.handle_map_cat_attack(): walk_timeout.reset() continue # Guild popup # Usually handled in combat_status, but sometimes delayed until after battle on slow PCs. if self.handle_guild_popup_cancel(): walk_timeout.reset() continue if self.handle_walk_out_of_step(): raise MapWalkError('walk_out_of_step') # Arrive arrive_predict = '' arrive_checker = False if self.is_in_map(): if not may_submarine_icon and grid.predict_fleet(): arrive_predict = '(is_fleet)' arrive_checker = True elif may_submarine_icon and grid.predict_current_fleet(): arrive_predict = '(may_submarine_icon, is_current_fleet)' arrive_checker = True elif self.config.MAP_WALK_USE_CURRENT_FLEET and grid.predict_current_fleet(): arrive_predict = '(MAP_WALK_USE_CURRENT_FLEET, is_current_fleet)' arrive_checker = True elif walk_timeout.reached() and grid.predict_current_fleet(): arrive_predict = '(walk_timeout, is_current_fleet)' arrive_checker = True if arrive_checker: if not arrive_timer.started(): logger.info(f'Arrive {location2node(location)} {arrive_predict}'.strip()) arrive_timer.start() arrive_unexpected_timer.start() if not arrive_timer.reached(): continue if expected and result not in expected: if arrive_unexpected_timer.reached(): logger.warning('Arrive with unexpected result') else: continue if is_portal: location = self.map[location].portal_link self.camera = location logger.info(f'Arrive {location2node(location)} confirm. Result: {result}. Expected: {expected}') arrived = True break # Story if expected == 'story': if self.handle_story_skip(): result = 'story' continue # End if ambushed_retry.started() and ambushed_retry.reached(): break if walk_timeout.reached(): logger.warning('Walk timeout. Retrying.') self.predict() self.ensure_edge_insight(skip_first_update=False) break # End if arrived: # Ammo grid needs to click again, otherwise the next click doesn't work. if self.map[location].may_ammo: self.device.click(grid) break self.map[self.fleet_current].is_fleet = False self.map[location].wipe_out() self.map[location].is_fleet = True self.__setattr__('fleet_%s_location' % self.fleet_current_index, location) if result_mystery == 'get_carrier': self.full_scan_carrier() if result == 'combat': self.round_battle(after_battle=True) self.predict() self.round_next() if self.round_is_new: if result != 'combat': self.predict() self.full_scan_movable(enemy_cleared=result == 'combat') self.find_path_initial() raise MapEnemyMoved if self.round_maze_changed: self.find_path_initial() raise MapEnemyMoved self.find_path_initial()
def _reward_mission(self): """ Returns: bool: If rewarded. """ if not self.config.ENABLE_MISSION_REWARD: return False logger.hr('Mission reward') if not self.appear(MISSION_NOTICE): logger.info('No mission reward') return False self.ui_goto(page_mission, skip_first_screenshot=True) reward = False exit_timer = Timer(2) click_timer = Timer(1) timeout = Timer(10) exit_timer.start() timeout.start() while 1: self.device.screenshot() for button in [GET_ITEMS_1, GET_ITEMS_2]: if self.appear_then_click(button, offset=(30, 30), interval=1): exit_timer.reset() timeout.reset() reward = True continue for button in [MISSION_MULTI, MISSION_SINGLE]: if not click_timer.reached(): continue if self.appear_then_click(button, interval=1): exit_timer.reset() click_timer.reset() timeout.reset() continue if not self.appear(MISSION_CHECK): if self.appear_then_click(GET_SHIP, interval=1): click_timer.reset() exit_timer.reset() timeout.reset() continue if self.story_skip(): click_timer.reset() exit_timer.reset() timeout.reset() continue # End if reward and exit_timer.reached(): break if timeout.reached(): logger.warning('Wait get items timeout.') break self.ui_goto(page_main, skip_first_screenshot=True) return reward
class MapOperation(MysteryHandler, FleetPreparation, Retirement, FastForwardHandler): map_cat_attack_timer = Timer(2) def fleet_switch_click(self): """ Switch fleet. """ logger.info('Switch over') if self.appear_then_click(SWITCH_OVER): pass else: logger.warning('No buttons detected.') self.device.sleep((1, 1.5)) # self.ensure_no_info_bar() def enter_map(self, button, mode='normal'): """Enter a campaign. Args: button: Campaign to enter. mode (str): 'normal' or 'hard' or 'cd' """ logger.hr('Enter map') campaign_timer = Timer(2) map_timer = Timer(1) fleet_timer = Timer(1) checked_in_map = False while 1: self.device.screenshot() if not checked_in_map and self.is_in_map(): logger.info('Already in map, skip enter_map.') return False else: checked_in_map = True # Map preparation if map_timer.reached() and self.appear(MAP_PREPARATION): self.device.sleep(0.3) # Wait for map information. self.device.screenshot() if self.handle_map_clear_mode_stop(): self.enter_map_cancel() raise ScriptEnd( f'Reach condition: {self.config.CLEAR_MODE_STOP_CONDITION}' ) self.handle_fast_forward() self.device.click(MAP_PREPARATION) map_timer.reset() campaign_timer.reset() continue # Fleet preparation if fleet_timer.reached() and self.appear(FLEET_PREPARATION): if self.config.ENABLE_FLEET_CONTROL: if mode == 'normal' or mode == 'hard': self.fleet_preparation() self.device.click(FLEET_PREPARATION) fleet_timer.reset() campaign_timer.reset() continue # Retire if self.handle_retirement(): continue # Emotion if self.handle_combat_low_emotion(): continue # Urgent commission if self.handle_urgent_commission(): continue # Story skip if self.handle_story_skip(): campaign_timer.reset() continue # Enter campaign if campaign_timer.reached() and self.is_in_stage(): self.device.click(button) campaign_timer.reset() continue # End if self.handle_in_map_with_enemy_searching(): break return True def enter_map_cancel(self, skip_first_screenshot=True): logger.hr('Enter map cancel') while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() if self.appear(MAP_PREPARATION) or self.appear(FLEET_PREPARATION): self.device.click(MAP_PREPARATION_CANCEL) continue if self.is_in_stage(): break return True def withdraw(self): """ Withdraw campaign. """ logger.hr('Map withdraw') while 1: self.device.screenshot() if self.handle_popup_confirm(): continue if self.appear_then_click(WITHDRAW, interval=2): continue # End if self.handle_in_stage(): raise CampaignEnd('Withdraw') def handle_map_cat_attack(self): """ Click to skip the animation when cat attacks. """ if not self.map_cat_attack_timer.reached(): return False if np.sum( color_similarity_2d(self.image_area(MAP_CAT_ATTACK), (255, 231, 123)) > 221) > 100: logger.info('Skip map cat attack') self.device.click(MAP_CAT_ATTACK) self.map_cat_attack_timer.reset() return True return False def handle_fleet_reverse(self): """ The game chooses the fleet with a smaller index to be the first fleet, no matter what we choose in fleet preparation. Returns: bool: Fleet changed """ if (self.config.FLEET_2 == 0) or (self.config.FLEET_2 > self.config.FLEET_1): return False self.fleet_switch_click() return True def handle_spare_fleet(self): pass
class InfoHandler(ModuleBase): """ Class to handle all kinds of message. """ """ Info bar """ def info_bar_count(self): if self.appear(INFO_BAR_3): return 3 elif self.appear(INFO_BAR_2): return 2 elif self.appear(INFO_BAR_1): return 1 else: return 0 def handle_info_bar(self): if self.info_bar_count(): self.wait_until_disappear(INFO_BAR_1) return True else: return False def ensure_no_info_bar(self, timeout=0.6): timeout = Timer(timeout) timeout.start() while 1: self.device.screenshot() self.handle_info_bar() if timeout.reached(): break """ Popup info """ _popup_offset = (3, 30) def handle_popup_confirm(self, name=''): if self.appear(POPUP_CANCEL, offset=self._popup_offset) \ and self.appear(POPUP_CONFIRM, offset=self._popup_offset, interval=2): POPUP_CONFIRM.name = POPUP_CONFIRM.name + '_' + name self.device.click(POPUP_CONFIRM) POPUP_CONFIRM.name = POPUP_CONFIRM.name[:-len(name) - 1] return True else: return False def handle_popup_cancel(self, name=''): if self.appear(POPUP_CONFIRM, offset=self._popup_offset) \ and self.appear(POPUP_CANCEL, offset=self._popup_offset, interval=2): POPUP_CANCEL.name = POPUP_CANCEL.name + '_' + name self.device.click(POPUP_CANCEL) POPUP_CANCEL.name = POPUP_CANCEL.name[:-len(name) - 1] return True else: return False def handle_popup_single(self, name=''): if self.appear(GET_MISSION, offset=self._popup_offset, interval=2): prev_name = GET_MISSION.name GET_MISSION.name = POPUP_CONFIRM.name + '_' + name self.device.click(GET_MISSION) GET_MISSION.name = prev_name return True return False def handle_urgent_commission(self, save_get_items=None): """ Args: save_get_items (bool): Returns: bool: """ if save_get_items is None: save_get_items = self.config.ENABLE_SAVE_GET_ITEMS appear = self.appear(GET_MISSION, offset=True, interval=2) if appear: logger.info('Get urgent commission') if save_get_items: self.handle_info_bar() self.device.save_screenshot('get_mission') self.device.click(GET_MISSION) return appear def handle_combat_low_emotion(self): if not self.config.IGNORE_LOW_EMOTION_WARN: return False return self.handle_popup_confirm('IGNORE_LOW_EMOTION') def handle_use_data_key(self): if not self.config.USE_DATA_KEY: return False if not self.appear(POPUP_CONFIRM, offset=self._popup_offset) \ and not self.appear(POPUP_CANCEL, offset=self._popup_offset, interval=2): return False if self.appear(USE_DATA_KEY, offset=(20, 20)): self.device.click(USE_DATA_KEY_NOTIFIED) self.device.sleep((0.5, 0.8)) return self.handle_popup_confirm('USE_DATA_KEY') return False """ Guild popup info """ def handle_guild_popup_confirm(self): if self.appear(GUILD_POPUP_CANCEL, offset=self._popup_offset) \ and self.appear(GUILD_POPUP_CONFIRM, offset=self._popup_offset, interval=2): self.device.click(GUILD_POPUP_CONFIRM) return True return False def handle_guild_popup_cancel(self): if self.appear(GUILD_POPUP_CONFIRM, offset=self._popup_offset) \ and self.appear(GUILD_POPUP_CANCEL, offset=self._popup_offset, interval=2): self.device.click(GUILD_POPUP_CANCEL) return True return False """ Mission popup info """ def handle_mission_popup_go(self): if self.appear(MISSION_POPUP_ACK, offset=self._popup_offset) \ and self.appear(MISSION_POPUP_GO, offset=self._popup_offset, interval=2): self.device.click(MISSION_POPUP_GO) return True return False def handle_mission_popup_ack(self): if self.appear(MISSION_POPUP_GO, offset=self._popup_offset) \ and self.appear(MISSION_POPUP_ACK, offset=self._popup_offset, interval=2): self.device.click(MISSION_POPUP_ACK) return True return False """ Story """ story_popup_timout = Timer(10, count=20) map_has_fast_forward = False # Will be override in fast_forward.py # Area to detect the options, should include at least 3 options. _story_option_area = (730, 188, 1140, 480) # Background color of the left part of the option. _story_option_color = (99, 121, 156) _story_option_timer = Timer(2) def _story_option_buttons(self): """ Returns: list[Button]: List of story options, from upper to bottom. If no option found, return an empty list. """ image = color_similarity_2d(self.image_area(self._story_option_area), color=self._story_option_color) > 225 x_count = np.where(np.sum(image, axis=0) > 40)[0] if not len(x_count): return [] x_min, x_max = np.min(x_count), np.max(x_count) parameters = { # Option is 300`320px x 50~52px. 'height': 280, 'width': 45, 'distance': 50, # Chooses the relative height at which the peak width is measured as a percentage of its prominence. # 1.0 calculates the width of the peak at its lowest contour line, # while 0.5 evaluates at half the prominence height. # Must be at least 0. 'rel_height': 5, } y_count = np.sum(image, axis=1) peaks, properties = signal.find_peaks(y_count, **parameters) buttons = [] total = len(peaks) if not total: return [] for n, bases in enumerate(zip(properties['left_bases'], properties['right_bases'])): area = (x_min, bases[0], x_max, bases[1]) area = area_pad(area_offset(area, offset=self._story_option_area[:2]), pad=5) buttons.append( Button(area=area, color=self._story_option_color, button=area, name=f'STORY_OPTION_{n + 1}_OF_{total}')) return buttons def story_skip(self): if self.story_popup_timout.started() and not self.story_popup_timout.reached(): if self.handle_popup_confirm('STORY_SKIP'): self.story_popup_timout = Timer(10) self.interval_reset(STORY_SKIP) self.interval_reset(STORY_LETTERS_ONLY) return True if self.appear(STORY_LETTER_BLACK) and self.appear_then_click(STORY_LETTERS_ONLY, offset=True, interval=2): self.story_popup_timout.reset() return True if self._story_option_timer.reached() and self.appear(STORY_SKIP, offset=True, interval=0): options = self._story_option_buttons() if len(options): self.device.click(options[0]) self._story_option_timer.reset() self.story_popup_timout.reset() self.interval_reset(STORY_SKIP) self.interval_reset(STORY_LETTERS_ONLY) return True if self.appear_then_click(STORY_SKIP, offset=True, interval=2): self.story_popup_timout.reset() return True if self.appear_then_click(GAME_TIPS, offset=(20, 20), interval=2): self.story_popup_timout.reset() return True return False def handle_story_skip(self): if self.map_has_fast_forward: return False return self.story_skip() def ensure_no_story(self, skip_first_screenshot=True): logger.info('Ensure no story') story_timer = Timer(3, count=6).start() while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() if self.story_skip(): story_timer.reset() if story_timer.reached(): break def handle_map_after_combat_story(self): if not self.config.MAP_HAS_MAP_STORY: return False self.ensure_no_story()