mirror of
https://github.com/LmeSzinc/StarRailCopilot.git
synced 2024-11-25 01:49:41 +00:00
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:
commit
afa94986bc
BIN
assets/cn/base/popup/GET_LIGHT_CONE.png
Normal file
BIN
assets/cn/base/popup/GET_LIGHT_CONE.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
BIN
assets/en/base/popup/GET_LIGHT_CONE.png
Normal file
BIN
assets/en/base/popup/GET_LIGHT_CONE.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.8 KiB |
BIN
assets/share/combat/interact/DUNGEON_COMBAT_INTERACT_TEXT.png
Normal file
BIN
assets/share/combat/interact/DUNGEON_COMBAT_INTERACT_TEXT.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
@ -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")
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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:
|
||||||
|
@ -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):
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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():
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user