import cv2 import numpy as np from scipy import signal from module.base.button import Button, ButtonWrapper from module.base.timer import Timer from module.base.utils import area_size, crop, load_image, rgb2luma from module.logger import logger from module.ui.scroll import Scroll from tasks.base.assets.assets_base_popup import POPUP_CANCEL from tasks.base.ui import UI from tasks.combat.assets.assets_combat_support import COMBAT_SUPPORT_ADD, COMBAT_SUPPORT_LIST, \ COMBAT_SUPPORT_LIST_GRID, COMBAT_SUPPORT_LIST_SCROLL, COMBAT_SUPPORT_SELECTED from tasks.combat.assets.assets_combat_team import COMBAT_TEAM_DISMISSSUPPORT, COMBAT_TEAM_SUPPORT def get_position_in_original_image(position_in_croped_image, crop_area): """ Returns: tuple: (x, y) of position in original image """ return ( position_in_croped_image[0] + crop_area[0], position_in_croped_image[1] + crop_area[1]) if position_in_croped_image else None class SupportCharacter: _image_cache = {} _crop_area = COMBAT_SUPPORT_LIST_GRID.matched_button.area def __init__(self, name, screenshot, similarity=0.75): self.name = name self.image = self._scale_character() self.screenshot = crop(screenshot, SupportCharacter._crop_area) self.similarity = similarity self.button = self._find_character() def __bool__(self): # __bool__ is called when use an object of the class in a boolean context return self.button is not None def __str__(self): return f'SupportCharacter({self.name})' __repr__ = __str__ def _scale_character(self): """ Returns: Image: Character image after scaled """ if self.name in SupportCharacter._image_cache: logger.info(f"Using cached image of {self.name}") return SupportCharacter._image_cache[self.name] img = load_image(f"assets/character/{self.name}.png") scaled_img = cv2.resize(img, (86, 81)) SupportCharacter._image_cache[self.name] = scaled_img logger.info(f"Character {self.name} image cached") return scaled_img def _find_character(self): character = np.array(self.image) support_list_img = self.screenshot res = cv2.matchTemplate( character, support_list_img, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc = cv2.minMaxLoc(res) max_loc = get_position_in_original_image( max_loc, SupportCharacter._crop_area) character_width = character.shape[1] character_height = character.shape[0] return (max_loc[0], max_loc[1], max_loc[0] + character_width, max_loc[1] + character_height) \ if max_val >= self.similarity else None def selected_icon_search(self): """ Returns: tuple: (x1, y1, x2, y2) of selected icon search area """ return ( self.button[0], self.button[1] - 5, self.button[0] + 30, self.button[1]) if self.button else None class ArrowWrapper(ButtonWrapper): def find_center(self, image): res = cv2.matchTemplate( self.matched_button.image, image, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc = cv2.minMaxLoc(res) return ( ( (max_loc[0] + self.matched_button.image.shape[1] / 2), (max_loc[1] + self.matched_button.image.shape[0] / 2), ) if max_val > 0.75 else None ) class NextSupportCharacter: _arrow = ArrowWrapper( name="NextSupportCharacterArrow", share=Button( file='./assets/support/selected_character_arrow.png', area=None, search=None, color=None, button=None, ) ) _crop_area = (290, 115, 435, 634) def __init__(self, screenshot): self.name = "SupportCharacterArrow" self.screenshot = crop(screenshot, NextSupportCharacter._crop_area) self.arrow_center = self._find_center() self.button = self._get_next_support_character_button() def __bool__(self): return self.button is not None def _find_center(self): center = NextSupportCharacter._arrow.find_center(self.screenshot) center = get_position_in_original_image( center, NextSupportCharacter._crop_area) if center else None return center def _get_next_support_character_button(self): area = (self.arrow_center[0] - 200, min(self.arrow_center[1] + 65, 615), self.arrow_center[0] + 10, min( self.arrow_center[1] + 80, 620)) if self.arrow_center and self.arrow_center[1] < 510 else None return ButtonWrapper( name="NextSupportCharacterButton", share=Button( file='./assets/support/selected_character_arrow.png', area=area, search=area, # if next support was selected, the average color of the button will larger than 220 color=(220, 220, 220), button=area, ) ) if self.arrow_center and self.arrow_center[1] < 510 else None class SupportListScroll(Scroll): def cal_position(self, main): """ Args: main (ModuleBase): Returns: float: 0 to 1. """ image = main.device.image temp_area = list(self.area) temp_area[0] = int(temp_area[0] * 0.98) temp_area[2] = int(temp_area[2] * 1.02) line = rgb2luma(crop(image, temp_area)).flatten() width = area_size(temp_area)[0] parameters = { "height": 180, "prominence": 30, "distance": width * 0.75, } peaks, _ = signal.find_peaks(line, **parameters) peaks //= width self.length = len(peaks) middle = np.mean(peaks) position = (middle - self.length / 2) / (self.total - self.length) position = position if position > 0 else 0.0 position = position if position < 1 else 1.0 logger.attr( self.name, f"{position:.2f} ({middle}-{self.length / 2})/({self.total}-{self.length})") return position class CombatSupport(UI): def support_set(self, support_character_name: str = "FirstCharacter"): """ Args: support_character_name: Support character name Returns: bool: If clicked Pages: in: COMBAT_PREPARE mid: COMBAT_SUPPORT_LIST out: COMBAT_PREPARE """ logger.hr("Combat support") skip_first_screenshot = True selected_support = False while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() # End if self.appear(COMBAT_TEAM_DISMISSSUPPORT): return True # Click if self.appear(COMBAT_TEAM_SUPPORT, interval=1): self.device.click(COMBAT_TEAM_SUPPORT) self.interval_reset(COMBAT_TEAM_SUPPORT) continue if self.appear(POPUP_CANCEL, interval=1): logger.warning( "selected identical character, trying select another") self._cancel_popup() self._select_next_support() self.interval_reset(POPUP_CANCEL) continue if self.appear(COMBAT_SUPPORT_LIST, interval=1): if not selected_support and support_character_name != "FirstCharacter": self._search_support( support_character_name) # Search support selected_support = True self.device.click(COMBAT_SUPPORT_ADD) self.interval_reset(COMBAT_SUPPORT_LIST) continue def _search_support(self, support_character_name: str = "JingYuan"): """ Args: support_character_name: Support character name Returns: bool: True if found support else False Pages: in: COMBAT_SUPPORT_LIST out: COMBAT_SUPPORT_LIST """ logger.hr("Combat support search") scroll = SupportListScroll(area=COMBAT_SUPPORT_LIST_SCROLL.area, color=(194, 196, 205), name=COMBAT_SUPPORT_LIST_SCROLL.name) if scroll.appear(main=self): if not scroll.at_bottom(main=self): # Dropdown to load the entire support list, so large threshold is acceptable scroll.drag_threshold, backup = 0.2, scroll.drag_threshold scroll.set_bottom(main=self) scroll.drag_threshold = backup scroll.set_top(main=self) logger.info("Searching support") skip_first_screenshot = False while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() if not support_character_name.startswith("Trailblazer"): character = SupportCharacter( support_character_name, self.device.image) else: character = SupportCharacter(f"Stelle{support_character_name[11:]}", self.device.image) or SupportCharacter( f"Caelum{support_character_name[11:]}", self.device.image) if character: logger.info("Support found") if self._select_support(character): return True else: logger.warning("Support not selected") return False if not scroll.at_bottom(main=self): scroll.next_page(main=self) continue else: logger.info("Support not found") return False def _select_support(self, character: SupportCharacter): """ Args: character: Support character Pages: in: COMBAT_SUPPORT_LIST out: COMBAT_SUPPORT_LIST """ logger.hr("Combat support select") COMBAT_SUPPORT_SELECTED.matched_button.search = character.selected_icon_search() skip_first_screenshot = False interval = Timer(2) while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() # End if self.appear(COMBAT_SUPPORT_SELECTED, similarity=0.75): return True if interval.reached(): self.device.click(character) interval.reset() continue def _cancel_popup(self): """ Pages: in: CANCEL_POPUP out: COMBAT_SUPPORT_LIST """ logger.hr("Combat support cancel popup") skip_first_screenshot = True while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() # End if self.appear(COMBAT_SUPPORT_LIST): logger.info("Popup canceled") return if self.handle_popup_cancel(): continue def _select_next_support(self): """ Pages: in: COMBAT_SUPPORT_LIST out: COMBAT_SUPPORT_LIST """ skip_first_screenshot = True scroll = SupportListScroll(area=COMBAT_SUPPORT_LIST_SCROLL.area, color=(194, 196, 205), name=COMBAT_SUPPORT_LIST_SCROLL.name) interval = Timer(1) next_support = None if scroll.appear(main=self): while 1: if skip_first_screenshot: skip_first_screenshot = False else: self.device.screenshot() # End if next_support and self._next_support_selected(next_support): return if interval.reached(): next_support = NextSupportCharacter(self.device.image) if next_support: logger.info("Next support found, clicking") self.device.click(next_support.button) elif not scroll.at_bottom(main=self): scroll.next_page(main=self, page=0.4) else: logger.warning("No more support") return interval.reset() continue def _next_support_selected(self, next_support: NextSupportCharacter): """ Returns: bool: True if selected else False """ if self.match_color(next_support.button, threshold=20): logger.info("Next support selected") return True return False