Merge pull request #102 from LmeSzinc/dev

Dev
This commit is contained in:
LmeSzinc 2023-09-24 13:22:30 +08:00 committed by GitHub
commit 360c490dbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 838 additions and 59 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -75,19 +75,19 @@
"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",
"In_a_single_battle_inflict_3_Weakness_Break_of_different_Types": "achievable",
"Inflict_Weakness_Break_5_times": "achievable",
"Defeat_a_total_of_20_enemies": "achievable",
"Enter_combat_by_attacking_enemy_Weakness_and_win_3_times": "achievable",
"Use_Technique_2_times": "achievable",
"Go_on_assignment_1_time": "achievable",
"Take_1_photo": "achievable",
"Destroy_3_destructible_objects": "not_supported",
"Destroy_3_destructible_objects": "achievable",
"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": "achievable",
"Use_an_Ultimate_to_deal_the_final_blow_1_time": "not_supported",
"Use_an_Ultimate_to_deal_the_final_blow_1_time": "achievable",
"Level_up_any_character_1_time": "not_supported",
"Level_up_any_Light_Cone_1_time": "not_supported",
"Level_up_any_Relic_1_time": "not_supported",

View File

@ -0,0 +1,89 @@
import os
import re
from dataclasses import dataclass, fields
from module.base.code_generator import CodeGenerator
@dataclass
class RouteData:
name: str
route: str
plane: str
floor: str = 'F1'
position: tuple = None
class RouteExtract:
def __init__(self, folder):
self.folder = folder
def iter_files(self):
for path, folders, files in os.walk(self.folder):
path = path.replace('\\', '/')
for file in files:
if file.endswith('.py'):
yield f'{path}/{file}'
def extract_route(self, file):
print(f'Extract {file}')
with open(file, 'r', encoding='utf-8') as f:
content = f.read()
"""
def route_item_enemy(self):
self.enter_himeko_trial()
self.map_init(plane=Jarilo_BackwaterPass, position=(519.9, 361.5))
"""
regex = re.compile(
r'def (?P<func>[a-zA-Z0-9_]*?)\(self\):.*?'
r'self\.map_init\((.*?)\)'
, re.DOTALL)
file = file.replace(self.folder, '').replace('.py', '').replace('/', '_').strip('_')
module = f"{self.folder.strip('./').replace('/', '.')}.{file.replace('_', '.')}"
for result in regex.findall(content):
func, data = result
res = re.search(r'plane=([a-zA-Z_]*)', data)
if res:
plane = res.group(1)
else:
# Must contain plane
continue
res = re.search(r'floor=([\'"a-zA-Z0-9_]*)', data)
if res:
floor = res.group(1).strip('"\'')
else:
floor = 'F1'
res = re.search(r'position=\(([0-9.]*)[, ]+([0-9.]*)', data)
if res:
position = (float(res.group(1)), float(res.group(2)))
else:
position = None
yield RouteData(
name=f'{file}__{func}',
route=f'{module}:{func}',
plane=plane,
floor=floor,
position=position,
)
def write(self, file):
gen = CodeGenerator()
gen.Import("""
from tasks.map.route.base import RouteData
""")
for f in self.iter_files():
for row in self.extract_route(f):
with gen.Object(key=row.name, object_class='RouteData'):
for key in fields(row):
value = getattr(row, key.name)
gen.ObjectAttr(key.name, value)
gen.write(file)
if __name__ == '__main__':
os.chdir(os.path.join(os.path.dirname(__file__), '../'))
RouteExtract('./route/daily').write('./tasks/map/route/route/daily.py')

View File

@ -668,21 +668,21 @@ class ConfigUpdater:
set_daily('Clear_Cavern_of_Corrosion_1_times',
dungeon and deep_get(data, 'Dungeon.DungeonDaily.CavernOfCorrosion') != 'do_not_achieve')
# Combat requirements
set_daily('In_a_single_battle_inflict_3_Weakness_Break_of_different_Types', 'not_supported')
set_daily('Inflict_Weakness_Break_5_times', 'not_supported')
set_daily('Defeat_a_total_of_20_enemies', 'not_supported')
set_daily('Enter_combat_by_attacking_enemy_Weakness_and_win_3_times', 'not_supported')
set_daily('In_a_single_battle_inflict_3_Weakness_Break_of_different_Types', 'achievable')
set_daily('Inflict_Weakness_Break_5_times', 'achievable')
set_daily('Defeat_a_total_of_20_enemies', 'achievable')
set_daily('Enter_combat_by_attacking_enemy_Weakness_and_win_3_times', 'achievable')
set_daily('Use_Technique_2_times', 'achievable')
# Other game systems
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('Destroy_3_destructible_objects', 'achievable')
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',
dungeon and deep_get(data, 'Dungeon.DungeonSupport.Use') in ['when_daily', 'always_use'])
set_daily('Use_an_Ultimate_to_deal_the_final_blow_1_time', 'not_supported')
set_daily('Use_an_Ultimate_to_deal_the_final_blow_1_time', 'achievable')
# Build
set_daily('Level_up_any_character_1_time', 'not_supported')
set_daily('Level_up_any_Light_Cone_1_time', 'not_supported')

