diff --git a/assets/cn/base/popup/GET_LIGHT_CONE.png b/assets/cn/base/popup/GET_LIGHT_CONE.png new file mode 100644 index 000000000..0a88e28ad Binary files /dev/null and b/assets/cn/base/popup/GET_LIGHT_CONE.png differ diff --git a/assets/cn/dungeon/event/OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT.png b/assets/cn/dungeon/event/OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT.png deleted file mode 100644 index 5bac51466..000000000 Binary files a/assets/cn/dungeon/event/OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT.png and /dev/null differ diff --git a/assets/en/base/popup/GET_LIGHT_CONE.png b/assets/en/base/popup/GET_LIGHT_CONE.png new file mode 100644 index 000000000..b524f8acf Binary files /dev/null and b/assets/en/base/popup/GET_LIGHT_CONE.png differ diff --git a/assets/en/dungeon/event/OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT.png b/assets/en/dungeon/event/OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT.png deleted file mode 100644 index 22ddc015d..000000000 Binary files a/assets/en/dungeon/event/OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT.png and /dev/null differ diff --git a/assets/share/combat/interact/DUNGEON_COMBAT_INTERACT_TEXT.png b/assets/share/combat/interact/DUNGEON_COMBAT_INTERACT_TEXT.png new file mode 100644 index 000000000..48aed9ba0 Binary files /dev/null and b/assets/share/combat/interact/DUNGEON_COMBAT_INTERACT_TEXT.png differ diff --git a/assets/share/dungeon/event/OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT.png b/assets/share/dungeon/event/OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT.png new file mode 100644 index 000000000..84abdea5c Binary files /dev/null and b/assets/share/dungeon/event/OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT.png differ diff --git a/assets/share/dungeon/state/OCR_SIMUNI_POINT.png b/assets/share/dungeon/state/OCR_SIMUNI_POINT.png new file mode 100644 index 000000000..51bf72943 Binary files /dev/null and b/assets/share/dungeon/state/OCR_SIMUNI_POINT.png differ diff --git a/assets/share/dungeon/ui/OCR_SIMUNI_POINT_OFFSET.SEARCH.png b/assets/share/dungeon/state/OCR_SIMUNI_POINT_OFFSET.SEARCH.png similarity index 100% rename from assets/share/dungeon/ui/OCR_SIMUNI_POINT_OFFSET.SEARCH.png rename to assets/share/dungeon/state/OCR_SIMUNI_POINT_OFFSET.SEARCH.png diff --git a/assets/share/dungeon/ui/OCR_SIMUNI_POINT_OFFSET.png b/assets/share/dungeon/state/OCR_SIMUNI_POINT_OFFSET.png similarity index 100% rename from assets/share/dungeon/ui/OCR_SIMUNI_POINT_OFFSET.png rename to assets/share/dungeon/state/OCR_SIMUNI_POINT_OFFSET.png diff --git a/assets/share/dungeon/state/OCR_STAMINA.png b/assets/share/dungeon/state/OCR_STAMINA.png new file mode 100644 index 000000000..8a7dc0f12 Binary files /dev/null and b/assets/share/dungeon/state/OCR_STAMINA.png differ diff --git a/assets/share/dungeon/ui/OCR_SIMUNI_POINT.png b/assets/share/dungeon/ui/OCR_SIMUNI_POINT.png deleted file mode 100644 index 540d04be7..000000000 Binary files a/assets/share/dungeon/ui/OCR_SIMUNI_POINT.png and /dev/null differ diff --git a/module/webui/lang.py b/module/webui/lang.py index 1c9bc0ef4..12245c907 100644 --- a/module/webui/lang.py +++ b/module/webui/lang.py @@ -89,12 +89,15 @@ def readable_time(before: str) -> str: elif diff < 60: # < 1 min return t("Gui.Dashboard.JustNow") - elif diff < 3600: + elif diff < 5400: + # < 90 min return t("Gui.Dashboard.MinutesAgo", time=int(diff // 60)) - elif diff < 86400: + elif diff < 129600: + # < 36 hours return t("Gui.Dashboard.HoursAgo", time=int(diff // 3600)) elif diff < 1296000: + # < 15 days return t("Gui.Dashboard.DaysAgo", time=int(diff // 86400)) else: - # > 15 days + # >= 15 days return t("Gui.Dashboard.LongTimeAgo") diff --git a/module/webui/utils.py b/module/webui/utils.py index 7cd3a35ee..e69907e54 100644 --- a/module/webui/utils.py +++ b/module/webui/utils.py @@ -455,7 +455,11 @@ def get_localstorage(key): def re_fullmatch(pattern, string): if pattern == "datetime": - pattern = RE_DATETIME + try: + datetime.datetime.fromisoformat(string) + return True + except ValueError: + return False # elif: return re.fullmatch(pattern=pattern, string=string) diff --git a/tasks/base/assets/assets_base_popup.py b/tasks/base/assets/assets_base_popup.py index e640e5659..89ac6b632 100644 --- a/tasks/base/assets/assets_base_popup.py +++ b/tasks/base/assets/assets_base_popup.py @@ -13,6 +13,23 @@ BATTLE_PASS_NOTIFICATION = ButtonWrapper( button=(895, 595, 1180, 630), ), ) +GET_LIGHT_CONE = ButtonWrapper( + name='GET_LIGHT_CONE', + cn=Button( + file='./assets/cn/base/popup/GET_LIGHT_CONE.png', + area=(205, 321, 242, 339), + search=(185, 301, 262, 359), + color=(130, 130, 131), + button=(205, 321, 242, 339), + ), + en=Button( + file='./assets/en/base/popup/GET_LIGHT_CONE.png', + area=(260, 322, 306, 338), + search=(240, 302, 326, 358), + color=(147, 147, 148), + button=(260, 322, 306, 338), + ), +) GET_REWARD = ButtonWrapper( name='GET_REWARD', share=Button( diff --git a/tasks/base/popup.py b/tasks/base/popup.py index 074140bc4..000194150 100644 --- a/tasks/base/popup.py +++ b/tasks/base/popup.py @@ -96,3 +96,20 @@ class PopupHandler(ModuleBase): return True return False + + def handle_get_light_cone(self, interval=2) -> bool: + """ + Popup when getting a light cone from Echo of War. + + Args: + interval: + + Returns: + If handled. + """ + if self.appear(GET_LIGHT_CONE, interval=interval): + logger.info(f'{GET_LIGHT_CONE} -> {GET_REWARD}') + self.device.click(GET_REWARD) + return True + + return False diff --git a/tasks/base/ui.py b/tasks/base/ui.py index da63c2312..c93af5f33 100644 --- a/tasks/base/ui.py +++ b/tasks/base/ui.py @@ -321,6 +321,8 @@ class UI(MainPage): return True if self.handle_monthly_card_reward(): return True + if self.handle_get_light_cone(): + return True if self.appear(COMBAT_PREPARE, interval=5): logger.info(f'UI additional: {COMBAT_PREPARE} -> {CLOSE}') self.device.click(CLOSE) diff --git a/tasks/combat/assets/assets_combat_interact.py b/tasks/combat/assets/assets_combat_interact.py index 5b575ed58..790440071 100644 --- a/tasks/combat/assets/assets_combat_interact.py +++ b/tasks/combat/assets/assets_combat_interact.py @@ -13,6 +13,16 @@ DUNGEON_COMBAT_INTERACT = ButtonWrapper( button=(750, 411, 997, 448), ), ) +DUNGEON_COMBAT_INTERACT_TEXT = ButtonWrapper( + name='DUNGEON_COMBAT_INTERACT_TEXT', + share=Button( + file='./assets/share/combat/interact/DUNGEON_COMBAT_INTERACT_TEXT.png', + area=(790, 391, 1055, 456), + search=(770, 371, 1075, 476), + color=(47, 51, 53), + button=(790, 391, 1055, 456), + ), +) MAP_LOADING = ButtonWrapper( name='MAP_LOADING', share=Button( diff --git a/tasks/combat/combat.py b/tasks/combat/combat.py index 905656dd2..3412b2068 100644 --- a/tasks/combat/combat.py +++ b/tasks/combat/combat.py @@ -65,27 +65,6 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo return False - def combat_enter_from_map(self, skip_first_screenshot=True): - """ - Pages: - in: page_main, DUNGEON_COMBAT_INTERACT - out: COMBAT_PREPARE - """ - logger.info('Combat enter from map') - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - - if self.appear(COMBAT_PREPARE): - # Confirm page loaded - if self.image_color_count(COMBAT_PREPARE.button, color=(230, 230, 230), threshold=240, count=400): - logger.info(f'At {COMBAT_PREPARE}') - break - if self.handle_combat_interact(): - continue - def combat_prepare(self, team=1, support_character: str = None): """ Args: @@ -306,6 +285,8 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo logger.info(f'{COMBAT_AGAIN} -> {COMBAT_EXIT}') self.device.click(COMBAT_EXIT) continue + if self.handle_get_light_cone(): + continue def is_trailblaze_power_exhausted(self) -> bool: flag = self.config.stored.TrailblazePower.value < self.combat_wave_cost diff --git a/tasks/combat/interact.py b/tasks/combat/interact.py index 5d60b3c1c..110160077 100644 --- a/tasks/combat/interact.py +++ b/tasks/combat/interact.py @@ -1,6 +1,8 @@ from module.base.utils import color_similar, get_color +from module.logger import logger from tasks.base.ui import UI from tasks.combat.assets.assets_combat_interact import DUNGEON_COMBAT_INTERACT, MAP_LOADING +from tasks.combat.assets.assets_combat_prepare import COMBAT_PREPARE from tasks.map.assets.assets_map_control import A_BUTTON @@ -15,6 +17,27 @@ class CombatInteract(UI): return False + def combat_enter_from_map(self, skip_first_screenshot=True): + """ + Pages: + in: page_main, DUNGEON_COMBAT_INTERACT + out: COMBAT_PREPARE + """ + logger.info('Combat enter from map') + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if self.appear(COMBAT_PREPARE): + # Confirm page loaded + if self.image_color_count(COMBAT_PREPARE.button, color=(230, 230, 230), threshold=240, count=400): + logger.info(f'At {COMBAT_PREPARE}') + break + if self.handle_combat_interact(): + continue + def is_map_loading(self): if self.appear(MAP_LOADING, similarity=0.75): return True diff --git a/tasks/dungeon/assets/assets_dungeon_event.py b/tasks/dungeon/assets/assets_dungeon_event.py index 90e6f030f..f3e6af6a0 100644 --- a/tasks/dungeon/assets/assets_dungeon_event.py +++ b/tasks/dungeon/assets/assets_dungeon_event.py @@ -35,18 +35,11 @@ OCR_DOUBLE_EVENT_REMAIN = ButtonWrapper( ) OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT = ButtonWrapper( name='OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT', - cn=Button( - file='./assets/cn/dungeon/event/OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT.png', - area=(872, 515, 1192, 538), - search=(852, 495, 1212, 558), - color=(171, 139, 76), - button=(872, 515, 1192, 538), - ), - en=Button( - file='./assets/en/dungeon/event/OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT.png', - area=(814, 510, 1246, 530), - search=(794, 490, 1266, 550), - color=(194, 158, 86), - button=(814, 510, 1246, 530), + share=Button( + file='./assets/share/dungeon/event/OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT.png', + area=(799, 460, 1268, 626), + search=(779, 440, 1280, 646), + color=(59, 53, 48), + button=(799, 460, 1268, 626), ), ) diff --git a/tasks/dungeon/assets/assets_dungeon_state.py b/tasks/dungeon/assets/assets_dungeon_state.py new file mode 100644 index 000000000..f8f1477ea --- /dev/null +++ b/tasks/dungeon/assets/assets_dungeon_state.py @@ -0,0 +1,35 @@ +from module.base.button import Button, ButtonWrapper + +# This file was auto-generated, do not modify it manually. To generate: +# ``` python -m dev_tools.button_extract ``` + +OCR_SIMUNI_POINT = ButtonWrapper( + name='OCR_SIMUNI_POINT', + share=Button( + file='./assets/share/dungeon/state/OCR_SIMUNI_POINT.png', + area=(580, 237, 860, 277), + search=(560, 217, 880, 297), + color=(163, 170, 252), + button=(580, 237, 860, 277), + ), +) +OCR_SIMUNI_POINT_OFFSET = ButtonWrapper( + name='OCR_SIMUNI_POINT_OFFSET', + share=Button( + file='./assets/share/dungeon/state/OCR_SIMUNI_POINT_OFFSET.png', + area=(685, 250, 717, 273), + search=(583, 187, 883, 387), + color=(199, 200, 250), + button=(685, 250, 717, 273), + ), +) +OCR_STAMINA = ButtonWrapper( + name='OCR_STAMINA', + share=Button( + file='./assets/share/dungeon/state/OCR_STAMINA.png', + area=(675, 11, 1181, 64), + search=(655, 0, 1201, 84), + color=(75, 89, 125), + button=(675, 11, 1181, 64), + ), +) diff --git a/tasks/dungeon/assets/assets_dungeon_ui.py b/tasks/dungeon/assets/assets_dungeon_ui.py index b296a7243..8d04b5748 100644 --- a/tasks/dungeon/assets/assets_dungeon_ui.py +++ b/tasks/dungeon/assets/assets_dungeon_ui.py @@ -53,26 +53,6 @@ OCR_DUNGEON_NAV = ButtonWrapper( button=(117, 182, 423, 641), ), ) -OCR_SIMUNI_POINT = ButtonWrapper( - name='OCR_SIMUNI_POINT', - share=Button( - file='./assets/share/dungeon/ui/OCR_SIMUNI_POINT.png', - area=(580, 237, 820, 277), - search=(560, 217, 840, 297), - color=(166, 168, 252), - button=(580, 237, 820, 277), - ), -) -OCR_SIMUNI_POINT_OFFSET = ButtonWrapper( - name='OCR_SIMUNI_POINT_OFFSET', - share=Button( - file='./assets/share/dungeon/ui/OCR_SIMUNI_POINT_OFFSET.png', - area=(685, 250, 717, 273), - search=(583, 187, 883, 387), - color=(199, 200, 250), - button=(685, 250, 717, 273), - ), -) OCR_WEEKLY_LIMIT = ButtonWrapper( name='OCR_WEEKLY_LIMIT', share=Button( diff --git a/tasks/dungeon/dungeon.py b/tasks/dungeon/dungeon.py index dfde2b113..9abb08353 100644 --- a/tasks/dungeon/dungeon.py +++ b/tasks/dungeon/dungeon.py @@ -45,8 +45,12 @@ class Dungeon(DungeonUI, DungeonEvent, Combat): logger.info(f'Dungeon: {dungeon}, team={team}, wave_limit={wave_limit}, support_character={support_character}') if not skip_ui_switch: - self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index) - self.dungeon_goto(dungeon) + interact = self.get_dungeon_interact() + if interact is not None and interact == dungeon: + logger.info('Already nearby dungeon') + else: + self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index) + self.dungeon_goto(dungeon) if dungeon == KEYWORDS_DUNGEON_LIST.Stagnant_Shadow_Blaze: if self.handle_destructible_around_blaze(): @@ -59,7 +63,7 @@ class Dungeon(DungeonUI, DungeonEvent, Combat): if (dungeon.is_Calyx_Golden or dungeon.is_Calyx_Crimson) and \ self.running_double and self.config.stored.DungeonDouble.calyx > 0: calyx = self.get_double_event_remain_at_combat() - if calyx < self.config.stored.DungeonDouble.calyx: + if calyx is not None and calyx < self.config.stored.DungeonDouble.calyx: self.config.stored.DungeonDouble.calyx = calyx wave_limit = calyx if calyx == 0: @@ -67,7 +71,7 @@ class Dungeon(DungeonUI, DungeonEvent, Combat): if dungeon.is_Cavern_of_Corrosion and self.running_double and \ self.config.stored.DungeonDouble.relic > 0: relic = self.get_double_event_remain_at_combat() - if relic < self.config.stored.DungeonDouble.relic: + if relic is not None and relic < self.config.stored.DungeonDouble.relic: self.config.stored.DungeonDouble.relic = relic wave_limit = relic if relic == 0: diff --git a/tasks/dungeon/event.py b/tasks/dungeon/event.py index b6bf3560c..2afef0e49 100644 --- a/tasks/dungeon/event.py +++ b/tasks/dungeon/event.py @@ -45,14 +45,13 @@ class DungeonEvent(UI): def has_double_event_at_combat(self) -> bool: """ - TODO: OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT of relic may be different from that of calyx Pages: in: COMBAT_PREPARE """ has = self.image_color_count( OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT, - color=(252, 209, 123), - threshold=195, count=1000 + color=(231, 188, 103), + threshold=240, count=1000 ) logger.attr('Double event at combat', has) return has @@ -74,14 +73,23 @@ class DungeonEvent(UI): logger.attr('Double event remain', remain) return remain - def get_double_event_remain_at_combat(self) -> int: + def get_double_event_remain_at_combat(self) -> int | None: """ Pages: in: COMBAT_PREPARE """ - remain = 0 - if self.has_double_event_at_combat(): - remain = self._get_double_event_remain( - OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT) - logger.attr('Double event remain at combat', remain) - return remain + if not self.has_double_event_at_combat(): + logger.attr('Double event remain at combat', 0) + return 0 + + ocr = DoubleEventOcr(OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT) + for row in ocr.detect_and_ocr(self.device.image): + if '/' not in row.ocr_text: + continue + remain, _, total = ocr.format_result(row.ocr_text) + if total in [3, 12]: + logger.attr('Double event remain at combat', remain) + return remain + logger.warning('Double event appears but failed to get remain') + logger.attr('Double event remain at combat', None) + return None diff --git a/tasks/dungeon/state.py b/tasks/dungeon/state.py new file mode 100644 index 000000000..5249a2573 --- /dev/null +++ b/tasks/dungeon/state.py @@ -0,0 +1,122 @@ +import threading + +from module.base.timer import Timer +from module.base.utils import crop +from module.logger import logger +from module.ocr.ocr import DigitCounter +from tasks.base.ui import UI +from tasks.dungeon.assets.assets_dungeon_state import OCR_SIMUNI_POINT, OCR_SIMUNI_POINT_OFFSET, OCR_STAMINA + + +class OcrSimUniPoint(DigitCounter): + def after_process(self, result): + result = super().after_process(result) + result = result.replace('O', '0').replace('o', '0') + return result + + +class DungeonState(UI): + def dungeon_get_simuni_point(self, image=None) -> int: + """ + Page: + in: page_guide, Survival_Index, Simulated_Universe + """ + logger.info('Get simulated universe points') + if image is None: + image = self.device.image + + _ = OCR_SIMUNI_POINT_OFFSET.match_template(image) + OCR_SIMUNI_POINT.load_offset(OCR_SIMUNI_POINT_OFFSET) + area = ( + OCR_SIMUNI_POINT.area[0], + OCR_SIMUNI_POINT.button[1], + OCR_SIMUNI_POINT.area[2], + OCR_SIMUNI_POINT.button[3], + ) + + ocr = OcrSimUniPoint(OCR_SIMUNI_POINT) + value, _, total = ocr.ocr_single_line(crop(image, area), direct_ocr=True) + if total and value <= total: + logger.attr('SimulatedUniverse', f'{value}/{total}') + self.config.stored.SimulatedUniverse.set(value, total) + return value + else: + logger.warning(f'Invalid SimulatedUniverse points: {value}/{total}') + return 0 + + def dungeon_update_stamina(self, image=None, skip_first_screenshot=True): + """ + Returns: + bool: If success + + Pages: + in: page_guild, Survival_Index, Simulated_Universe + or page_rogue, LEVEL_CONFIRM + or rogue, REWARD_CLOSE + """ + ocr = DigitCounter(OCR_STAMINA) + timeout = Timer(1, count=2).start() + if image is None: + image = self.device.image + else: + skip_first_screenshot = True + + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + stamina = (0, 0, 0) + immersifier = (0, 0, 0) + + if timeout.reached(): + logger.warning('dungeon_update_stamina() timeout') + return False + + for row in ocr.detect_and_ocr(image): + if row.ocr_text.isdigit(): + continue + if row.ocr_text == '+': + continue + if '/' not in row.ocr_text: + continue + data = ocr.format_result(row.ocr_text) + if data[2] == self.config.stored.TrailblazePower.FIXED_TOTAL: + stamina = data + if data[2] == self.config.stored.Immersifier.FIXED_TOTAL: + immersifier = data + + if stamina[2] > 0 and immersifier[2] > 0: + break + if image is not None: + logger.warning('dungeon_update_stamina() ended') + return + + stamina = stamina[0] + immersifier = immersifier[0] + logger.attr('TrailblazePower', stamina) + logger.attr('Imersifier', immersifier) + with self.config.multi_set(): + self.config.stored.TrailblazePower.value = stamina + self.config.stored.Immersifier.value = immersifier + return True + + def dungeon_update_simuni(self): + """ + Update rogue weekly points, stamina, immersifier + Run in a new thread to be faster as data is not used immediately + + Page: + in: page_guide, Survival_Index, Simulated_Universe + """ + logger.info('dungeon_update_simuni') + + def func(image): + logger.info('Update thread start') + with self.config.multi_set(): + self.dungeon_get_simuni_point(image) + # self.dungeon_update_stamina(image) + + thread = threading.Thread(target=func, args=(self.device.image,)) + thread.start() diff --git a/tasks/dungeon/ui.py b/tasks/dungeon/ui.py index 91da95fd1..587878e79 100644 --- a/tasks/dungeon/ui.py +++ b/tasks/dungeon/ui.py @@ -6,24 +6,28 @@ from module.base.base import ModuleBase from module.base.button import ClickButton from module.base.timer import Timer from module.base.utils import get_color +from module.exception import ScriptError from module.logger import logger -from module.ocr.ocr import DigitCounter, Ocr, OcrResultButton +from module.ocr.ocr import Ocr, OcrResultButton from module.ocr.utils import split_and_pair_button_attr from module.ui.draggable_list import DraggableList from module.ui.switch import Switch from tasks.base.page import page_guide -from tasks.base.ui import UI +from tasks.combat.assets.assets_combat_interact import DUNGEON_COMBAT_INTERACT, DUNGEON_COMBAT_INTERACT_TEXT from tasks.combat.assets.assets_combat_prepare import COMBAT_PREPARE +from tasks.combat.interact import CombatInteract from tasks.dungeon.assets.assets_dungeon_ui import * from tasks.dungeon.keywords import ( DungeonList, DungeonNav, DungeonTab, KEYWORDS_DUNGEON_ENTRANCE, + KEYWORDS_DUNGEON_LIST, KEYWORDS_DUNGEON_NAV, KEYWORDS_DUNGEON_TAB ) from tasks.dungeon.keywords.classes import DungeonEntrance +from tasks.dungeon.state import DungeonState class DungeonTabSwitch(Switch): @@ -78,13 +82,6 @@ class OcrDungeonList(Ocr): return result -class OcrSimUniPoint(DigitCounter): - def after_process(self, result): - result = super().after_process(result) - result = result.replace('O', '0').replace('o', '0') - return result - - class OcrDungeonListLimitEntrance(OcrDungeonList): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -123,7 +120,7 @@ DUNGEON_LIST = DraggableDungeonList( ocr_class=OcrDungeonList, search_button=OCR_DUNGEON_LIST) -class DungeonUI(UI): +class DungeonUI(DungeonState, CombatInteract): def dungeon_tab_goto(self, state: DungeonTab): """ Args: @@ -233,31 +230,6 @@ class DungeonUI(UI): logger.info('No Forgotten_Hall in list skip waiting') return False - def dungeon_get_simuni_point(self) -> int: - """ - Page: - in: page_guide, Survival_Index, Simulated_Universe - """ - logger.info('Get simulated universe points') - _ = self.appear(OCR_SIMUNI_POINT_OFFSET) - OCR_SIMUNI_POINT.load_offset(OCR_SIMUNI_POINT_OFFSET) - area = ( - OCR_SIMUNI_POINT.area[0], - OCR_SIMUNI_POINT.button[1], - OCR_SIMUNI_POINT.area[2], - OCR_SIMUNI_POINT.button[3], - ) - - ocr = OcrSimUniPoint(OCR_SIMUNI_POINT) - value, _, total = ocr.ocr_single_line(self.image_crop(area), direct_ocr=True) - if total and value <= total: - logger.attr('SimulatedUniverse', f'{value}/{total}') - self.config.stored.SimulatedUniverse.set(value, total) - return value - else: - logger.warning(f'Invalid SimulatedUniverse points: {value}/{total}') - return 0 - def _dungeon_nav_goto(self, dungeon: DungeonList, skip_first_screenshot=True): """ Equivalent to `DUNGEON_NAV_LIST.select_row(dungeon.dungeon_nav, main=self)` @@ -301,7 +273,7 @@ class DungeonUI(UI): logger.info('DUNGEON_NAV_LIST at top') # Update points if possible if DUNGEON_NAV_LIST.is_row_selected(button, main=self): - self.dungeon_get_simuni_point() + self.dungeon_update_simuni() else: # To start from any list states. logger.info('DUNGEON_NAV_LIST not at top') @@ -404,6 +376,53 @@ class DungeonUI(UI): logger.warning(f'Cannot find dungeon entrance of {dungeon}') continue + def get_dungeon_interact(self) -> DungeonList | None: + """ + Pages: + in: page_main + """ + if not self.appear(DUNGEON_COMBAT_INTERACT): + logger.info('No dungeon interact') + return None + + ocr = OcrDungeonList(DUNGEON_COMBAT_INTERACT_TEXT) + result = ocr.detect_and_ocr(self.device.image) + + result = ' '.join([row.ocr_text for row in result]) + + # Calyx (Crimson): Bud of XXX -> Bud of XXX + result = re.sub(r'Calyx\s*\(.*?\):*', '', result) + # Stagnant Shadow: Shap XXX -> Shape of XXX + result = re.sub(r'Stagnant\s*Shadow[:\s]*\w*', 'Shape of', result) + # Cavern of Corrosion: Pa XXX -> Path of XXX + result = re.sub(r'Cavern\s*of\s*Corrosion[:\s]*\w*', 'Path of', result) + # Echo of War: XXX -> XXX + result = re.sub(r'Echo\s*of\s*War:*', '', result) + # Divine See -> Divine Seed + result = re.sub(r'Divine\s*\w*', 'Divine Seed', result) + # Destructio Beginning -> Destruction's Beginning + result = re.sub(r"Destruct[a-zA-Z0-9_']*", "Destruction's", result) + + # Dungeons + try: + dungeon = DungeonList.find(result) + logger.attr('DungeonInteract', dungeon) + return dungeon + except ScriptError: + pass + # Simulated Universe returns Simulated_Universe_World_1 + try: + dungeon = DungeonNav.find(result) + if dungeon == KEYWORDS_DUNGEON_NAV.Simulated_Universe: + dungeon = KEYWORDS_DUNGEON_LIST.Simulated_Universe_World_1 + logger.attr('DungeonInteract', dungeon) + return dungeon + except ScriptError: + pass + # Unknown + logger.attr('DungeonInteract', None) + return None + def dungeon_goto(self, dungeon: DungeonList): """ Returns: diff --git a/tasks/map/minimap/minimap.py b/tasks/map/minimap/minimap.py index 5fe623672..e686fc0a0 100644 --- a/tasks/map/minimap/minimap.py +++ b/tasks/map/minimap/minimap.py @@ -16,6 +16,7 @@ from module.base.utils import ( rgb2yuv ) from module.logger import logger +from tasks.map.interact.aim import subtract_blur from tasks.map.minimap.utils import ( convolve, cubic_find_maximum, @@ -223,7 +224,7 @@ class Minimap(MapResource): scale = self.DIRECTION_ROTATION_SCALE * self.DIRECTION_SEARCH_SCALE mapping = cv2.resize(image, None, fx=scale, fy=scale, interpolation=cv2.INTER_NEAREST) result = cv2.matchTemplate(self.ArrowRotateMap, mapping, cv2.TM_CCOEFF_NORMED) - result = cv2.subtract(result, cv2.GaussianBlur(result, (5, 5), 0)) + subtract_blur(result, 5) _, sim, _, loca = cv2.minMaxLoc(result) loca = np.array(loca) / self.DIRECTION_SEARCH_SCALE // (self.DIRECTION_RADIUS * 2) degree = int((loca[0] + loca[1] * 8) * 5) @@ -240,7 +241,7 @@ class Minimap(MapResource): precise_map = self.ArrowRotateMapAll[row[0]:row[1], :] result = cv2.matchTemplate(precise_map, mapping, cv2.TM_CCOEFF_NORMED) - result = cv2.subtract(result, cv2.GaussianBlur(result, (5, 5), 0)) + subtract_blur(result, 5) def to_map(x): return int((x * self.DIRECTION_RADIUS * 2) * self.POSITION_SEARCH_SCALE) @@ -271,11 +272,10 @@ class Minimap(MapResource): # Extract minimap = self.get_minimap(image, radius=self.MINIMAP_RADIUS) - _, _, v = cv2.split(rgb2yuv(minimap)) + image = rgb2yuv(minimap)[:, :, 2].copy() - image = cv2.subtract(128, v) - - image = cv2.GaussianBlur(image, (3, 3), 0) + cv2.subtract(128, image, dst=image) + cv2.GaussianBlur(image, (3, 3), 0, dst=image) # Expand circle into rectangle remap = cv2.remap(image, *self.RotationRemapData, cv2.INTER_LINEAR)[d * 1 // 10:d * 6 // 10].astype(np.float32) remap = cv2.resize(remap, None, fx=scale, fy=scale, interpolation=cv2.INTER_LINEAR)