diff --git a/assets/cn/combat/finish/COMBAT_AGAIN.SEARCH.png b/assets/cn/combat/finish/COMBAT_AGAIN.SEARCH.png new file mode 100644 index 000000000..fafad7ee3 Binary files /dev/null and b/assets/cn/combat/finish/COMBAT_AGAIN.SEARCH.png differ diff --git a/assets/cn/combat/prepare/COMBAT_PREPARE.SEARCH.png b/assets/cn/combat/prepare/COMBAT_PREPARE.SEARCH.png new file mode 100644 index 000000000..1052925e0 Binary files /dev/null and b/assets/cn/combat/prepare/COMBAT_PREPARE.SEARCH.png differ diff --git a/assets/en/combat/finish/COMBAT_AGAIN.SEARCH.png b/assets/en/combat/finish/COMBAT_AGAIN.SEARCH.png new file mode 100644 index 000000000..2d8481f5d Binary files /dev/null and b/assets/en/combat/finish/COMBAT_AGAIN.SEARCH.png differ diff --git a/assets/en/combat/prepare/COMBAT_PREPARE.SEARCH.png b/assets/en/combat/prepare/COMBAT_PREPARE.SEARCH.png new file mode 100644 index 000000000..1052925e0 Binary files /dev/null and b/assets/en/combat/prepare/COMBAT_PREPARE.SEARCH.png differ diff --git a/assets/share/dungeon/ui/OCR_WEEKLY_LIMIT.png b/assets/share/dungeon/ui/OCR_WEEKLY_LIMIT.png new file mode 100644 index 000000000..bc7b8eaa0 Binary files /dev/null and b/assets/share/dungeon/ui/OCR_WEEKLY_LIMIT.png differ diff --git a/src.py b/src.py index 1a24eb2ce..66a302530 100644 --- a/src.py +++ b/src.py @@ -26,6 +26,10 @@ class StarRailCopilot(AzurLaneAutoScript): from tasks.dungeon.dungeon import Dungeon Dungeon(config=self.config, device=self.device).run() + def weekly(self): + from tasks.dungeon.weekly import WeeklyDungeon + WeeklyDungeon(config=self.config, device=self.device).run() + def daily_quest(self): from tasks.daily.daily_quest import DailyQuestUI DailyQuestUI(config=self.config, device=self.device).run() diff --git a/tasks/combat/assets/assets_combat_finish.py b/tasks/combat/assets/assets_combat_finish.py index 50b82ce70..bfa013fa7 100644 --- a/tasks/combat/assets/assets_combat_finish.py +++ b/tasks/combat/assets/assets_combat_finish.py @@ -8,14 +8,14 @@ COMBAT_AGAIN = ButtonWrapper( cn=Button( file='./assets/cn/combat/finish/COMBAT_AGAIN.png', area=(846, 601, 924, 619), - search=(826, 581, 944, 639), + search=(709, 592, 979, 628), color=(162, 162, 162), button=(709, 592, 979, 628), ), en=Button( file='./assets/en/combat/finish/COMBAT_AGAIN.png', area=(809, 602, 902, 618), - search=(789, 582, 922, 638), + search=(709, 591, 981, 628), color=(159, 159, 159), button=(709, 591, 981, 628), ), diff --git a/tasks/combat/assets/assets_combat_prepare.py b/tasks/combat/assets/assets_combat_prepare.py index 4a3ea6de2..0c6f74da2 100644 --- a/tasks/combat/assets/assets_combat_prepare.py +++ b/tasks/combat/assets/assets_combat_prepare.py @@ -8,14 +8,14 @@ COMBAT_PREPARE = ButtonWrapper( cn=Button( file='./assets/cn/combat/prepare/COMBAT_PREPARE.png', area=(1071, 649, 1110, 667), - search=(1051, 629, 1130, 687), + search=(836, 640, 1225, 677), color=(141, 140, 141), button=(956, 640, 1224, 676), ), en=Button( file='./assets/en/combat/prepare/COMBAT_PREPARE.png', area=(1043, 650, 1137, 666), - search=(1023, 630, 1157, 686), + search=(836, 640, 1225, 677), color=(153, 154, 155), button=(956, 640, 1225, 676), ), diff --git a/tasks/combat/combat.py b/tasks/combat/combat.py index c0e7933d8..767197701 100644 --- a/tasks/combat/combat.py +++ b/tasks/combat/combat.py @@ -118,6 +118,8 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo continue if self.handle_ascension_dungeon_prepare(): continue + if self.handle_popup_confirm(): + continue def combat_execute(self, expected_end=None): """ @@ -165,7 +167,7 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo Pages: in: COMBAT_AGAIN """ - current = self.combat_get_trailblaze_power(expect_reduce=True) + current = self.combat_get_trailblaze_power(expect_reduce=self.combat_wave_cost > 0) # Wave limit if self.combat_wave_limit: if self.combat_wave_done + self.combat_waves > self.combat_wave_limit: @@ -181,6 +183,9 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo else: logger.info(f'Current has {current}, combat costs {self.combat_wave_cost}, can not run again') return False + elif self.combat_wave_cost <= 0: + logger.info(f'Free combat, combat costs {self.combat_wave_cost}, can not run again') + return False else: if current >= self.combat_wave_cost: logger.info(f'Current has {current}, combat costs {self.combat_wave_cost}, can run again') diff --git a/tasks/combat/prepare.py b/tasks/combat/prepare.py index f22afb496..e2f7359df 100644 --- a/tasks/combat/prepare.py +++ b/tasks/combat/prepare.py @@ -2,6 +2,7 @@ import re import module.config.server as server from module.base.timer import Timer +from module.base.utils import color_similar, get_color from module.logger import logger from module.ocr.ocr import Digit, DigitCounter from tasks.base.ui import UI @@ -75,7 +76,7 @@ class CombatPrepare(UI): # Empty result if total == 0: continue - # Confirm if it is > 180, sometimes just OCR errors + # Confirm if it is > 240, sometimes just OCR errors if current > 240 and timeout.reached(): break if expect_reduce and current >= self.config.stored.TrailblazePower.value: @@ -106,6 +107,12 @@ class CombatPrepare(UI): else: self.device.screenshot() + color = get_color(self.device.image, OCR_WAVE_COST.area) + if color_similar(color, (229, 231, 223), threshold=30): + logger.info(f'Combat is trailblaze power free') + self.combat_wave_cost = 0 + return 0 + cost = Digit(OCR_WAVE_COST).ocr_single_line(self.device.image) if cost == 10: if multi: diff --git a/tasks/dungeon/assets/assets_dungeon_ui.py b/tasks/dungeon/assets/assets_dungeon_ui.py index 87c0c9e91..b296a7243 100644 --- a/tasks/dungeon/assets/assets_dungeon_ui.py +++ b/tasks/dungeon/assets/assets_dungeon_ui.py @@ -73,6 +73,16 @@ OCR_SIMUNI_POINT_OFFSET = ButtonWrapper( button=(685, 250, 717, 273), ), ) +OCR_WEEKLY_LIMIT = ButtonWrapper( + name='OCR_WEEKLY_LIMIT', + share=Button( + file='./assets/share/dungeon/ui/OCR_WEEKLY_LIMIT.png', + area=(580, 225, 680, 257), + search=(560, 205, 700, 277), + color=(132, 192, 247), + button=(580, 225, 680, 257), + ), +) OPERATION_BRIEFING_CHECK = ButtonWrapper( name='OPERATION_BRIEFING_CHECK', share=Button( diff --git a/tasks/dungeon/dungeon.py b/tasks/dungeon/dungeon.py index e06bb5b3e..f81d72c14 100644 --- a/tasks/dungeon/dungeon.py +++ b/tasks/dungeon/dungeon.py @@ -1,4 +1,8 @@ +from datetime import timedelta + from module.base.utils import area_offset +from module.config.stored.classes import now +from module.config.utils import DEFAULT_TIME from module.logger import logger from tasks.combat.combat import Combat from tasks.daily.keywords import KEYWORDS_DAILY_QUEST @@ -7,6 +11,7 @@ from tasks.dungeon.keywords import DungeonList, KEYWORDS_DUNGEON_LIST, KEYWORDS_ from tasks.dungeon.ui import DungeonUI from tasks.battle_pass.keywords import KEYWORD_BATTLE_PASS_QUEST + class Dungeon(DungeonUI, DungeonEvent, Combat): called_daily_support = False achieved_daily_quest = False @@ -210,6 +215,8 @@ class Dungeon(DungeonUI, DungeonEvent, Combat): def delay_dungeon_task(self, dungeon): if dungeon.is_Cavern_of_Corrosion: limit = 80 + elif dungeon.is_Echo_of_War: + limit = 30 else: limit = 60 # Recover 1 trailbaze power each 6 minutes @@ -226,8 +233,14 @@ class Dungeon(DungeonUI, DungeonEvent, Combat): # Check daily if self.achieved_daily_quest: self.config.task_call('DailyQuest') - # Delay self - self.config.task_delay(minute=cover) + # Delay tasks + future = now() + timedelta(minutes=cover) + for task in ['Dungeon', 'Weekly']: + next_run = self.config.cross_get(keys=f'{task}.Scheduler.NextRun', default=DEFAULT_TIME) + if future > next_run: + logger.info(f"Delay task `{task}` to {future}") + self.config.cross_set(keys=f'{task}.Scheduler.NextRun', value=future) + self.config.task_stop() def handle_destructible_around_blaze(self): diff --git a/tasks/dungeon/ui.py b/tasks/dungeon/ui.py index 21c4eb6a9..5b5029928 100644 --- a/tasks/dungeon/ui.py +++ b/tasks/dungeon/ui.py @@ -428,7 +428,8 @@ class DungeonUI(UI): if dungeon.is_Calyx_Golden \ or dungeon.is_Calyx_Crimson \ or dungeon.is_Stagnant_Shadow \ - or dungeon.is_Cavern_of_Corrosion: + or dungeon.is_Cavern_of_Corrosion \ + or dungeon.is_Echo_of_War: self._dungeon_nav_goto(dungeon) self._dungeon_insight(dungeon) self._dungeon_enter(dungeon) diff --git a/tasks/dungeon/weekly.py b/tasks/dungeon/weekly.py new file mode 100644 index 000000000..fb71b7dd3 --- /dev/null +++ b/tasks/dungeon/weekly.py @@ -0,0 +1,88 @@ +from module.logger import logger +from module.ocr.ocr import DigitCounter +from tasks.daily.keywords import KEYWORDS_DAILY_QUEST +from tasks.dungeon.assets.assets_dungeon_ui import OCR_DUNGEON_LIST, OCR_WEEKLY_LIMIT +from tasks.dungeon.dungeon import Dungeon +from tasks.dungeon.keywords import DungeonList, KEYWORDS_DUNGEON_TAB +from tasks.dungeon.ui import DUNGEON_LIST + + +class WeeklyDungeon(Dungeon): + def require_compulsory_support(self) -> bool: + return False + + def _dungeon_run(self, dungeon: DungeonList, team: int = None, wave_limit: int = 0, support_character: str = None, + skip_ui_switch: bool = False): + if team is None: + team = self.config.Weekly_Team + # No support + support_character = '' + skip_ui_switch = True + return super()._dungeon_run( + dungeon=dungeon, team=team, wave_limit=wave_limit, + support_character=support_character, skip_ui_switch=skip_ui_switch) + + def get_weekly_remain(self) -> int: + """ + Pages: + in: page_guide, Survival_Index, KEYWORDS_DUNGEON_NAV.Echo_of_War + """ + ocr = DigitCounter(OCR_WEEKLY_LIMIT) + current, _, _ = ocr.ocr_single_line(self.device.image) + total = self.config.stored.EchoOfWar.FIXED_TOTAL + remain = total - current + if current <= total: + logger.attr('EchoOfWar', f'{current}/{total}') + self.config.stored.EchoOfWar.value = current + return current + else: + logger.warning(f'Invalid EchoOfWar limit: {current}/{total}') + return 0 + + def run(self): + # self.config.update_battle_pass_quests() + self.config.update_daily_quests() + self.called_daily_support = False + self.achieved_daily_quest = False + self.daily_quests = self.config.stored.DailyQuest.load_quests() + + dungeon = DungeonList.find(self.config.Weekly_Name) + + # UI switches + self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index) + # Equivalent to self.dungeon_goto(dungeon), but check limit remains + DUNGEON_LIST.search_button = OCR_DUNGEON_LIST + self._dungeon_nav_goto(dungeon) + + # Check limit + remain = self.get_weekly_remain() + if remain <= 0: + if KEYWORDS_DAILY_QUEST.Complete_Echo_of_War_1_times in self.daily_quests: + logger.info('Reached the limit to get Echo_of_War rewards, continue cause daily quests require it') + remain = 1 + else: + logger.info('Reached the limit to get Echo_of_War rewards, stop') + self.config.task_delay(server_update=True) + self.config.task_stop() + + self._dungeon_insight(dungeon) + self._dungeon_enter(dungeon) + + # Combat + count = self.dungeon_run(dungeon, wave_limit=min(remain, 3)) + + with self.config.multi_set(): + # Check daily quests + if count: + if KEYWORDS_DAILY_QUEST.Complete_Echo_of_War_1_times in self.daily_quests: + logger.info('Achieve daily quest Complete_Echo_of_War_1_times') + self.config.task_call('DailyQuest') + # Finished all remains + if count >= remain: + logger.info('All Echo_of_War rewards got') + self.config.task_delay(server_update=True) + self.config.task_stop() + + logger.warning(f'Unexpected Echo_of_War case, count={count}, remain={remain}') + self.config.task_delay(server_update=True) + self.config.task_stop()