diff --git a/assets/cn/statistics/REWARD_AREA.png b/assets/cn/statistics/REWARD_AREA.png new file mode 100644 index 0000000000..8e1acbcf50 Binary files /dev/null and b/assets/cn/statistics/REWARD_AREA.png differ diff --git a/assets/cn/statistics/REWARD_HEADER2.png b/assets/cn/statistics/REWARD_HEADER2.png new file mode 100644 index 0000000000..f214e384ab Binary files /dev/null and b/assets/cn/statistics/REWARD_HEADER2.png differ diff --git a/module/combat/auto_search_combat.py b/module/combat/auto_search_combat.py index 6d9643027e..3a634517b4 100644 --- a/module/combat/auto_search_combat.py +++ b/module/combat/auto_search_combat.py @@ -6,9 +6,10 @@ from module.handler.assets import AUTO_SEARCH_MAP_OPTION_ON from module.logger import logger from module.map.map_operation import MapOperation +from module.statistics.autosearch_reward import * -class AutoSearchCombat(MapOperation, Combat, CampaignStatus): +class AutoSearchCombat(MapOperation, Combat, CampaignStatus, AutosearchReward): _auto_search_in_stage_timer = Timer(3, count=6) _auto_search_status_confirm = False auto_search_oil_limit_triggered = False @@ -175,41 +176,53 @@ def auto_search_moving(self, skip_first_screenshot=True): checked_fleet = False checked_oil = False checked_coin = 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) - if not checked_oil or not checked_coin: - checked_oil = self.auto_search_watch_oil(checked_oil) - checked_coin = self.auto_search_watch_coin(checked_coin) - if self.handle_retirement(): - self.map_offensive_auto_search() - # Map offensive ends at is_combat_loading - break - if self.handle_auto_search_map_option(): - continue - if self.handle_combat_low_emotion(): - self._auto_search_status_confirm = True - continue - if self.handle_story_skip(): - continue - if self.handle_map_cat_attack(): - continue - if self.handle_vote_popup(): - continue + 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() + + if self.is_auto_search_running(): + checked_fleet = self.auto_search_watch_fleet(checked_fleet) + if not checked_oil or not checked_coin: + checked_oil = self.auto_search_watch_oil(checked_oil) + checked_coin = self.auto_search_watch_coin(checked_coin) + if self.handle_retirement(): + self.map_offensive_auto_search() + # Map offensive ends at is_combat_loading + break + if self.handle_auto_search_map_option(): + continue + if self.handle_combat_low_emotion(): + self._auto_search_status_confirm = 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_combat_executing(): - logger.info('is_combat_executing') - break - if self.is_in_auto_search_menu() or self._handle_auto_search_menu_missing(): - raise CampaignEnd + # End + if self.is_combat_loading(): + break + if self.is_combat_executing(): + logger.info('is_combat_executing') + break + if self.is_in_auto_search_menu() or self._handle_auto_search_menu_missing(): + if drop and self.is_in_auto_search_menu(): + while 1: + if self.wait_until_reward_stable() is True: + drop.handle_add(main=self) + else: + logger.info('area stable failed, this should not have happened, taking fallback screenshot') + drop.handle_add(main=self) + raise CampaignEnd + + raise CampaignEnd def auto_search_combat_execute(self, emotion_reduce, fleet_index): """ @@ -255,46 +268,57 @@ def auto_search_combat_execute(self, emotion_reduce, fleet_index): if emotion_reduce: self.emotion.reduce(fleet_index) auto = self.config.Fleet_Fleet1Mode if fleet_index == 1 else self.config.Fleet_Fleet2Mode + with self.stat.new( + genre=self.config.campaign_name, method=self.config.DropRecord_CombatRecord + ) as drop: + while 1: + self.device.screenshot() - 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(): + if self.handle_submarine_call(submarine_mode): continue - # bunch of popup handlers - if self.handle_popup_confirm('AUTO_SEARCH_COMBAT_EXECUTE'): - 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 - if self.handle_mission_popup_ack(): - continue - - # End - if self.is_in_auto_search_menu() or self._handle_auto_search_menu_missing(): - self.device.screenshot_interval_set() - raise CampaignEnd - if self.is_combat_executing(): - continue - if self.handle_get_ship(): - 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() - break + 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 + # bunch of popup handlers + if self.handle_popup_confirm('AUTO_SEARCH_COMBAT_EXECUTE'): + 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 + if self.handle_mission_popup_ack(): + continue + # needed drop here too, since it fails proper leaving if handle_get_ship triggers (even if that was false) + # End + if self.is_in_auto_search_menu() or self._handle_auto_search_menu_missing(): + self.device.screenshot_interval_set() + if drop and self.is_in_auto_search_menu(): + while 1: + if self.wait_until_reward_stable() is True: + drop.handle_add(main=self) + else: + logger.info('area stable failed, this should not have happened, taking fallback screenshot') + drop.handle_add(main=self) + raise CampaignEnd + + raise CampaignEnd + if self.is_combat_executing(): + continue + if self.handle_get_ship(): + 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() + break def auto_search_combat_status(self, skip_first_screenshot=True): """ diff --git a/module/statistics/assets.py b/module/statistics/assets.py index 5698ea92a4..5cbce79e04 100644 --- a/module/statistics/assets.py +++ b/module/statistics/assets.py @@ -7,3 +7,5 @@ CAMPAIGN_BONUS = Button(area={'cn': (404, 149, 439, 166), 'en': (406, 150, 477, 162), 'jp': (404, 150, 476, 167), 'tw': (404, 149, 439, 166)}, color={'cn': (188, 195, 207), 'en': (199, 204, 212), 'jp': (207, 211, 218), 'tw': (188, 195, 207)}, button={'cn': (404, 149, 439, 166), 'en': (406, 150, 477, 162), 'jp': (404, 150, 476, 167), 'tw': (404, 149, 439, 166)}, file={'cn': './assets/cn/statistics/CAMPAIGN_BONUS.png', 'en': './assets/en/statistics/CAMPAIGN_BONUS.png', 'jp': './assets/jp/statistics/CAMPAIGN_BONUS.png', 'tw': './assets/cn/statistics/CAMPAIGN_BONUS.png'}) ENEMY_NAME = Button(area={'cn': (781, 283, 965, 322), 'en': (781, 283, 965, 322), 'jp': (781, 283, 965, 322), 'tw': (781, 283, 965, 322)}, color={'cn': (92, 102, 119), 'en': (92, 102, 119), 'jp': (92, 102, 119), 'tw': (92, 102, 119)}, button={'cn': (781, 283, 965, 322), 'en': (781, 283, 965, 322), 'jp': (781, 283, 965, 322), 'tw': (781, 283, 965, 322)}, file={'cn': './assets/cn/statistics/ENEMY_NAME.png', 'en': './assets/en/statistics/ENEMY_NAME.png', 'jp': './assets/jp/statistics/ENEMY_NAME.png', 'tw': './assets/tw/statistics/ENEMY_NAME.png'}) GET_ITEMS_ODD = Button(area={'cn': (628, 294, 653, 397), 'en': (628, 294, 653, 397), 'jp': (628, 294, 653, 397), 'tw': (628, 294, 653, 397)}, color={'cn': (98, 103, 121), 'en': (98, 103, 121), 'jp': (98, 103, 121), 'tw': (98, 103, 121)}, button={'cn': (628, 294, 653, 397), 'en': (628, 294, 653, 397), 'jp': (628, 294, 653, 397), 'tw': (628, 294, 653, 397)}, file={'cn': './assets/cn/statistics/GET_ITEMS_ODD.png', 'en': './assets/en/statistics/GET_ITEMS_ODD.png', 'jp': './assets/jp/statistics/GET_ITEMS_ODD.png', 'tw': './assets/tw/statistics/GET_ITEMS_ODD.png'}) +REWARD_AREA = Button(area={'cn': (395, 181, 900, 593), 'en': (395, 181, 900, 593), 'jp': (395, 181, 900, 593), 'tw': (395, 181, 900, 593)}, color={'cn': (255, 255, 255), 'en': (255, 255, 255), 'jp': (255, 255, 255), 'tw': (255, 255, 255)}, button={'cn': (395, 181, 900, 593), 'en': (395, 181, 900, 593), 'jp': (395, 181, 900, 593), 'tw': (395, 181, 900, 593)}, file={'cn': './assets/cn/statistics/REWARD_AREA.png', 'en': './assets/cn/statistics/REWARD_AREA.png', 'jp': './assets/cn/statistics/REWARD_AREA.png', 'tw': './assets/cn/statistics/REWARD_AREA.png'}) +REWARD_HEADER2 = Button(area={'cn': (394, 146, 400, 170), 'en': (394, 146, 400, 170), 'jp': (394, 146, 400, 170), 'tw': (394, 146, 400, 170)}, color={'cn': (242, 243, 244), 'en': (242, 243, 244), 'jp': (242, 243, 244), 'tw': (242, 243, 244)}, button={'cn': (394, 146, 400, 170), 'en': (394, 146, 400, 170), 'jp': (394, 146, 400, 170), 'tw': (394, 146, 400, 170)}, file={'cn': './assets/cn/statistics/REWARD_HEADER2.png', 'en': './assets/cn/statistics/REWARD_HEADER2.png', 'jp': './assets/cn/statistics/REWARD_HEADER2.png', 'tw': './assets/cn/statistics/REWARD_HEADER2.png'}) diff --git a/module/statistics/autosearch_reward.py b/module/statistics/autosearch_reward.py new file mode 100644 index 0000000000..a82217faac --- /dev/null +++ b/module/statistics/autosearch_reward.py @@ -0,0 +1,97 @@ +import time +import cv2 +import datetime +from module.logger import logger +from module.base.utils import crop +from module.statistics.assets import REWARD_HEADER2, REWARD_AREA + +class AutosearchReward(): + + def wait_until_reward_stable(self, timeout=6.6, required_consecutive_matches=3): + """ + checks if all rewards have appeared, via offset matching header 2, after that waits till the reward area has no changes + e.g all drops appeared + + timeout: may need adjustment after gathering data; 5.84s longest atm\\ + required_consecutive_matches: 3 seems a good compromise + """ + try: + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + + self.device.screenshot() + + # 1. Detect header2 + if not self.appear(REWARD_HEADER2, offset=(200,0)): + logger.warning("Header not found within search area") + return False + + # 2. Apply detected offset to reward area + REWARD_AREA.load_offset(REWARD_HEADER2) + logger.info(f"Applied offset: {REWARD_HEADER2._button_offset}") + + # 3. Get adjusted coordinates + adjusted_area = REWARD_AREA.button + + # 4. Validate (prevent negative width) + if adjusted_area[0] >= adjusted_area[2]: + logger.warning(f"Invalid area after offset: {adjusted_area}") + return False + + # 5. stable area check + # similarity_threshold: in testing the similarity ranged in 0.002- 0.01 ranges so opted for 0.999 + + return self.reward_area_stable_check(monitor_area=adjusted_area, timeout=timeout, required_matches=required_consecutive_matches,similarity_threshold=0.999, timestamp=timestamp) + + except Exception as e: + logger.error(f"Offset application failed: {e}") + return False + + def reward_area_stable_check(self, monitor_area, timeout, required_matches, similarity_threshold, timestamp=""): + """ + checks if the previously assigned area has any changes + returns True for being stable after 3 same consecutive matches, while similarity was met or beaten + """ + start_time = time.time() + last_check = start_time + stabilization_start = None + consecutive_matches = 0 + check_count = 0 + + initial_gray = cv2.cvtColor(self.device.image, cv2.COLOR_BGR2GRAY) + last_luma = crop(initial_gray, monitor_area) + + while True: + current_time = time.time() + elapsed = current_time - start_time + + if elapsed >= timeout: + logger.warning(f'Timeout after {elapsed:.2f}s (never stabilized)') + return False + + if current_time - last_check >= 0.2: + check_count += 1 + last_check = current_time + + self.device.screenshot() + current_luma = cv2.cvtColor(self.device.image, cv2.COLOR_BGR2GRAY) + current_luma = crop(current_luma, monitor_area) + + res = cv2.matchTemplate(last_luma, current_luma, cv2.TM_CCOEFF_NORMED) + similarity = cv2.minMaxLoc(res)[1] + + if similarity >= similarity_threshold: + consecutive_matches += 1 + if stabilization_start is None: + stabilization_start = time.time() + + if consecutive_matches >= required_matches: + stabilization_time = time.time() - stabilization_start + total_time = time.time() - start_time + logger.info(f'Area stabilized after {total_time:.2f}s (took {stabilization_time:.2f}s to confirm)') + + return True + else: + consecutive_matches = 0 + stabilization_start = None + + last_luma = current_luma