diff --git a/assets/share/combat/support/FIRST_CHARACTER.png b/assets/share/combat/support/FIRST_CHARACTER.png new file mode 100644 index 000000000..5fdf9e2ca Binary files /dev/null and b/assets/share/combat/support/FIRST_CHARACTER.png differ diff --git a/assets/share/ornament/combat/CHARACTER_EMPTY_OE.SEARCH.png b/assets/share/ornament/combat/CHARACTER_EMPTY_OE.SEARCH.png new file mode 100644 index 000000000..d629ad019 Binary files /dev/null and b/assets/share/ornament/combat/CHARACTER_EMPTY_OE.SEARCH.png differ diff --git a/assets/share/ornament/combat/CHARACTER_EMPTY_OE.png b/assets/share/ornament/combat/CHARACTER_EMPTY_OE.png new file mode 100644 index 000000000..31036018d Binary files /dev/null and b/assets/share/ornament/combat/CHARACTER_EMPTY_OE.png differ diff --git a/assets/share/ornament/combat/OCR_DOUBLE_EVENT_REMAIN_AT_OE.png b/assets/share/ornament/combat/OCR_DOUBLE_EVENT_REMAIN_AT_OE.png new file mode 100644 index 000000000..67c291915 Binary files /dev/null and b/assets/share/ornament/combat/OCR_DOUBLE_EVENT_REMAIN_AT_OE.png differ diff --git a/assets/share/ornament/combat/SUPPORT_ADD.png b/assets/share/ornament/combat/SUPPORT_ADD.png new file mode 100644 index 000000000..fd190786a Binary files /dev/null and b/assets/share/ornament/combat/SUPPORT_ADD.png differ diff --git a/assets/share/ornament/combat/SUPPORT_DISMISS.png b/assets/share/ornament/combat/SUPPORT_DISMISS.png new file mode 100644 index 000000000..587a43ef0 Binary files /dev/null and b/assets/share/ornament/combat/SUPPORT_DISMISS.png differ diff --git a/tasks/combat/assets/assets_combat_support.py b/tasks/combat/assets/assets_combat_support.py index 982f5eaeb..a29e29a36 100644 --- a/tasks/combat/assets/assets_combat_support.py +++ b/tasks/combat/assets/assets_combat_support.py @@ -43,6 +43,16 @@ COMBAT_SUPPORT_LIST_SCROLL = ButtonWrapper( button=(472, 162, 476, 598), ), ) +FIRST_CHARACTER = ButtonWrapper( + name='FIRST_CHARACTER', + share=Button( + file='./assets/share/combat/support/FIRST_CHARACTER.png', + area=(136, 140, 386, 204), + search=(116, 120, 406, 224), + color=(255, 255, 255), + button=(136, 140, 386, 204), + ), +) SUPPORT_SELECTED = ButtonWrapper( name='SUPPORT_SELECTED', share=[ diff --git a/tasks/combat/support.py b/tasks/combat/support.py index 0eabf9542..867f353c4 100644 --- a/tasks/combat/support.py +++ b/tasks/combat/support.py @@ -8,8 +8,7 @@ from module.logger import logger from module.ui.scroll import AdaptiveScroll 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, SUPPORT_SELECTED +from tasks.combat.assets.assets_combat_support import * from tasks.combat.assets.assets_combat_team import COMBAT_TEAM_DISMISSSUPPORT, COMBAT_TEAM_SUPPORT @@ -260,6 +259,27 @@ class CombatSupport(UI): interval.reset() continue + def _select_first(self): + logger.hr("Combat support select") + logger.info(f'Select: first') + skip_first_screenshot = False + interval = Timer(2) + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # End + if SUPPORT_SELECTED.match_template(self.device.image, similarity=0.75): + logger.info('Character support selected') + return True + + if interval.reached(): + self.device.click(FIRST_CHARACTER) + interval.reset() + continue + def _cancel_popup(self): """ Pages: diff --git a/tasks/dungeon/dungeon.py b/tasks/dungeon/dungeon.py index 490ede9f0..bb4a40a49 100644 --- a/tasks/dungeon/dungeon.py +++ b/tasks/dungeon/dungeon.py @@ -76,6 +76,14 @@ class Dungeon(DungeonStamina, DungeonEvent, Combat): wave_limit = relic if relic == 0: return 0 + if dungeon.is_Ornament_Extraction and self.running_double and \ + self.config.stored.DungeonDouble.rogue > 0: + rogue = self.get_double_event_remain_at_combat() + if rogue is not None and rogue < self.config.stored.DungeonDouble.rogue: + self.config.stored.DungeonDouble.rogue = rogue + wave_limit = rogue + if rogue == 0: + return 0 # Combat self.dungeon = dungeon count = self.combat(team=team, wave_limit=wave_limit, support_character=support_character) diff --git a/tasks/dungeon/event.py b/tasks/dungeon/event.py index aad0108ae..300a2aae3 100644 --- a/tasks/dungeon/event.py +++ b/tasks/dungeon/event.py @@ -61,19 +61,19 @@ class DungeonEvent(UI): logger.attr('Double rogue', has) return has - def has_double_event_at_combat(self) -> bool: + def has_double_event_at_combat(self, button=OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT) -> bool: """ Pages: in: COMBAT_PREPARE """ has = self.image_color_count( - OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT, + button, color=(231, 188, 103), threshold=240, count=1000 ) # Anniversary 3x event has |= self.image_color_count( - OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT, + button, color=(229, 62, 44), threshold=221, count=50 ) @@ -109,16 +109,16 @@ class DungeonEvent(UI): logger.attr('Double event remain', remain) return remain - def get_double_event_remain_at_combat(self) -> int | None: + def get_double_event_remain_at_combat(self, button=OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT) -> int | None: """ Pages: in: COMBAT_PREPARE """ - if not self.has_double_event_at_combat(): + if not self.has_double_event_at_combat(button=button): logger.attr('Double event remain at combat', 0) return 0 - ocr = DoubleEventOcr(OCR_DOUBLE_EVENT_REMAIN_AT_COMBAT) + ocr = DoubleEventOcr(button) for row in ocr.detect_and_ocr(self.device.image): if not ocr.is_format_matched(row.ocr_text): continue diff --git a/tasks/ornament/assets/assets_ornament_combat.py b/tasks/ornament/assets/assets_ornament_combat.py new file mode 100644 index 000000000..4779e6b17 --- /dev/null +++ b/tasks/ornament/assets/assets_ornament_combat.py @@ -0,0 +1,45 @@ +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 ``` + +CHARACTER_EMPTY_OE = ButtonWrapper( + name='CHARACTER_EMPTY_OE', + share=Button( + file='./assets/share/ornament/combat/CHARACTER_EMPTY_OE.png', + area=(549, 513, 559, 537), + search=(525, 498, 824, 554), + color=(112, 112, 112), + button=(549, 513, 559, 537), + ), +) +OCR_DOUBLE_EVENT_REMAIN_AT_OE = ButtonWrapper( + name='OCR_DOUBLE_EVENT_REMAIN_AT_OE', + share=Button( + file='./assets/share/ornament/combat/OCR_DOUBLE_EVENT_REMAIN_AT_OE.png', + area=(812, 577, 1248, 631), + search=(792, 557, 1268, 651), + color=(124, 103, 61), + button=(812, 577, 1248, 631), + ), +) +SUPPORT_ADD = ButtonWrapper( + name='SUPPORT_ADD', + share=Button( + file='./assets/share/ornament/combat/SUPPORT_ADD.png', + area=(848, 518, 876, 537), + search=(828, 498, 896, 557), + color=(167, 200, 176), + button=(848, 518, 876, 537), + ), +) +SUPPORT_DISMISS = ButtonWrapper( + name='SUPPORT_DISMISS', + share=Button( + file='./assets/share/ornament/combat/SUPPORT_DISMISS.png', + area=(853, 513, 872, 538), + search=(833, 493, 892, 558), + color=(135, 135, 135), + button=(853, 513, 872, 538), + ), +) diff --git a/tasks/ornament/combat.py b/tasks/ornament/combat.py new file mode 100644 index 000000000..13a05c65d --- /dev/null +++ b/tasks/ornament/combat.py @@ -0,0 +1,142 @@ +from module.base.decorator import run_once +from module.exception import RequestHumanTakeover +from module.logger import logger +from tasks.base.assets.assets_base_popup import POPUP_CANCEL +from tasks.combat.assets.assets_combat_prepare import COMBAT_PREPARE +from tasks.combat.assets.assets_combat_support import COMBAT_SUPPORT_LIST +from tasks.combat.combat import Combat +from tasks.dungeon.event import DungeonEvent +from tasks.ornament.assets.assets_ornament_combat import * + + +class OrnamentCombat(DungeonEvent, Combat): + def combat_enter_from_map(self, skip_first_screenshot=True): + # Don't enter from map, UI too deep inside + # Enter from survival index instead + pass + + def get_double_event_remain_at_combat(self, button=OCR_DOUBLE_EVENT_REMAIN_AT_OE): + # Different position to OCR + return super().get_double_event_remain_at_combat(button) + + 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") + self.interval_clear(SUPPORT_ADD) + 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(SUPPORT_DISMISS): + return True + + # Click + if self.appear(SUPPORT_ADD, interval=2): + self.device.click(SUPPORT_ADD) + self.interval_reset(SUPPORT_ADD) + 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=2): + if not selected_support: + # In Ornament Extraction, first character isn't selected by default + if support_character_name == "FirstCharacter": + self._select_first() + else: + self._search_support(support_character_name) # Search support + selected_support = True + self.device.click(OCR_DOUBLE_EVENT_REMAIN_AT_OE) + self.interval_reset(COMBAT_SUPPORT_LIST) + continue + + def is_team_prepared(self) -> bool: + """ + Pages: + in: COMBAT_PREPARE + """ + slots = CHARACTER_EMPTY_OE.match_multi_template(self.device.image) + slots = 4 - len(slots) + logger.attr('TeamSlotsPrepared', slots) + return slots > 0 + + def combat_prepare(self, team=1, support_character: str = None): + """ + Args: + team: 1 to 6. + support_character: Support character name + + Returns: + bool: True + + Pages: + in: COMBAT_PREPARE + out: is_in_main + """ + + @run_once + def check_team_prepare(): + if not self.is_team_prepared(): + logger.error(f'Please prepare your team in Ornament Extraction') + raise RequestHumanTakeover + + logger.hr('Combat prepare') + skip_first_screenshot = True + if support_character: + # Block COMBAT_TEAM_PREPARE before support set + support_set = False + else: + support_set = True + logger.info([support_character, support_set]) + trial = 0 + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # End + if self.is_in_main(): + logger.info('Combat entered') + return True + # Relics full + # Clicking between COMBAT_PREPARE and COMBAT_TEAM_PREPARE + if trial > 5: + logger.critical('Failed to enter dungeon after 5 trial, probably because relics are full') + raise RequestHumanTakeover + if self.appear(SUPPORT_ADD): + check_team_prepare() + + # Click + if support_character and self.appear(SUPPORT_ADD, interval=2): + self.support_set(support_character) + self.interval_reset(SUPPORT_ADD) + support_set = True + continue + if support_set and self.appear(COMBAT_PREPARE, interval=5): + # Long loading after COMBAT_PREPARE + self.device.click(COMBAT_PREPARE) + trial += 1 + continue + if self.handle_popup_confirm(): + continue