from datetime import datetime from module.base.decorator import Config, cached_property from module.base.timer import Timer from module.campaign.campaign_base import CampaignBase from module.campaign.run import CampaignRun from module.combat.assets import BATTLE_PREPARATION from module.combat.emotion import Emotion from module.equipment.assets import * from module.exception import CampaignEnd, HardNotSatisfied from module.handler.assets import AUTO_SEARCH_MAP_OPTION_OFF from module.logger import logger from module.map.assets import FLEET_PREPARATION, MAP_PREPARATION from module.ocr.ocr import Digit from module.retire.assets import * from module.retire.dock import Dock # TEMPLATE_COMMON_CV and TEMPLATE_COMMON_DD are both used in function find_custom_candidates from module.retire.retirement import TEMPLATE_COMMON_CV, TEMPLATE_COMMON_DD from module.retire.scanner import ShipScanner from module.ui.assets import BACK_ARROW from module.ui.page import page_fleet FLEET_INDEX = Digit(OCR_FLEET_INDEX, letter=(90, 154, 255), threshold=128, alphabet='123456') SIM_VALUE = 0.92 class GemsCampaignOverride(CampaignBase): def handle_combat_low_emotion(self): """ Overwrite info_handler.handle_combat_low_emotion() If change vanguard is enabled, withdraw combat and change flagship and vanguard """ if self.config.GemsFarming_ChangeVanguard == 'disabled': result = self.handle_popup_confirm('IGNORE_LOW_EMOTION') if result: # Avoid clicking AUTO_SEARCH_MAP_OPTION_OFF self.interval_reset(AUTO_SEARCH_MAP_OPTION_OFF) return result if self.handle_popup_cancel('IGNORE_LOW_EMOTION'): self.config.GEMS_EMOTION_TRIGGERED = True logger.hr('EMOTION WITHDRAW') while 1: self.device.screenshot() if self.handle_story_skip(): continue if self.handle_popup_cancel('IGNORE_LOW_EMOTION'): continue if self.appear(BATTLE_PREPARATION, offset=(20, 20), interval=2): self.device.click(BACK_ARROW) continue if self.handle_auto_search_exit(): continue if self.is_in_stage(): break if self.is_in_map(): self.withdraw() break if self.appear(FLEET_PREPARATION, offset=(20, 50), interval=2) \ or self.appear(MAP_PREPARATION, offset=(20, 20), interval=2): self.enter_map_cancel() break raise CampaignEnd('Emotion withdraw') class GemsEmotion(Emotion): def check_reduce(self, battle): """ Override Emotion.check_reduce to trigger stop condition when emotion is too low before battle. """ if not self.is_calculate: return recovered, delay = self._check_reduce(battle) if delay: self.config.GEMS_EMOTION_TRIGGERED = True logger.info('Detect low emotion, pause current task') raise CampaignEnd('Emotion control') def wait(self, fleet_index): """ Override Emotion.wait to trigger stop condition when emotion is too low after battle. """ self.update() self.record() self.show() fleet = self.fleets[fleet_index - 1] recovered = fleet.get_recovered(expected_reduce=self.reduce_per_battle) if recovered > datetime.now(): self.config.GEMS_EMOTION_TRIGGERED = True class GemsFarming(CampaignRun, Dock): def load_campaign(self, name, folder='campaign_main'): super().load_campaign(name, folder) gems = self class GemsCampaign(GemsCampaignOverride, self.module.Campaign): @cached_property def emotion(self): return GemsEmotion(config=self.config) def fleet_preparation(self): try: return super().fleet_preparation() except HardNotSatisfied: pass prepared = gems.hard_fleet_prepare() if prepared: try: return super().fleet_preparation() except HardNotSatisfied: logger.warning('Hard fleet still not satisfied after preparation, delay GemsFarming') else: logger.warning('No ship available for hard fleet preparation, delay GemsFarming') gems.config.task_delay(minute=30) gems.config.task_stop() return False self.campaign = GemsCampaign(device=self.campaign.device, config=self.campaign.config) if not self.change_vanguard: self.campaign.config.override(Emotion_Mode='ignore') self.campaign.config.override(EnemyPriority_EnemyScaleBalanceWeight='S1_enemy_first') @property def change_flagship_equip(self): return 'equip' in self.config.GemsFarming_ChangeFlagship @property def change_vanguard(self): return 'ship' in self.config.GemsFarming_ChangeVanguard @property def change_vanguard_equip(self): return 'equip' in self.config.GemsFarming_ChangeVanguard @property def allow_high_flagship_level(self): return self.config.GemsFarming_ALLowHighFlagshipLevel @property def max_level(self): if self.config.SERVER == 'cn': return 100 else: return 70 @property def min_emotion(self): return (2 + self.campaign._map_battle) * self.campaign.emotion.reduce_per_battle _new_fleet_emotion = 0 @property def fleet_to_attack(self): if self.config.Fleet_FleetOrder == 'fleet1_standby_fleet2_all': return 2 else: return 1 @property def fleet_to_attack_index(self): if self.config.Fleet_FleetOrder == 'fleet1_standby_fleet2_all': return self.config.Fleet_Fleet2 else: return self.config.Fleet_Fleet1 @Config.when(Campaign_Mode='normal') def ui_goto_fleet(self): self.ui_ensure(page_fleet) # ui_ensure_index, set fleet letter = FLEET_INDEX next_button = FLEET_NEXT prev_button = FLEET_PREV interval = (0.2, 0.3) retry = Timer(1, count=2) for _ in self.loop(): current = letter.ocr(self.device.image) logger.attr("Index", current) # ui_ensure_index but ignore default value 0 # otherwise we would have 1 extra click switching from 1 to 4 if current == 0: continue diff = self.fleet_to_attack_index - current if diff == 0: break if retry.reached(): button = next_button if diff > 0 else prev_button self.device.multi_click(button, n=abs(diff), interval=interval) retry.reset() @Config.when(Campaign_Mode='hard') def ui_goto_fleet(self): if self.appear(FLEET_PREPARATION, offset=(20, 50)): return self.campaign.ensure_campaign_ui(self.stage, 'hard') self.campaign.ENTRANCE.area = self.campaign.ENTRANCE.button campaign_timer = Timer(5) map_timer = Timer(5) for _ in self.loop(): if self.appear(FLEET_PREPARATION, offset=(20, 50)): break if map_timer.reached() \ and self.campaign.handle_map_mode_switch('hard') \ and self.campaign.handle_map_preparation(): self.device.click(MAP_PREPARATION) map_timer.reset() campaign_timer.reset() # Retire if self.campaign.handle_retirement(): continue if campaign_timer.reached() and self.appear_then_click(self.campaign.ENTRANCE): campaign_timer.reset() continue @property def fleet_check_button(self): if self.config.Campaign_Mode == 'hard': return FLEET_PREPARATION return page_fleet.check_button @property def fleet_backline_1_button(self): if self.config.Campaign_Mode == 'hard': return globals()[f'FLEET_{self.fleet_to_attack}_BACKLINE_1'] return FLEET_ENTER_FLAGSHIP @property def fleet_vanguard_1_button(self): if self.config.Campaign_Mode == 'hard': return globals()[f'FLEET_{self.fleet_to_attack}_VANGUARD_1'] return FLEET_ENTER def ui_enter_ship(self, click_button, long_click=True): if long_click: enter_button_map = { FLEET_ENTER_FLAGSHIP: FLEET_DETAIL_ENTER_FLAGSHIP, FLEET_ENTER: FLEET_DETAIL_ENTER, } enter_button = enter_button_map.get(click_button) if enter_button is None: self.ship_info_enter(click_button=click_button, long_click=True, skip_first_screenshot=False) return self.ui_click(FLEET_DETAIL, appear_button=self.fleet_check_button, check_button=FLEET_DETAIL_CHECK, skip_first_screenshot=True) self.ship_info_enter(enter_button, long_click=False) else: self.ship_info_enter(click_button=click_button, check_button=DOCK_CHECK, long_click=False, skip_first_screenshot=False) def ui_leave_ship(self, check_button=None): if check_button is None: check_button = self.fleet_check_button if check_button == page_fleet.check_button: self.ui_back(check_button=[FLEET_DETAIL_CHECK, page_fleet.check_button]) self.ui_back(check_button=page_fleet.check_button) else: self.ui_back(check_button=check_button) def find_candidates(self, templates, scanner: ShipScanner, output=False): """ Find candidates based on template matching using a scanner. """ ships = scanner.scan(self.device.image, output=output) if not templates: return ships candidates = [] for ship in ships: for template in templates: if template.match(self.image_crop(ship.button, copy=False), similarity=SIM_VALUE): candidates.append(ship) break return candidates def find_custom_candidates(self, scanner, ship_type='cv'): """ Get the candidates of common rarity cv/dd, only for 'custom' GemsFarming_CommonCV/DD settings Args: scanner (ShipScanner): ship_type (str): 'cv' or 'dd' """ if ship_type.lower() not in ['cv', 'dd']: logger.warning(f'Invalid ship_type: {ship_type}') return [] ship_type = ship_type.upper() logger.info(f'Search for Common {ship_type}.') if ship_type.lower() == 'cv' and self.config.GemsFarming_CommonCV != 'custom': filter_string = self.config.COMMON_CV_FILTER else: filter_string = self.config.__getattribute__(f'GemsFarming_Common{ship_type}Filter') sort_dsc_first = ship_type.lower() == 'dd' common_ship = self.campaign.get_common_ship_filter(filter_string, ship_type=ship_type) templates = globals()[f'TEMPLATE_COMMON_{ship_type}'] find_first = True common_ship_candidates = {} for name in common_ship: template = templates[name.upper()] candidates = self.find_candidates([template], scanner, output=False) if find_first: find_first = False if candidates: logger.info(f'Find Common {ship_type} {name}.') return candidates common_ship_candidates[name] = candidates logger.info(f'No suitable {ship_type} was found, try reversed order.') self.dock_sort_method_dsc_set(not sort_dsc_first) for name in common_ship: template = templates[name.upper()] candidates = self.find_candidates([template], scanner, output=False) if candidates: logger.info(f'Find Common DD {name}.') return candidates elif common_ship_candidates[name]: logger.info(f'Find Common DD {name}.') self.dock_sort_method_dsc_set(sort_dsc_first, wait_loading=False) return common_ship_candidates[name] return [] def get_cv_templates(self): """ Get CV templates based on config.GemsFarming_CommonCV Returns: list[Template]: CV templates """ if self.config.GemsFarming_CommonCV in ['custom', 'any', 'eagle']: return [] else: templates = { 'bogue': TEMPLATE_BOGUE, 'hermes': TEMPLATE_HERMES, 'langley': TEMPLATE_LANGLEY, 'ranger': TEMPLATE_RANGER, } return [templates[self.config.GemsFarming_CommonCV]] def get_common_rarity_cv(self, max_level=31, min_emotion=0): """ Get a common rarity cv by config.GemsFarming_CommonCV If config.GemsFarming_CommonCV == 'any', return a common lv1 ~ lv31 cv by default. Returns: list[Ship]: Common rarity CVs that meet the level, emotion and fleet requirements. """ faction = 'eagle' if self.config.GemsFarming_CommonCV == 'eagle' else 'all' extra = 'can_limit_break' if self.allow_high_flagship_level else 'enhanceable' self.dock_favourite_set(False, wait_loading=False) self.dock_sort_method_dsc_set(False, wait_loading=False) self.dock_filter_set(index='cv', rarity='common', faction=faction, extra=extra, sort='total') logger.hr('FINDING FLAGSHIP') templates = self.get_cv_templates() if self.allow_high_flagship_level: min_level = self.max_level max_level = self.max_level else: min_level = 1 fleet = [0, self.fleet_to_attack_index] if self.allow_high_flagship_level else self.fleet_to_attack_index scanner = ShipScanner(level=(min_level, max_level), emotion=(min_emotion, 150), fleet=fleet, status='free') scanner.disable('rarity') if not self.allow_high_flagship_level: candidates = self.find_candidates(templates, scanner, output=True) if candidates: return candidates scanner.set_limitation(fleet=0) if self.config.GemsFarming_CommonCV in ['custom', 'any', 'eagle']: candidates = self.find_custom_candidates(scanner, ship_type='cv') return candidates else: candidates = self.find_candidates(templates, scanner, output=False) if candidates or templates == []: return candidates logger.info('No specific CV was found, try reversed order.') self.dock_sort_method_dsc_set(True) candidates = self.find_candidates(templates, scanner, output=True) return candidates @Config.when(Campaign_Mode='normal') def flagship_change_execute(self): self.ui_enter_ship(FLEET_ENTER_FLAGSHIP, long_click=False) candidate = self.get_common_rarity_cv(min_emotion=self.min_emotion) if candidate: ship = max(candidate, key=lambda s: (s.level, s.emotion)) if self.allow_high_flagship_level: self._new_fleet_emotion = min(ship.emotion, self._new_fleet_emotion) if self._new_fleet_emotion else ship.emotion self.dock_select_one(ship.button) self.dock_reset() self.dock_select_confirm(check_button=page_fleet.check_button) logger.info('Change flagship success') return True logger.info('Change flagship failed, no CV in common rarity.') self._new_fleet_emotion = 0 self.dock_reset() self.ui_leave_ship() return False @Config.when(Campaign_Mode='hard') def flagship_change_execute(self): unmount_button = globals()[f'FLEET_{self.fleet_to_attack}_BACKLINE_1'] mount_button = globals()[f'FLEET_{self.fleet_to_attack}_BACKLINE_3'] if self.appear(unmount_button, offset=(20, 20)): logger.info('No flagship to unmount, skip unmounting.') else: self.ui_enter_ship(unmount_button, long_click=False) self.ui_click(DOCK_UNMOUNT, check_button=FLEET_PREPARATION, appear_button=DOCK_CHECK, additional=self.ensure_no_info_bar, confirm_wait=1, retry_wait=5) self.ui_enter_ship(mount_button, long_click=False) candidate = self.get_common_rarity_cv(max_level=31, min_emotion=self.min_emotion) if candidate: ship = max(candidate, key=lambda s: (s.level, s.emotion)) if self.allow_high_flagship_level: self._new_fleet_emotion = min(ship.emotion, self._new_fleet_emotion) if self._new_fleet_emotion else ship.emotion self.dock_select_one(ship.button) self.dock_reset() self.dock_select_confirm(check_button=FLEET_PREPARATION) logger.info('Change flagship success') return True logger.info('Change flagship failed, try using leveled and exhausted CVs.') candidate = self.get_common_rarity_cv(max_level=self.max_level, min_emotion=0) if candidate: ship = min(candidate, key=lambda s: (s.level, -s.emotion)) if self.allow_high_flagship_level: self._new_fleet_emotion = min(ship.emotion, self._new_fleet_emotion) if self._new_fleet_emotion else ship.emotion self.dock_select_one(ship.button) self.dock_reset() self.dock_select_confirm(check_button=FLEET_PREPARATION) return False logger.info('Change flagship failed, no CV was found.') self._new_fleet_emotion = 0 self.dock_reset() self.ui_leave_ship() return False def flagship_change(self): """ Change flagship and flagship's equipment Returns: bool: True if flagship changed. """ logger.hr('Change flagship', level=1) logger.attr('ChangeFlagship', self.config.GemsFarming_ChangeFlagship) self.ui_goto_fleet() button = self.fleet_backline_1_button if self.change_flagship_equip and not self.appear(button, offset=(20, 20)): logger.hr('Unmount flagship equipments', level=2) self.ui_enter_ship(button, long_click=True) self.ship_equipment_take_off() self.ui_leave_ship() logger.hr('Change flagship', level=2) success = self.flagship_change_execute() if self.change_flagship_equip and not self.appear(button, offset=(20, 20)): logger.hr('Mount flagship equipments', level=2) self.ui_enter_ship(button, long_click=True) self.ship_equipment_take_on() self.ui_leave_ship() return success def get_dd_faction(self): if self.config.GemsFarming_CommonDD in ['any', 'custom']: faction = ['eagle', 'iron'] elif self.config.GemsFarming_CommonDD == 'favourite': faction = 'all' elif self.config.GemsFarming_CommonDD == 'z20_or_z21': faction = 'iron' elif self.config.GemsFarming_CommonDD == 'DDG': faction = 'dragon' elif self.config.GemsFarming_CommonDD in ['aulick_or_foote', 'cassin_or_downes']: faction = 'eagle' else: logger.error(f'Invalid CommonDD setting: {self.config.GemsFarming_CommonDD}') logger.error("Default to 'eagle' and 'iron' faction.") faction = ['eagle', 'iron'] return faction def get_dd_templates(self): """ Get DD templates based on config.GemsFarming_CommonDD Returns: list[Template]: DD templates """ if self.config.GemsFarming_CommonDD == 'aulick_or_foote': return [ TEMPLATE_AULICK, TEMPLATE_FOOTE ] elif self.config.GemsFarming_CommonDD == 'cassin_or_downes': return [ TEMPLATE_CASSIN_1, TEMPLATE_CASSIN_2, TEMPLATE_DOWNES_1, TEMPLATE_DOWNES_2 ] else: return [] def get_common_rarity_dd(self, min_emotion=0): """ Get a common rarity dd by config.GemsFarming_CommonDD If config.GemsFarming_CommonDD == 'any', return a common self.max_level dd by default. Returns: list[Ship]: Common rarity DDs that meet the emotion and fleet requirements. """ if self.config.GemsFarming_CommonDD == 'DDG': rarity = 'super_rare' extra = 'no_limit' else: rarity = 'common' extra = 'can_limit_break' faction = self.get_dd_faction() self.dock_favourite_set(self.config.GemsFarming_CommonDD == 'favourite', wait_loading=False) self.dock_sort_method_dsc_set(True, wait_loading=False) self.dock_filter_set(index='dd', rarity=rarity, faction=faction, extra=extra) logger.hr('FINDING VANGUARD') templates = self.get_dd_templates() max_level = 125 if self.config.GemsFarming_CommonDD == 'DDG' else self.max_level min_level = 30 if self.config.GemsFarming_ALLowLowVanguardLevel else max_level scanner = ShipScanner(level=(min_level, max_level), emotion=(min_emotion, 150), fleet=[0, self.fleet_to_attack_index], status='free') scanner.disable('rarity') if self.config.GemsFarming_CommonDD == 'custom': candidates = self.find_custom_candidates(scanner, ship_type='dd') return candidates else: candidates = self.find_candidates(templates, scanner, output=True) if candidates or templates == []: return candidates logger.info('No specific DD was found, try reversed order.') self.dock_sort_method_dsc_set(False) candidates = self.find_candidates(templates, scanner, output=True) return candidates @Config.when(Campaign_Mode='normal') def vanguard_change_execute(self): self.ui_enter_ship(FLEET_ENTER, long_click=False) candidate = self.get_common_rarity_dd(min_emotion=self.min_emotion) if candidate: ship = max(candidate, key=lambda s: s.emotion) self._new_fleet_emotion = min(ship.emotion, self._new_fleet_emotion) if self._new_fleet_emotion else ship.emotion self.dock_select_one(ship.button) self.dock_reset() self.dock_select_confirm(check_button=page_fleet.check_button) logger.info('Change vanguard success') return True logger.info('Change vanguard failed, no DD in common rarity.') self._new_fleet_emotion = 0 self.dock_reset() self.ui_leave_ship() return False @Config.when(Campaign_Mode='hard') def vanguard_change_execute(self): unmount_button = globals()[f'FLEET_{self.fleet_to_attack}_VANGUARD_1'] mount_button = globals()[f'FLEET_{self.fleet_to_attack}_VANGUARD_3'] if self.appear(unmount_button, offset=(20, 20)): logger.info('No flagship to unmount, skip unmounting.') else: self.ui_enter_ship(unmount_button, long_click=False) self.ui_click(DOCK_UNMOUNT, check_button=FLEET_PREPARATION, appear_button=DOCK_CHECK, additional=self.ensure_no_info_bar, confirm_wait=1, retry_wait=5) self.ui_enter_ship(mount_button, long_click=False) candidate = self.get_common_rarity_dd(min_emotion=self.min_emotion) if candidate: ship = max(candidate, key=lambda s: s.emotion) self._new_fleet_emotion = min(ship.emotion, self._new_fleet_emotion) if self._new_fleet_emotion else ship.emotion self.dock_select_one(ship.button) self.dock_reset() self.dock_select_confirm(check_button=FLEET_PREPARATION) logger.info('Change vanguard success') return True logger.info('Change vanguard failed, try using exhausted DDs.') candidate = self.get_common_rarity_dd(min_emotion=0) if candidate: ship = max(candidate, key=lambda s: s.emotion) self._new_fleet_emotion = min(ship.emotion, self._new_fleet_emotion) if self._new_fleet_emotion else ship.emotion self.dock_select_one(ship.button) self.dock_reset() self.dock_select_confirm(check_button=FLEET_PREPARATION) return False logger.info('Change vanguard failed, no DD was found.') self._new_fleet_emotion = 0 self.dock_reset() self.ui_leave_ship() return False def vanguard_change(self): """ Change vanguard and vanguard's equipment Returns: bool: True if vanguard changed. """ logger.hr('Change vanguard', level=1) logger.attr('ChangeVanguard', self.config.GemsFarming_ChangeVanguard) self.ui_goto_fleet() button = self.fleet_vanguard_1_button if self.change_vanguard_equip and not self.appear(button, offset=(20, 20)): logger.hr('Unmount vanguard equipments', level=2) self.ui_enter_ship(button, long_click=True) self.ship_equipment_take_off() self.ui_leave_ship() logger.hr('Change vanguard', level=2) success = self.vanguard_change_execute() if self.change_vanguard_equip and not self.appear(button, offset=(20, 20)): logger.hr('Mount vanguard equipments', level=2) self.ui_enter_ship(button, long_click=True) self.ship_equipment_take_on() self.ui_leave_ship() return success def hard_fleet_prepare(self): """ Fill empty hard-mode fleet slots after ships used for hard requirements were retired. Returns: bool: True if any slot has been filled. """ prepared = False if self.appear(self.fleet_backline_1_button, offset=(20, 20)): logger.info('Backline is empty, change flagship') self.flagship_change() prepared = True if self.appear(self.fleet_vanguard_1_button, offset=(20, 20)): logger.info('Vanguard is empty, change vanguard') self.vanguard_change() prepared = True return prepared _trigger_lv32: bool = False _trigger_emotion: bool = False def triggered_stop_condition(self, oil_check=True): # Lv32 limit if self.campaign.config.LV32_TRIGGERED and not self.allow_high_flagship_level: self._trigger_lv32 = True logger.hr('TRIGGERED LV32 LIMIT') return True if self.campaign.config.GEMS_EMOTION_TRIGGERED: self._trigger_emotion = True logger.hr('TRIGGERED EMOTION LIMIT') return True return super().triggered_stop_condition(oil_check=oil_check) def run(self, name, folder='campaign_main', mode='normal', total=0): """ Args: name (str): Name of .py file. folder (str): Name of the file folder under campaign. mode (str): `normal` or `hard` total (int): """ self.config.override(STOP_IF_REACH_LV32=True) while 1: self._trigger_lv32 = False is_limit = self.config.StopCondition_RunCount try: super().run(name=name, folder=folder, total=total) except CampaignEnd as e: if "Emotion" in e.args[0]: self._trigger_emotion = True else: raise # End if self._trigger_lv32 or self._trigger_emotion: self._new_fleet_emotion = 150 success = self.flagship_change() if self.change_vanguard: success = success and self.vanguard_change() if self.fleet_to_attack == 2: self.campaign.config.set_record(Emotion_Fleet2Value=self._new_fleet_emotion) else: self.campaign.config.set_record(Emotion_Fleet1Value=self._new_fleet_emotion) if is_limit and self.config.StopCondition_RunCount <= 0: logger.hr('Triggered stop condition: Run count') self.config.StopCondition_RunCount = 0 self.config.Scheduler_Enable = False break self._trigger_lv32 = False self.campaign.config.LV32_TRIGGERED = False self.campaign.config.GEMS_EMOTION_TRIGGERED = False # Scheduler if self.config.task_switched(): self._trigger_emotion = False self.campaign.ensure_auto_search_exit() self.config.task_stop() elif not success and (self.config.GemsFarming_DelayTaskIFNoFlagship \ or self._trigger_emotion): self._trigger_emotion = False self.campaign.ensure_auto_search_exit() self.config.task_delay(minute=30) self.config.task_stop() self._trigger_emotion = False continue else: break