diff --git a/assets/share/dungeon/ui/CALYX_WORLD_3.png b/assets/share/dungeon/ui/CALYX_WORLD_3.png deleted file mode 100644 index eeb73049b..000000000 Binary files a/assets/share/dungeon/ui/CALYX_WORLD_3.png and /dev/null differ diff --git a/assets/share/dungeon/ui/CALYX_WORLD_1.png b/assets/share/dungeon/ui_list/LIST_ASCENDING.png similarity index 75% rename from assets/share/dungeon/ui/CALYX_WORLD_1.png rename to assets/share/dungeon/ui_list/LIST_ASCENDING.png index 5457292d2..f6f6a8d8d 100644 Binary files a/assets/share/dungeon/ui/CALYX_WORLD_1.png and b/assets/share/dungeon/ui_list/LIST_ASCENDING.png differ diff --git a/assets/share/dungeon/ui/CALYX_WORLD_2.png b/assets/share/dungeon/ui_list/LIST_DESCENDING.png similarity index 73% rename from assets/share/dungeon/ui/CALYX_WORLD_2.png rename to assets/share/dungeon/ui_list/LIST_DESCENDING.png index 3b3e3edb7..3d34041c7 100644 Binary files a/assets/share/dungeon/ui/CALYX_WORLD_2.png and b/assets/share/dungeon/ui_list/LIST_DESCENDING.png differ diff --git a/assets/share/dungeon/ui/OCR_DUNGEON_LIST.BUTTON.png b/assets/share/dungeon/ui_list/OCR_DUNGEON_LIST.BUTTON.png similarity index 100% rename from assets/share/dungeon/ui/OCR_DUNGEON_LIST.BUTTON.png rename to assets/share/dungeon/ui_list/OCR_DUNGEON_LIST.BUTTON.png diff --git a/assets/share/dungeon/ui/OCR_DUNGEON_LIST.png b/assets/share/dungeon/ui_list/OCR_DUNGEON_LIST.png similarity index 100% rename from assets/share/dungeon/ui/OCR_DUNGEON_LIST.png rename to assets/share/dungeon/ui_list/OCR_DUNGEON_LIST.png diff --git a/assets/share/dungeon/ui_list/OCR_DUNGEON_NAME.png b/assets/share/dungeon/ui_list/OCR_DUNGEON_NAME.png new file mode 100644 index 000000000..4346f1da1 Binary files /dev/null and b/assets/share/dungeon/ui_list/OCR_DUNGEON_NAME.png differ diff --git a/assets/share/dungeon/ui_list/OCR_DUNGEON_NAME_ROGUE.png b/assets/share/dungeon/ui_list/OCR_DUNGEON_NAME_ROGUE.png new file mode 100644 index 000000000..3dd619f00 Binary files /dev/null and b/assets/share/dungeon/ui_list/OCR_DUNGEON_NAME_ROGUE.png differ diff --git a/assets/share/dungeon/ui_list/OCR_DUNGEON_TELEPORT.png b/assets/share/dungeon/ui_list/OCR_DUNGEON_TELEPORT.png new file mode 100644 index 000000000..5c1c74d8a Binary files /dev/null and b/assets/share/dungeon/ui_list/OCR_DUNGEON_TELEPORT.png differ diff --git a/dev_tools/keywords/dungeon_list.py b/dev_tools/keywords/dungeon_list.py index f9f67a902..93403a078 100644 --- a/dev_tools/keywords/dungeon_list.py +++ b/dev_tools/keywords/dungeon_list.py @@ -122,16 +122,27 @@ class GenerateDungeonList(GenerateKeyword): dungeon['name'] = 'Divergent_Universe_' + dungeon['name'] if 100 < dungeon['dungeon_id'] < 200: dungeon['name'] = 'Simulated_Universe_' + dungeon['name'] - # Reverse Divergent_Universe - start = 0 - end = 0 - for index, dungeon in enumerate(dungeons): - if dungeon['name'].startswith('Divergent_Universe'): - if start == 0: - start = index - end = index + 1 - if start > 0 and end > 0: - dungeons = dungeons[:start] + dungeons[start:end][::-1] + dungeons[end:] + + # Reverse dungeon list, latest at top + def reverse_on_name(d, prefix): + start = 0 + end = 0 + for index, dungeon in enumerate(d): + if dungeon['name'].startswith(prefix): + if start == 0: + start = index + end = index + 1 + if start > 0 and end > 0: + d = d[:start] + d[start:end][::-1] + d[end:] + return d + + dungeons = reverse_on_name(dungeons, 'Divergent_Universe') + dungeons = reverse_on_name(dungeons, 'Cavern_of_Corrosion') + dungeons = reverse_on_name(dungeons, 'Echo_of_War') + + # Reverse Calyx_Golden, sort by world + # Poor sort + dungeons[0:3], dungeons[6:9] = dungeons[6:9], dungeons[0:3] # Re-sort ID self.keyword_index = 0 diff --git a/tasks/daily/daily_quest.py b/tasks/daily/daily_quest.py index a5d7a9d7b..d60d2ad5a 100644 --- a/tasks/daily/daily_quest.py +++ b/tasks/daily/daily_quest.py @@ -21,7 +21,7 @@ from tasks.daily.synthesize import SynthesizeMaterialUI from tasks.daily.use_technique import UseTechniqueUI from tasks.dungeon.assets.assets_dungeon_ui import DAILY_TRAINING_CHECK from tasks.dungeon.keywords import KEYWORDS_DUNGEON_TAB -from tasks.dungeon.ui import DungeonUI +from tasks.dungeon.ui.ui import DungeonUI from tasks.item.consumable_usage import ConsumableUsageUI from tasks.item.relics import RelicsUI from tasks.map.route.loader import RouteLoader diff --git a/tasks/dungeon/assets/assets_dungeon_ui.py b/tasks/dungeon/assets/assets_dungeon_ui.py index ba77ca705..ba4730031 100644 --- a/tasks/dungeon/assets/assets_dungeon_ui.py +++ b/tasks/dungeon/assets/assets_dungeon_ui.py @@ -3,36 +3,6 @@ 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 ``` -CALYX_WORLD_1 = ButtonWrapper( - name='CALYX_WORLD_1', - share=Button( - file='./assets/share/dungeon/ui/CALYX_WORLD_1.png', - area=(490, 185, 540, 230), - search=(470, 165, 560, 250), - color=(197, 196, 196), - button=(490, 185, 540, 230), - ), -) -CALYX_WORLD_2 = ButtonWrapper( - name='CALYX_WORLD_2', - share=Button( - file='./assets/share/dungeon/ui/CALYX_WORLD_2.png', - area=(590, 185, 640, 230), - search=(570, 165, 660, 250), - color=(199, 198, 198), - button=(590, 185, 640, 230), - ), -) -CALYX_WORLD_3 = ButtonWrapper( - name='CALYX_WORLD_3', - share=Button( - file='./assets/share/dungeon/ui/CALYX_WORLD_3.png', - area=(689, 186, 739, 231), - search=(669, 166, 759, 251), - color=(158, 158, 158), - button=(689, 186, 739, 231), - ), -) DAILY_TRAINING_CHECK = ButtonWrapper( name='DAILY_TRAINING_CHECK', share=Button( @@ -73,16 +43,6 @@ LIST_LOADED_CHECK = ButtonWrapper( button=(576, 606, 951, 664), ), ) -OCR_DUNGEON_LIST = ButtonWrapper( - name='OCR_DUNGEON_LIST', - share=Button( - file='./assets/share/dungeon/ui/OCR_DUNGEON_LIST.png', - area=(581, 176, 1165, 661), - search=(561, 156, 1185, 681), - color=(212, 214, 220), - button=(440, 176, 588, 656), - ), -) OCR_DUNGEON_NAV = ButtonWrapper( name='OCR_DUNGEON_NAV', share=Button( diff --git a/tasks/dungeon/assets/assets_dungeon_ui_list.py b/tasks/dungeon/assets/assets_dungeon_ui_list.py new file mode 100644 index 000000000..275e63cb8 --- /dev/null +++ b/tasks/dungeon/assets/assets_dungeon_ui_list.py @@ -0,0 +1,65 @@ +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 ``` + +LIST_ASCENDING = ButtonWrapper( + name='LIST_ASCENDING', + share=Button( + file='./assets/share/dungeon/ui_list/LIST_ASCENDING.png', + area=(1125, 643, 1143, 661), + search=(1105, 623, 1163, 681), + color=(195, 194, 196), + button=(1125, 643, 1143, 661), + ), +) +LIST_DESCENDING = ButtonWrapper( + name='LIST_DESCENDING', + share=Button( + file='./assets/share/dungeon/ui_list/LIST_DESCENDING.png', + area=(1125, 643, 1143, 661), + search=(1105, 623, 1163, 681), + color=(195, 194, 196), + button=(1125, 643, 1143, 661), + ), +) +OCR_DUNGEON_LIST = ButtonWrapper( + name='OCR_DUNGEON_LIST', + share=Button( + file='./assets/share/dungeon/ui_list/OCR_DUNGEON_LIST.png', + area=(581, 176, 1165, 661), + search=(561, 156, 1185, 681), + color=(212, 214, 220), + button=(440, 176, 588, 656), + ), +) +OCR_DUNGEON_NAME = ButtonWrapper( + name='OCR_DUNGEON_NAME', + share=Button( + file='./assets/share/dungeon/ui_list/OCR_DUNGEON_NAME.png', + area=(563, 172, 788, 624), + search=(543, 152, 808, 644), + color=(245, 243, 245), + button=(563, 172, 788, 624), + ), +) +OCR_DUNGEON_NAME_ROGUE = ButtonWrapper( + name='OCR_DUNGEON_NAME_ROGUE', + share=Button( + file='./assets/share/dungeon/ui_list/OCR_DUNGEON_NAME_ROGUE.png', + area=(563, 292, 788, 624), + search=(543, 272, 808, 644), + color=(249, 247, 249), + button=(563, 292, 788, 624), + ), +) +OCR_DUNGEON_TELEPORT = ButtonWrapper( + name='OCR_DUNGEON_TELEPORT', + share=Button( + file='./assets/share/dungeon/ui_list/OCR_DUNGEON_TELEPORT.png', + area=(1013, 172, 1163, 624), + search=(993, 152, 1183, 644), + color=(231, 234, 230), + button=(1013, 172, 1163, 624), + ), +) diff --git a/tasks/dungeon/dungeon.py b/tasks/dungeon/dungeon.py index c628cd15e..0da8d343b 100644 --- a/tasks/dungeon/dungeon.py +++ b/tasks/dungeon/dungeon.py @@ -213,7 +213,7 @@ class Dungeon(DungeonStamina, DungeonEvent, Combat): elif require and not self.support_once: # Run with support all the way return self._dungeon_run(dungeon=dungeon, team=team, wave_limit=0, - support_character=self.config.DungeonSupport_Character) + support_character=self.config.DungeonSupport_Character) else: # Normal run @@ -250,10 +250,10 @@ class Dungeon(DungeonStamina, DungeonEvent, Combat): if self.has_double_rogue_event(): rogue = self.get_double_rogue_remain() if self.has_double_calyx_event(): - self._dungeon_nav_goto(KEYWORDS_DUNGEON_NAV.Calyx_Golden) + self.dungeon_nav_goto(KEYWORDS_DUNGEON_NAV.Calyx_Golden) calyx = self.get_double_event_remain() if self.has_double_relic_event(): - self._dungeon_nav_goto(KEYWORDS_DUNGEON_NAV.Cavern_of_Corrosion) + self.dungeon_nav_goto(KEYWORDS_DUNGEON_NAV.Cavern_of_Corrosion) relic = self.get_double_rogue_remain() with self.config.multi_set(): self.config.stored.DungeonDouble.calyx = calyx diff --git a/tasks/dungeon/keywords/dungeon.py b/tasks/dungeon/keywords/dungeon.py index 48e14fa7e..8a8648b64 100644 --- a/tasks/dungeon/keywords/dungeon.py +++ b/tasks/dungeon/keywords/dungeon.py @@ -3,38 +3,38 @@ from .classes import DungeonList # This file was auto-generated, do not modify it manually. To generate: # ``` python -m dev_tools.keyword_extract ``` -Calyx_Golden_Memories_Jarilo_VI = DungeonList( +Calyx_Golden_Memories_Penacony = DungeonList( id=1, - name='Calyx_Golden_Memories_Jarilo_VI', + name='Calyx_Golden_Memories_Penacony', cn='回忆之蕾', cht='回憶之蕾', en='Bud of Memories', jp='回憶の蕾', es='Flor de los recuerdos', - dungeon_id=1001, - plane_id=2010101, + dungeon_id=1014, + plane_id=2031301, ) -Calyx_Golden_Aether_Jarilo_VI = DungeonList( +Calyx_Golden_Aether_Penacony = DungeonList( id=2, - name='Calyx_Golden_Aether_Jarilo_VI', + name='Calyx_Golden_Aether_Penacony', cn='以太之蕾', cht='乙太之蕾', en='Bud of Aether', jp='エーテルの蕾', es='Flor de éter', - dungeon_id=1002, - plane_id=2011101, + dungeon_id=1015, + plane_id=2031201, ) -Calyx_Golden_Treasures_Jarilo_VI = DungeonList( +Calyx_Golden_Treasures_Penacony = DungeonList( id=3, - name='Calyx_Golden_Treasures_Jarilo_VI', + name='Calyx_Golden_Treasures_Penacony', cn='藏珍之蕾', cht='藏珍之蕾', en='Bud of Treasures', jp='秘蔵の蕾', es='Flor de tesoros', - dungeon_id=1003, - plane_id=2012101, + dungeon_id=1016, + plane_id=2031101, ) Calyx_Golden_Memories_The_Xianzhou_Luofu = DungeonList( id=4, @@ -69,38 +69,38 @@ Calyx_Golden_Treasures_The_Xianzhou_Luofu = DungeonList( dungeon_id=1013, plane_id=2022201, ) -Calyx_Golden_Memories_Penacony = DungeonList( +Calyx_Golden_Memories_Jarilo_VI = DungeonList( id=7, - name='Calyx_Golden_Memories_Penacony', + name='Calyx_Golden_Memories_Jarilo_VI', cn='回忆之蕾', cht='回憶之蕾', en='Bud of Memories', jp='回憶の蕾', es='Flor de los recuerdos', - dungeon_id=1014, - plane_id=2031301, + dungeon_id=1001, + plane_id=2010101, ) -Calyx_Golden_Aether_Penacony = DungeonList( +Calyx_Golden_Aether_Jarilo_VI = DungeonList( id=8, - name='Calyx_Golden_Aether_Penacony', + name='Calyx_Golden_Aether_Jarilo_VI', cn='以太之蕾', cht='乙太之蕾', en='Bud of Aether', jp='エーテルの蕾', es='Flor de éter', - dungeon_id=1015, - plane_id=2031201, + dungeon_id=1002, + plane_id=2011101, ) -Calyx_Golden_Treasures_Penacony = DungeonList( +Calyx_Golden_Treasures_Jarilo_VI = DungeonList( id=9, - name='Calyx_Golden_Treasures_Penacony', + name='Calyx_Golden_Treasures_Jarilo_VI', cn='藏珍之蕾', cht='藏珍之蕾', en='Bud of Treasures', jp='秘蔵の蕾', es='Flor de tesoros', - dungeon_id=1016, - plane_id=2031101, + dungeon_id=1003, + plane_id=2012101, ) Calyx_Crimson_Destruction_Herta_StorageZone = DungeonList( id=10, @@ -476,107 +476,8 @@ Stagnant_Shadow_Gloam = DungeonList( dungeon_id=1121, plane_id=2033201, ) -Cavern_of_Corrosion_Path_of_Gelid_Wind = DungeonList( - id=44, - name='Cavern_of_Corrosion_Path_of_Gelid_Wind', - cn='霜风之径', - cht='霜風之徑', - en='Path of Gelid Wind', - jp='霜風の路', - es='Senda del viento gélido', - dungeon_id=1201, - plane_id=2000201, -) -Cavern_of_Corrosion_Path_of_Jabbing_Punch = DungeonList( - id=45, - name='Cavern_of_Corrosion_Path_of_Jabbing_Punch', - cn='迅拳之径', - cht='迅拳之徑', - en='Path of Jabbing Punch', - jp='迅拳の路', - es='Senda de los puños rápidos', - dungeon_id=1202, - plane_id=2013101, -) -Cavern_of_Corrosion_Path_of_Drifting = DungeonList( - id=46, - name='Cavern_of_Corrosion_Path_of_Drifting', - cn='漂泊之径', - cht='漂泊之徑', - en='Path of Drifting', - jp='漂泊の路', - es='Senda de la deriva', - dungeon_id=1203, - plane_id=2013201, -) -Cavern_of_Corrosion_Path_of_Providence = DungeonList( - id=47, - name='Cavern_of_Corrosion_Path_of_Providence', - cn='睿治之径', - cht='睿治之徑', - en='Path of Providence', - jp='睿治の路', - es='Senda de la providencia', - dungeon_id=1204, - plane_id=2013401, -) -Cavern_of_Corrosion_Path_of_Holy_Hymn = DungeonList( - id=48, - name='Cavern_of_Corrosion_Path_of_Holy_Hymn', - cn='圣颂之径', - cht='聖頌之徑', - en='Path of Holy Hymn', - jp='聖頌の路', - es='Senda del himno sagrado', - dungeon_id=1205, - plane_id=2021101, -) -Cavern_of_Corrosion_Path_of_Conflagration = DungeonList( - id=49, - name='Cavern_of_Corrosion_Path_of_Conflagration', - cn='野焰之径', - cht='野焰之徑', - en='Path of Conflagration', - jp='野焔の路', - es='Senda de la conflagración', - dungeon_id=1206, - plane_id=2021201, -) -Cavern_of_Corrosion_Path_of_Elixir_Seekers = DungeonList( - id=50, - name='Cavern_of_Corrosion_Path_of_Elixir_Seekers', - cn='药使之径', - cht='藥使之徑', - en='Path of Elixir Seekers', - jp='薬使の路', - es='Senda de los elixires', - dungeon_id=1207, - plane_id=2023101, -) -Cavern_of_Corrosion_Path_of_Darkness = DungeonList( - id=51, - name='Cavern_of_Corrosion_Path_of_Darkness', - cn='幽冥之径', - cht='幽冥之徑', - en='Path of Darkness', - jp='幽冥の路', - es='Senda de la oscuridad', - dungeon_id=1208, - plane_id=2022301, -) -Cavern_of_Corrosion_Path_of_Dreamdive = DungeonList( - id=52, - name='Cavern_of_Corrosion_Path_of_Dreamdive', - cn='梦潜之径', - cht='夢潛之徑', - en='Path of Dreamdive', - jp='夢潜の路', - es='Senda de los sueños', - dungeon_id=1209, - plane_id=2031101, -) Cavern_of_Corrosion_Path_of_Cavalier = DungeonList( - id=53, + id=44, name='Cavern_of_Corrosion_Path_of_Cavalier', cn='勇骑之径', cht='勇騎之徑', @@ -586,52 +487,118 @@ Cavern_of_Corrosion_Path_of_Cavalier = DungeonList( dungeon_id=1210, plane_id=2033201, ) -Echo_of_War_Destruction_Beginning = DungeonList( - id=54, - name='Echo_of_War_Destruction_Beginning', - cn='毁灭的开端•历战余响', - cht='毀滅的開端•歷戰餘響', - en="Echo of War: Destruction's Beginning", - jp='歴戦余韻・壊滅の始まり', - es='El principio de la Destrucción', - dungeon_id=1301, - plane_id=2000301, +Cavern_of_Corrosion_Path_of_Dreamdive = DungeonList( + id=45, + name='Cavern_of_Corrosion_Path_of_Dreamdive', + cn='梦潜之径', + cht='夢潛之徑', + en='Path of Dreamdive', + jp='夢潜の路', + es='Senda de los sueños', + dungeon_id=1209, + plane_id=2031101, ) -Echo_of_War_End_of_the_Eternal_Freeze = DungeonList( - id=55, - name='Echo_of_War_End_of_the_Eternal_Freeze', - cn='寒潮的落幕•历战余响', - cht='寒潮的落幕•歷戰餘響', - en='Echo of War: End of the Eternal Freeze', - jp='歴戦余韻・寒波の幕切れ', - es='El fin del Hielo Eterno', - dungeon_id=1302, +Cavern_of_Corrosion_Path_of_Darkness = DungeonList( + id=46, + name='Cavern_of_Corrosion_Path_of_Darkness', + cn='幽冥之径', + cht='幽冥之徑', + en='Path of Darkness', + jp='幽冥の路', + es='Senda de la oscuridad', + dungeon_id=1208, + plane_id=2022301, +) +Cavern_of_Corrosion_Path_of_Elixir_Seekers = DungeonList( + id=47, + name='Cavern_of_Corrosion_Path_of_Elixir_Seekers', + cn='药使之径', + cht='藥使之徑', + en='Path of Elixir Seekers', + jp='薬使の路', + es='Senda de los elixires', + dungeon_id=1207, + plane_id=2023101, +) +Cavern_of_Corrosion_Path_of_Conflagration = DungeonList( + id=48, + name='Cavern_of_Corrosion_Path_of_Conflagration', + cn='野焰之径', + cht='野焰之徑', + en='Path of Conflagration', + jp='野焔の路', + es='Senda de la conflagración', + dungeon_id=1206, + plane_id=2021201, +) +Cavern_of_Corrosion_Path_of_Holy_Hymn = DungeonList( + id=49, + name='Cavern_of_Corrosion_Path_of_Holy_Hymn', + cn='圣颂之径', + cht='聖頌之徑', + en='Path of Holy Hymn', + jp='聖頌の路', + es='Senda del himno sagrado', + dungeon_id=1205, + plane_id=2021101, +) +Cavern_of_Corrosion_Path_of_Providence = DungeonList( + id=50, + name='Cavern_of_Corrosion_Path_of_Providence', + cn='睿治之径', + cht='睿治之徑', + en='Path of Providence', + jp='睿治の路', + es='Senda de la providencia', + dungeon_id=1204, plane_id=2013401, ) -Echo_of_War_Divine_Seed = DungeonList( - id=56, - name='Echo_of_War_Divine_Seed', - cn='不死的神实•历战余响', - cht='不死的神實•歷戰餘響', - en='Echo of War: Divine Seed', - jp='歴戦余韻・不死の神実', - es='Semilla divina', - dungeon_id=1303, - plane_id=2023201, +Cavern_of_Corrosion_Path_of_Drifting = DungeonList( + id=51, + name='Cavern_of_Corrosion_Path_of_Drifting', + cn='漂泊之径', + cht='漂泊之徑', + en='Path of Drifting', + jp='漂泊の路', + es='Senda de la deriva', + dungeon_id=1203, + plane_id=2013201, ) -Echo_of_War_Borehole_Planet_Old_Crater = DungeonList( - id=57, - name='Echo_of_War_Borehole_Planet_Old_Crater', - cn='蛀星的旧靥•历战余响', - cht='蛀星的舊靨•歷戰餘響', - en="Echo of War: Borehole Planet's Old Crater", - jp='歴戦余韻・星を蝕む往日の面影', - es='Cráter del planeta devorado', - dungeon_id=1304, - plane_id=2000401, +Cavern_of_Corrosion_Path_of_Jabbing_Punch = DungeonList( + id=52, + name='Cavern_of_Corrosion_Path_of_Jabbing_Punch', + cn='迅拳之径', + cht='迅拳之徑', + en='Path of Jabbing Punch', + jp='迅拳の路', + es='Senda de los puños rápidos', + dungeon_id=1202, + plane_id=2013101, +) +Cavern_of_Corrosion_Path_of_Gelid_Wind = DungeonList( + id=53, + name='Cavern_of_Corrosion_Path_of_Gelid_Wind', + cn='霜风之径', + cht='霜風之徑', + en='Path of Gelid Wind', + jp='霜風の路', + es='Senda del viento gélido', + dungeon_id=1201, + plane_id=2000201, +) +Echo_of_War_Inner_Beast_Battlefield = DungeonList( + id=54, + name='Echo_of_War_Inner_Beast_Battlefield', + cn='心兽的战场•历战余响', + cht='心獸的戰場•歷戰餘響', + en="Echo of War: Inner Beast's Battlefield", + jp='歴戦余韻・心獣の戦場', + es='Campo de batalla de la bestia interior', + dungeon_id=1306, + plane_id=2024201, ) Echo_of_War_Salutations_of_Ashen_Dreams = DungeonList( - id=58, + id=55, name='Echo_of_War_Salutations_of_Ashen_Dreams', cn='尘梦的赞礼•历战余响', cht='塵夢的讚禮•歷戰餘響', @@ -641,16 +608,49 @@ Echo_of_War_Salutations_of_Ashen_Dreams = DungeonList( dungeon_id=1305, plane_id=2033201, ) -Echo_of_War_Inner_Beast_Battlefield = DungeonList( +Echo_of_War_Borehole_Planet_Old_Crater = DungeonList( + id=56, + name='Echo_of_War_Borehole_Planet_Old_Crater', + cn='蛀星的旧靥•历战余响', + cht='蛀星的舊靨•歷戰餘響', + en="Echo of War: Borehole Planet's Old Crater", + jp='歴戦余韻・星を蝕む往日の面影', + es='Cráter del planeta devorado', + dungeon_id=1304, + plane_id=2000401, +) +Echo_of_War_Divine_Seed = DungeonList( + id=57, + name='Echo_of_War_Divine_Seed', + cn='不死的神实•历战余响', + cht='不死的神實•歷戰餘響', + en='Echo of War: Divine Seed', + jp='歴戦余韻・不死の神実', + es='Semilla divina', + dungeon_id=1303, + plane_id=2023201, +) +Echo_of_War_End_of_the_Eternal_Freeze = DungeonList( + id=58, + name='Echo_of_War_End_of_the_Eternal_Freeze', + cn='寒潮的落幕•历战余响', + cht='寒潮的落幕•歷戰餘響', + en='Echo of War: End of the Eternal Freeze', + jp='歴戦余韻・寒波の幕切れ', + es='El fin del Hielo Eterno', + dungeon_id=1302, + plane_id=2013401, +) +Echo_of_War_Destruction_Beginning = DungeonList( id=59, - name='Echo_of_War_Inner_Beast_Battlefield', - cn='心兽的战场•历战余响', - cht='心獸的戰場•歷戰餘響', - en="Echo of War: Inner Beast's Battlefield", - jp='歴戦余韻・心獣の戦場', - es='Campo de batalla de la bestia interior', - dungeon_id=1306, - plane_id=2024201, + name='Echo_of_War_Destruction_Beginning', + cn='毁灭的开端•历战余响', + cht='毀滅的開端•歷戰餘響', + en="Echo of War: Destruction's Beginning", + jp='歴戦余韻・壊滅の始まり', + es='El principio de la Destrucción', + dungeon_id=1301, + plane_id=2000301, ) Simulated_Universe_World_1 = DungeonList( id=60, diff --git a/tasks/dungeon/stamina.py b/tasks/dungeon/stamina.py index 41fd56354..092cd3bd7 100644 --- a/tasks/dungeon/stamina.py +++ b/tasks/dungeon/stamina.py @@ -6,7 +6,7 @@ from tasks.base.page import page_guide from tasks.combat.assets.assets_combat_stamina_status import ICON_SEARCH, IMMERSIFIER_ICON from tasks.dungeon.assets.assets_dungeon_stamina import * from tasks.dungeon.keywords import KEYWORDS_DUNGEON_TAB -from tasks.dungeon.ui import DungeonUI +from tasks.dungeon.ui.ui import DungeonUI class DungeonStamina(DungeonUI): diff --git a/tasks/dungeon/ui.py b/tasks/dungeon/ui.py deleted file mode 100644 index e558d038d..000000000 --- a/tasks/dungeon/ui.py +++ /dev/null @@ -1,770 +0,0 @@ -import re - -import cv2 -import numpy as np - -from module.base.base import ModuleBase -from module.base.button import ClickButton -from module.base.decorator import run_once -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 Ocr, OcrResultButton -from module.ocr.utils import split_and_pair_button_attr, split_and_pair_buttons -from module.ui.draggable_list import DraggableList -from module.ui.switch import Switch -from tasks.base.page import page_guide -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.dungeon.assets.assets_dungeon_ui import * -from tasks.dungeon.assets.assets_dungeon_ui_rogue 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 -from tasks.map.interact.aim import inrange -from tasks.map.keywords import KEYWORDS_MAP_WORLD, MapPlane - - -class DungeonTabSwitch(Switch): - SEARCH_BUTTON = TAB_SEARCH - - def add_state(self, state, check_button, click_button=None): - # Load search - if check_button is not None: - check_button.load_search(self.__class__.SEARCH_BUTTON.area) - if click_button is not None: - click_button.load_search(self.__class__.SEARCH_BUTTON.area) - return super().add_state(state, check_button, click_button) - - def click(self, state, main): - """ - Args: - state (str): - main (ModuleBase): - """ - button = self.get_data(state)['click_button'] - _ = main.appear(button) # Search button to load offset - main.device.click(button) - - -SWITCH_DUNGEON_TAB = DungeonTabSwitch('DungeonTab', is_selector=True) -SWITCH_DUNGEON_TAB.add_state( - KEYWORDS_DUNGEON_TAB.Operation_Briefing, - check_button=OPERATION_BRIEFING_CHECK, - click_button=OPERATION_BRIEFING_CLICK -) -SWITCH_DUNGEON_TAB.add_state( - KEYWORDS_DUNGEON_TAB.Daily_Training, - check_button=DAILY_TRAINING_CHECK, - click_button=DAILY_TRAINING_CLICK -) -SWITCH_DUNGEON_TAB.add_state( - KEYWORDS_DUNGEON_TAB.Survival_Index, - check_button=SURVIVAL_INDEX_CHECK, - click_button=SURVIVAL_INDEX_CLICK -) -SWITCH_DUNGEON_TAB.add_state( - KEYWORDS_DUNGEON_TAB.Simulated_Universe, - check_button=SIMULATED_UNIVERSE_CHECK, - click_button=SIMULATED_UNIVERSE_CLICK -) -SWITCH_DUNGEON_TAB.add_state( - KEYWORDS_DUNGEON_TAB.Treasures_Lightward, - check_button=TREASURES_LIGHTWARD_CHECK, - click_button=TREASURES_LIGHTWARD_CLICK -) - - -class OcrDungeonNav(Ocr): - def after_process(self, result): - result = super().after_process(result) - result = result.replace('#', '') - if self.lang == 'cn': - result = result.replace('萼喜', '萼') - result = result.replace('带', '滞') # 凝带虚影 - return result - - -class OcrDungeonList(Ocr): - def after_process(self, result): - # 乙太之蕾•雅利洛-Ⅵ - result = re.sub(r'-[VⅤ][IⅠ]', '-Ⅵ', result) - - # 苏乐达™热砂海选会场 - result = re.sub(r'(苏乐达|蘇樂達|SoulGlad|スラーダ|FelizAlma)[rtT]*M', r'\1', result) - - result = super().after_process(result) - - if self.lang == 'cn': - result = result.replace('翼', '巽') # 巽风之形 - result = result.replace('皖A0', '50').replace('皖', '') - # 燔灼之形•凝滞虚影 - result = result.replace('熠', '燔') - result = re.sub('^灼之形', '燔灼之形', result) - # 偃偶之形•凝滞虚影 - result = re.sub('^偶之形', '偃偶之形', result) - # 嗔怒之形•凝滞虚影 - result = re.sub('^怒之形', '嗔怒之形', result) - # 蛀星的旧·历战余响 - result = re.sub(r'蛀星的旧.*?历战', '蛀星的旧靥•历战', result) - - # 9支援仓段 - for word in 'Q9α': - result = result.removeprefix(word) - return result - - -class OcrDungeonListCalyxCrimson(OcrDungeonList): - def _match_result(self, *args, **kwargs): - """ - Convert MapPlane object to their corresponding DungeonList object - """ - plane = super()._match_result(*args, **kwargs) - if plane is not None: - for dungeon in DungeonList.instances.values(): - if dungeon.is_Calyx_Crimson and dungeon.plane == plane: - return dungeon - return plane - - -class OcrDungeonListLimitEntrance(OcrDungeonList): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.button = ClickButton((*self.button.area[:3], self.button.area[3] - 70)) - - -class OcrDungeonListCalyxCrimsonLimitEntrance(OcrDungeonListCalyxCrimson, OcrDungeonListLimitEntrance): - pass - - -class DraggableDungeonNav(DraggableList): - # 0.5 is the magic number to reach bottom in 1 swipe - # but relax we still have retires when magic doesn't work - drag_vector = (0.50, 0.52) - - -class DraggableDungeonList(DraggableList): - teleports: list[OcrResultButton] = [] - navigates: list[OcrResultButton] = [] - - # use_plane: True to use map planes to predict dungeons only. - # Can only be True in Calyx Crimson - use_plane = False - # limit_entrance: True to ensure the teleport button is insight - limit_entrance = False - - def load_rows(self, main: ModuleBase, allow_early_access=False): - """ - Args: - main: - allow_early_access: True to allow dungeons that are in temporarily early access during events - """ - relative_area = (0, 0, 1280, 120) - if self.use_plane: - self.keyword_class = [MapPlane, DungeonEntrance] - if self.limit_entrance: - self.ocr_class = OcrDungeonListCalyxCrimsonLimitEntrance - else: - self.ocr_class = OcrDungeonListCalyxCrimson - else: - self.keyword_class = [DungeonList, DungeonEntrance] - if self.limit_entrance: - self.ocr_class = OcrDungeonListLimitEntrance - else: - self.ocr_class = OcrDungeonList - super().load_rows(main=main) - - # Check early access dungeons - buttons = DUNGEON_LIST.cur_buttons.copy() - for name, button in split_and_pair_buttons( - DUNGEON_LIST.cur_buttons, - split_func=lambda x: x != KEYWORDS_DUNGEON_ENTRANCE.Enter, - relative_area=relative_area - ): - logger.warning(f'Early access dungeon: {name}') - buttons.remove(name) - buttons.remove(button) - - # Remove early access dungeons - if not allow_early_access: - DUNGEON_LIST.cur_buttons = buttons - # From super.load_rows(), re-calculate indexes - indexes = [self.keyword2index(row.matched_keyword) - for row in self.cur_buttons] - indexes = [index for index in indexes if index] - - if not indexes: - logger.warning(f'No valid rows loaded into {self}') - return - - self.cur_min = min(indexes) - self.cur_max = max(indexes) - logger.attr(self.name, f'{self.cur_min} - {self.cur_max}') - - # Replace dungeon.button with teleport - self.teleports = list(split_and_pair_button_attr( - DUNGEON_LIST.cur_buttons, - split_func=lambda x: x != KEYWORDS_DUNGEON_ENTRANCE.Teleport and x != KEYWORDS_DUNGEON_ENTRANCE.Enter, - relative_area=relative_area - )) - self.navigates = list(split_and_pair_button_attr( - DUNGEON_LIST.cur_buttons, - split_func=lambda x: x != KEYWORDS_DUNGEON_ENTRANCE.Navigate, - relative_area=relative_area - )) - - -DUNGEON_NAV_LIST = DraggableDungeonNav( - 'DungeonNavList', keyword_class=DungeonNav, ocr_class=OcrDungeonNav, search_button=OCR_DUNGEON_NAV) -DUNGEON_LIST = DraggableDungeonList( - 'DungeonList', keyword_class=[DungeonList, DungeonEntrance, MapPlane], - ocr_class=OcrDungeonList, search_button=OCR_DUNGEON_LIST) - - -class DungeonUI(DungeonState): - def dungeon_tab_goto(self, state: DungeonTab): - """ - Args: - state: - - Returns: - bool: If UI switched - - Examples: - self = DungeonUI('alas') - self.device.screenshot() - self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Operation_Briefing) - self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Daily_Training) - self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index) - """ - logger.hr('Dungeon tab goto', level=2) - ui_switched = self.ui_ensure(page_guide) - tab_switched = SWITCH_DUNGEON_TAB.set(state, main=self) - - if ui_switched or tab_switched: - if state == KEYWORDS_DUNGEON_TAB.Daily_Training: - logger.info(f'Tab goto {state}, wait until loaded') - self._dungeon_wait_daily_training_loaded() - elif state == KEYWORDS_DUNGEON_TAB.Survival_Index: - logger.info(f'Tab goto {state}, wait until loaded') - self._dungeon_wait_survival_index_loaded() - elif state == KEYWORDS_DUNGEON_TAB.Treasures_Lightward: - logger.info(f'Tab goto {state}, wait until loaded') - self._dungeon_wait_treasures_lightward_loaded() - return True - else: - return False - - def _dungeon_wait_daily_training_loaded(self, skip_first_screenshot=True): - """ - Returns: - bool: True if wait success, False if wait timeout. - - Pages: - in: page_guide, Daily_Training - """ - timeout = Timer(2, count=4).start() - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - - if timeout.reached(): - logger.warning('Wait daily training loaded timeout') - return False - color = get_color(self.device.image, DAILY_TRAINING_LOADED.area) - if np.mean(color) < 128: - logger.info('Daily training loaded') - return True - - def _dungeon_wait_survival_index_loaded(self, skip_first_screenshot=True): - """ - Returns: - bool: True if wait success, False if wait timeout. - - Pages: - in: page_guide, Survival_Index - """ - timeout = Timer(2, count=4).start() - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - - if timeout.reached(): - logger.warning('Wait survival index loaded timeout') - return False - if self.appear(SURVIVAL_INDEX_SU_LOADED): - logger.info('Survival index loaded, SURVIVAL_INDEX_SU_LOADED') - return True - if self.appear(SURVIVAL_INDEX_OE_LOADED): - logger.info('Survival index loaded, SURVIVAL_INDEX_OE_LOADED') - return True - - def _dungeon_survival_index_top_appear(self): - if self.appear(SURVIVAL_INDEX_SU_LOADED): - return True - if self.appear(SURVIVAL_INDEX_OE_LOADED): - return True - return False - - def _dungeon_wait_treasures_lightward_loaded(self, skip_first_screenshot=True): - """ - Returns: - bool: True if wait success, False if wait timeout. - - Pages: - in: page_guide, Survival_Index - """ - timeout = Timer(2, count=4).start() - TREASURES_LIGHTWARD_LOADED.set_search_offset((5, 5)) - TREASURES_LIGHTWARD_LOCKED.set_search_offset((5, 5)) - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - - if timeout.reached(): - logger.warning('Wait treasures lightward loaded timeout') - return False - if self.appear(TREASURES_LIGHTWARD_LOADED): - logger.info('Treasures lightward loaded (event unlocked)') - return True - if self.appear(TREASURES_LIGHTWARD_LOCKED): - logger.info('Treasures lightward loaded (event locked)') - return True - - def _dungeon_list_button_has_content(self): - # Check if having any content - # List background: 254, guild border: 225 - r, g, b = cv2.split(self.image_crop(LIST_LOADED_CHECK, copy=False)) - minimum = cv2.min(cv2.min(r, g), b) - minimum = inrange(minimum, lower=0, upper=180) - if minimum.size > 100: - return True - else: - return False - - def _dungeon_wait_until_dungeon_list_loaded(self, skip_first_screenshot=True): - timeout = Timer(1, count=3).start() - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - - # End - if timeout.reached(): - logger.warning('Wait until dungeon list loaded timeout') - return False - - if self._dungeon_list_button_has_content(): - logger.info('Dungeon list loaded') - return True - - def _dungeon_wait_until_echo_or_war_stabled(self, skip_first_screenshot=True): - """ - Returns: - bool: True if wait success, False if wait timeout. - - Pages: - in: page_guide, Survival_Index - """ - # Wait until Forgotten_Hall stabled - timeout = Timer(2, count=4).start() - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - - # End - if timeout.reached(): - logger.warning('Wait until Echo_of_War stabled timeout') - return False - - DUNGEON_NAV_LIST.load_rows(main=self) - - # End - button = DUNGEON_NAV_LIST.keyword2button(KEYWORDS_DUNGEON_NAV.Echo_of_War, show_warning=False) - if button: - # 513 is the top of the last row of DungeonNav - if button.area[1] > 513: - logger.info('DungeonNav row Echo_of_War stabled') - return True - else: - logger.info('No Echo_of_War in list skip waiting') - return False - - def _dungeon_nav_goto(self, nav: DungeonNav, skip_first_screenshot=True): - """ - Equivalent to `DUNGEON_NAV_LIST.select_row(dungeon.dungeon_nav, main=self)` - but with tricks to be faster - - Args: - nav: - skip_first_screenshot: - """ - logger.hr('Dungeon nav goto', level=2) - logger.info(f'Dungeon nav goto {nav}') - - # Wait rows - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - DUNGEON_NAV_LIST.load_rows(main=self) - if DUNGEON_NAV_LIST.cur_buttons: - break - - # Wait first row selected - timeout = Timer(0.5, count=2).start() - skip_first_screenshot = True - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - if timeout.reached(): - logger.info('DUNGEON_NAV_LIST not selected') - break - if button := DUNGEON_NAV_LIST.get_selected_row(main=self): - logger.info(f'DUNGEON_NAV_LIST selected at {button}') - break - - # Check if it's at the first page. - if DUNGEON_NAV_LIST.keyword2button(KEYWORDS_DUNGEON_NAV.Simulated_Universe, show_warning=False) \ - or DUNGEON_NAV_LIST.keyword2button(KEYWORDS_DUNGEON_NAV.Ornament_Extraction, show_warning=False): - # Going to use a faster method to navigate but can only start from list top - logger.info('DUNGEON_NAV_LIST at top') - # Update points if possible - # 2.3, No longer weekly points after Divergent Universe unlocked - # if DUNGEON_NAV_LIST.is_row_selected(button, main=self): - # self.dungeon_update_simuni() - # Treasures lightward is always at top - elif DUNGEON_NAV_LIST.keyword2button(KEYWORDS_DUNGEON_NAV.Forgotten_Hall, show_warning=False) \ - or DUNGEON_NAV_LIST.keyword2button(KEYWORDS_DUNGEON_NAV.Pure_Fiction, show_warning=False): - logger.info('DUNGEON_NAV_LIST at top') - else: - # To start from any list states. - logger.info('DUNGEON_NAV_LIST not at top') - DUNGEON_NAV_LIST.select_row(nav, main=self) - return True - - # Check the first page - if nav in [ - KEYWORDS_DUNGEON_NAV.Simulated_Universe, - KEYWORDS_DUNGEON_NAV.Divergent_Universe, - KEYWORDS_DUNGEON_NAV.Ornament_Extraction, - KEYWORDS_DUNGEON_NAV.Calyx_Golden, - KEYWORDS_DUNGEON_NAV.Calyx_Crimson, - KEYWORDS_DUNGEON_NAV.Stagnant_Shadow, - KEYWORDS_DUNGEON_NAV.Cavern_of_Corrosion, - KEYWORDS_DUNGEON_NAV.Forgotten_Hall, - KEYWORDS_DUNGEON_NAV.Pure_Fiction, - ]: - button = DUNGEON_NAV_LIST.keyword2button(nav) - if button: - DUNGEON_NAV_LIST.select_row(nav, main=self, insight=False) - return True - - # Check the second page - while 1: - DUNGEON_NAV_LIST.drag_page('down', main=self) - # No skip_first_screenshot since drag_page is just called - if self._dungeon_wait_until_echo_or_war_stabled(skip_first_screenshot=False): - DUNGEON_NAV_LIST.select_row(nav, main=self, insight=False) - return True - - def _dungeon_world_set(self, dungeon: DungeonList, skip_first_screenshot=True): - """ - Switch worlds in Calyx_Golden - - Returns: - bool: True if success to set - """ - logger.hr('Dungeon world set', level=2) - if not dungeon.is_Calyx_Golden: - logger.warning(f'Dungeon {dungeon} is not Calyx Golden, no need to set world') - return False - if dungeon.world is None: - logger.error(f'Dungeon {dungeon} does not belongs to any world') - return False - dic_world_button = { - KEYWORDS_MAP_WORLD.Jarilo_VI: CALYX_WORLD_1, - KEYWORDS_MAP_WORLD.The_Xianzhou_Luofu: CALYX_WORLD_2, - KEYWORDS_MAP_WORLD.Penacony: CALYX_WORLD_3, - } - button = dic_world_button.get(dungeon.world) - if button is None: - logger.error(f'Dungeon {dungeon} with world {dungeon.world} has no corresponding world button') - return False - - logger.info(f'Dungeon world set {dungeon.world}') - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - - # End - if self.image_color_count(button, color=(18, 18, 18), threshold=180, count=50): - logger.info(f'Dungeon world at {dungeon.world}') - return True - # Click - if self.ui_page_appear(page_guide, interval=2): - self.device.click(button) - continue - - def _dungeon_world_set_wrapper(self, dungeon: DungeonList, skip_first_screenshot=True): - """ - Switch worlds in Calyx_Golden with error handling - If world tab is not unlocked, fallback to Jarilo dungeons - """ - # Wait world tab - button = CALYX_WORLD_1 - tab = False - timeout = Timer(0.6, count=3).start() - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - # End - if timeout.reached(): - break - # Selected tab - if self.image_color_count(button, color=(18, 18, 18), threshold=180, count=50): - tab = True - break - # Unselected tab - if self.image_color_count(button, color=(134, 134, 134), threshold=180, count=50): - tab = True - break - - logger.attr('WorldTab', tab) - if not tab: - logger.warning('World tab is not unlocked, fallback to Jarilo dungeons') - if dungeon.is_Calyx_Golden_Memories: - dungeon = KEYWORDS_DUNGEON_LIST.Calyx_Golden_Treasures_Jarilo_VI - if dungeon.is_Calyx_Golden_Aether: - dungeon = KEYWORDS_DUNGEON_LIST.Calyx_Golden_Aether_Jarilo_VI - if dungeon.is_Calyx_Golden_Treasures: - dungeon = KEYWORDS_DUNGEON_LIST.Calyx_Golden_Treasures_Jarilo_VI - - self._dungeon_world_set(dungeon, skip_first_screenshot=skip_first_screenshot) - return dungeon - - def _dungeon_insight(self, dungeon: DungeonList): - """ - Pages: - in: page_guide, Survival_Index, nav including dungeon - out: page_guide, Survival_Index, nav including dungeon, dungeon insight - """ - logger.hr('Dungeon insight', level=2) - DUNGEON_LIST.use_plane = bool(dungeon.is_Calyx_Crimson) - # Insight dungeon - DUNGEON_LIST.insight_row(dungeon, main=self) - self.device.click_record_clear() - # Check if dungeon unlocked - for entrance in DUNGEON_LIST.navigates: - entrance: OcrResultButton = entrance - logger.warning(f'Teleport {entrance.matched_keyword} is not unlocked') - if entrance == dungeon: - logger.error(f'Trying to enter dungeon {dungeon}, but teleport is not unlocked') - return False - - # Find teleport button - if dungeon not in [tp.matched_keyword for tp in DUNGEON_LIST.teleports]: - # Dungeon name is insight but teleport button is not - logger.info('Dungeon name is insight, swipe down a little bit to find the teleport button') - if dungeon.is_Forgotten_Hall: - DUNGEON_LIST.drag_vector = (-0.4, -0.2) # Keyword loaded is reversed - else: - DUNGEON_LIST.drag_vector = (0.2, 0.4) - DUNGEON_LIST.limit_entrance = True - DUNGEON_LIST.insight_row(dungeon, main=self) - self.device.click_record_clear() - DUNGEON_LIST.drag_vector = DraggableList.drag_vector - DUNGEON_LIST.limit_entrance = False - DUNGEON_LIST.load_rows(main=self) - # Check if dungeon unlocked - for entrance in DUNGEON_LIST.navigates: - if entrance == dungeon: - logger.error(f'Trying to enter dungeon {dungeon}, but teleport is not unlocked') - return False - - return True - - def _dungeon_enter(self, dungeon, enter_check_button=COMBAT_PREPARE, skip_first_screenshot=True): - """ - Pages: - in: page_guide, Survival_Index, nav including dungeon - out: COMBAT_PREPARE, FORGOTTEN_HALL_CHECK - """ - logger.hr('Dungeon enter', level=2) - DUNGEON_LIST.use_plane = bool(dungeon.is_Calyx_Crimson) - skip_first_load = skip_first_screenshot - - @run_once - def screenshot_interval_set(): - self.device.screenshot_interval_set('combat') - - while 1: - if skip_first_screenshot: - skip_first_screenshot = False - else: - self.device.screenshot() - - # End - if self.appear(enter_check_button): - logger.info(f'Arrive {enter_check_button.name}') - self.device.screenshot_interval_set() - break - - # Additional - # Popup that confirm character switch - if self.handle_popup_confirm(): - self.interval_reset(page_guide.check_button) - continue - - # Click teleport - if self.appear(page_guide.check_button, interval=1): - if skip_first_load: - skip_first_load = False - else: - DUNGEON_LIST.load_rows(main=self) - entrance = DUNGEON_LIST.keyword2button(dungeon) - if entrance is not None: - self.device.click(entrance) - screenshot_interval_set() - self.interval_reset(page_guide.check_button) - continue - else: - 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 - - self.acquire_lang_checked() - - ocr = OcrDungeonList(DUNGEON_COMBAT_INTERACT_TEXT) - result = ocr.detect_and_ocr(self.device.image) - - dungeon = None - # Special match names in English - # Second row must have at least 3 characters which is the shortest name "Ire" - # Stangnant Shadow: Shape of - # Quanta - if len(result) == 2 and len(result[1].ocr_text) >= 3: - first, second = result[0].ocr_text, result[1].ocr_text - if re.search(r'Stagnant\s*Shadow', first): - dungeon = DungeonList.find_dungeon_by_string(en=second, is_Stagnant_Shadow=True) - elif re.search(r'Cavern\s*of\s*Corrosion', first): - dungeon = DungeonList.find_dungeon_by_string(en=second, is_Cavern_of_Corrosion=True) - elif re.search(r'Echo\s*of\s*War', first): - dungeon = DungeonList.find_dungeon_by_string(en=second, is_Echo_of_War=True) - elif re.search(r'Calyx[\s(]+Golden', first): - dungeon = DungeonList.find_dungeon_by_string(en=second, is_Calyx_Golden=True, world=self.plane.world) - elif re.search(r'Calyx[\s(]+Crimson', first): - dungeon = DungeonList.find_dungeon_by_string(en=second, is_Calyx_Crimson=True, plane=self.plane) - if dungeon is not None: - logger.attr('DungeonInteract', dungeon) - return dungeon - - # Join - result = ' '.join([row.ocr_text for row in result]) - - # Special match names in Chinese - # Only calyxes need spacial match - if res := re.search(r'(^.+之蕾)', result): - dungeon = DungeonList.find_dungeon_by_string(cn=res.group(1), is_Calyx_Crimson=True, plane=self.plane) - if dungeon is not None: - logger.attr('DungeonInteract', dungeon) - return dungeon - dungeon = DungeonList.find_dungeon_by_string(cn=res.group(1), is_Calyx_Golden=True, world=self.plane.world) - if dungeon is not None: - logger.attr('DungeonInteract', dungeon) - return dungeon - - # 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: - bool: If success - - Pages: - in: page_guide, Survival_Index - out: COMBAT_PREPARE if success - page_guide if failed - - Examples: - from tasks.dungeon.keywords import KEYWORDS_DUNGEON_LIST - self = DungeonUI('src') - self.device.screenshot() - self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index) - self.dungeon_goto(KEYWORDS_DUNGEON_LIST.Calyx_Crimson_Harmony) - """ - # Reset search button - DUNGEON_LIST.search_button = OCR_DUNGEON_LIST - - if dungeon.is_Calyx_Crimson \ - or dungeon.is_Stagnant_Shadow \ - or dungeon.is_Cavern_of_Corrosion \ - or dungeon.is_Echo_of_War \ - or dungeon.is_Ornament_Extraction: - self._dungeon_nav_goto(dungeon.dungeon_nav) - self._dungeon_wait_until_dungeon_list_loaded() - self._dungeon_insight(dungeon) - self._dungeon_enter(dungeon) - return True - if dungeon.is_Calyx_Golden: - self._dungeon_nav_goto(dungeon.dungeon_nav) - self._dungeon_wait_until_dungeon_list_loaded() - dungeon = self._dungeon_world_set_wrapper(dungeon) - self._dungeon_wait_until_dungeon_list_loaded() - self._dungeon_insight(dungeon) - self._dungeon_enter(dungeon) - return True - - logger.error(f'Goto dungeon {dungeon} is not supported') - return False diff --git a/tasks/dungeon/ui/interact.py b/tasks/dungeon/ui/interact.py new file mode 100644 index 000000000..89be8ba23 --- /dev/null +++ b/tasks/dungeon/ui/interact.py @@ -0,0 +1,85 @@ +import re + +from module.exception import ScriptError +from module.logger import logger +from tasks.base.ui import UI +from tasks.combat.assets.assets_combat_interact import DUNGEON_COMBAT_INTERACT, DUNGEON_COMBAT_INTERACT_TEXT +from tasks.dungeon.keywords import ( + DungeonList, + DungeonNav, + KEYWORDS_DUNGEON_LIST, + KEYWORDS_DUNGEON_NAV +) +from tasks.dungeon.ui.llist import OcrDungeonName + + +class DungeonUIInteract(UI): + 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 + + self.acquire_lang_checked() + + ocr = OcrDungeonName(DUNGEON_COMBAT_INTERACT_TEXT) + result = ocr.detect_and_ocr(self.device.image) + + dungeon = None + # Special match names in English + # Second row must have at least 3 characters which is the shortest name "Ire" + # Stangnant Shadow: Shape of + # Quanta + if len(result) == 2 and len(result[1].ocr_text) >= 3: + first, second = result[0].ocr_text, result[1].ocr_text + if re.search(r'Stagnant\s*Shadow', first): + dungeon = DungeonList.find_dungeon_by_string(en=second, is_Stagnant_Shadow=True) + elif re.search(r'Cavern\s*of\s*Corrosion', first): + dungeon = DungeonList.find_dungeon_by_string(en=second, is_Cavern_of_Corrosion=True) + elif re.search(r'Echo\s*of\s*War', first): + dungeon = DungeonList.find_dungeon_by_string(en=second, is_Echo_of_War=True) + elif re.search(r'Calyx[\s(]+Golden', first): + dungeon = DungeonList.find_dungeon_by_string(en=second, is_Calyx_Golden=True, world=self.plane.world) + elif re.search(r'Calyx[\s(]+Crimson', first): + dungeon = DungeonList.find_dungeon_by_string(en=second, is_Calyx_Crimson=True, plane=self.plane) + if dungeon is not None: + logger.attr('DungeonInteract', dungeon) + return dungeon + + # Join + result = ' '.join([row.ocr_text for row in result]) + + # Special match names in Chinese + # Only calyxes need spacial match + if res := re.search(r'(^.+之蕾)', result): + dungeon = DungeonList.find_dungeon_by_string(cn=res.group(1), is_Calyx_Crimson=True, plane=self.plane) + if dungeon is not None: + logger.attr('DungeonInteract', dungeon) + return dungeon + dungeon = DungeonList.find_dungeon_by_string(cn=res.group(1), is_Calyx_Golden=True, world=self.plane.world) + if dungeon is not None: + logger.attr('DungeonInteract', dungeon) + return dungeon + + # 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 diff --git a/tasks/dungeon/ui/llist.py b/tasks/dungeon/ui/llist.py new file mode 100644 index 000000000..b47671c16 --- /dev/null +++ b/tasks/dungeon/ui/llist.py @@ -0,0 +1,385 @@ +import re + +import cv2 +from pponnxcr.predict_system import BoxedResult + +from module.base.base import ModuleBase +from module.base.decorator import run_once +from module.base.timer import Timer +from module.base.utils import area_center, area_offset, crop, image_size +from module.logger import logger +from module.ocr.ocr import Ocr, OcrResultButton +from module.ocr.utils import split_and_pair_button_attr, split_and_pair_buttons +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_prepare import COMBAT_PREPARE +from tasks.dungeon.assets.assets_dungeon_ui_list import * +from tasks.dungeon.keywords import ( + DungeonList, + KEYWORDS_DUNGEON_ENTRANCE, + KEYWORDS_DUNGEON_LIST +) +from tasks.dungeon.keywords.classes import DungeonEntrance +from tasks.map.keywords import MapPlane + +LIST_SORTING = Switch('DUNGEON_LIST_SORTING', is_selector=True) +LIST_SORTING.add_state('Ascending', check_button=LIST_ASCENDING) +LIST_SORTING.add_state('Descending', check_button=LIST_DESCENDING) +LIST_SORTING.Ascending = 'Ascending' +LIST_SORTING.Descending = 'Descending' + + +class OcrDungeonName(Ocr): + def after_process(self, result): + # 乙太之蕾•雅利洛-Ⅵ + result = re.sub(r'-[VⅤ][IⅠ]', '-Ⅵ', result) + + # 苏乐达™热砂海选会场 + result = re.sub(r'(苏乐达|蘇樂達|SoulGlad|スラーダ|FelizAlma)[rtT]*M', r'\1', result) + + result = super().after_process(result) + + if self.lang == 'cn': + result = result.replace('翼', '巽') # 巽风之形 + result = result.replace('皖A0', '50').replace('皖', '') + # 燔灼之形•凝滞虚影 + result = result.replace('熠', '燔') + result = re.sub('^灼之形', '燔灼之形', result) + # 偃偶之形•凝滞虚影 + result = re.sub('^偶之形', '偃偶之形', result) + # 嗔怒之形•凝滞虚影 + result = re.sub('^怒之形', '嗔怒之形', result) + # 蛀星的旧·历战余响 + result = re.sub(r'蛀星的旧.*?历战', '蛀星的旧靥•历战', result) + + # 9支援仓段 + for word in 'Q9α': + result = result.removeprefix(word) + return result + + +class OcrDungeonList(OcrDungeonName): + # Keep __init__ parameter unused + def __init__(self, button: ButtonWrapper = None, lang=None, name=None): + super().__init__(button=button, lang=lang, name='OcrDungeonList') + self.limit_entrance = False + + def detect_and_ocr(self, image, direct_ocr=False) -> list[BoxedResult]: + if self.button != OCR_DUNGEON_NAME: + return super().detect_and_ocr(image, direct_ocr=direct_ocr) + + # Concat OCR_DUNGEON_NAME and OCR_DUNGEON_TELEPORT + # so they can be OCRed at one time + left = crop(image, OCR_DUNGEON_NAME.area, copy=False) + right = crop(image, OCR_DUNGEON_TELEPORT.area, copy=False) + lw, lh = image_size(left) + rw, rh = image_size(right) + if lh != rh: + logger.error('OCR_DUNGEON_NAME and OCR_DUNGEON_TELEPORT does not have same height, image cannot concat') + image = cv2.hconcat([left, right]) + + if self.limit_entrance: + w, h = image_size(image) + image = crop(image, (0, 0, w, h - 70), copy=False) + + results = super().detect_and_ocr(image, direct_ocr=True) + + # Move box + for result in results: + x, _ = area_center(result.box) + # Belongs to right image + if x >= lw: + result.box = area_offset(result.box, offset=(-lw, 0)) + result.box = area_offset(result.box, offset=OCR_DUNGEON_TELEPORT.area[:2]) + # Belongs to left image + else: + result.box = area_offset(result.box, offset=OCR_DUNGEON_NAME.area[:2]) + + return results + + +class OcrDungeonListUsingPlane(OcrDungeonList): + def _match_result(self, *args, **kwargs): + """ + Convert MapPlane object to their corresponding DungeonList object + """ + plane = super()._match_result(*args, **kwargs) + if plane is not None: + for dungeon in DungeonList.instances.values(): + if dungeon.is_Calyx_Golden and dungeon.plane == plane: + return dungeon + return plane + + +class OcrDungeonListLimitEntrance(OcrDungeonList): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.limit_entrance = True + + +class OcrDungeonListUsingPlaneLimitEntrance(OcrDungeonListUsingPlane, OcrDungeonListLimitEntrance): + pass + + +class DraggableDungeonList(DraggableList): + teleports: list[OcrResultButton] = [] + navigates: list[OcrResultButton] = [] + + # use_plane: True to use map planes to predict dungeons only. + # Can only be True in Calyx Crimson + use_plane = False + # limit_entrance: True to ensure the teleport button is insight + limit_entrance = False + + def load_rows(self, main: ModuleBase, allow_early_access=False): + """ + Args: + main: + allow_early_access: True to allow dungeons that are in temporarily early access during events + """ + relative_area = (0, 0, 1280, 120) + if self.use_plane: + self.keyword_class = [MapPlane, DungeonEntrance] + if self.limit_entrance: + self.ocr_class = OcrDungeonListUsingPlaneLimitEntrance + else: + self.ocr_class = OcrDungeonListUsingPlane + else: + self.keyword_class = [DungeonList, DungeonEntrance] + if self.limit_entrance: + self.ocr_class = OcrDungeonListLimitEntrance + else: + self.ocr_class = OcrDungeonList + super().load_rows(main=main) + + # Check early access dungeons + buttons = DUNGEON_LIST.cur_buttons.copy() + for name, button in split_and_pair_buttons( + DUNGEON_LIST.cur_buttons, + split_func=lambda x: x != KEYWORDS_DUNGEON_ENTRANCE.Enter, + relative_area=relative_area + ): + logger.warning(f'Early access dungeon: {name}') + buttons.remove(name) + buttons.remove(button) + + # Remove early access dungeons + if not allow_early_access: + DUNGEON_LIST.cur_buttons = buttons + # From super.load_rows(), re-calculate indexes + indexes = [self.keyword2index(row.matched_keyword) + for row in self.cur_buttons] + indexes = [index for index in indexes if index] + + if not indexes: + logger.warning(f'No valid rows loaded into {self}') + return + + self.cur_min = min(indexes) + self.cur_max = max(indexes) + logger.attr(self.name, f'{self.cur_min} - {self.cur_max}') + + # Replace dungeon.button with teleport + self.teleports = list(split_and_pair_button_attr( + self.cur_buttons, + split_func=lambda x: x != KEYWORDS_DUNGEON_ENTRANCE.Teleport and x != KEYWORDS_DUNGEON_ENTRANCE.Enter, + relative_area=relative_area + )) + self.navigates = list(split_and_pair_button_attr( + self.cur_buttons, + split_func=lambda x: x != KEYWORDS_DUNGEON_ENTRANCE.Navigate, + relative_area=relative_area + )) + + +DUNGEON_LIST = DraggableDungeonList( + 'DungeonList', keyword_class=[DungeonList, DungeonEntrance, MapPlane], + ocr_class=OcrDungeonList, search_button=OCR_DUNGEON_NAME) + + +class DungeonUIList(UI): + def _dungeon_list_reset(self): + """ + Reset list to top + + Returns: + bool: If success + """ + logger.info('Dungeon list reset') + current = LIST_SORTING.get(main=self) + if current == LIST_SORTING.Descending: + another = LIST_SORTING.Ascending + elif current == LIST_SORTING.Ascending: + another = LIST_SORTING.Descending + else: + logger.warning('Unknown dungeon LIST_SORTING') + return False + + LIST_SORTING.set(another, main=self) + LIST_SORTING.set(current, main=self) + return True + + def _dungeon_insight_index(self, dungeon: DungeonList): + """ + Insight a dungeon using pre-defined dungeon indexes from DUNGEON_LIST + + Pages: + in: page_guide, Survival_Index, nav including dungeon + out: page_guide, Survival_Index, nav including dungeon, dungeon insight + """ + logger.hr('Dungeon insight (index)', level=2) + if dungeon.is_Ornament_Extraction: + # Limit drag area in iOrnament_Extraction + DUNGEON_LIST.search_button = OCR_DUNGEON_NAME_ROGUE + elif dungeon.is_Echo_of_War: + DUNGEON_LIST.search_button = OCR_DUNGEON_LIST + else: + DUNGEON_LIST.search_button = OCR_DUNGEON_NAME + # Predict dungeon by plane name in calyxes where dungeons share the same names + DUNGEON_LIST.use_plane = bool(dungeon.is_Calyx) + DUNGEON_LIST.check_row_order = True + + # Insight dungeon + DUNGEON_LIST.insight_row(dungeon, main=self) + self.device.click_record_clear() + # Check if dungeon unlocked + for entrance in DUNGEON_LIST.navigates: + entrance: OcrResultButton = entrance + logger.warning(f'Teleport {entrance.matched_keyword} is not unlocked') + if entrance == dungeon: + logger.error(f'Trying to enter dungeon {dungeon}, but teleport is not unlocked') + return False + + # Find teleport button + if dungeon not in [tp.matched_keyword for tp in DUNGEON_LIST.teleports]: + # Dungeon name is insight but teleport button is not + logger.info('Dungeon name is insight, swipe down a little bit to find the teleport button') + if dungeon.is_Forgotten_Hall: + DUNGEON_LIST.drag_vector = (-0.4, -0.2) # Keyword loaded is reversed + else: + DUNGEON_LIST.drag_vector = (0.2, 0.4) + DUNGEON_LIST.limit_entrance = True + DUNGEON_LIST.insight_row(dungeon, main=self) + self.device.click_record_clear() + DUNGEON_LIST.drag_vector = DraggableList.drag_vector + DUNGEON_LIST.limit_entrance = False + DUNGEON_LIST.load_rows(main=self) + # Check if dungeon unlocked + for entrance in DUNGEON_LIST.navigates: + if entrance.matched_keyword == dungeon: + logger.error(f'Trying to enter dungeon {dungeon}, but teleport is not unlocked') + return False + + return True + + def _dungeon_insight_sort(self, dungeon: DungeonList): + """ + Insight a dungeon using sorter and plain drag, reset list on error + """ + logger.hr('Dungeon insight (sort)', level=2) + logger.info(f'Dungeon insight: {dungeon}') + DUNGEON_LIST.search_button = OCR_DUNGEON_NAME + DUNGEON_LIST.use_plane = bool(dungeon.is_Calyx_Golden) + DUNGEON_LIST.check_row_order = False + + for _ in range(3): + visited = set() + end_count = 0 + self.device.click_record_clear() + while 1: + visited_count = len(visited) + # Load + DUNGEON_LIST.load_rows(main=self, allow_early_access=True) + for entrance in DUNGEON_LIST.teleports: + if entrance.matched_keyword == dungeon: + logger.info(f'Found dungeon {dungeon}') + return True + for entrance in DUNGEON_LIST.navigates: + if entrance.matched_keyword == dungeon: + logger.error(f'Trying to enter dungeon {dungeon}, but teleport is not unlocked') + return False + + # Check end + for entrance in DUNGEON_LIST.cur_buttons: + visited.add(entrance.matched_keyword.name) + if len(visited) <= visited_count: + logger.warning('No more rows loaded') + end_count += 1 + if end_count >= 3: + logger.error('Dungeon list reached end but target dungeon not found') + break + + # Drag down + DUNGEON_LIST.drag_page('down', main=self) + self.wait_until_stable(DUNGEON_LIST.search_button, timer=Timer( + 0, count=0), timeout=Timer(1.5, count=5)) + + self._dungeon_list_reset() + + logger.error('Failed to insight dungeon after 3 trial') + return False + + def dungeon_insight(self, dungeon: DungeonList): + """ + Insight a dungeon + + Pages: + in: page_guide, Survival_Index, nav including dungeon + out: page_guide, Survival_Index, nav including dungeon, dungeon insight + """ + if dungeon.is_Calyx_Crimson or dungeon.is_Stagnant_Shadow: + # Having dungeon sorting and early access + self._dungeon_insight_sort(dungeon) + else: + self._dungeon_insight_index(dungeon) + + def _dungeon_enter(self, dungeon, enter_check_button=COMBAT_PREPARE, skip_first_screenshot=True): + """ + Pages: + in: page_guide, Survival_Index, nav including dungeon + out: COMBAT_PREPARE, FORGOTTEN_HALL_CHECK + """ + logger.hr('Dungeon enter', level=2) + DUNGEON_LIST.use_plane = bool(dungeon.is_Calyx_Crimson) + skip_first_load = skip_first_screenshot + + @run_once + def screenshot_interval_set(): + self.device.screenshot_interval_set('combat') + + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # End + if self.appear(enter_check_button): + logger.info(f'Arrive {enter_check_button.name}') + break + + # Additional + # Popup that confirm character switch + if self.handle_popup_confirm(): + self.interval_reset(page_guide.check_button) + continue + + # Click teleport + if self.appear(page_guide.check_button, interval=1): + if skip_first_load: + skip_first_load = False + else: + DUNGEON_LIST.load_rows(main=self) + entrance = DUNGEON_LIST.keyword2button(dungeon) + if entrance is not None: + self.device.click(entrance) + screenshot_interval_set() + self.interval_reset(page_guide.check_button) + continue + else: + logger.warning(f'Cannot find dungeon entrance of {dungeon}') + continue + + self.device.screenshot_interval_set() diff --git a/tasks/dungeon/ui/nav.py b/tasks/dungeon/ui/nav.py new file mode 100644 index 000000000..c173bb883 --- /dev/null +++ b/tasks/dungeon/ui/nav.py @@ -0,0 +1,351 @@ +import cv2 +import numpy as np + +from module.base.base import ModuleBase +from module.base.timer import Timer +from module.base.utils import get_color +from module.logger import logger +from module.ocr.ocr import Ocr +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.dungeon.assets.assets_dungeon_ui import * +from tasks.dungeon.assets.assets_dungeon_ui_rogue import * +from tasks.dungeon.keywords import ( + DungeonNav, + DungeonTab, + KEYWORDS_DUNGEON_NAV, + KEYWORDS_DUNGEON_TAB +) +from tasks.map.interact.aim import inrange + + +class DungeonTabSwitch(Switch): + SEARCH_BUTTON = TAB_SEARCH + + def add_state(self, state, check_button, click_button=None): + # Load search + if check_button is not None: + check_button.load_search(self.__class__.SEARCH_BUTTON.area) + if click_button is not None: + click_button.load_search(self.__class__.SEARCH_BUTTON.area) + return super().add_state(state, check_button, click_button) + + def click(self, state, main): + """ + Args: + state (str): + main (ModuleBase): + """ + button = self.get_data(state)['click_button'] + _ = main.appear(button) # Search button to load offset + main.device.click(button) + + +SWITCH_DUNGEON_TAB = DungeonTabSwitch('DungeonTab', is_selector=True) +SWITCH_DUNGEON_TAB.add_state( + KEYWORDS_DUNGEON_TAB.Operation_Briefing, + check_button=OPERATION_BRIEFING_CHECK, + click_button=OPERATION_BRIEFING_CLICK +) +SWITCH_DUNGEON_TAB.add_state( + KEYWORDS_DUNGEON_TAB.Daily_Training, + check_button=DAILY_TRAINING_CHECK, + click_button=DAILY_TRAINING_CLICK +) +SWITCH_DUNGEON_TAB.add_state( + KEYWORDS_DUNGEON_TAB.Survival_Index, + check_button=SURVIVAL_INDEX_CHECK, + click_button=SURVIVAL_INDEX_CLICK +) +SWITCH_DUNGEON_TAB.add_state( + KEYWORDS_DUNGEON_TAB.Simulated_Universe, + check_button=SIMULATED_UNIVERSE_CHECK, + click_button=SIMULATED_UNIVERSE_CLICK +) +SWITCH_DUNGEON_TAB.add_state( + KEYWORDS_DUNGEON_TAB.Treasures_Lightward, + check_button=TREASURES_LIGHTWARD_CHECK, + click_button=TREASURES_LIGHTWARD_CLICK +) + + +class OcrDungeonNav(Ocr): + def after_process(self, result): + result = super().after_process(result) + result = result.replace('#', '') + if self.lang == 'cn': + result = result.replace('萼喜', '萼') + result = result.replace('带', '滞') # 凝带虚影 + return result + + +class DraggableDungeonNav(DraggableList): + # 0.5 is the magic number to reach bottom in 1 swipe + # but relax we still have retires when magic doesn't work + drag_vector = (0.50, 0.52) + + +DUNGEON_NAV_LIST = DraggableDungeonNav( + 'DungeonNavList', keyword_class=DungeonNav, ocr_class=OcrDungeonNav, search_button=OCR_DUNGEON_NAV) + + +class DungeonUINav(UI): + def dungeon_tab_goto(self, state: DungeonTab): + """ + Args: + state: + + Returns: + bool: If UI switched + + Examples: + self = DungeonUI('alas') + self.device.screenshot() + self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Operation_Briefing) + self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Daily_Training) + self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index) + """ + logger.hr('Dungeon tab goto', level=2) + ui_switched = self.ui_ensure(page_guide) + tab_switched = SWITCH_DUNGEON_TAB.set(state, main=self) + + if ui_switched or tab_switched: + if state == KEYWORDS_DUNGEON_TAB.Daily_Training: + logger.info(f'Tab goto {state}, wait until loaded') + self._dungeon_wait_daily_training_loaded() + elif state == KEYWORDS_DUNGEON_TAB.Survival_Index: + logger.info(f'Tab goto {state}, wait until loaded') + self._dungeon_wait_survival_index_loaded() + elif state == KEYWORDS_DUNGEON_TAB.Treasures_Lightward: + logger.info(f'Tab goto {state}, wait until loaded') + self._dungeon_wait_treasures_lightward_loaded() + return True + else: + return False + + def _dungeon_wait_daily_training_loaded(self, skip_first_screenshot=True): + """ + Returns: + bool: True if wait success, False if wait timeout. + + Pages: + in: page_guide, Daily_Training + """ + timeout = Timer(2, count=4).start() + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if timeout.reached(): + logger.warning('Wait daily training loaded timeout') + return False + color = get_color(self.device.image, DAILY_TRAINING_LOADED.area) + if np.mean(color) < 128: + logger.info('Daily training loaded') + return True + + def _dungeon_wait_survival_index_loaded(self, skip_first_screenshot=True): + """ + Returns: + bool: True if wait success, False if wait timeout. + + Pages: + in: page_guide, Survival_Index + """ + timeout = Timer(2, count=4).start() + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if timeout.reached(): + logger.warning('Wait survival index loaded timeout') + return False + if self.appear(SURVIVAL_INDEX_SU_LOADED): + logger.info('Survival index loaded, SURVIVAL_INDEX_SU_LOADED') + return True + if self.appear(SURVIVAL_INDEX_OE_LOADED): + logger.info('Survival index loaded, SURVIVAL_INDEX_OE_LOADED') + return True + + def _dungeon_survival_index_top_appear(self): + if self.appear(SURVIVAL_INDEX_SU_LOADED): + return True + if self.appear(SURVIVAL_INDEX_OE_LOADED): + return True + return False + + def _dungeon_wait_treasures_lightward_loaded(self, skip_first_screenshot=True): + """ + Returns: + bool: True if wait success, False if wait timeout. + + Pages: + in: page_guide, Survival_Index + """ + timeout = Timer(2, count=4).start() + TREASURES_LIGHTWARD_LOADED.set_search_offset((5, 5)) + TREASURES_LIGHTWARD_LOCKED.set_search_offset((5, 5)) + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + if timeout.reached(): + logger.warning('Wait treasures lightward loaded timeout') + return False + if self.appear(TREASURES_LIGHTWARD_LOADED): + logger.info('Treasures lightward loaded (event unlocked)') + return True + if self.appear(TREASURES_LIGHTWARD_LOCKED): + logger.info('Treasures lightward loaded (event locked)') + return True + + def _dungeon_list_button_has_content(self): + # Check if having any content + # List background: 254, guild border: 225 + r, g, b = cv2.split(self.image_crop(LIST_LOADED_CHECK, copy=False)) + minimum = cv2.min(cv2.min(r, g), b) + minimum = inrange(minimum, lower=0, upper=180) + if minimum.size > 100: + return True + else: + return False + + def _dungeon_wait_until_dungeon_list_loaded(self, skip_first_screenshot=True): + timeout = Timer(1, count=3).start() + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # End + if timeout.reached(): + logger.warning('Wait until dungeon list loaded timeout') + return False + + if self._dungeon_list_button_has_content(): + logger.info('Dungeon list loaded') + return True + + def _dungeon_wait_until_echo_or_war_stabled(self, skip_first_screenshot=True): + """ + Returns: + bool: True if wait success, False if wait timeout. + + Pages: + in: page_guide, Survival_Index + """ + # Wait until Forgotten_Hall stabled + timeout = Timer(2, count=4).start() + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # End + if timeout.reached(): + logger.warning('Wait until Echo_of_War stabled timeout') + return False + + DUNGEON_NAV_LIST.load_rows(main=self) + + # End + button = DUNGEON_NAV_LIST.keyword2button(KEYWORDS_DUNGEON_NAV.Echo_of_War, show_warning=False) + if button: + # 513 is the top of the last row of DungeonNav + if button.area[1] > 513: + logger.info('DungeonNav row Echo_of_War stabled') + return True + else: + logger.info('No Echo_of_War in list skip waiting') + return False + + def dungeon_nav_goto(self, nav: DungeonNav, skip_first_screenshot=True): + """ + Equivalent to `DUNGEON_NAV_LIST.select_row(dungeon.dungeon_nav, main=self)` + but with tricks to be faster + + Args: + nav: + skip_first_screenshot: + """ + logger.hr('Dungeon nav goto', level=2) + logger.info(f'Dungeon nav goto {nav}') + + # Wait rows + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + DUNGEON_NAV_LIST.load_rows(main=self) + if DUNGEON_NAV_LIST.cur_buttons: + break + + # Wait first row selected + timeout = Timer(0.5, count=2).start() + skip_first_screenshot = True + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + if timeout.reached(): + logger.info('DUNGEON_NAV_LIST not selected') + break + if button := DUNGEON_NAV_LIST.get_selected_row(main=self): + logger.info(f'DUNGEON_NAV_LIST selected at {button}') + break + + # Check if it's at the first page. + if DUNGEON_NAV_LIST.keyword2button(KEYWORDS_DUNGEON_NAV.Simulated_Universe, show_warning=False) \ + or DUNGEON_NAV_LIST.keyword2button(KEYWORDS_DUNGEON_NAV.Ornament_Extraction, show_warning=False): + # Going to use a faster method to navigate but can only start from list top + logger.info('DUNGEON_NAV_LIST at top') + # Update points if possible + # 2.3, No longer weekly points after Divergent Universe unlocked + # if DUNGEON_NAV_LIST.is_row_selected(button, main=self): + # self.dungeon_update_simuni() + # Treasures lightward is always at top + elif DUNGEON_NAV_LIST.keyword2button(KEYWORDS_DUNGEON_NAV.Forgotten_Hall, show_warning=False) \ + or DUNGEON_NAV_LIST.keyword2button(KEYWORDS_DUNGEON_NAV.Pure_Fiction, show_warning=False): + logger.info('DUNGEON_NAV_LIST at top') + else: + # To start from any list states. + logger.info('DUNGEON_NAV_LIST not at top') + DUNGEON_NAV_LIST.select_row(nav, main=self) + return True + + # Check the first page + if nav in [ + KEYWORDS_DUNGEON_NAV.Simulated_Universe, + KEYWORDS_DUNGEON_NAV.Divergent_Universe, + KEYWORDS_DUNGEON_NAV.Ornament_Extraction, + KEYWORDS_DUNGEON_NAV.Calyx_Golden, + KEYWORDS_DUNGEON_NAV.Calyx_Crimson, + KEYWORDS_DUNGEON_NAV.Stagnant_Shadow, + KEYWORDS_DUNGEON_NAV.Cavern_of_Corrosion, + KEYWORDS_DUNGEON_NAV.Forgotten_Hall, + KEYWORDS_DUNGEON_NAV.Pure_Fiction, + ]: + button = DUNGEON_NAV_LIST.keyword2button(nav) + if button: + DUNGEON_NAV_LIST.select_row(nav, main=self, insight=False) + return True + + # Check the second page + while 1: + DUNGEON_NAV_LIST.drag_page('down', main=self) + # No skip_first_screenshot since drag_page is just called + if self._dungeon_wait_until_echo_or_war_stabled(skip_first_screenshot=False): + DUNGEON_NAV_LIST.select_row(nav, main=self, insight=False) + return True diff --git a/tasks/dungeon/state.py b/tasks/dungeon/ui/state.py similarity index 100% rename from tasks/dungeon/state.py rename to tasks/dungeon/ui/state.py diff --git a/tasks/dungeon/ui/ui.py b/tasks/dungeon/ui/ui.py new file mode 100644 index 000000000..ad9c4042a --- /dev/null +++ b/tasks/dungeon/ui/ui.py @@ -0,0 +1,41 @@ +from module.logger import logger +from tasks.dungeon.keywords import DungeonList +from tasks.dungeon.ui.interact import DungeonUIInteract +from tasks.dungeon.ui.llist import DungeonUIList +from tasks.dungeon.ui.nav import DungeonUINav +from tasks.dungeon.ui.state import DungeonState + + +class DungeonUI(DungeonState, DungeonUINav, DungeonUIList, DungeonUIInteract): + def dungeon_goto(self, dungeon: DungeonList): + """ + Returns: + bool: If success + + Pages: + in: page_guide, Survival_Index + out: COMBAT_PREPARE if success + page_guide if failed + + Examples: + from tasks.dungeon.keywords import KEYWORDS_DUNGEON_LIST + self = DungeonUI('src') + self.device.screenshot() + self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index) + self.dungeon_goto(KEYWORDS_DUNGEON_LIST.Calyx_Crimson_Harmony) + """ + # Reset search button + if dungeon.is_Calyx_Golden \ + or dungeon.is_Calyx_Crimson \ + or dungeon.is_Stagnant_Shadow \ + or dungeon.is_Cavern_of_Corrosion \ + or dungeon.is_Echo_of_War \ + or dungeon.is_Ornament_Extraction: + self.dungeon_nav_goto(dungeon.dungeon_nav) + self._dungeon_wait_until_dungeon_list_loaded() + self.dungeon_insight(dungeon) + self._dungeon_enter(dungeon) + return True + + logger.error(f'Goto dungeon {dungeon} is not supported') + return False diff --git a/tasks/dungeon/ui_rogue.py b/tasks/dungeon/ui/ui_rogue.py similarity index 93% rename from tasks/dungeon/ui_rogue.py rename to tasks/dungeon/ui/ui_rogue.py index 1d2304115..a73968cf5 100644 --- a/tasks/dungeon/ui_rogue.py +++ b/tasks/dungeon/ui/ui_rogue.py @@ -3,9 +3,11 @@ from module.base.utils import random_rectangle_vector from module.logger import logger from tasks.base.page import page_guide from tasks.dungeon.assets.assets_dungeon_ui import * +from tasks.dungeon.assets.assets_dungeon_ui_list import OCR_DUNGEON_LIST from tasks.dungeon.assets.assets_dungeon_ui_rogue import * from tasks.dungeon.keywords import KEYWORDS_DUNGEON_NAV, KEYWORDS_DUNGEON_TAB -from tasks.dungeon.ui import DungeonUI, SWITCH_DUNGEON_TAB +from tasks.dungeon.ui.nav import SWITCH_DUNGEON_TAB +from tasks.dungeon.ui.ui import DungeonUI from tasks.forgotten_hall.assets.assets_forgotten_hall_ui import TELEPORT @@ -41,7 +43,7 @@ class DungeonRogueUI(DungeonUI): logger.info(f'Tab goto {state}, wait until loaded') self._dungeon_wait_until_rogue_loaded() # Switch nav - self._dungeon_nav_goto(KEYWORDS_DUNGEON_NAV.Simulated_Universe) + self.dungeon_nav_goto(KEYWORDS_DUNGEON_NAV.Simulated_Universe) # No idea how to wait list loaded # List is not able to swipe without fully loaded self.wait_until_stable(LIST_LOADED_CHECK) @@ -59,7 +61,7 @@ class DungeonRogueUI(DungeonUI): if self.appear(SURVIVAL_INDEX_SU_LOADED): logger.info('Already at nav Simulated_Universe') else: - self._dungeon_nav_goto(KEYWORDS_DUNGEON_NAV.Simulated_Universe) + self.dungeon_nav_goto(KEYWORDS_DUNGEON_NAV.Simulated_Universe) def _dungeon_wait_until_rogue_loaded(self, skip_first_screenshot=True): """ diff --git a/tasks/dungeon/weekly.py b/tasks/dungeon/weekly.py index 1fb29992e..616035a23 100644 --- a/tasks/dungeon/weekly.py +++ b/tasks/dungeon/weekly.py @@ -2,10 +2,9 @@ from module.config.utils import get_server_next_monday_update 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.assets.assets_dungeon_ui import OCR_WEEKLY_LIMIT from tasks.dungeon.dungeon import Dungeon from tasks.dungeon.keywords import DungeonList, KEYWORDS_DUNGEON_NAV, KEYWORDS_DUNGEON_TAB -from tasks.dungeon.ui import DUNGEON_LIST class OcrWeeklyLimit(DigitCounter): @@ -78,8 +77,7 @@ class WeeklyDungeon(Dungeon): # 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(KEYWORDS_DUNGEON_NAV.Echo_of_War) + self.dungeon_nav_goto(KEYWORDS_DUNGEON_NAV.Echo_of_War) self._dungeon_wait_until_dungeon_list_loaded() monday = get_server_next_monday_update(self.config.Scheduler_ServerUpdate) @@ -94,7 +92,7 @@ class WeeklyDungeon(Dungeon): self.config.task_delay(target=monday) self.config.task_stop() - self._dungeon_insight(dungeon) + self.dungeon_insight(dungeon) self._dungeon_enter(dungeon) # Combat diff --git a/tasks/forgotten_hall/ui.py b/tasks/forgotten_hall/ui.py index 08b1aa7a8..98dfc1a91 100644 --- a/tasks/forgotten_hall/ui.py +++ b/tasks/forgotten_hall/ui.py @@ -10,8 +10,8 @@ from module.ocr.keyword import Keyword from module.ocr.ocr import Ocr, OcrResultButton from module.ui.draggable_list import DraggableList from tasks.base.assets.assets_base_page import FORGOTTEN_HALL_CHECK, MAP_EXIT +from tasks.dungeon.ui.ui import DungeonUI from tasks.dungeon.keywords import DungeonList, KEYWORDS_DUNGEON_LIST, KEYWORDS_DUNGEON_NAV, KEYWORDS_DUNGEON_TAB -from tasks.dungeon.ui import DungeonUI from tasks.forgotten_hall.assets.assets_forgotten_hall_nav import * from tasks.forgotten_hall.assets.assets_forgotten_hall_ui import * from tasks.forgotten_hall.keywords import ForgottenHallStage, KEYWORDS_FORGOTTEN_HALL_STAGE @@ -187,7 +187,7 @@ class ForgottenHallUI(DungeonUI, ForgottenHallTeam): logger.info('Already in forgotten hall') else: self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Treasures_Lightward) - self._dungeon_nav_goto(KEYWORDS_DUNGEON_NAV.Forgotten_Hall) + self.dungeon_nav_goto(KEYWORDS_DUNGEON_NAV.Forgotten_Hall) self.stage_choose(dungeon) logger.info(f'Stage list select: {stage_keyword}') diff --git a/tasks/ornament/combat.py b/tasks/ornament/combat.py index c1c6c730c..5d880ea2d 100644 --- a/tasks/ornament/combat.py +++ b/tasks/ornament/combat.py @@ -6,7 +6,7 @@ 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.dungeon.dungeon import Dungeon -from tasks.dungeon.state import DungeonState +from tasks.dungeon.ui.state import DungeonState from tasks.map.route.loader import RouteLoader from tasks.map.route.route.daily import OrnamentExtraction__route from tasks.ornament.assets.assets_ornament_combat import * diff --git a/tasks/rogue/entry/entry.py b/tasks/rogue/entry/entry.py index 6a94b51b9..f18019b24 100644 --- a/tasks/rogue/entry/entry.py +++ b/tasks/rogue/entry/entry.py @@ -14,8 +14,8 @@ from tasks.base.assets.assets_base_page import MAP_EXIT from tasks.base.page import page_guide, page_item, page_main, page_rogue from tasks.dungeon.keywords import DungeonList from tasks.dungeon.keywords.dungeon import Simulated_Universe_World_1 -from tasks.dungeon.state import OcrSimUniPoint -from tasks.dungeon.ui_rogue import DungeonRogueUI +from tasks.dungeon.ui.state import OcrSimUniPoint +from tasks.dungeon.ui.ui_rogue import DungeonRogueUI from tasks.forgotten_hall.assets.assets_forgotten_hall_ui import TELEPORT from tasks.rogue.assets.assets_rogue_entry import ( LEVEL_CONFIRM, diff --git a/tasks/rogue/event/reward.py b/tasks/rogue/event/reward.py index a280c24ad..b7937005f 100644 --- a/tasks/rogue/event/reward.py +++ b/tasks/rogue/event/reward.py @@ -4,7 +4,7 @@ from module.base.timer import Timer from module.logger import logger from tasks.base.assets.assets_base_popup import GET_REWARD from tasks.combat.interact import CombatInteract -from tasks.dungeon.state import DungeonState +from tasks.dungeon.ui.state import DungeonState from tasks.rogue.assets.assets_rogue_reward import REWARD_CLOSE, USE_IMMERSIFIER, USE_STAMINA from tasks.rogue.blessing.ui import RogueUI