Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 29 KiB |
BIN
assets/share/forgotten_hall/team/CHARACTER_1.BUTTON.png
Normal file
After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 9.5 KiB |
BIN
assets/share/forgotten_hall/team/CHARACTER_2.BUTTON.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
assets/share/forgotten_hall/team/CHARACTER_2.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
assets/share/forgotten_hall/team/CHARACTER_3.BUTTON.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
assets/share/forgotten_hall/team/CHARACTER_3.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
assets/share/forgotten_hall/team/CHARACTER_4.BUTTON.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
assets/share/forgotten_hall/team/CHARACTER_4.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
BIN
assets/share/forgotten_hall/ui/LAST_VASTIGES_CHECK.SEARCH.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
assets/share/forgotten_hall/ui/LAST_VASTIGES_CHECK.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
assets/share/forgotten_hall/ui/LAST_VASTIGES_CLICK.SEARCH.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
assets/share/forgotten_hall/ui/LAST_VASTIGES_CLICK.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
assets/share/forgotten_hall/ui/MEMORY_OF_CHAOS_CHECK.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
assets/share/forgotten_hall/ui/MEMORY_OF_CHAOS_CLICK.png
Normal file
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 250 KiB |
BIN
assets/share/forgotten_hall/ui/TELEPORT.SEARCH.png
Normal file
After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.0 KiB |
@ -35,7 +35,7 @@
|
||||
},
|
||||
"Dungeon": {
|
||||
"Scheduler": {
|
||||
"Enable": false,
|
||||
"Enable": true,
|
||||
"NextRun": "2020-01-01 00:00:00",
|
||||
"Command": "Dungeon",
|
||||
"ServerUpdate": "04:00"
|
||||
@ -64,29 +64,29 @@
|
||||
},
|
||||
"DailyQuest": {
|
||||
"Scheduler": {
|
||||
"Enable": false,
|
||||
"Enable": true,
|
||||
"NextRun": "2020-01-01 00:00:00",
|
||||
"Command": "DailyQuest",
|
||||
"ServerUpdate": "04:00"
|
||||
},
|
||||
"AchievableQuest": {
|
||||
"Complete_1_Daily_Mission": "not_supported",
|
||||
"Clear_Calyx_Golden_1_times": "not_set",
|
||||
"Complete_Calyx_Crimson_1_time": "not_set",
|
||||
"Clear_Stagnant_Shadow_1_times": "not_set",
|
||||
"Clear_Cavern_of_Corrosion_1_times": "not_set",
|
||||
"Clear_Calyx_Golden_1_times": "achievable",
|
||||
"Complete_Calyx_Crimson_1_time": "achievable",
|
||||
"Clear_Stagnant_Shadow_1_times": "achievable",
|
||||
"Clear_Cavern_of_Corrosion_1_times": "achievable",
|
||||
"In_a_single_battle_inflict_3_Weakness_Break_of_different_Types": "not_supported",
|
||||
"Inflict_Weakness_Break_5_times": "not_supported",
|
||||
"Defeat_a_total_of_20_enemies": "not_supported",
|
||||
"Enter_combat_by_attacking_enemy_Weakness_and_win_3_times": "not_supported",
|
||||
"Use_Technique_2_times": "achievable",
|
||||
"Go_on_assignment_1_time": "not_set",
|
||||
"Go_on_assignment_1_time": "achievable",
|
||||
"Take_1_photo": "achievable",
|
||||
"Destroy_3_destructible_objects": "not_supported",
|
||||
"Complete_Forgotten_Hall_1_time": "not_supported",
|
||||
"Complete_Forgotten_Hall_1_time": "achievable",
|
||||
"Complete_Echo_of_War_1_times": "not_supported",
|
||||
"Complete_1_stage_in_Simulated_Universe_Any_world": "not_supported",
|
||||
"Obtain_victory_in_combat_with_support_characters_1_time": "not_set",
|
||||
"Obtain_victory_in_combat_with_support_characters_1_time": "achievable",
|
||||
"Use_an_Ultimate_to_deal_the_final_blow_1_time": "not_supported",
|
||||
"Level_up_any_character_1_time": "not_supported",
|
||||
"Level_up_any_Light_Cone_1_time": "not_supported",
|
||||
@ -103,7 +103,7 @@
|
||||
},
|
||||
"BattlePass": {
|
||||
"Scheduler": {
|
||||
"Enable": false,
|
||||
"Enable": true,
|
||||
"NextRun": "2020-01-01 00:00:00",
|
||||
"Command": "BattlePass",
|
||||
"ServerUpdate": "04:00"
|
||||
@ -115,7 +115,7 @@
|
||||
},
|
||||
"Assignment": {
|
||||
"Scheduler": {
|
||||
"Enable": false,
|
||||
"Enable": true,
|
||||
"NextRun": "2020-01-01 00:00:00",
|
||||
"Command": "Assignment",
|
||||
"ServerUpdate": "04:00"
|
||||
|
@ -112,7 +112,7 @@ class KeywordExtract:
|
||||
|
||||
def iter_guide(self) -> t.Iterable[int]:
|
||||
file = os.path.join(TextMap.DATA_FOLDER, './ExcelOutput/GameplayGuideData.json')
|
||||
visited = set()
|
||||
# visited = set()
|
||||
temp_save = ""
|
||||
for data in read_file(file).values():
|
||||
hash_ = deep_get(data, keys='Name.Hash')
|
||||
@ -121,11 +121,14 @@ class KeywordExtract:
|
||||
temp_save = hash_
|
||||
continue
|
||||
if '忘却之庭' in name:
|
||||
if name in visited:
|
||||
continue
|
||||
visited.add(name)
|
||||
continue
|
||||
# if name in visited:
|
||||
# continue
|
||||
# visited.add(name)
|
||||
yield hash_
|
||||
yield temp_save
|
||||
# 'Memory of Chaos' is not a real dungeon, but represents a group
|
||||
yield '混沌回忆'
|
||||
|
||||
def find_keyword(self, keyword, lang) -> tuple[int, str]:
|
||||
"""
|
||||
|
@ -20,8 +20,22 @@ class Filter:
|
||||
self.filter = []
|
||||
|
||||
def load(self, string):
|
||||
"""
|
||||
Load a filter string, filters are connected with ">"
|
||||
|
||||
There are also tons of unicode characters similar to ">"
|
||||
> \u003E correct
|
||||
> \uFF1E
|
||||
﹥ \uFE65
|
||||
› \u203a
|
||||
˃ \u02c3
|
||||
ᐳ \u1433
|
||||
❯ \u276F
|
||||
"""
|
||||
string = str(string)
|
||||
self.filter_raw = [f.strip(' \t\r\n') for f in string.split('>')]
|
||||
string = re.sub(r'[ \t\r\n]', '', string)
|
||||
string = re.sub(r'[>﹥›˃ᐳ❯]', '>', string)
|
||||
self.filter_raw = string.split('>')
|
||||
self.filter = [self.parse_filter(f) for f in self.filter_raw]
|
||||
|
||||
def is_preset(self, filter):
|
||||
|
@ -166,7 +166,7 @@
|
||||
"Scheduler": {
|
||||
"Enable": {
|
||||
"type": "checkbox",
|
||||
"value": false,
|
||||
"value": true,
|
||||
"option": [
|
||||
true,
|
||||
false
|
||||
@ -407,7 +407,7 @@
|
||||
"Scheduler": {
|
||||
"Enable": {
|
||||
"type": "checkbox",
|
||||
"value": false,
|
||||
"value": true,
|
||||
"option": [
|
||||
true,
|
||||
false
|
||||
@ -827,7 +827,7 @@
|
||||
"Scheduler": {
|
||||
"Enable": {
|
||||
"type": "checkbox",
|
||||
"value": false,
|
||||
"value": true,
|
||||
"option": [
|
||||
true,
|
||||
false
|
||||
@ -870,7 +870,7 @@
|
||||
"Scheduler": {
|
||||
"Enable": {
|
||||
"type": "checkbox",
|
||||
"value": false,
|
||||
"value": true,
|
||||
"option": [
|
||||
true,
|
||||
false
|
||||
|
@ -5,10 +5,17 @@
|
||||
# ==================== Alas ====================
|
||||
|
||||
|
||||
# ==================== Farm ====================
|
||||
|
||||
|
||||
# ==================== Daily ====================
|
||||
|
||||
|
||||
# ==================== Tools ====================
|
||||
Dungeon:
|
||||
Scheduler:
|
||||
Enable: true
|
||||
DailyQuest:
|
||||
Scheduler:
|
||||
Enable: true
|
||||
BattlePass:
|
||||
Scheduler:
|
||||
Enable: true
|
||||
Assignment:
|
||||
Scheduler:
|
||||
Enable: true
|
||||
|
@ -10,7 +10,7 @@ class ManualConfig:
|
||||
|
||||
SCHEDULER_PRIORITY = """
|
||||
Restart
|
||||
> BattlePass > DailyQuest > Dungeon > Assignment
|
||||
> BattlePass > DailyQuest > Assignment > Dungeon
|
||||
"""
|
||||
|
||||
"""
|
||||
|
@ -677,7 +677,7 @@ class ConfigUpdater:
|
||||
set_daily('Go_on_assignment_1_time', deep_get(data, 'Assignment.Scheduler.Enable'))
|
||||
set_daily('Take_1_photo', 'achievable')
|
||||
set_daily('Destroy_3_destructible_objects', 'not_supported')
|
||||
set_daily('Complete_Forgotten_Hall_1_time', 'not_supported')
|
||||
set_daily('Complete_Forgotten_Hall_1_time', 'achievable')
|
||||
set_daily('Complete_Echo_of_War_1_times', 'not_supported')
|
||||
set_daily('Complete_1_stage_in_Simulated_Universe_Any_world', 'not_supported')
|
||||
set_daily('Obtain_victory_in_combat_with_support_characters_1_time',
|
||||
|
@ -50,7 +50,7 @@
|
||||
},
|
||||
"Enable": {
|
||||
"name": "Enable Task",
|
||||
"help": "Join this task to scheduler.\nTask commission, research, reward are force to enable.",
|
||||
"help": "Join this task to scheduler.",
|
||||
"True": "Enabled",
|
||||
"False": "False"
|
||||
},
|
||||
@ -493,7 +493,7 @@
|
||||
},
|
||||
"Complete_Forgotten_Hall_1_time": {
|
||||
"name": "Complete Forgotten Hall 1 time",
|
||||
"help": "",
|
||||
"help": "Choose the first four characters to do stage 1 once, please ensure that account build is sufficient to do stage 1",
|
||||
"achievable": "Achievable",
|
||||
"not_set": "Not Set",
|
||||
"not_supported": "Not Supported Yet"
|
||||
|
@ -493,7 +493,7 @@
|
||||
},
|
||||
"Complete_Forgotten_Hall_1_time": {
|
||||
"name": "完成1次「忘却之庭」",
|
||||
"help": "",
|
||||
"help": "选前四个角色打一次深渊一,请保证帐号练度足够打深渊一",
|
||||
"achievable": "可完成",
|
||||
"not_set": "未设置",
|
||||
"not_supported": "暂未支持"
|
||||
|
@ -50,7 +50,7 @@
|
||||
},
|
||||
"Enable": {
|
||||
"name": "啟用該功能",
|
||||
"help": "將這個任務加入調度器\n委託、科研、收穫任務是強制打開的",
|
||||
"help": "將這個任務加入調度器",
|
||||
"True": "已啟用",
|
||||
"False": "False"
|
||||
},
|
||||
@ -493,7 +493,7 @@
|
||||
},
|
||||
"Complete_Forgotten_Hall_1_time": {
|
||||
"name": "完成1次「忘卻之庭」",
|
||||
"help": "",
|
||||
"help": "選前四個角色打一次深淵一,請確保帳號練度足夠打深淵一",
|
||||
"achievable": "可完成",
|
||||
"not_set": "未設定",
|
||||
"not_supported": "暫未支援"
|
||||
|
35
route/daily/forgotten_hall/stage_1.py
Normal file
@ -0,0 +1,35 @@
|
||||
from module.logger import logger
|
||||
from tasks.base.assets.assets_base_popup import POPUP_SINGLE
|
||||
from tasks.dungeon.keywords import KEYWORDS_DUNGEON_LIST
|
||||
from tasks.forgotten_hall.keywords import KEYWORDS_FORGOTTEN_HALL_STAGE
|
||||
from tasks.forgotten_hall.ui import ForgottenHallUI
|
||||
from tasks.map.control.waypoint import Waypoint
|
||||
from tasks.map.keywords.plane import Jarilo_BackwaterPass
|
||||
from tasks.map.route.base import RouteBase
|
||||
|
||||
|
||||
class Route(RouteBase, ForgottenHallUI):
|
||||
def combat_execute(self, expected_end=None):
|
||||
# Challenge completed, return button appears
|
||||
def combat_ended():
|
||||
return self.appear(POPUP_SINGLE)
|
||||
|
||||
return super().combat_execute(expected_end=combat_ended)
|
||||
|
||||
def route(self):
|
||||
"""
|
||||
Pages:
|
||||
in: Any
|
||||
out: page_forgotten_hall
|
||||
"""
|
||||
logger.hr('Forgotten hall stage 1')
|
||||
self.stage_goto(KEYWORDS_DUNGEON_LIST.The_Last_Vestiges_of_Towering_Citadel,
|
||||
KEYWORDS_FORGOTTEN_HALL_STAGE.Stage_1)
|
||||
self.team_choose_first_4()
|
||||
self.enter_forgotten_hall_dungeon()
|
||||
|
||||
self.map_init(plane=Jarilo_BackwaterPass, position=(369.4, 643.4))
|
||||
self.clear_enemy(
|
||||
Waypoint((313.4, 643.4)).run_2x()
|
||||
)
|
||||
self.exit_dungeon()
|
@ -253,6 +253,16 @@ MAP_CHECK = ButtonWrapper(
|
||||
button=(46, 19, 68, 54),
|
||||
),
|
||||
)
|
||||
MAP_EXIT = ButtonWrapper(
|
||||
name='MAP_EXIT',
|
||||
share=Button(
|
||||
file='./assets/share/base/page/MAP_EXIT.png',
|
||||
area=(27, 46, 44, 74),
|
||||
search=(7, 26, 64, 94),
|
||||
color=(142, 144, 148),
|
||||
button=(27, 46, 44, 74),
|
||||
),
|
||||
)
|
||||
MAP_GOTO_WORLD = ButtonWrapper(
|
||||
name='MAP_GOTO_WORLD',
|
||||
share=Button(
|
||||
|
@ -48,7 +48,7 @@ POPUP_CANCEL = ButtonWrapper(
|
||||
share=Button(
|
||||
file='./assets/share/base/popup/POPUP_CANCEL.png',
|
||||
area=(428, 537, 452, 561),
|
||||
search=(334, 453, 594, 653),
|
||||
search=(339, 446, 482, 653),
|
||||
color=(98, 92, 80),
|
||||
button=(419, 531, 512, 567),
|
||||
),
|
||||
@ -58,7 +58,7 @@ POPUP_CONFIRM = ButtonWrapper(
|
||||
share=Button(
|
||||
file='./assets/share/base/popup/POPUP_CONFIRM.png',
|
||||
area=(776, 537, 800, 561),
|
||||
search=(683, 464, 943, 644),
|
||||
search=(686, 446, 829, 644),
|
||||
color=(95, 90, 78),
|
||||
button=(767, 531, 862, 567),
|
||||
),
|
||||
@ -68,7 +68,7 @@ POPUP_SINGLE = ButtonWrapper(
|
||||
share=Button(
|
||||
file='./assets/share/base/popup/POPUP_SINGLE.png',
|
||||
area=(602, 458, 626, 482),
|
||||
search=(511, 365, 771, 575),
|
||||
search=(512, 446, 655, 644),
|
||||
color=(95, 90, 77),
|
||||
button=(578, 451, 705, 489),
|
||||
),
|
||||
|
@ -4,7 +4,7 @@ from module.base.timer import Timer
|
||||
from module.exception import GameNotRunningError, GamePageUnknownError
|
||||
from module.logger import logger
|
||||
from module.ocr.ocr import Ocr
|
||||
from tasks.base.assets.assets_base_page import CLOSE
|
||||
from tasks.base.assets.assets_base_page import CLOSE, MAP_EXIT
|
||||
from tasks.base.main_page import MainPage
|
||||
from tasks.base.page import Page, page_main
|
||||
from tasks.base.popup import PopupHandler
|
||||
@ -271,7 +271,7 @@ class UI(PopupHandler, MainPage):
|
||||
continue
|
||||
|
||||
def is_in_main(self):
|
||||
return self.appear(page_main.check_button)
|
||||
return self.appear(page_main.check_button) or self.appear(MAP_EXIT)
|
||||
|
||||
def ui_goto_main(self):
|
||||
return self.ui_ensure(destination=page_main)
|
||||
|
@ -118,8 +118,11 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo
|
||||
if self.handle_ascension_dungeon_prepare():
|
||||
continue
|
||||
|
||||
def combat_execute(self):
|
||||
def combat_execute(self, expected_end=None):
|
||||
"""
|
||||
Args:
|
||||
expected_end: A function returns bool, True represents end.
|
||||
|
||||
Pages:
|
||||
in: is_combat_executing
|
||||
out: COMBAT_AGAIN
|
||||
@ -135,7 +138,14 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo
|
||||
self.device.screenshot()
|
||||
|
||||
# End
|
||||
if callable(expected_end) and expected_end():
|
||||
logger.info(f'Combat execute ended at {expected_end.__name__}')
|
||||
break
|
||||
if self.appear(COMBAT_AGAIN):
|
||||
logger.info(f'Combat execute ended at {COMBAT_AGAIN}')
|
||||
break
|
||||
if self.is_in_main():
|
||||
logger.info(f'Combat execute ended at page_main')
|
||||
break
|
||||
|
||||
# Daemon
|
||||
|
@ -22,6 +22,7 @@ from tasks.dungeon.keywords import KEYWORDS_DUNGEON_TAB
|
||||
from tasks.dungeon.ui import DungeonUI
|
||||
from tasks.item.consumable_usage import ConsumableUsageUI
|
||||
from tasks.item.relics import RelicsUI
|
||||
from tasks.map.route.loader import RouteLoader
|
||||
|
||||
|
||||
class DailyQuestOcr(Ocr):
|
||||
@ -52,6 +53,8 @@ class DailyQuestOcr(Ocr):
|
||||
result = result.replace('wor(d', 'world')
|
||||
# Echo/ofWar
|
||||
result = result.replace('cho/of', 'cho of')
|
||||
# Catyx(Golden).1.times
|
||||
result = result.replace('atyx', 'alyx')
|
||||
if "progress" in result.lower():
|
||||
result = "In Progress"
|
||||
if "claimed" in result.lower():
|
||||
@ -59,7 +62,7 @@ class DailyQuestOcr(Ocr):
|
||||
return result
|
||||
|
||||
|
||||
class DailyQuestUI(DungeonUI):
|
||||
class DailyQuestUI(DungeonUI, RouteLoader):
|
||||
def _ensure_position(self, direction: str, skip_first_screenshot=True):
|
||||
interval = Timer(5)
|
||||
if direction == 'left':
|
||||
@ -256,6 +259,9 @@ class DailyQuestUI(DungeonUI):
|
||||
if KEYWORDS_DAILY_QUEST.Salvage_any_Relic in quests:
|
||||
if RelicsUI(self.config, self.device).salvage_relic():
|
||||
done += 1
|
||||
if KEYWORDS_DAILY_QUEST.Complete_Forgotten_Hall_1_time in quests:
|
||||
self.route_run('daily.forgotten_hall.stage_1')
|
||||
done += 1
|
||||
|
||||
return done
|
||||
|
||||
|
@ -56,8 +56,8 @@ class UseTechniqueUI(MapControlJoystick, ForgottenHallUI):
|
||||
logger.hr('Use techniques', level=2)
|
||||
self.stage_goto(KEYWORDS_DUNGEON_LIST.The_Last_Vestiges_of_Towering_Citadel,
|
||||
KEYWORDS_FORGOTTEN_HALL_STAGE.Stage_1)
|
||||
self._choose_first_character()
|
||||
self._enter_forgotten_hall_dungeon()
|
||||
self.team_choose_first()
|
||||
self.enter_forgotten_hall_dungeon()
|
||||
self._use_technique(count, skip_first_screenshot=skip_first_screenshot)
|
||||
self.exit_dungeon()
|
||||
self.ui_goto_main()
|
||||
|
@ -50,11 +50,15 @@ class DungeonList(Keyword):
|
||||
|
||||
@cached_property
|
||||
def is_Forgotten_Hall(self):
|
||||
return ('Forgotten_Hall' in self.name) or ('Last_Vestiges' in self.name)
|
||||
|
||||
@cached_property
|
||||
def is_Last_Vestiges(self):
|
||||
return 'Last_Vestiges' in self.name
|
||||
for word in [
|
||||
'Forgotten_Hall',
|
||||
'Memory_of_Chaos',
|
||||
'Last_Vestiges',
|
||||
'Navis_Astriger',
|
||||
]:
|
||||
if word in self.name:
|
||||
return True
|
||||
return False
|
||||
|
||||
@cached_property
|
||||
def is_daily_dungeon(self):
|
||||
|
@ -315,107 +315,19 @@ The_Voyage_of_Navis_Astriger = DungeonList(
|
||||
en='The Voyage of Navis Astriger',
|
||||
jp='天艟求仙放浪記',
|
||||
)
|
||||
Favor_of_Amber = DungeonList(
|
||||
id=40,
|
||||
name='Favor_of_Amber',
|
||||
cn='琥珀恩赐•忘却之庭',
|
||||
cht='琥珀恩賜•忘卻之庭',
|
||||
en='Favor of Amber',
|
||||
jp='琥珀の賜物・忘却の庭',
|
||||
)
|
||||
Frostscar_Reverie = DungeonList(
|
||||
id=41,
|
||||
name='Frostscar_Reverie',
|
||||
cn='霜痕旧梦•忘却之庭',
|
||||
cht='霜痕舊夢•忘卻之庭',
|
||||
en='Frostscar Reverie',
|
||||
jp='霜跡に旧夢・忘却の庭',
|
||||
)
|
||||
Everwinter_Trials = DungeonList(
|
||||
id=42,
|
||||
name='Everwinter_Trials',
|
||||
cn='永冬试炼•忘却之庭',
|
||||
cht='永冬試煉•忘卻之庭',
|
||||
en='Everwinter Trials',
|
||||
jp='常冬の試練・忘却の庭',
|
||||
)
|
||||
Coldiron_Tribulation = DungeonList(
|
||||
id=43,
|
||||
name='Coldiron_Tribulation',
|
||||
cn='寒铁砥砺•忘却之庭',
|
||||
cht='寒鐵砥礪•忘卻之庭',
|
||||
en='Coldiron Tribulation',
|
||||
jp='寒鉄練磨・忘却の庭',
|
||||
)
|
||||
Hyperborean_Search_for_Warmth = DungeonList(
|
||||
id=44,
|
||||
name='Hyperborean_Search_for_Warmth',
|
||||
cn='蹈冰寻火•忘却之庭',
|
||||
cht='蹈冰尋火•忘卻之庭',
|
||||
en='Hyperborean Search for Warmth',
|
||||
jp='氷踏みて炎求む・忘却の庭',
|
||||
)
|
||||
Stormquell = DungeonList(
|
||||
id=45,
|
||||
name='Stormquell',
|
||||
cn='风暴止息•忘却之庭',
|
||||
cht='風暴止息•忘卻之庭',
|
||||
en='Stormquell',
|
||||
jp='止息せし嵐・忘却の庭',
|
||||
)
|
||||
Adrift_in_Astral_Seas = DungeonList(
|
||||
id=46,
|
||||
name='Adrift_in_Astral_Seas',
|
||||
cn='孤航天海•忘却之庭',
|
||||
cht='孤航太海•忘卻之庭',
|
||||
en='Adrift in Astral Seas',
|
||||
jp='天海の孤航・忘却の庭',
|
||||
)
|
||||
Raintear_Strife = DungeonList(
|
||||
id=47,
|
||||
name='Raintear_Strife',
|
||||
cn='泪雨长战•忘却之庭',
|
||||
cht='淚雨長戰•忘卻之庭',
|
||||
en='Raintear Strife',
|
||||
jp='涙雨戦争・忘却の庭',
|
||||
)
|
||||
Traces_of_Sanctus_Medicus = DungeonList(
|
||||
id=48,
|
||||
name='Traces_of_Sanctus_Medicus',
|
||||
cn='药王垂迹•忘却之庭',
|
||||
cht='藥王垂跡•忘卻之庭',
|
||||
en='Traces of Sanctus Medicus',
|
||||
jp='薬王の垂迹・忘却の庭',
|
||||
)
|
||||
Ethereal_Shipcraft_Forgotten_Hall = DungeonList(
|
||||
id=49,
|
||||
name='Ethereal_Shipcraft_Forgotten_Hall',
|
||||
cn='迷梦造舸•忘却之庭',
|
||||
cht='迷夢造舸•忘卻之庭',
|
||||
en='Ethereal Shipcraft — Forgotten Hall',
|
||||
jp='迷夢造舟・忘却の庭',
|
||||
)
|
||||
A_Shot_From_the_Sky_Forgotten_Hall = DungeonList(
|
||||
id=50,
|
||||
name='A_Shot_From_the_Sky_Forgotten_Hall',
|
||||
cn='天裂一射•忘却之庭',
|
||||
cht='天裂一射•忘卻之庭',
|
||||
en='A Shot From the Sky — Forgotten Hall',
|
||||
jp='天裂の一射・忘却の庭',
|
||||
)
|
||||
Mara_and_Null_Forgotten_Hall = DungeonList(
|
||||
id=51,
|
||||
name='Mara_and_Null_Forgotten_Hall',
|
||||
cn='魔阴空劫•忘却之庭',
|
||||
cht='魔陰空劫•忘卻之庭',
|
||||
en='Mara and Null — Forgotten Hall',
|
||||
jp='魔陰空劫・忘却の庭',
|
||||
)
|
||||
The_Last_Vestiges_of_Towering_Citadel = DungeonList(
|
||||
id=52,
|
||||
id=40,
|
||||
name='The_Last_Vestiges_of_Towering_Citadel',
|
||||
cn='永屹之城遗秘',
|
||||
cht='永屹之城遺秘',
|
||||
en='The Last Vestiges of Towering Citadel',
|
||||
jp='永屹の城の秘密',
|
||||
)
|
||||
Memory_of_Chaos = DungeonList(
|
||||
id=41,
|
||||
name='Memory_of_Chaos',
|
||||
cn='混沌回忆',
|
||||
cht='混沌回憶',
|
||||
en='Memory of Chaos',
|
||||
jp='混沌の記憶',
|
||||
)
|
||||
|
@ -9,7 +9,6 @@ from module.ocr.ocr import DigitCounter, Ocr, OcrResultButton
|
||||
from module.ocr.utils import split_and_pair_button_attr
|
||||
from module.ui.draggable_list import DraggableList
|
||||
from module.ui.switch import Switch
|
||||
from tasks.base.assets.assets_base_page import FORGOTTEN_HALL_CHECK
|
||||
from tasks.base.page import page_guide
|
||||
from tasks.base.ui import UI
|
||||
from tasks.combat.assets.assets_combat_prepare import COMBAT_PREPARE
|
||||
@ -219,12 +218,15 @@ class DungeonUI(UI):
|
||||
DUNGEON_NAV_LIST.load_rows(main=self)
|
||||
|
||||
# End
|
||||
button = DUNGEON_NAV_LIST.keyword2button(KEYWORDS_DUNGEON_NAV.Forgotten_Hall)
|
||||
button = DUNGEON_NAV_LIST.keyword2button(KEYWORDS_DUNGEON_NAV.Forgotten_Hall, show_warning=False)
|
||||
if button:
|
||||
# 513 is the top of the last row of DungeonNav
|
||||
if button.area[1] > 513:
|
||||
logger.info('DungeonNav row Forgotten_Hall stabled')
|
||||
return True
|
||||
else:
|
||||
logger.info('No Forgotten_Hall in list skip waiting')
|
||||
return False
|
||||
|
||||
def dungeon_get_simuni_point(self) -> int:
|
||||
"""
|
||||
@ -431,11 +433,6 @@ class DungeonUI(UI):
|
||||
self._dungeon_insight(dungeon)
|
||||
self._dungeon_enter(dungeon)
|
||||
return True
|
||||
if dungeon.is_Forgotten_Hall:
|
||||
self._dungeon_nav_goto(dungeon)
|
||||
self._dungeon_insight(dungeon)
|
||||
self._dungeon_enter(dungeon, enter_check_button=FORGOTTEN_HALL_CHECK)
|
||||
return True
|
||||
|
||||
logger.error(f'Goto dungeon {dungeon} is not supported')
|
||||
return False
|
||||
|
@ -1,95 +0,0 @@
|
||||
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 ```
|
||||
|
||||
DUNGEON_ENTER_CHECKED = ButtonWrapper(
|
||||
name='DUNGEON_ENTER_CHECKED',
|
||||
share=Button(
|
||||
file='./assets/share/forgotten_hall/DUNGEON_ENTER_CHECKED.png',
|
||||
area=(29, 242, 43, 251),
|
||||
search=(9, 222, 63, 271),
|
||||
color=(173, 173, 173),
|
||||
button=(29, 242, 43, 251),
|
||||
),
|
||||
)
|
||||
EFFECT_NOTIFICATION = ButtonWrapper(
|
||||
name='EFFECT_NOTIFICATION',
|
||||
share=Button(
|
||||
file='./assets/share/forgotten_hall/EFFECT_NOTIFICATION.png',
|
||||
area=(237, 302, 315, 380),
|
||||
search=(217, 282, 335, 400),
|
||||
color=(128, 114, 85),
|
||||
button=(237, 302, 315, 380),
|
||||
),
|
||||
)
|
||||
ENTER_FORGOTTEN_HALL_DUNGEON = ButtonWrapper(
|
||||
name='ENTER_FORGOTTEN_HALL_DUNGEON',
|
||||
share=Button(
|
||||
file='./assets/share/forgotten_hall/ENTER_FORGOTTEN_HALL_DUNGEON.png',
|
||||
area=(989, 649, 1231, 684),
|
||||
search=(969, 629, 1251, 704),
|
||||
color=(214, 214, 217),
|
||||
button=(989, 649, 1231, 684),
|
||||
),
|
||||
)
|
||||
ENTRANCE_CHECKED = ButtonWrapper(
|
||||
name='ENTRANCE_CHECKED',
|
||||
share=Button(
|
||||
file='./assets/share/forgotten_hall/ENTRANCE_CHECKED.png',
|
||||
area=(55, 632, 71, 650),
|
||||
search=(35, 612, 91, 670),
|
||||
color=(156, 156, 157),
|
||||
button=(55, 632, 71, 650),
|
||||
),
|
||||
)
|
||||
EXIT_CONFIRM = ButtonWrapper(
|
||||
name='EXIT_CONFIRM',
|
||||
share=Button(
|
||||
file='./assets/share/forgotten_hall/EXIT_CONFIRM.png',
|
||||
area=(776, 458, 800, 482),
|
||||
search=(756, 438, 820, 502),
|
||||
color=(94, 88, 76),
|
||||
button=(776, 458, 800, 482),
|
||||
),
|
||||
)
|
||||
EXIT_DUNGEON = ButtonWrapper(
|
||||
name='EXIT_DUNGEON',
|
||||
share=Button(
|
||||
file='./assets/share/forgotten_hall/EXIT_DUNGEON.png',
|
||||
area=(15, 45, 45, 75),
|
||||
search=(0, 25, 65, 95),
|
||||
color=(113, 117, 124),
|
||||
button=(15, 45, 45, 75),
|
||||
),
|
||||
)
|
||||
FIRST_CHARACTER = ButtonWrapper(
|
||||
name='FIRST_CHARACTER',
|
||||
share=Button(
|
||||
file='./assets/share/forgotten_hall/FIRST_CHARACTER.png',
|
||||
area=(54, 100, 150, 212),
|
||||
search=(34, 80, 170, 232),
|
||||
color=(145, 125, 103),
|
||||
button=(54, 100, 150, 212),
|
||||
),
|
||||
)
|
||||
LAST_VERTIGES = ButtonWrapper(
|
||||
name='LAST_VERTIGES',
|
||||
share=Button(
|
||||
file='./assets/share/forgotten_hall/LAST_VERTIGES.png',
|
||||
area=(1163, 105, 1203, 125),
|
||||
search=(1143, 85, 1223, 145),
|
||||
color=(221, 185, 225),
|
||||
button=(1163, 105, 1203, 125),
|
||||
),
|
||||
)
|
||||
OCR_STAGE = ButtonWrapper(
|
||||
name='OCR_STAGE',
|
||||
share=Button(
|
||||
file='./assets/share/forgotten_hall/OCR_STAGE.png',
|
||||
area=(0, 281, 1280, 581),
|
||||
search=(0, 261, 1280, 601),
|
||||
color=(29, 48, 92),
|
||||
button=(0, 0, 1000, 100),
|
||||
),
|
||||
)
|
105
tasks/forgotten_hall/assets/assets_forgotten_hall_ui.py
Normal file
@ -0,0 +1,105 @@
|
||||
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 ```
|
||||
|
||||
DUNGEON_ENTER_CHECKED = ButtonWrapper(
|
||||
name='DUNGEON_ENTER_CHECKED',
|
||||
share=Button(
|
||||
file='./assets/share/forgotten_hall/ui/DUNGEON_ENTER_CHECKED.png',
|
||||
area=(29, 242, 43, 251),
|
||||
search=(9, 222, 63, 271),
|
||||
color=(173, 173, 173),
|
||||
button=(29, 242, 43, 251),
|
||||
),
|
||||
)
|
||||
EFFECT_NOTIFICATION = ButtonWrapper(
|
||||
name='EFFECT_NOTIFICATION',
|
||||
share=Button(
|
||||
file='./assets/share/forgotten_hall/ui/EFFECT_NOTIFICATION.png',
|
||||
area=(237, 302, 315, 380),
|
||||
search=(217, 282, 335, 400),
|
||||
color=(128, 114, 85),
|
||||
button=(237, 302, 315, 380),
|
||||
),
|
||||
)
|
||||
ENTER_FORGOTTEN_HALL_DUNGEON = ButtonWrapper(
|
||||
name='ENTER_FORGOTTEN_HALL_DUNGEON',
|
||||
share=Button(
|
||||
file='./assets/share/forgotten_hall/ui/ENTER_FORGOTTEN_HALL_DUNGEON.png',
|
||||
area=(989, 649, 1231, 684),
|
||||
search=(969, 629, 1251, 704),
|
||||
color=(214, 214, 217),
|
||||
button=(989, 649, 1231, 684),
|
||||
),
|
||||
)
|
||||
ENTRANCE_CHECKED = ButtonWrapper(
|
||||
name='ENTRANCE_CHECKED',
|
||||
share=Button(
|
||||
file='./assets/share/forgotten_hall/ui/ENTRANCE_CHECKED.png',
|
||||
area=(55, 632, 71, 650),
|
||||
search=(35, 612, 91, 670),
|
||||
color=(156, 156, 157),
|
||||
button=(55, 632, 71, 650),
|
||||
),
|
||||
)
|
||||
LAST_VASTIGES_CHECK = ButtonWrapper(
|
||||
name='LAST_VASTIGES_CHECK',
|
||||
share=Button(
|
||||
file='./assets/share/forgotten_hall/ui/LAST_VASTIGES_CHECK.png',
|
||||
area=(25, 190, 96, 262),
|
||||
search=(11, 73, 111, 461),
|
||||
color=(94, 126, 162),
|
||||
button=(25, 190, 96, 262),
|
||||
),
|
||||
)
|
||||
LAST_VASTIGES_CLICK = ButtonWrapper(
|
||||
name='LAST_VASTIGES_CLICK',
|
||||
share=Button(
|
||||
file='./assets/share/forgotten_hall/ui/LAST_VASTIGES_CLICK.png',
|
||||
area=(32, 196, 90, 254),
|
||||
search=(11, 73, 111, 461),
|
||||
color=(49, 64, 87),
|
||||
button=(32, 196, 90, 254),
|
||||
),
|
||||
)
|
||||
MEMORY_OF_CHAOS_CHECK = ButtonWrapper(
|
||||
name='MEMORY_OF_CHAOS_CHECK',
|
||||
share=Button(
|
||||
file='./assets/share/forgotten_hall/ui/MEMORY_OF_CHAOS_CHECK.png',
|
||||
area=(24, 90, 96, 162),
|
||||
search=(4, 70, 116, 182),
|
||||
color=(140, 116, 159),
|
||||
button=(24, 90, 96, 162),
|
||||
),
|
||||
)
|
||||
MEMORY_OF_CHAOS_CLICK = ButtonWrapper(
|
||||
name='MEMORY_OF_CHAOS_CLICK',
|
||||
share=Button(
|
||||
file='./assets/share/forgotten_hall/ui/MEMORY_OF_CHAOS_CLICK.png',
|
||||
area=(32, 96, 90, 154),
|
||||
search=(12, 76, 110, 174),
|
||||
color=(74, 60, 96),
|
||||
button=(32, 96, 90, 154),
|
||||
),
|
||||
)
|
||||
OCR_STAGE = ButtonWrapper(
|
||||
name='OCR_STAGE',
|
||||
share=Button(
|
||||
file='./assets/share/forgotten_hall/ui/OCR_STAGE.png',
|
||||
area=(0, 281, 1280, 581),
|
||||
search=(0, 261, 1280, 601),
|
||||
color=(29, 48, 92),
|
||||
button=(0, 0, 1000, 100),
|
||||
),
|
||||
)
|
||||
TELEPORT = ButtonWrapper(
|
||||
name='TELEPORT',
|
||||
share=Button(
|
||||
file='./assets/share/forgotten_hall/ui/TELEPORT.png',
|
||||
area=(1019, 451, 1037, 470),
|
||||
search=(993, 176, 1088, 658),
|
||||
color=(166, 165, 166),
|
||||
button=(1019, 451, 1037, 470),
|
||||
),
|
||||
)
|
74
tasks/forgotten_hall/team.py
Normal file
@ -0,0 +1,74 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from module.base.utils import color_similarity_2d, get_color
|
||||
from module.logger import logger
|
||||
from tasks.base.ui import UI
|
||||
from tasks.forgotten_hall.assets.assets_forgotten_hall_team import *
|
||||
from tasks.forgotten_hall.assets.assets_forgotten_hall_ui import ENTER_FORGOTTEN_HALL_DUNGEON, ENTRANCE_CHECKED
|
||||
|
||||
|
||||
class ForgottenHallTeam(UI):
|
||||
def team_prepared(self):
|
||||
# White button, with a color of (214, 214, 214)
|
||||
color = get_color(self.device.image, ENTER_FORGOTTEN_HALL_DUNGEON.area)
|
||||
return np.mean(color) > 180
|
||||
|
||||
def team_choose_first(self, skip_first_screenshot=True):
|
||||
"""
|
||||
A temporary method used to choose the first character only
|
||||
"""
|
||||
logger.info('Team choose first')
|
||||
self.interval_clear(ENTRANCE_CHECKED)
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
else:
|
||||
self.device.screenshot()
|
||||
|
||||
if self.team_prepared():
|
||||
logger.info("First character is chosen")
|
||||
break
|
||||
if self.appear(ENTRANCE_CHECKED, interval=2):
|
||||
self.device.click(CHARACTER_1)
|
||||
continue
|
||||
|
||||
def is_character_chosen(self, button: ButtonWrapper) -> bool:
|
||||
image = color_similarity_2d(self.image_crop(button), color=(255, 255, 255))
|
||||
color = cv2.mean(image)[0]
|
||||
# print(button, color)
|
||||
# Chosen:
|
||||
# CHARACTER_1 210.0230034722222
|
||||
# CHARACTER_2 210.12022569444443
|
||||
# CHARACTER_3 211.09244791666666
|
||||
# CHARACTER_4 210.48046875
|
||||
# Not chosen
|
||||
# CHARACTER_1 122.38671875
|
||||
# CHARACTER_2 124.72960069444444
|
||||
# CHARACTER_3 136.55989583333331
|
||||
# CHARACTER_4 129.76432291666666
|
||||
return color > 180
|
||||
|
||||
def team_choose_first_4(self, skip_first_screenshot=True):
|
||||
"""
|
||||
Choose the first 4 characters in list.
|
||||
"""
|
||||
logger.info('Team choose first 4')
|
||||
self.interval_clear(ENTRANCE_CHECKED)
|
||||
characters = [CHARACTER_1, CHARACTER_2, CHARACTER_3, CHARACTER_4]
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
else:
|
||||
self.device.screenshot()
|
||||
|
||||
chosen_list = [self.is_character_chosen(c) for c in characters]
|
||||
if all(chosen_list):
|
||||
logger.info("First 4 characters are chosen")
|
||||
break
|
||||
if self.appear(ENTRANCE_CHECKED, interval=2):
|
||||
for character, chosen in zip(characters, chosen_list):
|
||||
if not chosen:
|
||||
self.device.click(character)
|
||||
# Casual sleep, game may not respond that fast
|
||||
self.device.sleep((0.1, 0.2))
|
@ -4,16 +4,17 @@ from pponnxcr.predict_system import BoxedResult
|
||||
|
||||
from module.base.base import ModuleBase
|
||||
from module.base.timer import Timer
|
||||
from module.base.utils import area_offset, color_similarity_2d, crop, get_color
|
||||
from module.base.utils import area_offset, color_similarity_2d, crop
|
||||
from module.logger.logger import logger
|
||||
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
|
||||
from tasks.dungeon.keywords import DungeonList, KEYWORDS_DUNGEON_TAB
|
||||
from tasks.base.assets.assets_base_page import FORGOTTEN_HALL_CHECK, MAP_EXIT
|
||||
from tasks.dungeon.keywords import DungeonList, KEYWORDS_DUNGEON_LIST, KEYWORDS_DUNGEON_TAB
|
||||
from tasks.dungeon.ui import DungeonUI
|
||||
from tasks.forgotten_hall.assets.assets_forgotten_hall import *
|
||||
from tasks.forgotten_hall.keywords import *
|
||||
from tasks.forgotten_hall.assets.assets_forgotten_hall_ui import *
|
||||
from tasks.forgotten_hall.keywords import ForgottenHallStage
|
||||
from tasks.forgotten_hall.team import ForgottenHallTeam
|
||||
from tasks.map.control.joystick import MapControlJoystick
|
||||
|
||||
|
||||
@ -103,8 +104,60 @@ STAGE_LIST = DraggableStageList("ForgottenHallStageList", keyword_class=Forgotte
|
||||
check_row_order=False, drag_direction="right")
|
||||
|
||||
|
||||
class ForgottenHallUI(DungeonUI):
|
||||
def stage_goto(self, forgotten_hall: DungeonList, stage_keyword: ForgottenHallStage):
|
||||
class ForgottenHallUI(DungeonUI, ForgottenHallTeam):
|
||||
def handle_effect_popup(self):
|
||||
if self.appear(EFFECT_NOTIFICATION, interval=2):
|
||||
if self.appear_then_click(MEMORY_OF_CHAOS_CHECK):
|
||||
return True
|
||||
if self.appear_then_click(MEMORY_OF_CHAOS_CLICK):
|
||||
return True
|
||||
# No match, click whatever
|
||||
MEMORY_OF_CHAOS_CHECK.clear_offset()
|
||||
self.device.click(MEMORY_OF_CHAOS_CHECK)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def stage_choose(self, dungeon: DungeonList, skip_first_screenshot=True):
|
||||
"""
|
||||
Pages:
|
||||
in: page_forgotten_hall, FORGOTTEN_HALL_CHECK
|
||||
or page_guide, Survival_Index, Forgotten_Hall
|
||||
out: page_forgotten_hall, FORGOTTEN_HALL_CHECK, selected at the given dungeon tab
|
||||
"""
|
||||
logger.info(f'Stage choose {dungeon}')
|
||||
if dungeon == KEYWORDS_DUNGEON_LIST.Memory_of_Chaos:
|
||||
check_button = MEMORY_OF_CHAOS_CHECK
|
||||
click_button = MEMORY_OF_CHAOS_CLICK
|
||||
elif dungeon == KEYWORDS_DUNGEON_LIST.The_Last_Vestiges_of_Towering_Citadel:
|
||||
check_button = LAST_VASTIGES_CHECK
|
||||
click_button = LAST_VASTIGES_CLICK
|
||||
else:
|
||||
logger.error(f'Choosing {dungeon} in forgotten hall is not supported')
|
||||
return
|
||||
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
else:
|
||||
self.device.screenshot()
|
||||
|
||||
# interval used in end condition
|
||||
# After clicking `click_button`, `click_button` appears, then screen goes black for a little while
|
||||
# interval prevents `check_button` being triggered in the next 0.3s
|
||||
if self.match_template_color(check_button, interval=0.3):
|
||||
logger.info(f'Stage chose at {dungeon}')
|
||||
break
|
||||
if self.handle_effect_popup():
|
||||
continue
|
||||
if self.appear_then_click(TELEPORT, interval=2):
|
||||
continue
|
||||
if self.match_template_color(click_button, interval=1):
|
||||
self.device.click(click_button)
|
||||
self.interval_reset(check_button)
|
||||
continue
|
||||
|
||||
def stage_goto(self, dungeon: DungeonList, stage_keyword: ForgottenHallStage):
|
||||
"""
|
||||
Examples:
|
||||
self = ForgottenHallUI('alas')
|
||||
@ -112,19 +165,34 @@ class ForgottenHallUI(DungeonUI):
|
||||
self.stage_goto(KEYWORDS_DUNGEON_LIST.The_Last_Vestiges_of_Towering_Citadel,
|
||||
KEYWORDS_FORGOTTEN_HALL_STAGE.Stage_8)
|
||||
"""
|
||||
if not forgotten_hall.is_Forgotten_Hall:
|
||||
logger.warning("DungeonList Chosen is not a forgotten hall")
|
||||
if not dungeon in [
|
||||
KEYWORDS_DUNGEON_LIST.Memory_of_Chaos,
|
||||
KEYWORDS_DUNGEON_LIST.The_Last_Vestiges_of_Towering_Citadel,
|
||||
|
||||
]:
|
||||
logger.error(f'DungeonList Chosen is not a forgotten hall: {dungeon}')
|
||||
return
|
||||
if not forgotten_hall.is_Last_Vestiges and stage_keyword.id > 10:
|
||||
logger.warning(f"This dungeon does not have stage that greater than 10. {stage_keyword.id} is chosen")
|
||||
if dungeon == KEYWORDS_DUNGEON_LIST.Memory_of_Chaos and stage_keyword.id > 10:
|
||||
logger.error(f'This dungeon "{dungeon}" does not have stage that greater than 10. '
|
||||
f'{stage_keyword.id} is chosen')
|
||||
return
|
||||
|
||||
if not self.appear(FORGOTTEN_HALL_CHECK):
|
||||
if self.appear(FORGOTTEN_HALL_CHECK):
|
||||
logger.info('Already in forgotten hall')
|
||||
else:
|
||||
self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index)
|
||||
self.dungeon_goto(forgotten_hall)
|
||||
self._dungeon_nav_goto(dungeon)
|
||||
|
||||
self.stage_choose(dungeon)
|
||||
STAGE_LIST.select_row(stage_keyword, main=self)
|
||||
|
||||
def exit_dungeon(self, skip_first_screenshot=True):
|
||||
"""
|
||||
Pages:
|
||||
in: page_main, in forgotten hall map
|
||||
out: page_forgotten_hall, FORGOTTEN_HALL_CHECK
|
||||
"""
|
||||
logger.info('Exit dungeon')
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
@ -135,37 +203,20 @@ class ForgottenHallUI(DungeonUI):
|
||||
logger.info("Forgotten hall dungeon exited")
|
||||
break
|
||||
|
||||
if self.appear_then_click(EXIT_DUNGEON):
|
||||
if self.appear_then_click(MAP_EXIT):
|
||||
continue
|
||||
if self.appear_then_click(EXIT_CONFIRM):
|
||||
if self.handle_popup_confirm():
|
||||
continue
|
||||
if self.handle_popup_single():
|
||||
continue
|
||||
|
||||
def _choose_first_character(self, skip_first_screenshot=True):
|
||||
"""
|
||||
A temporary method used to choose the first character only
|
||||
"""
|
||||
interval = Timer(1)
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
else:
|
||||
self.device.screenshot()
|
||||
|
||||
if self._forgotten_hall_enter_appear():
|
||||
logger.info("First character is chosen")
|
||||
break
|
||||
if interval.reached():
|
||||
self.device.click(FIRST_CHARACTER)
|
||||
interval.reset()
|
||||
|
||||
def _forgotten_hall_enter_appear(self):
|
||||
# White button, with a color of (214, 214, 214)
|
||||
color = get_color(self.device.image, ENTER_FORGOTTEN_HALL_DUNGEON.area)
|
||||
return np.mean(color) > 180
|
||||
|
||||
def _enter_forgotten_hall_dungeon(self, skip_first_screenshot=True):
|
||||
def enter_forgotten_hall_dungeon(self, skip_first_screenshot=True):
|
||||
"""
|
||||
called after team is set
|
||||
|
||||
Pages:
|
||||
in: ENTRANCE_CHECKED, ENTER_FORGOTTEN_HALL_DUNGEON
|
||||
out: page_main, in forgotten hall map
|
||||
"""
|
||||
interval = Timer(3)
|
||||
timeout = Timer(3)
|
||||
@ -184,7 +235,7 @@ class ForgottenHallUI(DungeonUI):
|
||||
else:
|
||||
timeout.reset()
|
||||
|
||||
if interval.reached() and self._forgotten_hall_enter_appear():
|
||||
if interval.reached() and self.team_prepared():
|
||||
self.device.click(ENTER_FORGOTTEN_HALL_DUNGEON)
|
||||
interval.reset()
|
||||
|
||||
@ -199,4 +250,4 @@ class ForgottenHallUI(DungeonUI):
|
||||
if self.match_template_color(DUNGEON_ENTER_CHECKED):
|
||||
logger.info("Forgotten hall dungeon entered")
|
||||
break
|
||||
joystick.handle_map_2x_run()
|
||||
joystick.handle_map_run_2x()
|
||||
|
@ -2,14 +2,16 @@ from functools import cached_property
|
||||
|
||||
from module.base.timer import Timer
|
||||
from module.logger import logger
|
||||
from tasks.combat.combat import Combat
|
||||
from tasks.map.assets.assets_map_control import ROTATION_SWIPE_AREA
|
||||
from tasks.map.control.joystick import JoystickContact, MapControlJoystick
|
||||
from tasks.map.control.waypoint import Waypoint, ensure_waypoint
|
||||
from tasks.map.control.joystick import JoystickContact
|
||||
from tasks.map.control.waypoint import Waypoint, ensure_waypoints
|
||||
from tasks.map.interact.aim import AimDetectorMixin
|
||||
from tasks.map.minimap.minimap import Minimap
|
||||
from tasks.map.resource.const import diff_to_180_180
|
||||
|
||||
|
||||
class MapControl(MapControlJoystick):
|
||||
class MapControl(Combat, AimDetectorMixin):
|
||||
@cached_property
|
||||
def minimap(self) -> Minimap:
|
||||
return Minimap()
|
||||
@ -78,7 +80,7 @@ class MapControl(MapControlJoystick):
|
||||
waypoint: Waypoint,
|
||||
end_opt=True,
|
||||
skip_first_screenshot=False
|
||||
):
|
||||
) -> list[str]:
|
||||
"""
|
||||
Point to point walk.
|
||||
|
||||
@ -92,6 +94,9 @@ class MapControl(MapControlJoystick):
|
||||
True to enable endpoint optimizations,
|
||||
character will smoothly approach target position
|
||||
skip_first_screenshot:
|
||||
|
||||
Returns:
|
||||
list[str]: A list of walk result
|
||||
"""
|
||||
logger.hr('Goto', level=2)
|
||||
logger.info(f'Goto {waypoint}')
|
||||
@ -99,51 +104,120 @@ class MapControl(MapControlJoystick):
|
||||
self.device.click_record_clear()
|
||||
|
||||
end_opt = end_opt and waypoint.end_opt
|
||||
allow_2x_run = waypoint.speed in ['2x_run']
|
||||
allow_straight_run = waypoint.speed in ['2x_run', 'straight_run']
|
||||
allow_run = waypoint.speed in ['2x_run', 'straight_run', 'run']
|
||||
allow_run_2x = waypoint.speed in ['run_2x']
|
||||
allow_straight_run = waypoint.speed in ['run_2x', 'straight_run']
|
||||
allow_run = waypoint.speed in ['run_2x', 'straight_run', 'run']
|
||||
allow_walk = True
|
||||
allow_rotation_set = True
|
||||
last_rotation = 0
|
||||
|
||||
result = []
|
||||
|
||||
direction_interval = Timer(0.5, count=1)
|
||||
rotation_interval = Timer(0.3, count=1)
|
||||
aim_interval = Timer(0.3, count=1)
|
||||
attacked_enemy = Timer(1.2, count=4)
|
||||
attacked_item = Timer(0.6, count=2)
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
else:
|
||||
self.device.screenshot()
|
||||
|
||||
# End
|
||||
for expected in waypoint.expected_end:
|
||||
if callable(expected):
|
||||
if expected():
|
||||
logger.info(f'Walk result add: {expected.__name__}')
|
||||
result.append(expected.__name__)
|
||||
return result
|
||||
if self.is_combat_executing():
|
||||
logger.info('Walk result add: enemy')
|
||||
result.append('enemy')
|
||||
contact.up()
|
||||
logger.hr('Combat', level=2)
|
||||
self.combat_execute()
|
||||
if waypoint.early_stop:
|
||||
return result
|
||||
|
||||
# The following detection require page_main
|
||||
if not self.is_in_main():
|
||||
attacked_enemy.clear()
|
||||
attacked_item.clear()
|
||||
continue
|
||||
|
||||
# Update
|
||||
self.minimap.update(self.device.image)
|
||||
if aim_interval.reached_and_reset():
|
||||
self.aim.predict(self.device.image)
|
||||
diff = self.minimap.position_diff(waypoint.position)
|
||||
direction = self.minimap.position2direction(waypoint.position)
|
||||
rotation_diff = self.minimap.direction_diff(direction)
|
||||
logger.info(f'Position diff: {diff}, rotation: {rotation_diff}')
|
||||
|
||||
# Interact
|
||||
if self.aim.aimed_enemy:
|
||||
if 'enemy' in waypoint.expected_end:
|
||||
if self.handle_map_A():
|
||||
allow_run_2x = allow_straight_run = allow_run = allow_walk = False
|
||||
attacked_enemy.reset()
|
||||
direction_interval.reset()
|
||||
rotation_interval.reset()
|
||||
if attacked_enemy.started():
|
||||
attacked_enemy.reset()
|
||||
if self.aim.aimed_item:
|
||||
if 'item' in waypoint.expected_end:
|
||||
if self.handle_map_A():
|
||||
allow_run_2x = allow_straight_run = allow_run = allow_walk = False
|
||||
attacked_item.reset()
|
||||
direction_interval.reset()
|
||||
rotation_interval.reset()
|
||||
if attacked_item.started():
|
||||
attacked_item.reset()
|
||||
else:
|
||||
if attacked_item.started() and attacked_item.reached():
|
||||
logger.info('Walk result add: item')
|
||||
result.append('item')
|
||||
if waypoint.early_stop:
|
||||
return result
|
||||
|
||||
# Arrive
|
||||
if self.minimap.is_position_near(waypoint.position, threshold=waypoint.get_threshold(end_opt)):
|
||||
logger.info(f'Arrive {waypoint}')
|
||||
break
|
||||
if not attacked_enemy.started() and not attacked_item.started():
|
||||
if self.minimap.is_position_near(waypoint.position, threshold=waypoint.get_threshold(end_opt)):
|
||||
if not waypoint.expected_end or waypoint.match_results(result):
|
||||
logger.info(f'Arrive waypoint: {waypoint}')
|
||||
return result
|
||||
else:
|
||||
if waypoint.unexpected_confirm.reached():
|
||||
logger.info(f'Arrive waypoint with unexpected result: {waypoint}')
|
||||
return result
|
||||
else:
|
||||
waypoint.unexpected_confirm.reset()
|
||||
|
||||
# Switch run case
|
||||
diff = self.minimap.position_diff(waypoint.position)
|
||||
|
||||
if end_opt:
|
||||
if allow_2x_run and diff < 20:
|
||||
logger.info(f'Approaching target, diff={round(diff, 1)}, disallow 2x_run')
|
||||
allow_2x_run = False
|
||||
if allow_straight_run and diff < 15:
|
||||
if allow_run_2x and diff < 20:
|
||||
logger.info(f'Approaching target, diff={round(diff, 1)}, disallow run_2x')
|
||||
allow_run_2x = False
|
||||
if allow_straight_run and diff < 15 and not allow_rotation_set:
|
||||
logger.info(f'Approaching target, diff={round(diff, 1)}, disallow straight_run')
|
||||
direction_interval = Timer(0.2)
|
||||
self.map_2x_run_timer.reset()
|
||||
aim_interval = Timer(0.1)
|
||||
self.map_run_2x_timer.reset()
|
||||
allow_straight_run = False
|
||||
if allow_run and diff < 7:
|
||||
logger.info(f'Approaching target, diff={round(diff, 1)}, disallow run')
|
||||
direction_interval = Timer(0.2)
|
||||
aim_interval = Timer(0.2)
|
||||
allow_run = False
|
||||
|
||||
# Control
|
||||
direction = self.minimap.position2direction(waypoint.position)
|
||||
if allow_2x_run:
|
||||
# Run with 2x_run button
|
||||
if allow_run_2x:
|
||||
# Run with run_2x button
|
||||
# - Set rotation once
|
||||
# - Continuous fine-tuning direction
|
||||
# - Enable 2x_run
|
||||
# - Enable run_2x
|
||||
if allow_rotation_set:
|
||||
# Cache rotation cause rotation detection has a higher error rate
|
||||
last_rotation = self.minimap.rotation
|
||||
@ -158,12 +232,12 @@ class MapControl(MapControlJoystick):
|
||||
if direction_interval.reached():
|
||||
contact.set(direction=diff_to_180_180(direction - last_rotation), run=True)
|
||||
direction_interval.reset()
|
||||
self.handle_map_2x_run(run=True)
|
||||
self.handle_map_run_2x(run=True)
|
||||
elif allow_straight_run:
|
||||
# Run straight forward
|
||||
# - Set rotation once
|
||||
# - Continuous fine-tuning direction
|
||||
# - Disable 2x_run
|
||||
# - Disable run_2x
|
||||
if allow_rotation_set:
|
||||
# Cache rotation cause rotation detection has a higher error rate
|
||||
last_rotation = self.minimap.rotation
|
||||
@ -178,67 +252,102 @@ class MapControl(MapControlJoystick):
|
||||
if direction_interval.reached():
|
||||
contact.set(direction=diff_to_180_180(direction - last_rotation), run=True)
|
||||
direction_interval.reset()
|
||||
self.handle_map_2x_run(run=False)
|
||||
self.handle_map_run_2x(run=False)
|
||||
elif allow_run:
|
||||
# Run
|
||||
# - No rotation set
|
||||
# - Continuous fine-tuning direction
|
||||
# - Disable 2x_run
|
||||
# - Disable run_2x
|
||||
if allow_rotation_set:
|
||||
last_rotation = self.minimap.rotation
|
||||
allow_rotation_set = False
|
||||
if direction_interval.reached():
|
||||
contact.set(direction=diff_to_180_180(direction - last_rotation), run=True)
|
||||
self.handle_map_2x_run(run=False)
|
||||
else:
|
||||
direction_interval.reset()
|
||||
self.handle_map_run_2x(run=False)
|
||||
elif allow_walk:
|
||||
# Walk
|
||||
# - Continuous fine-tuning direction
|
||||
# - Disable 2x_run
|
||||
# - Disable run_2x
|
||||
if allow_rotation_set:
|
||||
last_rotation = self.minimap.rotation
|
||||
allow_rotation_set = False
|
||||
if direction_interval.reached():
|
||||
contact.set(direction=diff_to_180_180(direction - last_rotation), run=False)
|
||||
direction_interval.reset()
|
||||
self.handle_map_2x_run(run=False)
|
||||
self.handle_map_run_2x(run=False)
|
||||
else:
|
||||
contact.up()
|
||||
|
||||
def goto(
|
||||
self,
|
||||
*waypoints,
|
||||
skip_first_screenshot=True
|
||||
):
|
||||
def goto(self, *waypoints):
|
||||
"""
|
||||
Go along a list of position, or goto target position
|
||||
Go along a list of position, or goto target position.
|
||||
|
||||
Args:
|
||||
waypoints:
|
||||
position (x, y) to goto, or a list of position to go along.
|
||||
Waypoint object to goto, or a list of Waypoint objects to go along.
|
||||
|
||||
skip_first_screenshot:
|
||||
waypoints: position (x, y), a list of position to go along,
|
||||
or a list of Waypoint objects to go along.
|
||||
"""
|
||||
logger.hr('Goto', level=1)
|
||||
waypoints = [ensure_waypoint(point) for point in waypoints]
|
||||
self.map_A_timer.clear()
|
||||
self.map_E_timer.clear()
|
||||
self.map_run_2x_timer.clear()
|
||||
waypoints = ensure_waypoints(waypoints)
|
||||
logger.info(f'Go along {len(waypoints)} waypoints')
|
||||
end_list = [False for _ in waypoints]
|
||||
end_list[-1] = True
|
||||
|
||||
with JoystickContact(self) as contact:
|
||||
for point, end in zip(waypoints, end_list):
|
||||
point: Waypoint
|
||||
self._goto(
|
||||
for waypoint, end in zip(waypoints, end_list):
|
||||
waypoint: Waypoint
|
||||
result = self._goto(
|
||||
contact=contact,
|
||||
waypoint=point,
|
||||
waypoint=waypoint,
|
||||
end_opt=end,
|
||||
skip_first_screenshot=skip_first_screenshot
|
||||
skip_first_screenshot=True,
|
||||
)
|
||||
skip_first_screenshot = True
|
||||
expected = waypoint.expected_to_str(waypoint.expected_end)
|
||||
logger.info(f'Arrive waypoint, expected: {expected}, result: {result}')
|
||||
matched = waypoint.match_results(result)
|
||||
if not waypoint.expected_end or matched:
|
||||
logger.info(f'Arrive waypoint with expected result: {matched}')
|
||||
else:
|
||||
logger.warning(f'Arrive waypoint with unexpected result: {result}')
|
||||
|
||||
end_point = waypoints[-1]
|
||||
if end_point.end_rotation is not None:
|
||||
logger.hr('End rotation', level=1)
|
||||
logger.hr('End rotation', level=2)
|
||||
self.rotation_set(end_point.end_rotation, threshold=end_point.end_rotation_threshold)
|
||||
|
||||
def clear_item(self, *waypoints):
|
||||
"""
|
||||
Go along a list of position and clear destructive object at last.
|
||||
|
||||
Args:
|
||||
waypoints: position (x, y), a list of position to go along.
|
||||
or a list of Waypoint objects to go along.
|
||||
"""
|
||||
logger.hr('Clear item', level=1)
|
||||
waypoints = ensure_waypoints(waypoints)
|
||||
end_point = waypoints[-1]
|
||||
end_point.expected_end.append('item')
|
||||
|
||||
self.goto(*waypoints)
|
||||
|
||||
def clear_enemy(self, *waypoints):
|
||||
"""
|
||||
Go along a list of position and enemy at last.
|
||||
|
||||
Args:
|
||||
waypoints: position (x, y), a list of position to go along.
|
||||
or a list of Waypoint objects to go along.
|
||||
"""
|
||||
logger.hr('Clear item', level=1)
|
||||
waypoints = ensure_waypoints(waypoints)
|
||||
end_point = waypoints[-1]
|
||||
end_point.expected_end.append('enemy')
|
||||
|
||||
self.goto(*waypoints)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Control test in Himeko trial
|
||||
@ -248,17 +357,14 @@ if __name__ == '__main__':
|
||||
self.device.screenshot()
|
||||
self.minimap.init_position((519, 359))
|
||||
# Visit 3 items
|
||||
self.goto(
|
||||
Waypoint((577.6, 363.4)),
|
||||
self.clear_item(
|
||||
Waypoint((587.6, 366.9)).run_2x(),
|
||||
)
|
||||
self.goto(
|
||||
Waypoint((577.5, 369.4), end_rotation=200),
|
||||
)
|
||||
self.goto(
|
||||
Waypoint((581.5, 387.3)),
|
||||
Waypoint((577.4, 411.5)),
|
||||
self.clear_item((575.5, 377.4))
|
||||
self.clear_item(
|
||||
# Go through arched door
|
||||
Waypoint((581.5, 383.3)).run().set_threshold(3),
|
||||
Waypoint((575.7, 417.2)).run(),
|
||||
)
|
||||
# Goto boss
|
||||
self.goto(
|
||||
Waypoint((607.6, 425.3)),
|
||||
)
|
||||
self.clear_enemy((613.5, 427.3))
|
||||
|
@ -16,7 +16,7 @@ from tasks.map.assets.assets_map_control import *
|
||||
class JoystickContact:
|
||||
CENTER = (JOYSTICK.area[0] + JOYSTICK.area[2]) / 2, (JOYSTICK.area[1] + JOYSTICK.area[3]) / 2
|
||||
# Minimum radius 49px
|
||||
RADIUS_WALK = (55, 65)
|
||||
RADIUS_WALK = (25, 40)
|
||||
# Minimum radius 103px
|
||||
RADIUS_RUN = (105, 115)
|
||||
|
||||
@ -114,7 +114,7 @@ class JoystickContact:
|
||||
direction (int, float): Direction to goto (0~360)
|
||||
run: True for character running, False for walking
|
||||
"""
|
||||
logger.info(f'JoystickContact set to {direction}')
|
||||
logger.info(f'JoystickContact set to {direction}, run={run}')
|
||||
point = JoystickContact.direction2screen(direction, run=run)
|
||||
builder = self.builder
|
||||
|
||||
@ -136,7 +136,7 @@ class JoystickContact:
|
||||
# Character starts moving, RUN button is still unavailable in a short time.
|
||||
# Assume available in 0.3s
|
||||
# We still have reties if 0.3s is incorrect.
|
||||
self.main.map_2x_run_timer.set_current(0.7)
|
||||
self.main.map_run_2x_timer.set_current(0.7)
|
||||
self.main.joystick_lost_timer.reset()
|
||||
|
||||
self.prev_point = point
|
||||
@ -145,7 +145,7 @@ class JoystickContact:
|
||||
class MapControlJoystick(UI):
|
||||
map_A_timer = Timer(1)
|
||||
map_E_timer = Timer(1)
|
||||
map_2x_run_timer = Timer(1)
|
||||
map_run_2x_timer = Timer(1)
|
||||
|
||||
joystick_lost_timer = Timer(1, count=2)
|
||||
|
||||
@ -235,7 +235,7 @@ class MapControlJoystick(UI):
|
||||
|
||||
return False
|
||||
|
||||
def handle_map_2x_run(self, run=True):
|
||||
def handle_map_run_2x(self, run=True):
|
||||
"""
|
||||
Keep character running.
|
||||
Note that RUN button can only be clicked when character is moving.
|
||||
@ -245,13 +245,13 @@ class MapControlJoystick(UI):
|
||||
"""
|
||||
is_running = self.image_color_count(RUN_BUTTON, color=(208, 183, 138), threshold=221, count=100)
|
||||
|
||||
if run and not is_running and self.map_2x_run_timer.reached():
|
||||
if run and not is_running and self.map_run_2x_timer.reached():
|
||||
self.device.click(RUN_BUTTON)
|
||||
self.map_2x_run_timer.reset()
|
||||
self.map_run_2x_timer.reset()
|
||||
return True
|
||||
if not run and is_running and self.map_2x_run_timer.reached():
|
||||
if not run and is_running and self.map_run_2x_timer.reached():
|
||||
self.device.click(RUN_BUTTON)
|
||||
self.map_2x_run_timer.reset()
|
||||
self.map_run_2x_timer.reset()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
@ -1,4 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from module.base.timer import Timer
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -11,7 +13,7 @@ class Waypoint:
|
||||
# If `threshold` is not set, `waypoint_threshold` and `endpoint_threshold` are used
|
||||
waypoint_threshold: int = 10
|
||||
endpoint_threshold: int = 3
|
||||
# Max move speed, '2x_run', 'straight_run', 'run', 'walk'
|
||||
# Max move speed, 'run_2x', 'straight_run', 'run', 'walk'
|
||||
# See MapControl._goto() for details of each speed level
|
||||
speed: str = 'straight_run'
|
||||
|
||||
@ -25,11 +27,50 @@ class Waypoint:
|
||||
end_rotation: int = None
|
||||
end_rotation_threshold: int = 15
|
||||
|
||||
"""
|
||||
Walk
|
||||
"""
|
||||
# A list of expected events, e.g. ['enemy', 'item']
|
||||
# - "enemy", finished any combat
|
||||
# - "item", destroyed any destructive objects
|
||||
# - "interact", have map interact option (interact is not handled)
|
||||
# - callable, A function that returns bool, True represents stop
|
||||
# Or empty list [] for just walking
|
||||
expected_end: list = field(default_factory=lambda: [])
|
||||
# If triggered any expected event, consider arrive and stop walking
|
||||
early_stop: bool = True
|
||||
# Confirm timer if arrived but didn't trigger any expected event
|
||||
unexpected_confirm: Timer = field(default_factory=lambda: Timer(2, count=6))
|
||||
|
||||
def __str__(self):
|
||||
return f'Waypoint({self.position})'
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def run_2x(self) -> "Waypoint":
|
||||
"""
|
||||
Product a Waypoint object with overridden "speed",
|
||||
see Waypoint class for args.
|
||||
"""
|
||||
self.speed = 'run_2x'
|
||||
return self
|
||||
|
||||
def straight_run(self) -> "Waypoint":
|
||||
self.speed = 'straight_run'
|
||||
return self
|
||||
|
||||
def run(self) -> "Waypoint":
|
||||
self.speed = 'run'
|
||||
return self
|
||||
|
||||
def walk(self) -> "Waypoint":
|
||||
self.speed = 'walk'
|
||||
return self
|
||||
|
||||
def set_threshold(self, threshold) -> "Waypoint":
|
||||
self.threshold = threshold
|
||||
return self
|
||||
|
||||
def get_threshold(self, end):
|
||||
"""
|
||||
Args:
|
||||
@ -45,6 +86,27 @@ class Waypoint:
|
||||
else:
|
||||
return self.waypoint_threshold
|
||||
|
||||
@staticmethod
|
||||
def expected_to_str(results: list) -> list[str]:
|
||||
return [result.__name__ if callable(result) else str(result) for result in results]
|
||||
|
||||
def match_results(self, results) -> list[str]:
|
||||
"""
|
||||
Args:
|
||||
results:
|
||||
|
||||
Returns:
|
||||
list[str]: A list if matched results
|
||||
"""
|
||||
if not results and not self.expected_end:
|
||||
return []
|
||||
|
||||
results = set(self.expected_to_str(results))
|
||||
expected_end = set(self.expected_to_str(self.expected_end))
|
||||
same = results.intersection(expected_end)
|
||||
|
||||
return list(same)
|
||||
|
||||
|
||||
def ensure_waypoint(point) -> Waypoint:
|
||||
"""
|
||||
@ -54,26 +116,13 @@ def ensure_waypoint(point) -> Waypoint:
|
||||
Returns:
|
||||
Waypoint:
|
||||
"""
|
||||
|
||||
if isinstance(point, Waypoint):
|
||||
return point
|
||||
return Waypoint(point)
|
||||
|
||||
|
||||
@dataclass(repr=False)
|
||||
class Waypoint2xRun(Waypoint):
|
||||
speed: str = '2x_run'
|
||||
|
||||
|
||||
@dataclass(repr=False)
|
||||
class WaypointStraightRun(Waypoint):
|
||||
speed: str = 'straight_run'
|
||||
|
||||
|
||||
@dataclass(repr=False)
|
||||
class WaypointRun(Waypoint):
|
||||
speed: str = 'run'
|
||||
|
||||
|
||||
@dataclass(repr=False)
|
||||
class WaypointWalk(Waypoint):
|
||||
speed: str = 'walk'
|
||||
def ensure_waypoints(points) -> list[Waypoint]:
|
||||
if not isinstance(points, (list, tuple)):
|
||||
points = [points]
|
||||
return [ensure_waypoint(point) for point in points]
|
||||
|
@ -46,8 +46,8 @@ class PositionPredictState:
|
||||
|
||||
|
||||
class Minimap(MapResource):
|
||||
def init_position(self, position: tuple[int, int]):
|
||||
logger.info(f"init_position:{position}")
|
||||
def init_position(self, position: tuple[int | float, int | float]):
|
||||
logger.info(f"init_position: {position}")
|
||||
self.position = position
|
||||
|
||||
def _predict_position(self, image, scale=1.0):
|
||||
@ -388,9 +388,9 @@ if __name__ == '__main__':
|
||||
# MapResource.SRCMAP = '../srcmap/srcmap'
|
||||
self = Minimap()
|
||||
# Set plane, assume starting from Jarilo_AdministrativeDistrict
|
||||
self.set_plane('Jarilo_AdministrativeDistrict', floor='F1')
|
||||
self.set_plane('Jarilo_BackwaterPass', floor='F1')
|
||||
|
||||
ui = UI('alas')
|
||||
ui = UI('src')
|
||||
ui.device.disable_stuck_detection()
|
||||
# Set starter point. Starter point will be calculated if it's missing but may contain errors.
|
||||
# With starter point set, position is only searched around starter point and new position becomes new starter point.
|
||||
|
@ -101,7 +101,9 @@ class ResourceConst:
|
||||
Returns:
|
||||
float: Distance to current position
|
||||
"""
|
||||
return np.linalg.norm(np.subtract(target, self.position))
|
||||
diff = np.linalg.norm(np.subtract(target, self.position))
|
||||
diff = round(diff, 3)
|
||||
return diff
|
||||
|
||||
def is_position_near(self, target, threshold=5):
|
||||
return self.position_diff(target) <= threshold
|
||||
@ -118,6 +120,7 @@ class ResourceConst:
|
||||
theta = np.rad2deg(np.arccos(-diff[1] / np.linalg.norm(diff)))
|
||||
if diff[0] < 0:
|
||||
theta = 360 - theta
|
||||
theta = round(theta, 3)
|
||||
return theta
|
||||
|
||||
def direction_diff(self, target):
|
||||
|
50
tasks/map/route/base.py
Normal file
@ -0,0 +1,50 @@
|
||||
from tasks.map.control.control import MapControl
|
||||
from tasks.map.control.waypoint import Waypoint
|
||||
from tasks.map.keywords import MapPlane
|
||||
|
||||
|
||||
class RouteBase(MapControl):
|
||||
"""
|
||||
Base class of `Route`
|
||||
Every `Route` class must implement method `route()`
|
||||
"""
|
||||
|
||||
def route_example(self):
|
||||
"""
|
||||
Pages:
|
||||
in: page_main
|
||||
out: page_main
|
||||
Doesn't matter if in/out are not page_main, just be clear what you're doing
|
||||
"""
|
||||
self.map_init(
|
||||
plane=...,
|
||||
floor=...,
|
||||
position=...,
|
||||
)
|
||||
self.clear_enemy(
|
||||
Waypoint(...).run_2x(),
|
||||
Waypoint(...),
|
||||
)
|
||||
|
||||
def map_init(
|
||||
self,
|
||||
plane: MapPlane | str,
|
||||
floor: str = 'F1',
|
||||
position: tuple[int | float, int | float] = None
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
plane (MapPlane, str): Such as Jarilo_AdministrativeDistrict
|
||||
floor (str):
|
||||
position: Initialize the starter point of minimap tracking
|
||||
Leaving None will trigger brute-force starter point finding.
|
||||
"""
|
||||
try:
|
||||
if self.device.image is None:
|
||||
self.device.screenshot()
|
||||
except AttributeError:
|
||||
self.device.screenshot()
|
||||
|
||||
self.minimap.set_plane(plane, floor=floor)
|
||||
if position is not None:
|
||||
self.minimap.init_position(position)
|
38
tasks/map/route/loader.py
Normal file
@ -0,0 +1,38 @@
|
||||
import importlib
|
||||
import os
|
||||
|
||||
from module.exception import RequestHumanTakeover
|
||||
from module.logger import logger
|
||||
from tasks.base.ui import UI
|
||||
from tasks.map.route.base import RouteBase
|
||||
|
||||
|
||||
class RouteLoader(UI):
|
||||
route: RouteBase
|
||||
|
||||
def route_run(self, route: str):
|
||||
"""
|
||||
Args:
|
||||
route: .py module path such as `daily.forgotten_hall.stage1`
|
||||
which will load `./route/daily/forgotten_hall/stage1.py`
|
||||
"""
|
||||
folder, name = route.rsplit('.', maxsplit=1)
|
||||
path = f'./route/{route.replace(".", "/")}.py'
|
||||
try:
|
||||
module = importlib.import_module(f'route.{folder}.{name}')
|
||||
except ModuleNotFoundError:
|
||||
logger.critical(f'Route file not found: {route} ({path})')
|
||||
if not os.path.exists(path):
|
||||
logger.critical(f'Route file not exists: {path}')
|
||||
raise RequestHumanTakeover
|
||||
|
||||
# config = copy.deepcopy(self.config).merge(module.Config())
|
||||
config = self.config
|
||||
device = self.device
|
||||
try:
|
||||
self.route = module.Route(config=config, device=device)
|
||||
return self.route.route()
|
||||
except AttributeError as e:
|
||||
logger.critical(e)
|
||||
logger.critical(f'Route file {route} ({path}) must define Route.route()')
|
||||
raise RequestHumanTakeover
|