View File

@ -437,28 +437,28 @@
},
"In_a_single_battle_inflict_3_Weakness_Break_of_different_Types": {
"name": "In a single battle, inflict 3 Weakness Break of different Types",
"help": "",
"help": "Achievable by default, will do Himeko trial",
"achievable": "Achievable",
"not_set": "Not Set",
"not_supported": "Not Supported Yet"
},
"Inflict_Weakness_Break_5_times": {
"name": "Inflict Weakness Break 5 times",
"help": "",
"help": "Achievable by default, will do Himeko trial",
"achievable": "Achievable",
"not_set": "Not Set",
"not_supported": "Not Supported Yet"
},
"Defeat_a_total_of_20_enemies": {
"name": "Defeat a total of 20 enemies",
"help": "",
"help": "Achievable by default, will do Himeko trial",
"achievable": "Achievable",
"not_set": "Not Set",
"not_supported": "Not Supported Yet"
},
"Enter_combat_by_attacking_enemy_Weakness_and_win_3_times": {
"name": "Enter combat by attacking enemy's Weakness and win 3 times",
"help": "",
"help": "Achievable by default, will do Himeko trial",
"achievable": "Achievable",
"not_set": "Not Set",
"not_supported": "Not Supported Yet"
@ -486,14 +486,14 @@
},
"Destroy_3_destructible_objects": {
"name": "Destroy 3 destructible objects",
"help": "",
"help": "Achievable by default, will do Himeko trial",
"achievable": "Achievable",
"not_set": "Not Set",
"not_supported": "Not Supported Yet"
},
"Complete_Forgotten_Hall_1_time": {
"name": "Complete Forgotten Hall 1 time",
"help": "Choose the first four characters to do stage 1 once, please ensure that account build is sufficient to do stage 1",
"help": "Achievable by default, will 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"
@ -521,7 +521,7 @@
},
"Use_an_Ultimate_to_deal_the_final_blow_1_time": {
"name": "Use an Ultimate to deal the final blow 1 time",
"help": "",
"help": "Achievable by default, will do Himeko trial",
"achievable": "Achievable",
"not_set": "Not Set",
"not_supported": "Not Supported Yet"

View File

@ -437,28 +437,28 @@
},
"In_a_single_battle_inflict_3_Weakness_Break_of_different_Types": {
"name": "单场战斗中触发3种不同属性的弱点击破",
"help": "",
"help": "默认可完成,将做姬子试用",
"achievable": "可完成",
"not_set": "未设置",
"not_supported": "暂未支持"
},
"Inflict_Weakness_Break_5_times": {
"name": "累计触发弱点击破效果5次",
"help": "",
"help": "默认可完成,将做姬子试用",
"achievable": "可完成",
"not_set": "未设置",
"not_supported": "暂未支持"
},
"Defeat_a_total_of_20_enemies": {
"name": "累计消灭20个敌人",
"help": "",
"help": "默认可完成,将做姬子试用",
"achievable": "可完成",
"not_set": "未设置",
"not_supported": "暂未支持"
},
"Enter_combat_by_attacking_enemy_Weakness_and_win_3_times": {
"name": "利用弱点进入战斗并获胜3次",
"help": "",
"help": "默认可完成,将做姬子试用",
"achievable": "可完成",
"not_set": "未设置",
"not_supported": "暂未支持"
@ -486,14 +486,14 @@
},
"Destroy_3_destructible_objects": {
"name": "累计击碎3个可破坏物",
"help": "",
"help": "默认可完成,将做姬子试用",
"achievable": "可完成",
"not_set": "未设置",
"not_supported": "暂未支持"
},
"Complete_Forgotten_Hall_1_time": {
"name": "完成1次「忘却之庭」",
"help": "前四个角色打一次深渊一,请保证帐号练度足够打深渊一",
"help": "默认可完成,将使用前四个角色打一次深渊一,请保证帐号练度足够打深渊一",
"achievable": "可完成",
"not_set": "未设置",
"not_supported": "暂未支持"
@ -521,7 +521,7 @@
},
"Use_an_Ultimate_to_deal_the_final_blow_1_time": {
"name": "施放终结技造成制胜一击1次",
"help": "",
"help": "默认可完成,将做姬子试用",
"achievable": "可完成",
"not_set": "未设置",
"not_supported": "暂未支持"

View File

@ -437,35 +437,35 @@
},
"In_a_single_battle_inflict_3_Weakness_Break_of_different_Types": {
"name": "單場戰鬥中觸發3種不同屬性的弱點擊破",
"help": "",
"help": "預設可完成,將做姬子試用",
"achievable": "可完成",
"not_set": "未設定",
"not_supported": "暫未支援"
},
"Inflict_Weakness_Break_5_times": {
"name": "累積觸發弱點擊破效果5次",
"help": "",
"help": "預設可完成,將做姬子試用",
"achievable": "可完成",
"not_set": "未設定",
"not_supported": "暫未支援"
},
"Defeat_a_total_of_20_enemies": {
"name": "累積消滅20個敵人",
"help": "",
"help": "預設可完成,將做姬子試用",
"achievable": "可完成",
"not_set": "未設定",
"not_supported": "暫未支援"
},
"Enter_combat_by_attacking_enemy_Weakness_and_win_3_times": {
"name": "利用弱點進入戰鬥並獲勝3次",
"help": "",
"help": "預設可完成,將做姬子試用",
"achievable": "可完成",
"not_set": "未設定",
"not_supported": "暫未支援"
},
"Use_Technique_2_times": {
"name": "累積施放2次秘技",
"help": "默認可完成將前往深淵一施放2次秘技",
"help": "預設可完成將前往深淵一施放2次秘技",
"achievable": "可完成",
"not_set": "未設定",
"not_supported": "暫未支援"
@ -479,21 +479,21 @@
},
"Take_1_photo": {
"name": "拍照1次",
"help": "默认可完成",
"help": "預設可完成",
"achievable": "可完成",
"not_set": "未設定",
"not_supported": "暫未支援"
},
"Destroy_3_destructible_objects": {
"name": "累積擊碎3個可破壞物",
"help": "",
"help": "預設可完成,將做姬子試用",
"achievable": "可完成",
"not_set": "未設定",
"not_supported": "暫未支援"
},
"Complete_Forgotten_Hall_1_time": {
"name": "完成1次「忘卻之庭」",
"help": "前四個角色打一次深淵一,請確保帳號練度足夠打深淵一",
"help": "預設可完成,將使用前四個角色打一次深淵一,請確保帳號練度足夠打深淵一",
"achievable": "可完成",
"not_set": "未設定",
"not_supported": "暫未支援"
@ -521,7 +521,7 @@
},
"Use_an_Ultimate_to_deal_the_final_blow_1_time": {
"name": "施放終結技造成制勝一擊1次",
"help": "",
"help": "預設可完成,將做姬子試用",
"achievable": "可完成",
"not_set": "未設定",
"not_supported": "暫未支援"
@ -549,28 +549,28 @@
},
"Salvage_any_Relic": {
"name": "分解任意1件遺器",
"help": "默認可完成,將分解遺器稀有度倒序的第一個",
"help": "預設可完成,將分解遺器稀有度倒序的第一個",
"achievable": "可完成",
"not_set": "未設定",
"not_supported": "暫未支援"
},
"Synthesize_Consumable_1_time": {
"name": "合成1次消耗品",
"help": "默認可完成,將合成最低級零食",
"help": "預設可完成,將合成最低級零食",
"achievable": "可完成",
"not_set": "未設定",
"not_supported": "暫未支援"
},
"Synthesize_material_1_time": {
"name": "合成1次素材",
"help": "默認可完成,將合成最低級素材",
"help": "預設可完成,將合成最低級素材",
"achievable": "可完成",
"not_set": "未設定",
"not_supported": "暫未支援"
},
"Use_Consumables_1_time": {
"name": "使用1件消耗品",
"help": "默認可完成,將使用護具,無材料時先合成再使用",
"help": "預設可完成,將使用護具,無材料時先合成再使用",
"achievable": "可完成",
"not_set": "未設定",
"not_supported": "暫未支援"

122
route/daily/HimekoTrial.py Normal file
View File

@ -0,0 +1,122 @@
from module.logger import logger
from tasks.combat.combat import Combat
from tasks.daily.assets.assets_daily_trial import START_TRIAL
from tasks.daily.trail import CharacterTrial
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, Combat, CharacterTrial):
def handle_combat_state(self, auto=True, speed_2x=True):
# No auto in character trial
auto = False
return super().handle_combat_state(auto=auto, speed_2x=speed_2x)
def wait_next_skill(self, expected_end=None, skip_first_screenshot=True):
# Ended at START_TRIAL
def combat_end():
return self.match_template_color(START_TRIAL)
return super().wait_next_skill(expected_end=combat_end, skip_first_screenshot=skip_first_screenshot)
def combat_execute(self, expected_end=None):
# Battle 1/3
# Enemy cleared by follow up
self.wait_next_skill()
# Battle 2/3
# Himeko E
# Rest are cleared by follow up
self.use_E()
self.wait_next_skill()
# Battle 3/3
# Himeko E
self.use_E()
self.wait_next_skill()
# Herta A, or Natasha A, depends on who wasn't being attacked
self.use_A()
self.wait_next_skill()
# Natasha A, this will also cause weakness break
# To achieve In_a_single_battle_inflict_3_Weakness_Break_of_different_Types
self.use_A()
self.wait_next_skill()
# Himeko Q
# To achieve Use_an_Ultimate_to_deal_the_final_blow_1_time
# May kill the enemy
self.use_Q(1)
if not self.wait_next_skill():
return
# Herta Q
# To achieve Use_an_Ultimate_to_deal_the_final_blow_1_time
# May kill the enemy
self.use_Q(2)
if not self.wait_next_skill():
return
# Combat should end here, just incase
logger.warning(f'Himeko trial is not going as expected')
for _ in range(3):
self.use_E()
if not self.wait_next_skill():
return
def route_item_enemy(self):
self.enter_himeko_trial()
self.map_init(plane=Jarilo_BackwaterPass, position=(519.9, 361.5))
# Visit 3 items
self.clear_item(
Waypoint((587.6, 366.9)).run_2x(),
)
self.clear_item(
Waypoint((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.clear_enemy(
Waypoint((613.5, 427.3)),
)
def route_item(self):
self.enter_himeko_trial()
self.map_init(plane=Jarilo_BackwaterPass, position=(519.9, 361.5))
# Visit 3 items
self.clear_item(
Waypoint((587.6, 366.9)).run_2x(),
)
self.clear_item(
Waypoint((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(),
)
# Exit
self.exit_trial()
def route_enemy(self):
self.enter_himeko_trial()
self.map_init(plane=Jarilo_BackwaterPass, position=(519.9, 361.5))
# Goto boss
self.clear_enemy(
# Before the corner, turn right
Waypoint((571.7, 371.3)).run_2x(),
# Go through arched door
Waypoint((581.5, 383.3)).run_2x(),
# Boss
Waypoint((613.5, 427.3)).run_2x(),
)
def exit(self):
# Fake map_init to expose this method
# self.map_init(plane=Jarilo_BackwaterPass, position=(519.9, 361.5))
self.exit_trial_to_main()

View 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 ```
CHECK_A = ButtonWrapper(
name='CHECK_A',
share=Button(
file='./assets/share/combat/skill/CHECK_A.png',
area=(967, 513, 1087, 623),
search=(947, 493, 1107, 643),
color=(91, 91, 97),
button=(967, 513, 1087, 623),
),
)
CHECK_E = ButtonWrapper(
name='CHECK_E',
share=Button(
file='./assets/share/combat/skill/CHECK_E.png',
area=(1111, 429, 1261, 519),
search=(1091, 409, 1280, 539),
color=(80, 81, 89),
button=(1111, 429, 1261, 519),
),
)
IN_SKILL = ButtonWrapper(
name='IN_SKILL',
share=Button(
file='./assets/share/combat/skill/IN_SKILL.png',
area=(35, 619, 59, 635),
search=(15, 599, 79, 655),
color=(106, 107, 110),
button=(35, 619, 59, 635),
),
)
USE_A = ButtonWrapper(
name='USE_A',
share=Button(
file='./assets/share/combat/skill/USE_A.png',
area=(1037, 579, 1097, 639),
search=(1017, 559, 1117, 659),
color=(107, 108, 111),
button=(1037, 579, 1097, 639),
),
)
USE_E = ButtonWrapper(
name='USE_E',
share=Button(
file='./assets/share/combat/skill/USE_E.png',
area=(1154, 490, 1214, 550),
search=(1134, 470, 1234, 570),
color=(120, 123, 127),
button=(1154, 490, 1214, 550),
),
)
USE_Q1 = ButtonWrapper(
name='USE_Q1',
share=Button(
file='./assets/share/combat/skill/USE_Q1.png',
area=(217, 548, 253, 578),
search=(197, 528, 273, 598),
color=(145, 151, 225),
button=(217, 548, 253, 578),
),
)
USE_Q2 = ButtonWrapper(
name='USE_Q2',
share=Button(
file='./assets/share/combat/skill/USE_Q2.png',
area=(405, 546, 441, 576),
search=(385, 526, 461, 596),
color=(155, 131, 207),
button=(405, 546, 441, 576),
),
)
USE_Q3 = ButtonWrapper(
name='USE_Q3',
share=Button(
file='./assets/share/combat/skill/USE_Q3.png',
area=(593, 545, 629, 575),
search=(573, 525, 649, 595),
color=(208, 208, 202),
button=(593, 545, 629, 575),
),
)
USE_Q4 = ButtonWrapper(
name='USE_Q4',
share=Button(
file='./assets/share/combat/skill/USE_Q4.png',
area=(781, 545, 817, 575),
search=(761, 525, 837, 595),
color=(151, 113, 206),
button=(781, 545, 817, 575),
),
)
USE_Q_AIM = ButtonWrapper(
name='USE_Q_AIM',
share=Button(
file='./assets/share/combat/skill/USE_Q_AIM.png',
area=(1074, 521, 1174, 621),
search=(1054, 501, 1194, 641),
color=(82, 60, 64),
button=(1074, 521, 1174, 621),
),
)

View File

@ -6,13 +6,14 @@ from tasks.combat.assets.assets_combat_prepare import COMBAT_PREPARE
from tasks.combat.assets.assets_combat_team import COMBAT_TEAM_PREPARE, COMBAT_TEAM_SUPPORT
from tasks.combat.interact import CombatInteract
from tasks.combat.prepare import CombatPrepare
from tasks.combat.skill import CombatSkill
from tasks.combat.state import CombatState
from tasks.combat.support import CombatSupport
from tasks.combat.team import CombatTeam
from tasks.map.control.joystick import MapControlJoystick
class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSupport, MapControlJoystick):
class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSupport, CombatSkill, MapControlJoystick):
def handle_combat_prepare(self):
"""
Returns:

126
tasks/combat/skill.py Normal file
View File

@ -0,0 +1,126 @@
from module.base.button import match_template
from module.base.timer import Timer
from module.logger import logger
from tasks.base.ui import UI
from tasks.combat.assets.assets_combat_skill import *
class CombatSkill(UI):
def is_in_skill(self) -> bool:
"""
Combat paused, require manual skill use
"""
if not self.appear(IN_SKILL):
return False
if not self.image_color_count(IN_SKILL, color=(255, 255, 255), threshold=221, count=50):
return False
return True
def _skill_click(self, button, skip_first_screenshot=True):
"""
Click a skill button.
Not in skill page means skill has been used and skill animation is ongoing
"""
logger.info(f'Skill use: {button}')
interval = Timer(1)
clicked = False
prev_image = None
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
if self.is_in_skill():
if interval.reached():
prev_image = self.image_crop(button)
self.device.click(button)
interval.reset()
clicked = True
continue
else:
# Skill animation on going
if clicked:
logger.info(f'Skill used: {button}')
break
# New skill icon
if prev_image is not None:
if not match_template(self.image_crop(button), prev_image):
logger.info(f'Skill used: {button}')
break
def _skill_switch(self, check_button, click_button, skip_first_screenshot=True):
"""
Switch to A or E
"""
logger.info(f'Skill switch: {check_button}')
interval = Timer(1)
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
# Raw brown border
if self.image_color_count(check_button, color=(220, 196, 145), threshold=221, count=50):
logger.info(f'Skill switched: {check_button}')
break
if self.is_in_skill():
if interval.reached():
self.device.click(click_button)
interval.reset()
continue
def wait_next_skill(self, expected_end=None, skip_first_screenshot=True):
"""
Args:
expected_end: A function returns bool, True represents end.
skip_first_screenshot:
Returns:
bool: True if is_in_skill
False if triggered expected_end
"""
logger.info('Wait next skill')
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
if self.is_in_skill():
return True
if callable(expected_end) and expected_end():
logger.info(f'Combat execute ended at {expected_end.__name__}')
return False
def use_A(self) -> bool:
logger.hr('Use A')
self._skill_switch(check_button=CHECK_A, click_button=USE_A)
self._skill_click(USE_A)
return True
def use_E(self) -> bool:
logger.hr('Use E')
self._skill_switch(check_button=CHECK_E, click_button=USE_E)
self._skill_click(USE_E)
return True
def use_Q(self, position: int) -> bool:
"""
Args:
position: 1 to 4
"""
logger.hr(f'Use Q {position}')
try:
button = [USE_Q1, USE_Q2, USE_Q3, USE_Q4][position - 1]
except IndexError:
logger.error(f'use_Q: position {position} does not exist')
return False
self._skill_click(button)
self._skill_click(USE_Q_AIM)
return True

View File

@ -0,0 +1,75 @@
from module.base.button import Button, ButtonWrapper
# This file was auto-generated, do not modify it manually. To generate:
# ``` python -m dev_tools.button_extract ```
CHARACTER_TRIAL = ButtonWrapper(
name='CHARACTER_TRIAL',
share=Button(
file='./assets/share/daily/trial/CHARACTER_TRIAL.png',
area=(416, 672, 559, 677),
search=(396, 652, 579, 697),
color=(238, 238, 239),
button=(414, 649, 561, 678),
),
)
HIMEKO_CHECK = ButtonWrapper(
name='HIMEKO_CHECK',
share=Button(
file='./assets/share/daily/trial/HIMEKO_CHECK.png',
area=(597, 165, 646, 192),
search=(577, 145, 666, 212),
color=(207, 137, 123),
button=(597, 165, 646, 192),
),
)
HIMEKO_CLICK = ButtonWrapper(
name='HIMEKO_CLICK',
share=Button(
file='./assets/share/daily/trial/HIMEKO_CLICK.png',
area=(88, 142, 125, 178),
search=(42, 124, 176, 425),
color=(202, 144, 136),
button=(88, 142, 125, 178),
),
)
INFO_CLOSE = ButtonWrapper(
name='INFO_CLOSE',
share=Button(
file='./assets/share/daily/trial/INFO_CLOSE.png',
area=(1129, 222, 1159, 252),
search=(1109, 202, 1179, 272),
color=(69, 69, 70),
button=(1129, 222, 1159, 252),
),
)
REGULAR_GACHA_CHECK = ButtonWrapper(
name='REGULAR_GACHA_CHECK',
share=Button(
file='./assets/share/daily/trial/REGULAR_GACHA_CHECK.png',
area=(69, 332, 124, 367),
search=(15, 146, 150, 478),
color=(100, 92, 82),
button=(69, 332, 124, 367),
),
)
REGULAR_GACHA_CLICK = ButtonWrapper(
name='REGULAR_GACHA_CLICK',
share=Button(
file='./assets/share/daily/trial/REGULAR_GACHA_CLICK.png',
area=(63, 324, 118, 349),
search=(15, 146, 150, 478),
color=(36, 34, 30),
button=(63, 324, 118, 349),
),
)
START_TRIAL = ButtonWrapper(
name='START_TRIAL',
share=Button(
file='./assets/share/daily/trial/START_TRIAL.png',
area=(905, 634, 919, 674),
search=(885, 614, 939, 694),
color=(240, 238, 238),
button=(919, 636, 1191, 672),
),
)

View File

@ -23,6 +23,7 @@ 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
from tasks.map.route.route import ROUTE_DAILY
class DailyQuestOcr(Ocr):
@ -260,9 +261,50 @@ class DailyQuestUI(DungeonUI, RouteLoader):
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')
self.route_run(ROUTE_DAILY.ForgottenHallStage1__route)
done += 1
"""
enemy x1 In_a_single_battle_inflict_3_Weakness_Break_of_different_Types
enemy x1 Inflict_Weakness_Break_5_times
enemy x2 Defeat_a_total_of_20_enemies
enemy x3 Enter_combat_by_attacking_enemy_Weakness_and_win_3_times
item x1 Destroy_3_destructible_objects
enemy x1 Use_an_Ultimate_to_deal_the_final_blow_1_time
"""
enemy = 0
item = 0
quests = [
KEYWORDS_DAILY_QUEST.Enter_combat_by_attacking_enemy_Weakness_and_win_3_times,
]
if KEYWORDS_DAILY_QUEST.In_a_single_battle_inflict_3_Weakness_Break_of_different_Types in quests:
enemy = max(enemy, 1)
if KEYWORDS_DAILY_QUEST.Inflict_Weakness_Break_5_times in quests:
enemy = max(enemy, 1)
if KEYWORDS_DAILY_QUEST.Defeat_a_total_of_20_enemies in quests:
enemy = max(enemy, 2)
if KEYWORDS_DAILY_QUEST.Enter_combat_by_attacking_enemy_Weakness_and_win_3_times in quests:
enemy = max(enemy, 3)
if KEYWORDS_DAILY_QUEST.Destroy_3_destructible_objects in quests:
item = max(item, 1)
if KEYWORDS_DAILY_QUEST.Use_an_Ultimate_to_deal_the_final_blow_1_time in quests:
enemy = max(enemy, 1)
logger.info(f'Himeko trial, enemy={enemy}, item={item}')
for run in [1, 2, 3]:
if enemy >= run and item >= run:
self.route_run(ROUTE_DAILY.HimekoTrial__route_item_enemy)
done += 1
elif enemy >= run:
self.route_run(ROUTE_DAILY.HimekoTrial__route_enemy)
done += 1
elif item >= run:
self.route_run(ROUTE_DAILY.HimekoTrial__route_item)
done += 1
else:
break
if max(enemy, item) > 0:
self.route_run(ROUTE_DAILY.HimekoTrial__exit)
return done
def run(self):

119
tasks/daily/trail.py Normal file
View File

@ -0,0 +1,119 @@
from module.base.timer import Timer
from module.logger import logger
from tasks.base.assets.assets_base_page import CLOSE, MAP_EXIT
from tasks.base.page import page_gacha
from tasks.base.ui import UI
from tasks.daily.assets.assets_daily_trial import *
class CharacterTrial(UI):
def enter_himeko_trial(self):
"""
Pages:
in: Any
out: page_main, in himeko trial
"""
logger.info('Enter Himeko trial')
switched = False
if self.match_template_color(HIMEKO_CHECK):
logger.info(f'Already at {HIMEKO_CHECK}')
elif self.match_template_color(REGULAR_GACHA_CHECK):
logger.info(f'Already at {REGULAR_GACHA_CHECK}')
elif self.ui_page_appear(page_gacha):
logger.info(f'Already at {page_gacha}')
else:
switched = self.ui_ensure(page_gacha)
# page_gacha -> in himeko trial
skip_first_screenshot = True
info_closed = False
info_timeout = Timer(2, count=4).start()
first_gacha = switched
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
# End
if self.is_in_main():
if info_closed:
logger.info('In Himeko trial')
break
if info_timeout.reached():
logger.info('In Himeko trial (wait INFO_CLOSE timeout)')
break
else:
info_timeout.reset()
# In map
if self.appear_then_click(INFO_CLOSE):
info_closed = True
continue
# Switch to Himeko trial
if self.appear(START_TRIAL) \
and not self.appear(HIMEKO_CHECK) \
and self.appear_then_click(HIMEKO_CLICK, interval=2):
continue
if self.appear(HIMEKO_CHECK) \
and self.match_template_color(START_TRIAL, interval=2):
self.device.click(START_TRIAL)
continue
# Switch to regular trial
if self.match_template_color(REGULAR_GACHA_CHECK) \
and self.match_color(CHARACTER_TRIAL, interval=2):
self.device.click(CHARACTER_TRIAL)
continue
if self.match_template_color(REGULAR_GACHA_CLICK, interval=2):
# Poor sleep indeed, clicks won't be response unless other elements are loaded
# Waiting for gacha banner moving
if first_gacha:
self.device.sleep(0.3)
first_gacha = False
self.device.click(REGULAR_GACHA_CLICK)
continue
def exit_trial(self, skip_first_screenshot=True):
"""
Pages:
in: page_main, in trial
out: START_TRIAL
"""
logger.info('Exit trial')
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
if self.match_template_color(START_TRIAL):
break
if self.appear_then_click(MAP_EXIT):
continue
if self.handle_popup_confirm():
continue
def exit_trial_to_main(self, skip_first_screenshot=True):
"""
Pages:
in: START_TRIAL
out: page_main
"""
logger.info('Exit trial to main')
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
if self.is_in_main():
break
if self.match_template_color(START_TRIAL, interval=2):
logger.info(f'{START_TRIAL} -> {CLOSE}')
self.device.click(CLOSE)
continue
if self.match_color(CHARACTER_TRIAL, interval=2):
logger.info(f'{CHARACTER_TRIAL} -> {CLOSE}')
self.device.click(CLOSE)
continue

View File

@ -2,6 +2,7 @@ from functools import cached_property
from module.base.timer import Timer
from module.logger import logger
from tasks.base.assets.assets_base_page import CLOSE
from tasks.combat.combat import Combat
from tasks.map.assets.assets_map_control import ROTATION_SWIPE_AREA
from tasks.map.control.joystick import JoystickContact
@ -74,6 +75,18 @@ class MapControl(Combat, AimDetectorMixin):
interval.reset()
continue
def walk_additional(self) -> bool:
"""
Handle popups during walk
Returns:
bool: If handled
"""
if self.appear_then_click(CLOSE):
return True
return False
def _goto(
self,
contact: JoystickContact,
@ -139,6 +152,10 @@ class MapControl(Combat, AimDetectorMixin):
self.combat_execute()
if waypoint.early_stop:
return result
if self.walk_additional():
attacked_enemy.clear()
attacked_item.clear()
continue
# The following detection require page_main
if not self.is_in_main():

View File

@ -1,8 +1,19 @@
from dataclasses import dataclass
from tasks.map.control.control import MapControl
from tasks.map.control.waypoint import Waypoint
from tasks.map.keywords import MapPlane
@dataclass
class RouteData:
name: str
route: str
plane: str
floor: str = 'F1'
position: tuple = None
class RouteBase(MapControl):
"""
Base class of `Route`

View File

@ -1,38 +1,71 @@
import importlib
import os
from module.exception import RequestHumanTakeover
from module.base.decorator import del_cached_property
from module.exception import ScriptError
from module.logger import logger
from tasks.base.ui import UI
from tasks.map.route.base import RouteBase
from tasks.map.route.base import RouteBase, RouteData
class RouteLoader(UI):
route: RouteBase
route_module: str = ''
route_func: str = ''
route_obj: RouteBase
def route_run(self, route: str):
def route_delete(self):
del_cached_property(self, 'route_obj')
self.route_module = ''
self.route_func = ''
def route_run(self, route: RouteData | str):
"""
Args:
route: .py module path such as `daily.forgotten_hall.stage1`
which will load `./route/daily/forgotten_hall/stage1.py`
route: .py module path such as `route.daily.ForgottenHallStage1:route`
which will load `./route/daily/ForgottenHallStage1.py` and run `Route.route()`
"""
folder, name = route.rsplit('.', maxsplit=1)
path = f'./route/{route.replace(".", "/")}.py'
logger.hr('Route run', level=1)
if isinstance(route, RouteData):
route = route.route
logger.attr('Route', route)
try:
module = importlib.import_module(f'route.{folder}.{name}')
module, func = route.split(':')
except ValueError:
logger.critical(f'Route invalid: {route}')
raise ScriptError
path = f'./{module.replace(".", "/")}.py'
# Import route file
try:
module_obj = importlib.import_module(f'{module}')
except ModuleNotFoundError:
logger.critical(f'Route file not found: {route} ({path})')
logger.critical(f'Route file not found: {module} ({path})')
if not os.path.exists(path):
logger.critical(f'Route file not exists: {path}')
raise RequestHumanTakeover
raise ScriptError
# config = copy.deepcopy(self.config).merge(module.Config())
config = self.config
device = self.device
# Create route object
# Reuse the previous one
if self.route_module != module:
# config = copy.deepcopy(self.config).merge(module.Config())
config = self.config
device = self.device
try:
self.route_obj = module_obj.Route(config=config, device=device)
except AttributeError as e:
logger.critical(e)
logger.critical(f'Route file {route} ({path}) must define class Route')
raise ScriptError
self.route_module = module
# Get route func
try:
self.route = module.Route(config=config, device=device)
return self.route.route()
func_obj = self.route_obj.__getattribute__(func)
except AttributeError as e:
logger.critical(e)
logger.critical(f'Route file {route} ({path}) must define Route.route()')
raise RequestHumanTakeover
logger.critical(f'Route class in {route} ({path}) does not have method {func}')
raise ScriptError
self.route_func = func
# Run
func_obj()

View File

@ -0,0 +1 @@
import tasks.map.route.route.daily as ROUTE_DAILY

View File

@ -0,0 +1,38 @@
from tasks.map.route.base import RouteData
ForgottenHallStage1__route = RouteData(
name='ForgottenHallStage1__route',
route='route.daily.ForgottenHallStage1:route',
plane='Jarilo_BackwaterPass',
floor='F1',
position=(369.4, 643.4),
)
HimekoTrial__route_item_enemy = RouteData(
name='HimekoTrial__route_item_enemy',
route='route.daily.HimekoTrial:route_item_enemy',
plane='Jarilo_BackwaterPass',
floor='F1',
position=(519.9, 361.5),
)
HimekoTrial__route_item = RouteData(
name='HimekoTrial__route_item',
route='route.daily.HimekoTrial:route_item',
plane='Jarilo_BackwaterPass',
floor='F1',
position=(519.9, 361.5),
)
HimekoTrial__route_enemy = RouteData(
name='HimekoTrial__route_enemy',
route='route.daily.HimekoTrial:route_enemy',
plane='Jarilo_BackwaterPass',
floor='F1',
position=(519.9, 361.5),
)
HimekoTrial__exit = RouteData(
name='HimekoTrial__exit',
route='route.daily.HimekoTrial:exit',
plane='Jarilo_BackwaterPass',
floor='F1',
position=(519.9, 361.5),
)