Add: Task assignment
BIN
assets/cn/assignment/claim/CLAIM.BUTTON.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/cn/assignment/claim/CLAIM.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
assets/cn/assignment/claim/CLOSE_REPORT.BUTTON.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
assets/cn/assignment/claim/CLOSE_REPORT.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
assets/cn/assignment/claim/REDISPATCH.BUTTON.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/cn/assignment/claim/REDISPATCH.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
assets/cn/assignment/dispatch/CHARACTER_LIST.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
assets/cn/assignment/dispatch/CONFIRM_ASSIGNMENT.BUTTON.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
assets/cn/assignment/dispatch/CONFIRM_ASSIGNMENT.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
assets/cn/assignment/ui/DISPATCHED.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
assets/share/assignment/claim/OCR_ASSIGNMENT_REPORT_TIME.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
assets/share/assignment/dispatch/ASSIGNMENT_STARTED_CHECK.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
assets/share/assignment/dispatch/CHARACTER_1.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
assets/share/assignment/dispatch/CHARACTER_2.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
assets/share/assignment/dispatch/DURATION_12.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/share/assignment/dispatch/DURATION_20.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/share/assignment/dispatch/DURATION_4.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
assets/share/assignment/dispatch/DURATION_8.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/share/assignment/dispatch/EMPTY_SLOT.SEARCH.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
assets/share/assignment/dispatch/EMPTY_SLOT.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
assets/share/assignment/ui/CHARACTER_MATERIALS.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/share/assignment/ui/ENTRY_LOADED.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
assets/share/assignment/ui/EXP_MATERIALS_CREDITS.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
assets/share/assignment/ui/OCR_ASSIGNMENT_LIMIT.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
assets/share/assignment/ui/OCR_ASSIGNMENT_LIST.png
Normal file
After Width: | Height: | Size: 127 KiB |
BIN
assets/share/assignment/ui/OCR_ASSIGNMENT_TIME.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
assets/share/assignment/ui/SYNTHESIS_MATERIALS.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
assets/share/base/page/ASSIGNMENT_CHECK.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
assets/share/base/page/MENU_GOTO_ASSIGNMENT.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
@ -2,6 +2,7 @@ import os
|
||||
import re
|
||||
import typing as t
|
||||
from functools import cached_property
|
||||
from collections import namedtuple
|
||||
|
||||
from module.base.code_generator import CodeGenerator
|
||||
from module.config.utils import deep_get, read_file
|
||||
@ -12,7 +13,7 @@ UI_LANGUAGES = ['cn', 'cht', 'en', 'jp']
|
||||
|
||||
def text_to_variable(text):
|
||||
text = re.sub("'s |s' ", '_', text)
|
||||
text = re.sub('[ \-—:\']+', '_', text)
|
||||
text = re.sub('[ \-—:\'/]+', '_', text)
|
||||
text = re.sub(r'[(),#]|</?\w+>', '', text)
|
||||
# text = re.sub(r'[#_]?\d+(_times?)?', '', text)
|
||||
return text
|
||||
@ -154,6 +155,16 @@ class KeywordExtract:
|
||||
quest_keywords = [self.text_map[lang].find(quest_hash)[1] for quest_hash in quests_hash]
|
||||
self.load_keywords(quest_keywords, lang)
|
||||
|
||||
def generate_assignment_keywords(self):
|
||||
KeywordFromFile = namedtuple('KeywordFromFile', ('file', 'class_name', 'output_file'))
|
||||
for keyword in (
|
||||
KeywordFromFile('ExpeditionGroup.json', 'AssignmentGroup', './tasks/assignment/keywords/group.py'),
|
||||
KeywordFromFile('ExpeditionData.json', 'AssignmentEntry','./tasks/assignment/keywords/entry.py')
|
||||
):
|
||||
file = os.path.join(TextMap.DATA_FOLDER, 'ExcelOutput', keyword.file)
|
||||
self.load_keywords(deep_get(data, 'Name.Hash') for data in read_file(file).values())
|
||||
self.write_keywords(keyword_class=keyword.class_name, output_file=keyword.output_file)
|
||||
|
||||
def generate(self):
|
||||
self.load_keywords(['模拟宇宙', '拟造花萼(金)', '拟造花萼(赤)', '凝滞虚影', '侵蚀隧洞', '历战余响', '忘却之庭'])
|
||||
self.write_keywords(keyword_class='DungeonNav', output_file='./tasks/dungeon/keywords/nav.py')
|
||||
@ -170,6 +181,7 @@ class KeywordExtract:
|
||||
self.write_keywords(keyword_class='DungeonEntrance', output_file='./tasks/dungeon/keywords/dungeon_entrance.py')
|
||||
self.load_keywords(['奖励', '任务'])
|
||||
self.write_keywords(keyword_class='BattlePassTab', output_file='./tasks/battle_pass/keywords/tab.py')
|
||||
self.generate_assignment_keywords()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -26,6 +26,9 @@ class Keyword:
|
||||
"""
|
||||
Instance attributes and methods
|
||||
"""
|
||||
@cached_property
|
||||
def ch(self) -> str:
|
||||
return self.cn
|
||||
|
||||
@cached_property
|
||||
def cn_parsed(self) -> str:
|
||||
|
@ -1,7 +1,8 @@
|
||||
import re
|
||||
import time
|
||||
from datetime import timedelta
|
||||
|
||||
import cv2
|
||||
import re
|
||||
from ppocronnx.predict_system import BoxedResult
|
||||
|
||||
import module.config.server as server
|
||||
@ -257,3 +258,44 @@ class DigitCounter(Ocr):
|
||||
else:
|
||||
logger.warning(f'No digit counter found in {result}')
|
||||
return 0, 0, 0
|
||||
|
||||
|
||||
class Duration(Ocr):
|
||||
@cached_property
|
||||
def timedelta_regex(self):
|
||||
hour_regex = {
|
||||
'ch': '小时',
|
||||
'en': 'h\s*'
|
||||
}[self.lang]
|
||||
minute_regex = {
|
||||
'ch': '分钟',
|
||||
'en': 'm\s*'
|
||||
}[self.lang]
|
||||
second_regex = {
|
||||
'ch': '秒',
|
||||
'en': 's'
|
||||
}[self.lang]
|
||||
ret = rf'\D*((?P<hours>\d{{1,2}}){hour_regex})?'
|
||||
ret += rf'((?P<minutes>\d{{1,2}}){minute_regex})?'
|
||||
ret += rf'((?P<seconds>\d{{1,2}}){second_regex})?'
|
||||
return re.compile(ret)
|
||||
|
||||
def format_result(self, result: str) -> timedelta:
|
||||
"""
|
||||
Do OCR on a duration, such as `2h 13m 30s`, `2h`, `13m 30s`, `9s`
|
||||
|
||||
Returns:
|
||||
timedelta:
|
||||
"""
|
||||
matched = self.timedelta_regex.match(result)
|
||||
if matched is None:
|
||||
return timedelta()
|
||||
hours = self._sanitize_number(matched.group('hours'))
|
||||
minutes = self._sanitize_number(matched.group('minutes'))
|
||||
seconds = self._sanitize_number(matched.group('seconds'))
|
||||
return timedelta(hours=hours, minutes=minutes, seconds=seconds)
|
||||
|
||||
def _sanitize_number(self, number) -> int:
|
||||
if number is None:
|
||||
return 0
|
||||
return int(number)
|
||||
|
@ -28,6 +28,7 @@ class DraggableList:
|
||||
keyword_class,
|
||||
ocr_class,
|
||||
search_button: ButtonWrapper,
|
||||
active_color: tuple[int, int, int]
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
@ -42,6 +43,7 @@ class DraggableList:
|
||||
keyword_class = keyword_class[0]
|
||||
self.known_rows = list(keyword_class.instances.values())
|
||||
self.search_button = search_button
|
||||
self.active_color = active_color
|
||||
|
||||
self.row_min = 1
|
||||
self.row_max = len(self.known_rows)
|
||||
@ -83,12 +85,14 @@ class DraggableList:
|
||||
self.cur_buttons = self.ocr_class(self.search_button) \
|
||||
.matched_ocr(main.device.image, self.keyword_class)
|
||||
# Get indexes
|
||||
indexes = [self.keyword2index(row.matched_keyword) for row in self.cur_buttons]
|
||||
indexes = [self.keyword2index(row.matched_keyword)
|
||||
for row in self.cur_buttons]
|
||||
indexes = [index for index in indexes if index]
|
||||
# Check row order
|
||||
if len(indexes) >= 2:
|
||||
if not np.all(np.diff(indexes) > 0):
|
||||
logger.warning(f'Rows given to {self} are not ascending sorted')
|
||||
logger.warning(
|
||||
f'Rows given to {self} are not ascending sorted')
|
||||
if not indexes:
|
||||
logger.warning(f'No valid rows loaded into {self}')
|
||||
return
|
||||
@ -157,7 +161,8 @@ class DraggableList:
|
||||
elif self.cur_max < row_index:
|
||||
self.drag_page('down', main=main)
|
||||
# Wait for bottoming out
|
||||
main.wait_until_stable(self.search_button, timer=Timer(0, count=0), timeout=Timer(1.5, count=5))
|
||||
main.wait_until_stable(self.search_button, timer=Timer(
|
||||
0, count=0), timeout=Timer(1.5, count=5))
|
||||
skip_first_screenshot = True
|
||||
|
||||
return True
|
||||
@ -168,7 +173,7 @@ class DraggableList:
|
||||
return False
|
||||
|
||||
# Having gold letters
|
||||
if main.image_color_count(button, color=(190, 175, 124), threshold=221, count=50):
|
||||
if main.image_color_count(button, color=self.active_color, threshold=221, count=50):
|
||||
return True
|
||||
|
||||
return False
|
||||
@ -183,7 +188,8 @@ class DraggableList:
|
||||
Returns:
|
||||
If success
|
||||
"""
|
||||
result = self.insight_row(row, main=main, skip_first_screenshot=skip_first_screenshot)
|
||||
result = self.insight_row(
|
||||
row, main=main, skip_first_screenshot=skip_first_screenshot)
|
||||
if not result:
|
||||
return False
|
||||
|
||||
|
@ -7,7 +7,7 @@ from module.logger import logger
|
||||
|
||||
class Switch:
|
||||
"""
|
||||
A wrapper to handle switches in game, switch among states with reties.
|
||||
A wrapper to handle switches in game, switch among states with retries.
|
||||
|
||||
Examples:
|
||||
# Definitions
|
||||
|
45
tasks/assignment/assets/assets_assignment_claim.py
Normal file
@ -0,0 +1,45 @@
|
||||
from module.base.button import Button, ButtonWrapper
|
||||
|
||||
# This file was auto-generated, do not modify it manually. To generate:
|
||||
# ``` python -m dev_tools.button_extract ```
|
||||
|
||||
CLAIM = ButtonWrapper(
|
||||
name='CLAIM',
|
||||
cn=Button(
|
||||
file='./assets/cn/assignment/claim/CLAIM.png',
|
||||
area=(1031, 652, 1101, 674),
|
||||
search=(1011, 632, 1121, 694),
|
||||
color=(169, 134, 66),
|
||||
button=(920, 644, 1210, 683),
|
||||
),
|
||||
)
|
||||
CLOSE_REPORT = ButtonWrapper(
|
||||
name='CLOSE_REPORT',
|
||||
cn=Button(
|
||||
file='./assets/cn/assignment/claim/CLOSE_REPORT.png',
|
||||
area=(397, 598, 472, 623),
|
||||
search=(377, 578, 492, 643),
|
||||
color=(159, 157, 153),
|
||||
button=(290, 592, 579, 630),
|
||||
),
|
||||
)
|
||||
OCR_ASSIGNMENT_REPORT_TIME = ButtonWrapper(
|
||||
name='OCR_ASSIGNMENT_REPORT_TIME',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/claim/OCR_ASSIGNMENT_REPORT_TIME.png',
|
||||
area=(894, 191, 1003, 216),
|
||||
search=(874, 171, 1023, 236),
|
||||
color=(62, 63, 63),
|
||||
button=(894, 191, 1003, 216),
|
||||
),
|
||||
)
|
||||
REDISPATCH = ButtonWrapper(
|
||||
name='REDISPATCH',
|
||||
cn=Button(
|
||||
file='./assets/cn/assignment/claim/REDISPATCH.png',
|
||||
area=(784, 598, 901, 622),
|
||||
search=(764, 578, 921, 642),
|
||||
color=(158, 157, 155),
|
||||
button=(700, 592, 987, 629),
|
||||
),
|
||||
)
|
105
tasks/assignment/assets/assets_assignment_dispatch.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 ```
|
||||
|
||||
ASSIGNMENT_STARTED_CHECK = ButtonWrapper(
|
||||
name='ASSIGNMENT_STARTED_CHECK',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/dispatch/ASSIGNMENT_STARTED_CHECK.png',
|
||||
area=(1174, 297, 1211, 514),
|
||||
search=(1154, 277, 1231, 534),
|
||||
color=(86, 81, 78),
|
||||
button=(1174, 297, 1211, 514),
|
||||
),
|
||||
)
|
||||
CHARACTER_1 = ButtonWrapper(
|
||||
name='CHARACTER_1',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/dispatch/CHARACTER_1.png',
|
||||
area=(116, 212, 206, 312),
|
||||
search=(96, 192, 226, 332),
|
||||
color=(149, 134, 123),
|
||||
button=(116, 212, 206, 312),
|
||||
),
|
||||
)
|
||||
CHARACTER_2 = ButtonWrapper(
|
||||
name='CHARACTER_2',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/dispatch/CHARACTER_2.png',
|
||||
area=(228, 211, 318, 311),
|
||||
search=(208, 191, 338, 331),
|
||||
color=(184, 161, 172),
|
||||
button=(228, 211, 318, 311),
|
||||
),
|
||||
)
|
||||
CHARACTER_LIST = ButtonWrapper(
|
||||
name='CHARACTER_LIST',
|
||||
cn=Button(
|
||||
file='./assets/cn/assignment/dispatch/CHARACTER_LIST.png',
|
||||
area=(90, 165, 170, 186),
|
||||
search=(70, 145, 190, 206),
|
||||
color=(156, 154, 152),
|
||||
button=(90, 165, 170, 186),
|
||||
),
|
||||
)
|
||||
CONFIRM_ASSIGNMENT = ButtonWrapper(
|
||||
name='CONFIRM_ASSIGNMENT',
|
||||
cn=Button(
|
||||
file='./assets/cn/assignment/dispatch/CONFIRM_ASSIGNMENT.png',
|
||||
area=(1024, 653, 1104, 672),
|
||||
search=(1004, 633, 1124, 692),
|
||||
color=(154, 154, 153),
|
||||
button=(920, 645, 1208, 682),
|
||||
),
|
||||
)
|
||||
DURATION_12 = ButtonWrapper(
|
||||
name='DURATION_12',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/dispatch/DURATION_12.png',
|
||||
area=(762, 563, 862, 588),
|
||||
search=(742, 543, 882, 608),
|
||||
color=(63, 58, 50),
|
||||
button=(762, 563, 862, 588),
|
||||
),
|
||||
)
|
||||
DURATION_20 = ButtonWrapper(
|
||||
name='DURATION_20',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/dispatch/DURATION_20.png',
|
||||
area=(882, 564, 982, 589),
|
||||
search=(862, 544, 1002, 609),
|
||||
color=(64, 60, 52),
|
||||
button=(882, 564, 982, 589),
|
||||
),
|
||||
)
|
||||
DURATION_4 = ButtonWrapper(
|
||||
name='DURATION_4',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/dispatch/DURATION_4.png',
|
||||
area=(522, 564, 622, 589),
|
||||
search=(502, 544, 642, 609),
|
||||
color=(164, 142, 109),
|
||||
button=(522, 564, 622, 589),
|
||||
),
|
||||
)
|
||||
DURATION_8 = ButtonWrapper(
|
||||
name='DURATION_8',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/dispatch/DURATION_8.png',
|
||||
area=(640, 564, 740, 589),
|
||||
search=(620, 544, 760, 609),
|
||||
color=(63, 58, 49),
|
||||
button=(640, 564, 740, 589),
|
||||
),
|
||||
)
|
||||
EMPTY_SLOT = ButtonWrapper(
|
||||
name='EMPTY_SLOT',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/dispatch/EMPTY_SLOT.png',
|
||||
area=(1075, 562, 1110, 597),
|
||||
search=(1054, 542, 1220, 616),
|
||||
color=(200, 200, 195),
|
||||
button=(1075, 562, 1110, 597),
|
||||
),
|
||||
)
|
85
tasks/assignment/assets/assets_assignment_ui.py
Normal file
@ -0,0 +1,85 @@
|
||||
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_MATERIALS = ButtonWrapper(
|
||||
name='CHARACTER_MATERIALS',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/ui/CHARACTER_MATERIALS.png',
|
||||
area=(123, 81, 307, 134),
|
||||
search=(103, 61, 327, 154),
|
||||
color=(234, 233, 229),
|
||||
button=(123, 81, 307, 134),
|
||||
),
|
||||
)
|
||||
DISPATCHED = ButtonWrapper(
|
||||
name='DISPATCHED',
|
||||
cn=Button(
|
||||
file='./assets/cn/assignment/ui/DISPATCHED.png',
|
||||
area=(1032, 652, 1095, 674),
|
||||
search=(1012, 632, 1115, 694),
|
||||
color=(99, 93, 85),
|
||||
button=(1032, 652, 1095, 674),
|
||||
),
|
||||
)
|
||||
ENTRY_LOADED = ButtonWrapper(
|
||||
name='ENTRY_LOADED',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/ui/ENTRY_LOADED.png',
|
||||
area=(446, 164, 466, 615),
|
||||
search=(426, 144, 486, 635),
|
||||
color=(203, 202, 194),
|
||||
button=(446, 164, 466, 615),
|
||||
),
|
||||
)
|
||||
EXP_MATERIALS_CREDITS = ButtonWrapper(
|
||||
name='EXP_MATERIALS_CREDITS',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/ui/EXP_MATERIALS_CREDITS.png',
|
||||
area=(343, 83, 527, 133),
|
||||
search=(323, 63, 547, 153),
|
||||
color=(222, 221, 217),
|
||||
button=(343, 83, 527, 133),
|
||||
),
|
||||
)
|
||||
OCR_ASSIGNMENT_LIMIT = ButtonWrapper(
|
||||
name='OCR_ASSIGNMENT_LIMIT',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/ui/OCR_ASSIGNMENT_LIMIT.png',
|
||||
area=(1095, 86, 1179, 126),
|
||||
search=(1075, 66, 1199, 146),
|
||||
color=(62, 61, 60),
|
||||
button=(1095, 86, 1179, 126),
|
||||
),
|
||||
)
|
||||
OCR_ASSIGNMENT_LIST = ButtonWrapper(
|
||||
name='OCR_ASSIGNMENT_LIST',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/ui/OCR_ASSIGNMENT_LIST.png',
|
||||
area=(141, 160, 502, 621),
|
||||
search=(121, 140, 522, 641),
|
||||
color=(202, 200, 194),
|
||||
button=(141, 160, 502, 621),
|
||||
),
|
||||
)
|
||||
OCR_ASSIGNMENT_TIME = ButtonWrapper(
|
||||
name='OCR_ASSIGNMENT_TIME',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/ui/OCR_ASSIGNMENT_TIME.png',
|
||||
area=(588, 566, 926, 588),
|
||||
search=(568, 546, 946, 608),
|
||||
color=(128, 111, 89),
|
||||
button=(588, 566, 926, 588),
|
||||
),
|
||||
)
|
||||
SYNTHESIS_MATERIALS = ButtonWrapper(
|
||||
name='SYNTHESIS_MATERIALS',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/ui/SYNTHESIS_MATERIALS.png',
|
||||
area=(558, 85, 748, 135),
|
||||
search=(538, 65, 768, 155),
|
||||
color=(230, 229, 225),
|
||||
button=(558, 85, 748, 135),
|
||||
),
|
||||
)
|
126
tasks/assignment/assignment.py
Normal file
@ -0,0 +1,126 @@
|
||||
from datetime import datetime
|
||||
|
||||
from module.logger import logger
|
||||
from module.ocr.ocr import Duration
|
||||
from tasks.assignment.assets.assets_assignment_claim import CLAIM
|
||||
from tasks.assignment.assets.assets_assignment_dispatch import EMPTY_SLOT
|
||||
from tasks.assignment.assets.assets_assignment_ui import (DISPATCHED,
|
||||
OCR_ASSIGNMENT_TIME)
|
||||
from tasks.assignment.claim import AssignmentClaim
|
||||
from tasks.assignment.keywords import *
|
||||
from tasks.base.page import page_assignment
|
||||
|
||||
|
||||
class Assignment(AssignmentClaim):
|
||||
def run(self, assignments: list[AssignmentEntry] = None, duration: int = None):
|
||||
if assignments is None:
|
||||
assignments = [AssignmentEntry.find(
|
||||
x.strip()) for x in self.config.Assignment_Filter.split('>')]
|
||||
if duration is None:
|
||||
duration = self.config.Assignment_Duration
|
||||
|
||||
self.ui_ensure(page_assignment)
|
||||
# Iterate in user-specified order, return undispatched ones
|
||||
undispatched = list(self._check_inlist(assignments, duration))
|
||||
_, _, total = self._limit_status
|
||||
# There are unchecked assignments
|
||||
if total > len(self.dispatched):
|
||||
self._check_all()
|
||||
_, remain, _ = self._limit_status
|
||||
for assignment in undispatched[:remain]:
|
||||
self.goto_entry(assignment)
|
||||
self.dispatch(assignment, duration, check_limit=False)
|
||||
if remain < len(undispatched):
|
||||
logger.warning(
|
||||
f'The following assignments can not be dispatched due to limit: {", ".join([x.name for x in undispatched])}')
|
||||
self._dispatch_remain(duration, remain - len(undispatched))
|
||||
|
||||
# Scheduler
|
||||
delay = min(self.dispatched.values())
|
||||
logger.info(f'Delay assignment check to {str(delay)}')
|
||||
self.config.task_delay(target=delay)
|
||||
|
||||
def _check_inlist(self, assignments: list[AssignmentEntry], duration: int):
|
||||
"""
|
||||
Dispatch assignments according to user config
|
||||
|
||||
Args:
|
||||
assignments (list[AssignmentEntry]): user specified assignments
|
||||
duration (int): user specified duration
|
||||
"""
|
||||
if not assignments:
|
||||
return
|
||||
logger.hr('Assignment check inlist', level=2)
|
||||
logger.info(
|
||||
f'User specified assignments: {", ".join([x.name for x in assignments])}')
|
||||
for assignment in assignments:
|
||||
self.goto_entry(assignment)
|
||||
if self.appear(CLAIM):
|
||||
self.claim(assignment, duration, should_redispatch=True)
|
||||
continue
|
||||
if self.appear(DISPATCHED):
|
||||
self.dispatched[assignment] = datetime.now() + Duration(
|
||||
OCR_ASSIGNMENT_TIME).ocr_single_line(self.device.image)
|
||||
continue
|
||||
if self.appear(EMPTY_SLOT):
|
||||
dispatched = self.dispatch(assignment, duration)
|
||||
if not dispatched:
|
||||
yield assignment
|
||||
|
||||
def _check_all(self):
|
||||
"""
|
||||
States of assignments from top to bottom are in following order:
|
||||
1. Claimable
|
||||
2. Dispatched
|
||||
3. Dispatchable
|
||||
Break when a dispatchable assignment is encountered
|
||||
"""
|
||||
logger.hr('Assignment check all', level=2)
|
||||
for group in self._iter_groups():
|
||||
self.goto_group(group)
|
||||
entries = self._iter_entries()
|
||||
for _ in range(len(group.entries)):
|
||||
assignment = next(entries)
|
||||
if assignment in self.dispatched:
|
||||
continue
|
||||
self.goto_entry(assignment)
|
||||
if self.appear(CLAIM):
|
||||
self.claim(assignment, None, should_redispatch=False)
|
||||
continue
|
||||
if self.appear(DISPATCHED):
|
||||
self.dispatched[assignment] = datetime.now() + Duration(
|
||||
OCR_ASSIGNMENT_TIME).ocr_single_line(self.device.image)
|
||||
continue
|
||||
if self.appear(EMPTY_SLOT):
|
||||
break
|
||||
|
||||
def _dispatch_remain(self, duration: int, remain: int):
|
||||
"""
|
||||
Dispatch assignments according to preset priority
|
||||
|
||||
Args:
|
||||
duration (int): user specified duration
|
||||
remain (int):
|
||||
The number of remaining assignments after
|
||||
processing the ones specified by user
|
||||
"""
|
||||
if remain <= 0:
|
||||
return
|
||||
logger.hr('Assignment dispatch remain', level=2)
|
||||
logger.warning(f'{remain} remain')
|
||||
logger.info(
|
||||
'Dispatch remaining assignments according to preset priority')
|
||||
group_priority = (
|
||||
KEYWORDS_ASSIGNMENT_GROUP.EXP_Materials_Credits,
|
||||
KEYWORDS_ASSIGNMENT_GROUP.Character_Materials,
|
||||
KEYWORDS_ASSIGNMENT_GROUP.Synthesis_Materials
|
||||
)
|
||||
for group in group_priority:
|
||||
for assignment in group.entries:
|
||||
if assignment in self.dispatched:
|
||||
continue
|
||||
self.goto_entry(assignment)
|
||||
self.dispatch(assignment, duration, check_limit=False)
|
||||
remain -= 1
|
||||
if remain <= 0:
|
||||
return
|
68
tasks/assignment/claim.py
Normal file
@ -0,0 +1,68 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from module.base.timer import Timer
|
||||
from module.ocr.ocr import Duration
|
||||
from tasks.assignment.assets.assets_assignment_claim import *
|
||||
from tasks.assignment.assets.assets_assignment_dispatch import EMPTY_SLOT
|
||||
from tasks.assignment.assets.assets_assignment_ui import DISPATCHED
|
||||
from tasks.assignment.dispatch import AssignmentDispatch
|
||||
from tasks.assignment.keywords import AssignmentEntry
|
||||
|
||||
|
||||
class AssignmentClaim(AssignmentDispatch):
|
||||
def claim(self, assignment: AssignmentEntry, duration_expected: int, should_redispatch: bool):
|
||||
"""
|
||||
Args:
|
||||
assignment (AssignmentEntry):
|
||||
duration_expected (int): user specified duration
|
||||
should_redispatch (bool):
|
||||
|
||||
Pages:
|
||||
in: CLAIM
|
||||
out: DISPATCHED(succeed) or EMPTY_SLOT(fail)
|
||||
"""
|
||||
redispatched = False
|
||||
skip_first_screenshot = True
|
||||
counter = Timer(1, count=4).start()
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
else:
|
||||
self.device.screenshot()
|
||||
# End
|
||||
if self.appear(EMPTY_SLOT) or self.appear(DISPATCHED):
|
||||
if counter.reached():
|
||||
break
|
||||
continue
|
||||
# Claim reward
|
||||
if self.appear(CLAIM, interval=2):
|
||||
self.device.click(CLAIM)
|
||||
continue
|
||||
if self.appear(REDISPATCH, interval=2):
|
||||
redispatched = should_redispatch and self._is_duration_expected(
|
||||
duration_expected)
|
||||
if redispatched:
|
||||
self._confirm_assignment(REDISPATCH)
|
||||
self.dispatched[assignment] = datetime.now(
|
||||
) + timedelta(hours=duration_expected)
|
||||
else:
|
||||
self.device.click(CLOSE_REPORT)
|
||||
continue
|
||||
# Re-select duration and dispatch
|
||||
if should_redispatch and not redispatched:
|
||||
self.dispatch(assignment, duration_expected, check_limit=False)
|
||||
|
||||
def _is_duration_expected(self, duration: int) -> bool:
|
||||
"""
|
||||
Check whether duration in assignment report page
|
||||
is the same as user specified
|
||||
|
||||
Args:
|
||||
duration (int): user specified duration
|
||||
|
||||
Returns:
|
||||
bool: If same.
|
||||
"""
|
||||
duration_reported: timedelta = Duration(
|
||||
OCR_ASSIGNMENT_REPORT_TIME).ocr_single_line(self.device.image)
|
||||
return duration_reported.total_seconds() == duration*3600
|
104
tasks/assignment/dispatch.py
Normal file
@ -0,0 +1,104 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from module.base.timer import Timer
|
||||
from module.logger import logger
|
||||
from tasks.assignment.assets.assets_assignment_dispatch import *
|
||||
from tasks.assignment.assets.assets_assignment_ui import DISPATCHED
|
||||
from tasks.assignment.keywords import *
|
||||
from tasks.assignment.ui import AssignmentSwitch, AssignmentUI
|
||||
|
||||
ASSIGNMENT_DURATION_SWITCH = AssignmentSwitch(
|
||||
'AssignmentDurationSwitch',
|
||||
(160, 130, 100)
|
||||
)
|
||||
ASSIGNMENT_DURATION_SWITCH.add_state('4', DURATION_4)
|
||||
ASSIGNMENT_DURATION_SWITCH.add_state('8', DURATION_8)
|
||||
ASSIGNMENT_DURATION_SWITCH.add_state('12', DURATION_12)
|
||||
ASSIGNMENT_DURATION_SWITCH.add_state('20', DURATION_20)
|
||||
|
||||
|
||||
class AssignmentDispatch(AssignmentUI):
|
||||
dispatched: dict[AssignmentEntry, datetime] = dict()
|
||||
|
||||
def dispatch(self, assignment: AssignmentEntry, duration: int, check_limit: bool = True) -> bool:
|
||||
"""
|
||||
Args:
|
||||
assignment (AssignmentEntry):
|
||||
duration (int): user specified duration
|
||||
check_limit (bool):
|
||||
|
||||
Pages:
|
||||
in: EMPTY_SLOT
|
||||
out: DISPATCHED(succeed) or EMPTY_SLOT(fail)
|
||||
"""
|
||||
if check_limit and self._limit_status[1] == 0:
|
||||
return False
|
||||
self._select_characters()
|
||||
self._select_duration(duration)
|
||||
self._confirm_assignment(CONFIRM_ASSIGNMENT)
|
||||
self.dispatched[assignment] = datetime.now() + \
|
||||
timedelta(hours=duration)
|
||||
return True
|
||||
|
||||
def _select_characters(self):
|
||||
"""
|
||||
Pages:
|
||||
in: EMPTY_SLOT
|
||||
out: CHARACTER_LIST
|
||||
"""
|
||||
skip_first_screenshot = True
|
||||
click_timer = Timer(1, count=3).start()
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
else:
|
||||
self.device.screenshot()
|
||||
# End
|
||||
if not self.appear(EMPTY_SLOT):
|
||||
logger.info('Assignment slots are all filled')
|
||||
break
|
||||
# Ensure character list
|
||||
if not self.appear(CHARACTER_LIST):
|
||||
if click_timer.reached_and_reset():
|
||||
self.device.click(EMPTY_SLOT)
|
||||
continue
|
||||
# Select
|
||||
if click_timer.reached_and_reset():
|
||||
self.device.click(CHARACTER_1)
|
||||
self.device.click(CHARACTER_2)
|
||||
|
||||
def _select_duration(self, duration: int):
|
||||
if duration not in {4, 8, 12, 20}:
|
||||
logger.warning(
|
||||
f'Duration {duration} is out of scope, reset it to 20')
|
||||
duration = 20
|
||||
ASSIGNMENT_DURATION_SWITCH.set(str(duration), self)
|
||||
|
||||
def _confirm_assignment(self, dispatch_button: ButtonWrapper) -> bool:
|
||||
"""
|
||||
Args:
|
||||
dispatch_button (ButtonWrapper):
|
||||
Button to be clicked, CONFIRM_ASSIGNMENT or REDISPATCH
|
||||
|
||||
Pages:
|
||||
in: CONFIRM_ASSIGNMENT or REDISPATCH
|
||||
out: DISPATCHED
|
||||
"""
|
||||
skip_first_screenshot = True
|
||||
counter = Timer(1, count=3).start()
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
else:
|
||||
self.device.screenshot()
|
||||
# End
|
||||
if self.appear(DISPATCHED) and self.appear(ASSIGNMENT_STARTED_CHECK):
|
||||
if counter.reached():
|
||||
logger.info(f'Assignment started')
|
||||
break
|
||||
continue
|
||||
# Click
|
||||
if self.appear(dispatch_button, interval=2):
|
||||
self.device.click(dispatch_button)
|
||||
counter.reset()
|
||||
continue
|
30
tasks/assignment/keywords/__init__.py
Normal file
@ -0,0 +1,30 @@
|
||||
import tasks.assignment.keywords.entry as KEYWORDS_ASSIGNMENT_ENTRY
|
||||
import tasks.assignment.keywords.group as KEYWORDS_ASSIGNMENT_GROUP
|
||||
from tasks.assignment.keywords.classes import AssignmentGroup, AssignmentEntry
|
||||
|
||||
KEYWORDS_ASSIGNMENT_GROUP.Character_Materials.entries = (
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Nine_Billion_Names,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Destruction_of_the_Destroyer,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Winter_Soldiers,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Born_to_Obey,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Root_Out_the_Turpitude,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Fire_Lord_Inflames_Blades_of_War,
|
||||
)
|
||||
KEYWORDS_ASSIGNMENT_GROUP.EXP_Materials_Credits.entries = (
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Nameless_Land_Nameless_People,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Akashic_Records,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.The_Invisible_Hand,
|
||||
)
|
||||
KEYWORDS_ASSIGNMENT_GROUP.Synthesis_Materials.entries = (
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Abandoned_and_Insulted,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Spring_of_Life,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.The_Land_of_Gold,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.The_Blossom_in_the_Storm,
|
||||
)
|
||||
for group in (
|
||||
KEYWORDS_ASSIGNMENT_GROUP.Character_Materials,
|
||||
KEYWORDS_ASSIGNMENT_GROUP.EXP_Materials_Credits,
|
||||
KEYWORDS_ASSIGNMENT_GROUP.Synthesis_Materials,
|
||||
):
|
||||
for entry in group.entries:
|
||||
entry.group = group
|
20
tasks/assignment/keywords/classes.py
Normal file
@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar
|
||||
|
||||
from module.ocr.keyword import Keyword
|
||||
|
||||
|
||||
@dataclass
|
||||
class AssignmentGroup(Keyword):
|
||||
instances: ClassVar = {}
|
||||
entries: tuple[AssignmentEntry] = ()
|
||||
|
||||
|
||||
@dataclass
|
||||
class AssignmentEntry(Keyword):
|
||||
instances: ClassVar = {}
|
||||
group: AssignmentGroup = None
|
||||
def __hash__(self) -> int:
|
||||
return super().__hash__()
|
||||
|
109
tasks/assignment/keywords/entry.py
Normal file
@ -0,0 +1,109 @@
|
||||
from .classes import AssignmentEntry
|
||||
|
||||
# This file was auto-generated, do not modify it manually. To generate:
|
||||
# ``` python -m dev_tools.keyword_extract ```
|
||||
|
||||
Nine_Billion_Names = AssignmentEntry(
|
||||
id=1,
|
||||
name='Nine_Billion_Names',
|
||||
cn='九十亿个名字',
|
||||
cht='九十億個名字',
|
||||
en='Nine Billion Names',
|
||||
jp='九十億の御名',
|
||||
)
|
||||
Destruction_of_the_Destroyer = AssignmentEntry(
|
||||
id=2,
|
||||
name='Destruction_of_the_Destroyer',
|
||||
cn='毁灭者的覆灭',
|
||||
cht='毀滅者的覆滅',
|
||||
en='Destruction of the Destroyer',
|
||||
jp='壊滅者の覆没',
|
||||
)
|
||||
Winter_Soldiers = AssignmentEntry(
|
||||
id=3,
|
||||
name='Winter_Soldiers',
|
||||
cn='寒冬的战士们',
|
||||
cht='寒冬的戰士們',
|
||||
en='Winter Soldiers',
|
||||
jp='寒冬の戦士たち',
|
||||
)
|
||||
Born_to_Obey = AssignmentEntry(
|
||||
id=4,
|
||||
name='Born_to_Obey',
|
||||
cn='生而服从',
|
||||
cht='生而服從',
|
||||
en='Born to Obey',
|
||||
jp='生まれながらに服従する',
|
||||
)
|
||||
Root_Out_the_Turpitude = AssignmentEntry(
|
||||
id=5,
|
||||
name='Root_Out_the_Turpitude',
|
||||
cn='根除恶孽',
|
||||
cht='根除惡孽',
|
||||
en='Root Out the Turpitude',
|
||||
jp='悪孽を根絶やしに',
|
||||
)
|
||||
Fire_Lord_Inflames_Blades_of_War = AssignmentEntry(
|
||||
id=6,
|
||||
name='Fire_Lord_Inflames_Blades_of_War',
|
||||
cn='火帝动炉销剑戟',
|
||||
cht='火帝動爐銷劍戟',
|
||||
en='Fire Lord Inflames Blades of War',
|
||||
jp='剣戟を焼却する火帝炉',
|
||||
)
|
||||
Nameless_Land_Nameless_People = AssignmentEntry(
|
||||
id=7,
|
||||
name='Nameless_Land_Nameless_People',
|
||||
cn='无名之地,无名之人',
|
||||
cht='無名之地,無名之人',
|
||||
en='Nameless Land, Nameless People',
|
||||
jp='無名の地、無名の人',
|
||||
)
|
||||
Akashic_Records = AssignmentEntry(
|
||||
id=8,
|
||||
name='Akashic_Records',
|
||||
cn='阿卡夏记录',
|
||||
cht='阿卡夏紀錄',
|
||||
en='Akashic Records',
|
||||
jp='アーカーシャの記録',
|
||||
)
|
||||
The_Invisible_Hand = AssignmentEntry(
|
||||
id=9,
|
||||
name='The_Invisible_Hand',
|
||||
cn='看不见的手',
|
||||
cht='看不見的手',
|
||||
en='The Invisible Hand',
|
||||
jp='見えざる手',
|
||||
)
|
||||
Abandoned_and_Insulted = AssignmentEntry(
|
||||
id=10,
|
||||
name='Abandoned_and_Insulted',
|
||||
cn='被废弃与损害的',
|
||||
cht='被廢棄與損害的',
|
||||
en='Abandoned and Insulted',
|
||||
jp='捨てられしものと傷つけられしもの',
|
||||
)
|
||||
Spring_of_Life = AssignmentEntry(
|
||||
id=11,
|
||||
name='Spring_of_Life',
|
||||
cn='生命之泉',
|
||||
cht='生命之泉',
|
||||
en='Spring of Life',
|
||||
jp='生命の泉',
|
||||
)
|
||||
The_Land_of_Gold = AssignmentEntry(
|
||||
id=12,
|
||||
name='The_Land_of_Gold',
|
||||
cn='黄金大地',
|
||||
cht='黃金大地',
|
||||
en='The Land of Gold',
|
||||
jp='黄金の大地',
|
||||
)
|
||||
The_Blossom_in_the_Storm = AssignmentEntry(
|
||||
id=13,
|
||||
name='The_Blossom_in_the_Storm',
|
||||
cn='风暴中怒放的花',
|
||||
cht='風暴中怒放的花',
|
||||
en='The Blossom in the Storm',
|
||||
jp='嵐の中で咲き誇る花',
|
||||
)
|
29
tasks/assignment/keywords/group.py
Normal file
@ -0,0 +1,29 @@
|
||||
from .classes import AssignmentGroup
|
||||
|
||||
# This file was auto-generated, do not modify it manually. To generate:
|
||||
# ``` python -m dev_tools.keyword_extract ```
|
||||
|
||||
Character_Materials = AssignmentGroup(
|
||||
id=1,
|
||||
name='Character_Materials',
|
||||
cn='专属材料',
|
||||
cht='專屬素材',
|
||||
en='Character Materials',
|
||||
jp='専用素材',
|
||||
)
|
||||
EXP_Materials_Credits = AssignmentGroup(
|
||||
id=2,
|
||||
name='EXP_Materials_Credits',
|
||||
cn='经验材料/信用点',
|
||||
cht='經驗素材/信用點',
|
||||
en='EXP Materials/Credits',
|
||||
jp='経験値素材/信用ポイント',
|
||||
)
|
||||
Synthesis_Materials = AssignmentGroup(
|
||||
id=3,
|
||||
name='Synthesis_Materials',
|
||||
cn='合成材料',
|
||||
cht='合成材料',
|
||||
en='Synthesis Materials',
|
||||
jp='合成材料',
|
||||
)
|
162
tasks/assignment/ui.py
Normal file
@ -0,0 +1,162 @@
|
||||
import re
|
||||
from functools import cached_property
|
||||
from typing import Iterator
|
||||
|
||||
from module.base.base import ModuleBase
|
||||
from module.base.timer import Timer
|
||||
from module.logger import logger
|
||||
from module.ocr.ocr import DigitCounter, Ocr
|
||||
from module.ui.draggable_list import DraggableList
|
||||
from module.ui.switch import Switch
|
||||
from tasks.assignment.assets.assets_assignment_ui import *
|
||||
from tasks.assignment.keywords import *
|
||||
from tasks.base.page import page_assignment
|
||||
from tasks.base.ui import UI
|
||||
|
||||
|
||||
class AssignmentSwitch(Switch):
|
||||
def __init__(self, name, active_color: tuple[int, int, int], is_selector=True):
|
||||
super().__init__(name, is_selector)
|
||||
self.active_color = active_color
|
||||
|
||||
def get(self, main: ModuleBase):
|
||||
"""
|
||||
Use image_color_count instead to determine whether the button is selected/active
|
||||
|
||||
Args:
|
||||
main (ModuleBase):
|
||||
|
||||
Returns:
|
||||
str: state name or 'unknown'.
|
||||
"""
|
||||
for data in self.state_list:
|
||||
if main.image_color_count(data['check_button'], self.active_color):
|
||||
return data['state']
|
||||
|
||||
return 'unknown'
|
||||
|
||||
|
||||
class AssignmentOcr(Ocr):
|
||||
OCR_REPLACE = {
|
||||
'ch': [
|
||||
(KEYWORDS_ASSIGNMENT_ENTRY.Winter_Soldiers.name, '[黑]冬的战士们'),
|
||||
(KEYWORDS_ASSIGNMENT_ENTRY.Born_to_Obey.name, '[牛]而服从'),
|
||||
(KEYWORDS_ASSIGNMENT_ENTRY.Root_Out_the_Turpitude.name,
|
||||
'根除恶[擎薯尊掌鞋]?'),
|
||||
(KEYWORDS_ASSIGNMENT_ENTRY.Akashic_Records.name, '阿[未][夏复]记录'),
|
||||
]
|
||||
}
|
||||
|
||||
@cached_property
|
||||
def ocr_regex(self) -> re.Pattern | None:
|
||||
rules = AssignmentOcr.OCR_REPLACE.get(self.lang)
|
||||
if rules is None:
|
||||
return None
|
||||
return re.compile('|'.join('(?P<%s>%s)' % pair for pair in rules))
|
||||
|
||||
def after_process(self, result: str):
|
||||
result = super().after_process(result)
|
||||
if self.ocr_regex is None:
|
||||
return result
|
||||
matched = self.ocr_regex.fullmatch(result)
|
||||
if matched is None:
|
||||
return result
|
||||
matched = getattr(KEYWORDS_ASSIGNMENT_ENTRY, matched.lastgroup)
|
||||
matched = getattr(matched, self.lang)
|
||||
logger.attr(name=f'{self.name} after_process',
|
||||
text=f'{result} -> {matched}')
|
||||
return matched
|
||||
|
||||
|
||||
ASSIGNMENT_TOP_SWITCH = AssignmentSwitch(
|
||||
'AssignmentTopSwitch',
|
||||
(240, 240, 240)
|
||||
)
|
||||
ASSIGNMENT_TOP_SWITCH.add_state(
|
||||
KEYWORDS_ASSIGNMENT_GROUP.Character_Materials,
|
||||
check_button=CHARACTER_MATERIALS
|
||||
)
|
||||
ASSIGNMENT_TOP_SWITCH.add_state(
|
||||
KEYWORDS_ASSIGNMENT_GROUP.EXP_Materials_Credits,
|
||||
check_button=EXP_MATERIALS_CREDITS
|
||||
)
|
||||
ASSIGNMENT_TOP_SWITCH.add_state(
|
||||
KEYWORDS_ASSIGNMENT_GROUP.Synthesis_Materials,
|
||||
check_button=SYNTHESIS_MATERIALS
|
||||
)
|
||||
|
||||
ASSIGNMENT_ENTRY_LIST = DraggableList(
|
||||
'AssignmentEntryList',
|
||||
keyword_class=AssignmentEntry,
|
||||
ocr_class=AssignmentOcr,
|
||||
search_button=OCR_ASSIGNMENT_LIST,
|
||||
active_color=(40, 40, 40)
|
||||
)
|
||||
|
||||
|
||||
class AssignmentUI(UI):
|
||||
def goto_group(self, group: AssignmentGroup):
|
||||
"""
|
||||
Args:
|
||||
group: AssignmentGroup
|
||||
|
||||
Examples:
|
||||
self = AssignmentUI('src')
|
||||
self.device.screenshot()
|
||||
self.goto_group(KEYWORDS_ASSIGNMENT_GROUP.Character_Materials)
|
||||
"""
|
||||
self.ui_ensure(page_assignment)
|
||||
logger.hr('Assignment group goto', level=3)
|
||||
if ASSIGNMENT_TOP_SWITCH.set(group, main=self):
|
||||
self._wait_until_entry_loaded()
|
||||
|
||||
def goto_entry(self, entry: AssignmentEntry):
|
||||
"""
|
||||
Args:
|
||||
entry: AssignmentEntry
|
||||
|
||||
Examples:
|
||||
self = AssignmentUI('src')
|
||||
self.device.screenshot()
|
||||
self.goto_entry(KEYWORDS_ASSIGNMENT_ENTRY.Nameless_Land_Nameless_People)
|
||||
"""
|
||||
self.goto_group(entry.group)
|
||||
ASSIGNMENT_ENTRY_LIST.select_row(entry, self)
|
||||
|
||||
def _wait_until_entry_loaded(self):
|
||||
skip_first_screenshot = True
|
||||
timeout = Timer(2, count=3).start()
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
else:
|
||||
self.device.screenshot()
|
||||
|
||||
if timeout.reached():
|
||||
logger.warning('Wait entry loaded timeout')
|
||||
break
|
||||
# Maybe not reliable
|
||||
if self.image_color_count(ENTRY_LOADED, (35, 35, 35)):
|
||||
logger.info('Entry loaded')
|
||||
break
|
||||
|
||||
@property
|
||||
def _limit_status(self) -> tuple[int, int, int]:
|
||||
self.device.screenshot()
|
||||
return DigitCounter(OCR_ASSIGNMENT_LIMIT).ocr_single_line(self.device.image)
|
||||
|
||||
def _iter_groups(self) -> Iterator[AssignmentGroup]:
|
||||
for state in ASSIGNMENT_TOP_SWITCH.state_list:
|
||||
yield state['state']
|
||||
|
||||
def _iter_entries(self) -> Iterator[AssignmentEntry]:
|
||||
"""
|
||||
Iterate entries from top to bottom
|
||||
"""
|
||||
while 1:
|
||||
ASSIGNMENT_ENTRY_LIST.load_rows(self)
|
||||
for button in ASSIGNMENT_ENTRY_LIST.cur_buttons:
|
||||
yield button.matched_keyword
|
||||
ASSIGNMENT_ENTRY_LIST.drag_page('down', self)
|
||||
self.wait_until_stable(ASSIGNMENT_ENTRY_LIST.search_button, timer=Timer(
|
||||
0, count=0), timeout=Timer(1.5, count=5))
|
@ -3,6 +3,16 @@ 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 ```
|
||||
|
||||
ASSIGNMENT_CHECK = ButtonWrapper(
|
||||
name='ASSIGNMENT_CHECK',
|
||||
share=Button(
|
||||
file='./assets/share/base/page/ASSIGNMENT_CHECK.png',
|
||||
area=(45, 21, 70, 53),
|
||||
search=(25, 1, 90, 73),
|
||||
color=(162, 145, 112),
|
||||
button=(45, 21, 70, 53),
|
||||
),
|
||||
)
|
||||
BATTLE_PASS_CHECK = ButtonWrapper(
|
||||
name='BATTLE_PASS_CHECK',
|
||||
share=Button(
|
||||
@ -213,6 +223,16 @@ MENU_CHECK = ButtonWrapper(
|
||||
button=(1222, 638, 1252, 669),
|
||||
),
|
||||
)
|
||||
MENU_GOTO_ASSIGNMENT = ButtonWrapper(
|
||||
name='MENU_GOTO_ASSIGNMENT',
|
||||
share=Button(
|
||||
file='./assets/share/base/page/MENU_GOTO_ASSIGNMENT.png',
|
||||
area=(1090, 269, 1153, 328),
|
||||
search=(1070, 249, 1173, 348),
|
||||
color=(71, 71, 74),
|
||||
button=(1090, 269, 1153, 328),
|
||||
),
|
||||
)
|
||||
MENU_GOTO_CAMERA = ButtonWrapper(
|
||||
name='MENU_GOTO_CAMERA',
|
||||
share=Button(
|
||||
|
@ -135,3 +135,8 @@ page_menu.link(MENU_GOTO_CAMERA, destination=page_camera)
|
||||
page_synthesize = Page(SYNTHESIZE_CHECK)
|
||||
page_synthesize.link(CLOSE, destination=page_menu)
|
||||
page_menu.link(MENU_GOTO_SYNTHESIZE, destination=page_synthesize)
|
||||
|
||||
# Assignment
|
||||
page_assignment = Page(ASSIGNMENT_CHECK)
|
||||
page_assignment.link(CLOSE, destination=page_main)
|
||||
page_menu.link(MENU_GOTO_ASSIGNMENT, destination=page_assignment)
|
||||
|
@ -92,10 +92,10 @@ class DraggableDungeonList(DraggableList):
|
||||
|
||||
|
||||
DUNGEON_NAV_LIST = DraggableList(
|
||||
'DungeonNavList', keyword_class=DungeonNav, ocr_class=OcrDungeonNav, search_button=OCR_DUNGEON_NAV)
|
||||
'DungeonNavList', keyword_class=DungeonNav, ocr_class=OcrDungeonNav, search_button=OCR_DUNGEON_NAV, active_color=(190, 175, 124))
|
||||
DUNGEON_LIST = DraggableDungeonList(
|
||||
'DungeonList', keyword_class=[DungeonList, DungeonEntrance],
|
||||
ocr_class=OcrDungeonList, search_button=OCR_DUNGEON_LIST)
|
||||
ocr_class=OcrDungeonList, search_button=OCR_DUNGEON_LIST, active_color=(190, 175, 124))
|
||||
|
||||
|
||||
class DungeonUI(UI):
|
||||
|