Merge branch 'bug_fix' into dev

# Conflicts:
#	tasks/base/popup.py
#	tasks/base/ui.py
#	tasks/dungeon/state.py
#	tasks/dungeon/ui.py
This commit is contained in:
LmeSzinc 2023-11-03 01:36:05 +08:00
commit afa94986bc
14 changed files with 146 additions and 34 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -89,12 +89,15 @@ def readable_time(before: str) -> str:
elif diff < 60: elif diff < 60:
# < 1 min # < 1 min
return t("Gui.Dashboard.JustNow") return t("Gui.Dashboard.JustNow")
elif diff < 3600: elif diff < 5400:
# < 90 min
return t("Gui.Dashboard.MinutesAgo", time=int(diff // 60)) return t("Gui.Dashboard.MinutesAgo", time=int(diff // 60))
elif diff < 86400: elif diff < 129600:
# < 36 hours
return t("Gui.Dashboard.HoursAgo", time=int(diff // 3600)) return t("Gui.Dashboard.HoursAgo", time=int(diff // 3600))
elif diff < 1296000: elif diff < 1296000:
# < 15 days
return t("Gui.Dashboard.DaysAgo", time=int(diff // 86400)) return t("Gui.Dashboard.DaysAgo", time=int(diff // 86400))
else: else:
# > 15 days # >= 15 days
return t("Gui.Dashboard.LongTimeAgo") return t("Gui.Dashboard.LongTimeAgo")

View File

@ -455,7 +455,11 @@ def get_localstorage(key):
def re_fullmatch(pattern, string): def re_fullmatch(pattern, string):
if pattern == "datetime": if pattern == "datetime":
pattern = RE_DATETIME try:
datetime.datetime.fromisoformat(string)
return True
except ValueError:
return False
# elif: # elif:
return re.fullmatch(pattern=pattern, string=string) return re.fullmatch(pattern=pattern, string=string)

View File

@ -13,6 +13,23 @@ BATTLE_PASS_NOTIFICATION = ButtonWrapper(
button=(895, 595, 1180, 630), button=(895, 595, 1180, 630),
), ),
) )
GET_LIGHT_CONE = ButtonWrapper(
name='GET_LIGHT_CONE',
cn=Button(
file='./assets/cn/base/popup/GET_LIGHT_CONE.png',
area=(205, 321, 242, 339),
search=(185, 301, 262, 359),
color=(130, 130, 131),
button=(205, 321, 242, 339),
),
en=Button(
file='./assets/en/base/popup/GET_LIGHT_CONE.png',
area=(260, 322, 306, 338),
search=(240, 302, 326, 358),
color=(147, 147, 148),
button=(260, 322, 306, 338),
),
)
GET_REWARD = ButtonWrapper( GET_REWARD = ButtonWrapper(
name='GET_REWARD', name='GET_REWARD',
share=Button( share=Button(

View File

@ -100,6 +100,23 @@ class PopupHandler(ModuleBase):
return False return False
def handle_get_light_cone(self, interval=2) -> bool:
"""
Popup when getting a light cone from Echo of War.
Args:
interval:
Returns:
If handled.
"""
if self.appear(GET_LIGHT_CONE, interval=interval):
logger.info(f'{GET_LIGHT_CONE} -> {GET_REWARD}')
self.device.click(GET_REWARD)
return True
return False
def handle_ui_close(self, appear_button: ButtonWrapper | Callable, interval=2) -> bool: def handle_ui_close(self, appear_button: ButtonWrapper | Callable, interval=2) -> bool:
""" """
Args: Args:

View File

@ -321,6 +321,8 @@ class UI(MainPage):
return True return True
if self.handle_monthly_card_reward(): if self.handle_monthly_card_reward():
return True return True
if self.handle_get_light_cone():
return True
if self.handle_ui_close(COMBAT_PREPARE, interval=5): if self.handle_ui_close(COMBAT_PREPARE, interval=5):
return True return True
if self.appear_then_click(COMBAT_EXIT, interval=5): if self.appear_then_click(COMBAT_EXIT, interval=5):

View File

@ -13,6 +13,16 @@ DUNGEON_COMBAT_INTERACT = ButtonWrapper(
button=(750, 411, 997, 448), button=(750, 411, 997, 448),
), ),
) )
DUNGEON_COMBAT_INTERACT_TEXT = ButtonWrapper(
name='DUNGEON_COMBAT_INTERACT_TEXT',
share=Button(
file='./assets/share/combat/interact/DUNGEON_COMBAT_INTERACT_TEXT.png',
area=(790, 391, 1055, 456),
search=(770, 371, 1075, 476),
color=(47, 51, 53),
button=(790, 391, 1055, 456),
),
)
MAP_LOADING = ButtonWrapper( MAP_LOADING = ButtonWrapper(
name='MAP_LOADING', name='MAP_LOADING',
share=Button( share=Button(

View File

@ -64,27 +64,6 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo
return False return False
def combat_enter_from_map(self, skip_first_screenshot=True):
"""
Pages:
in: page_main, DUNGEON_COMBAT_INTERACT
out: COMBAT_PREPARE
"""
logger.info('Combat enter from map')
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
if self.appear(COMBAT_PREPARE):
# Confirm page loaded
if self.image_color_count(COMBAT_PREPARE.button, color=(230, 230, 230), threshold=240, count=400):
logger.info(f'At {COMBAT_PREPARE}')
break
if self.handle_combat_interact():
continue
def combat_prepare(self, team=1, support_character: str = None): def combat_prepare(self, team=1, support_character: str = None):
""" """
Args: Args:
@ -301,6 +280,8 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo
logger.info(f'{COMBAT_AGAIN} -> {COMBAT_EXIT}') logger.info(f'{COMBAT_AGAIN} -> {COMBAT_EXIT}')
self.device.click(COMBAT_EXIT) self.device.click(COMBAT_EXIT)
continue continue
if self.handle_get_light_cone():
continue
def is_trailblaze_power_exhausted(self) -> bool: def is_trailblaze_power_exhausted(self) -> bool:
flag = self.config.stored.TrailblazePower.value < self.combat_wave_cost flag = self.config.stored.TrailblazePower.value < self.combat_wave_cost

View File

@ -1,6 +1,8 @@
from module.base.utils import color_similar, get_color from module.base.utils import color_similar, get_color
from module.logger import logger
from tasks.base.ui import UI from tasks.base.ui import UI
from tasks.combat.assets.assets_combat_interact import DUNGEON_COMBAT_INTERACT, MAP_LOADING from tasks.combat.assets.assets_combat_interact import DUNGEON_COMBAT_INTERACT, MAP_LOADING
from tasks.combat.assets.assets_combat_prepare import COMBAT_PREPARE
from tasks.map.assets.assets_map_control import A_BUTTON from tasks.map.assets.assets_map_control import A_BUTTON
@ -15,6 +17,27 @@ class CombatInteract(UI):
return False return False
def combat_enter_from_map(self, skip_first_screenshot=True):
"""
Pages:
in: page_main, DUNGEON_COMBAT_INTERACT
out: COMBAT_PREPARE
"""
logger.info('Combat enter from map')
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
if self.appear(COMBAT_PREPARE):
# Confirm page loaded
if self.image_color_count(COMBAT_PREPARE.button, color=(230, 230, 230), threshold=240, count=400):
logger.info(f'At {COMBAT_PREPARE}')
break
if self.handle_combat_interact():
continue
def is_map_loading(self): def is_map_loading(self):
if self.appear(MAP_LOADING, similarity=0.75): if self.appear(MAP_LOADING, similarity=0.75):
return True return True

View File

@ -45,8 +45,12 @@ class Dungeon(DungeonUI, DungeonEvent, Combat):
logger.info(f'Dungeon: {dungeon}, team={team}, wave_limit={wave_limit}, support_character={support_character}') logger.info(f'Dungeon: {dungeon}, team={team}, wave_limit={wave_limit}, support_character={support_character}')
if not skip_ui_switch: if not skip_ui_switch:
self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index) interact = self.get_dungeon_interact()
self.dungeon_goto(dungeon) if interact is not None and interact == dungeon:
logger.info('Already nearby dungeon')
else:
self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index)
self.dungeon_goto(dungeon)
if dungeon == KEYWORDS_DUNGEON_LIST.Stagnant_Shadow_Blaze: if dungeon == KEYWORDS_DUNGEON_LIST.Stagnant_Shadow_Blaze:
if self.handle_destructible_around_blaze(): if self.handle_destructible_around_blaze():

View File

@ -6,19 +6,23 @@ from module.base.base import ModuleBase
from module.base.button import ClickButton from module.base.button import ClickButton
from module.base.timer import Timer from module.base.timer import Timer
from module.base.utils import get_color from module.base.utils import get_color
from module.exception import ScriptError
from module.logger import logger from module.logger import logger
from module.ocr.ocr import Ocr, OcrResultButton from module.ocr.ocr import Ocr, OcrResultButton
from module.ocr.utils import split_and_pair_button_attr from module.ocr.utils import split_and_pair_button_attr
from module.ui.draggable_list import DraggableList from module.ui.draggable_list import DraggableList
from module.ui.switch import Switch from module.ui.switch import Switch
from tasks.base.page import page_guide from tasks.base.page import page_guide
from tasks.combat.assets.assets_combat_interact import DUNGEON_COMBAT_INTERACT, DUNGEON_COMBAT_INTERACT_TEXT
from tasks.combat.assets.assets_combat_prepare import COMBAT_PREPARE from tasks.combat.assets.assets_combat_prepare import COMBAT_PREPARE
from tasks.combat.interact import CombatInteract
from tasks.dungeon.assets.assets_dungeon_ui import * from tasks.dungeon.assets.assets_dungeon_ui import *
from tasks.dungeon.keywords import ( from tasks.dungeon.keywords import (
DungeonList, DungeonList,
DungeonNav, DungeonNav,
DungeonTab, DungeonTab,
KEYWORDS_DUNGEON_ENTRANCE, KEYWORDS_DUNGEON_ENTRANCE,
KEYWORDS_DUNGEON_LIST,
KEYWORDS_DUNGEON_NAV, KEYWORDS_DUNGEON_NAV,
KEYWORDS_DUNGEON_TAB KEYWORDS_DUNGEON_TAB
) )
@ -116,7 +120,7 @@ DUNGEON_LIST = DraggableDungeonList(
ocr_class=OcrDungeonList, search_button=OCR_DUNGEON_LIST) ocr_class=OcrDungeonList, search_button=OCR_DUNGEON_LIST)
class DungeonUI(DungeonState): class DungeonUI(DungeonState, CombatInteract):
def dungeon_tab_goto(self, state: DungeonTab): def dungeon_tab_goto(self, state: DungeonTab):
""" """
Args: Args:
@ -372,6 +376,53 @@ class DungeonUI(DungeonState):
logger.warning(f'Cannot find dungeon entrance of {dungeon}') logger.warning(f'Cannot find dungeon entrance of {dungeon}')
continue continue
def get_dungeon_interact(self) -> DungeonList | None:
"""
Pages:
in: page_main
"""
if not self.appear(DUNGEON_COMBAT_INTERACT):
logger.info('No dungeon interact')
return None
ocr = OcrDungeonList(DUNGEON_COMBAT_INTERACT_TEXT)
result = ocr.detect_and_ocr(self.device.image)
result = ' '.join([row.ocr_text for row in result])
# Calyx (Crimson): Bud of XXX -> Bud of XXX
result = re.sub(r'Calyx\s*\(.*?\):*', '', result)
# Stagnant Shadow: Shap XXX -> Shape of XXX
result = re.sub(r'Stagnant\s*Shadow[:\s]*\w*', 'Shape of', result)
# Cavern of Corrosion: Pa XXX -> Path of XXX
result = re.sub(r'Cavern\s*of\s*Corrosion[:\s]*\w*', 'Path of', result)
# Echo of War: XXX -> XXX
result = re.sub(r'Echo\s*of\s*War:*', '', result)
# Divine See -> Divine Seed
result = re.sub(r'Divine\s*\w*', 'Divine Seed', result)
# Destructio Beginning -> Destruction's Beginning
result = re.sub(r"Destruct[a-zA-Z0-9_']*", "Destruction's", result)
# Dungeons
try:
dungeon = DungeonList.find(result)
logger.attr('DungeonInteract', dungeon)
return dungeon
except ScriptError:
pass
# Simulated Universe returns Simulated_Universe_World_1
try:
dungeon = DungeonNav.find(result)
if dungeon == KEYWORDS_DUNGEON_NAV.Simulated_Universe:
dungeon = KEYWORDS_DUNGEON_LIST.Simulated_Universe_World_1
logger.attr('DungeonInteract', dungeon)
return dungeon
except ScriptError:
pass
# Unknown
logger.attr('DungeonInteract', None)
return None
def dungeon_goto(self, dungeon: DungeonList): def dungeon_goto(self, dungeon: DungeonList):
""" """
Returns: Returns:

View File

@ -16,6 +16,7 @@ from module.base.utils import (
rgb2yuv rgb2yuv
) )
from module.logger import logger from module.logger import logger
from tasks.map.interact.aim import subtract_blur
from tasks.map.minimap.utils import ( from tasks.map.minimap.utils import (
convolve, convolve,
cubic_find_maximum, cubic_find_maximum,
@ -264,7 +265,7 @@ class Minimap(MapResource):
scale = self.DIRECTION_ROTATION_SCALE * self.DIRECTION_SEARCH_SCALE scale = self.DIRECTION_ROTATION_SCALE * self.DIRECTION_SEARCH_SCALE
mapping = cv2.resize(image, None, fx=scale, fy=scale, interpolation=cv2.INTER_NEAREST) mapping = cv2.resize(image, None, fx=scale, fy=scale, interpolation=cv2.INTER_NEAREST)
result = cv2.matchTemplate(self.ArrowRotateMap, mapping, cv2.TM_CCOEFF_NORMED) result = cv2.matchTemplate(self.ArrowRotateMap, mapping, cv2.TM_CCOEFF_NORMED)
result = cv2.subtract(result, cv2.GaussianBlur(result, (5, 5), 0)) subtract_blur(result, 5)
_, sim, _, loca = cv2.minMaxLoc(result) _, sim, _, loca = cv2.minMaxLoc(result)
loca = np.array(loca) / self.DIRECTION_SEARCH_SCALE // (self.DIRECTION_RADIUS * 2) loca = np.array(loca) / self.DIRECTION_SEARCH_SCALE // (self.DIRECTION_RADIUS * 2)
degree = int((loca[0] + loca[1] * 8) * 5) degree = int((loca[0] + loca[1] * 8) * 5)
@ -281,7 +282,7 @@ class Minimap(MapResource):
precise_map = self.ArrowRotateMapAll[row[0]:row[1], :] precise_map = self.ArrowRotateMapAll[row[0]:row[1], :]
result = cv2.matchTemplate(precise_map, mapping, cv2.TM_CCOEFF_NORMED) result = cv2.matchTemplate(precise_map, mapping, cv2.TM_CCOEFF_NORMED)
result = cv2.subtract(result, cv2.GaussianBlur(result, (5, 5), 0)) subtract_blur(result, 5)
def to_map(x): def to_map(x):
return int((x * self.DIRECTION_RADIUS * 2) * self.POSITION_SEARCH_SCALE) return int((x * self.DIRECTION_RADIUS * 2) * self.POSITION_SEARCH_SCALE)
@ -312,11 +313,10 @@ class Minimap(MapResource):
# Extract # Extract
minimap = self.get_minimap(image, radius=self.MINIMAP_RADIUS) minimap = self.get_minimap(image, radius=self.MINIMAP_RADIUS)
_, _, v = cv2.split(rgb2yuv(minimap)) image = rgb2yuv(minimap)[:, :, 2].copy()
image = cv2.subtract(128, v) cv2.subtract(128, image, dst=image)
cv2.GaussianBlur(image, (3, 3), 0, dst=image)
image = cv2.GaussianBlur(image, (3, 3), 0)
# Expand circle into rectangle # Expand circle into rectangle
remap = cv2.remap(image, *self.RotationRemapData, cv2.INTER_LINEAR)[d * 1 // 10:d * 6 // 10].astype(np.float32) remap = cv2.remap(image, *self.RotationRemapData, cv2.INTER_LINEAR)[d * 1 // 10:d * 6 // 10].astype(np.float32)
remap = cv2.resize(remap, None, fx=scale, fy=scale, interpolation=cv2.INTER_LINEAR) remap = cv2.resize(remap, None, fx=scale, fy=scale, interpolation=cv2.INTER_LINEAR)