mirror of
https://github.com/LmeSzinc/StarRailCopilot.git
synced 2024-11-15 22:19:18 +00:00
Add: Detect domain exit and go
This commit is contained in:
parent
62150e60b9
commit
312cc6f617
BIN
assets/share/rogue/exit/OCR_DOMAIN_EXIT.png
Normal file
BIN
assets/share/rogue/exit/OCR_DOMAIN_EXIT.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.8 KiB |
@ -28,7 +28,19 @@ class OcrPlaneName(Ocr):
|
||||
result = result.replace('omaini', 'omain')
|
||||
# Domain=Combat
|
||||
result = result.replace('=', '')
|
||||
# Domain--Occunrence
|
||||
# Domain'--Occurence
|
||||
# Domain-Qccurrence
|
||||
result = result.replace('cunr', 'cur').replace('uren', 'urren').replace('Qcc', 'Occ')
|
||||
|
||||
# 区域-战
|
||||
result = re.sub(r'区域.*战$', '区域战斗', result)
|
||||
# 区域-事伴, 区域-事祥
|
||||
result = result.replace('事伴', '事件').replace('事祥', '事件')
|
||||
# 医域-战斗
|
||||
result = result.replace('医域', '区域')
|
||||
# 区域-战半
|
||||
result = result.replace('战半', '战斗')
|
||||
# 累塔的办公室
|
||||
result = result.replace('累塔', '黑塔')
|
||||
if '星港' in result:
|
||||
|
@ -179,6 +179,11 @@ class MapControl(Combat, AimDetectorMixin):
|
||||
rotation_diff = self.minimap.rotation_diff(direction)
|
||||
logger.info(f'Pdiff: {diff}, Ddiff: {direction}, Rdiff: {rotation_diff}')
|
||||
|
||||
def contact_direction():
|
||||
if waypoint.lock_direction is not None:
|
||||
return waypoint.lock_direction
|
||||
return diff_to_180_180(direction - last_rotation)
|
||||
|
||||
# Interact
|
||||
if self.aim.aimed_enemy:
|
||||
if 'enemy' in waypoint.expected_end:
|
||||
@ -204,6 +209,9 @@ class MapControl(Combat, AimDetectorMixin):
|
||||
result.append('item')
|
||||
if waypoint.early_stop:
|
||||
return result
|
||||
if waypoint.interact_radius > 0:
|
||||
if diff < waypoint.interact_radius:
|
||||
self.handle_combat_interact()
|
||||
|
||||
# Arrive
|
||||
if near := self.minimap.is_position_near(waypoint.position, threshold=waypoint.get_threshold(end_opt)):
|
||||
@ -231,7 +239,7 @@ class MapControl(Combat, AimDetectorMixin):
|
||||
aim_interval = Timer(0.1)
|
||||
self.map_run_2x_timer.reset()
|
||||
allow_straight_run = False
|
||||
if allow_run and diff < 7:
|
||||
if allow_run and diff < 7 and waypoint.min_speed == 'walk':
|
||||
logger.info(f'Approaching target, diff={round(diff, 1)}, disallow run')
|
||||
direction_interval = Timer(0.2)
|
||||
aim_interval = Timer(0.2)
|
||||
@ -255,7 +263,7 @@ class MapControl(Combat, AimDetectorMixin):
|
||||
rotation_interval.reset()
|
||||
direction_interval.reset()
|
||||
if direction_interval.reached():
|
||||
contact.set(direction=diff_to_180_180(direction - last_rotation), run=True)
|
||||
contact.set(direction=contact_direction(), run=True)
|
||||
direction_interval.reset()
|
||||
self.handle_map_run_2x(run=True)
|
||||
elif allow_straight_run:
|
||||
@ -275,7 +283,7 @@ class MapControl(Combat, AimDetectorMixin):
|
||||
rotation_interval.reset()
|
||||
direction_interval.reset()
|
||||
if direction_interval.reached():
|
||||
contact.set(direction=diff_to_180_180(direction - last_rotation), run=True)
|
||||
contact.set(direction=contact_direction(), run=True)
|
||||
direction_interval.reset()
|
||||
self.handle_map_run_2x(run=False)
|
||||
elif allow_run:
|
||||
@ -287,7 +295,7 @@ class MapControl(Combat, AimDetectorMixin):
|
||||
last_rotation = self.minimap.rotation
|
||||
allow_rotation_set = False
|
||||
if direction_interval.reached():
|
||||
contact.set(direction=diff_to_180_180(direction - last_rotation), run=True)
|
||||
contact.set(direction=contact_direction(), run=True)
|
||||
direction_interval.reset()
|
||||
self.handle_map_run_2x(run=False)
|
||||
elif allow_walk:
|
||||
@ -298,7 +306,7 @@ class MapControl(Combat, AimDetectorMixin):
|
||||
last_rotation = self.minimap.rotation
|
||||
allow_rotation_set = False
|
||||
if direction_interval.reached():
|
||||
contact.set(direction=diff_to_180_180(direction - last_rotation), run=False)
|
||||
contact.set(direction=contact_direction(), run=False)
|
||||
direction_interval.reset()
|
||||
self.handle_map_run_2x(run=False)
|
||||
else:
|
||||
|
@ -81,7 +81,7 @@ class JoystickContact:
|
||||
def direction2screen(cls, direction, run=True):
|
||||
"""
|
||||
Args:
|
||||
direction (int, float): Direction to goto (0~360)
|
||||
direction (int, float): Direction to goto (-180~180)
|
||||
run: True for character running, False for walking
|
||||
|
||||
Returns:
|
||||
@ -111,7 +111,7 @@ class JoystickContact:
|
||||
Set joystick to given position
|
||||
|
||||
Args:
|
||||
direction (int, float): Direction to goto (0~360)
|
||||
direction (int, float): Direction to goto (-180~180)
|
||||
run: True for character running, False for walking
|
||||
"""
|
||||
logger.info(f'JoystickContact set to {direction}, run={run}')
|
||||
|
@ -16,6 +16,13 @@ class Waypoint:
|
||||
# Max move speed, 'run_2x', 'straight_run', 'run', 'walk'
|
||||
# See MapControl._goto() for details of each speed level
|
||||
speed: str = 'run'
|
||||
# Min move speed, 'run' or 'walk'
|
||||
min_speed: str = 'walk'
|
||||
# Lock joystick direction on the way to this waypoint, -180~180
|
||||
lock_direction: int = None
|
||||
# Trigger handle_combat_interact() if position diff < radius
|
||||
# Usually to use 7 if interact is required, 0 for no interact
|
||||
interact_radius: int = 0
|
||||
|
||||
"""
|
||||
The following attributes are only be used if this waypoint is the end point of goto()
|
||||
|
15
tasks/rogue/assets/assets_rogue_exit.py
Normal file
15
tasks/rogue/assets/assets_rogue_exit.py
Normal file
@ -0,0 +1,15 @@
|
||||
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 ```
|
||||
|
||||
OCR_DOMAIN_EXIT = ButtonWrapper(
|
||||
name='OCR_DOMAIN_EXIT',
|
||||
share=Button(
|
||||
file='./assets/share/rogue/exit/OCR_DOMAIN_EXIT.png',
|
||||
area=(0, 0, 1280, 320),
|
||||
search=(0, 0, 1280, 340),
|
||||
color=(255, 255, 255),
|
||||
button=(0, 0, 1280, 320),
|
||||
),
|
||||
)
|
@ -135,8 +135,6 @@ class RouteBase(RouteBase_, RogueExit, RogueEvent):
|
||||
"""
|
||||
if self.ui_page_appear(page_rogue):
|
||||
return True
|
||||
if self.handle_combat_interact():
|
||||
return False
|
||||
return False
|
||||
|
||||
def clear_event(self, *waypoints):
|
||||
@ -147,6 +145,7 @@ class RouteBase(RouteBase_, RogueExit, RogueEvent):
|
||||
waypoints = ensure_waypoints(waypoints)
|
||||
end_point = waypoints[-1]
|
||||
end_point.endpoint_threshold = 1.5
|
||||
end_point.interact_radius = 7
|
||||
end_point.expected_end.append(self._domain_event_expected_end)
|
||||
|
||||
result = self.goto(*waypoints)
|
||||
@ -194,9 +193,6 @@ class RouteBase(RouteBase_, RogueExit, RogueEvent):
|
||||
|
||||
if self.handle_popup_confirm():
|
||||
return False
|
||||
if self.minimap.position_diff(self.waypoint.position) < 7:
|
||||
if self.handle_combat_interact():
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
@ -236,6 +232,7 @@ class RouteBase(RouteBase_, RogueExit, RogueEvent):
|
||||
logger.hr('Domain single exit', level=1)
|
||||
waypoints = ensure_waypoints(waypoints)
|
||||
end_point = waypoints[-1]
|
||||
end_point.interact_radius = 7
|
||||
end_point.expected_end.append(self._domain_exit_expected_end)
|
||||
|
||||
result = self.goto(*waypoints)
|
||||
@ -251,8 +248,22 @@ class RouteBase(RouteBase_, RogueExit, RogueEvent):
|
||||
end_point.end_rotation_threshold = 10
|
||||
result = self.goto(*waypoints)
|
||||
|
||||
# TODO: Domain exit detection
|
||||
pass
|
||||
logger.hr('Find domain exit', level=2)
|
||||
direction = self.predict_door_by_name(self.device.image)
|
||||
direction_limit = 55
|
||||
if direction is not None:
|
||||
logger.warning(f'Unexpected direction to go: {direction}, limited in {direction_limit}')
|
||||
if abs(direction) > direction_limit:
|
||||
if direction > 0:
|
||||
direction = direction_limit
|
||||
elif direction < 0:
|
||||
direction = -direction_limit
|
||||
end_point.end_rotation = None
|
||||
end_point.min_speed = 'run'
|
||||
end_point.interact_radius = 50
|
||||
end_point.expected_end.append(self._domain_exit_expected_end)
|
||||
end_point.lock_direction = direction
|
||||
self.goto(end_point)
|
||||
|
||||
return result
|
||||
|
||||
|
@ -1,11 +1,82 @@
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from module.base.timer import Timer
|
||||
from module.base.utils import Points, extract_white_letters
|
||||
from module.logger import logger
|
||||
from tasks.base.assets.assets_base_main_page import OCR_MAP_NAME
|
||||
from tasks.base.main_page import OcrPlaneName
|
||||
from tasks.base.page import page_rogue
|
||||
from tasks.combat.interact import CombatInteract
|
||||
from tasks.map.keywords import KEYWORDS_MAP_PLANE, MapPlane
|
||||
from tasks.rogue.assets.assets_rogue_exit import OCR_DOMAIN_EXIT
|
||||
from tasks.rogue.assets.assets_rogue_reward import ROGUE_REPORT
|
||||
from tasks.rogue.assets.assets_rogue_ui import BLESSING_CONFIRM
|
||||
|
||||
|
||||
def area_center(area):
|
||||
"""
|
||||
Get the center of an area
|
||||
|
||||
Args:
|
||||
area: (upper_left_x, upper_left_y, bottom_right_x, bottom_right_y)
|
||||
|
||||
Returns:
|
||||
tuple: (x, y)
|
||||
"""
|
||||
x1, y1, x2, y2 = area
|
||||
return (x1 + x2) / 2, (y1 + y2) / 2
|
||||
|
||||
|
||||
class OcrDomainExit(OcrPlaneName):
|
||||
merge_thres_x = 50
|
||||
|
||||
def pre_process(self, image):
|
||||
image = extract_white_letters(image, threshold=255)
|
||||
image = cv2.merge([image, image, image])
|
||||
return image
|
||||
|
||||
def detect_and_ocr(self, *args, **kwargs):
|
||||
# Try hard to lower TextSystem.box_thresh
|
||||
backup = self.model.text_detector.box_thresh
|
||||
self.model.text_detector.box_thresh = 0.2
|
||||
|
||||
result = super().detect_and_ocr(*args, **kwargs)
|
||||
|
||||
self.model.text_detector.box_thresh = backup
|
||||
return result
|
||||
|
||||
def _match_result(
|
||||
self,
|
||||
result: str,
|
||||
keyword_classes,
|
||||
lang: str = None,
|
||||
ignore_punctuation=True,
|
||||
ignore_digit=True):
|
||||
matched = super()._match_result(result, keyword_classes, lang, ignore_punctuation, ignore_digit)
|
||||
|
||||
# Name may be covered by minimap, "Domain - " is missing,
|
||||
# check keywords like "Combat"
|
||||
if matched is None:
|
||||
for domain in MapPlane.instances.values():
|
||||
domain: MapPlane = domain
|
||||
if not domain.rogue_domain:
|
||||
continue
|
||||
|
||||
name = domain._keywords_to_find(ignore_punctuation=False)[0]
|
||||
try:
|
||||
name = re.split('[ \-—]', name)[-1]
|
||||
except IndexError:
|
||||
pass
|
||||
if name in result:
|
||||
return domain
|
||||
|
||||
return matched
|
||||
|
||||
|
||||
class RogueExit(CombatInteract):
|
||||
def domain_exit_interact(self, skip_first_screenshot=True):
|
||||
"""
|
||||
@ -57,3 +128,103 @@ class RogueExit(CombatInteract):
|
||||
if self.handle_popup_confirm():
|
||||
confirm.reset()
|
||||
continue
|
||||
|
||||
@staticmethod
|
||||
def screen2direction(point):
|
||||
"""
|
||||
Args:
|
||||
point: Coordinate on screenshot
|
||||
|
||||
Returns:
|
||||
float: Direction to move, -180~180
|
||||
"""
|
||||
screen_middle = (640.0, 360.0)
|
||||
vanish_point = np.array((640.0, 247.34))
|
||||
distant_point = np.array((1509.46, 247.34))
|
||||
name_y = 77.60
|
||||
foot_y = 621.82
|
||||
|
||||
door_projection_bottom = (
|
||||
Points([point]).link(vanish_point).get_x(name_y)[0],
|
||||
foot_y,
|
||||
)
|
||||
door_bottom = (
|
||||
point[0],
|
||||
Points([door_projection_bottom]).link(vanish_point).get_y(point[0])[0],
|
||||
)
|
||||
door_distant = (
|
||||
Points([door_bottom]).link(distant_point).get_x(foot_y)[0],
|
||||
foot_y,
|
||||
)
|
||||
planar_door = (
|
||||
door_projection_bottom[0] - screen_middle[0],
|
||||
door_projection_bottom[0] - door_distant[0],
|
||||
)
|
||||
if abs(planar_door[0]) < 5:
|
||||
direction = 0
|
||||
else:
|
||||
direction = np.rad2deg(np.arctan(planar_door[0] / planar_door[1]))
|
||||
|
||||
planar_door = (round(planar_door[0], 1), round(planar_door[1], 1))
|
||||
direction = round(direction, 1)
|
||||
logger.info(f'PlanarDoor: {planar_door}, direction: {direction}')
|
||||
return direction
|
||||
|
||||
def predict_door_by_name(self, image) -> Optional[float]:
|
||||
# Paint current name black
|
||||
x1, y1, x2, y2 = OCR_MAP_NAME.area
|
||||
image[y1:y2, x1:x2] = (0, 0, 0)
|
||||
|
||||
ocr = OcrDomainExit(OCR_DOMAIN_EXIT)
|
||||
results = ocr.matched_ocr(image, keyword_classes=MapPlane)
|
||||
centers = [area_center(result.area) for result in results]
|
||||
logger.info(f'DomainDoor: {centers}')
|
||||
directions = [self.screen2direction(center) for center in centers]
|
||||
|
||||
count = len(centers)
|
||||
if count == 0:
|
||||
logger.warning('No domain exit found')
|
||||
return None
|
||||
if count == 1:
|
||||
logger.info(f'Goto next domain: {results[0]}')
|
||||
return directions[0]
|
||||
|
||||
# Doors >= 2
|
||||
for expect in [
|
||||
KEYWORDS_MAP_PLANE.Rogue_DomainBoss,
|
||||
KEYWORDS_MAP_PLANE.Rogue_DomainElite,
|
||||
KEYWORDS_MAP_PLANE.Rogue_DomainRespite,
|
||||
]:
|
||||
for domain, direction in zip(results, directions):
|
||||
if domain == expect:
|
||||
logger.warning('Found multiple doors but has unique domain in it')
|
||||
logger.info(f'Goto next domain: {domain}')
|
||||
return direction
|
||||
if self.config.RoguePath_DomainStrategy == 'leave':
|
||||
for expect in [
|
||||
KEYWORDS_MAP_PLANE.Rogue_DomainTransaction,
|
||||
KEYWORDS_MAP_PLANE.Rogue_DomainOccurrence,
|
||||
KEYWORDS_MAP_PLANE.Rogue_DomainEncounter,
|
||||
KEYWORDS_MAP_PLANE.Rogue_DomainCombat,
|
||||
]:
|
||||
for domain, direction in zip(results, directions):
|
||||
if domain == expect:
|
||||
logger.info(f'Goto next domain: {domain}')
|
||||
return direction
|
||||
elif self.config.RoguePath_DomainStrategy == 'fight':
|
||||
for expect in [
|
||||
KEYWORDS_MAP_PLANE.Rogue_DomainCombat,
|
||||
KEYWORDS_MAP_PLANE.Rogue_DomainEncounter,
|
||||
KEYWORDS_MAP_PLANE.Rogue_DomainOccurrence,
|
||||
KEYWORDS_MAP_PLANE.Rogue_DomainTransaction,
|
||||
]:
|
||||
for domain, direction in zip(results, directions):
|
||||
if domain == expect:
|
||||
logger.info(f'Goto next domain: {domain}')
|
||||
return direction
|
||||
else:
|
||||
logger.error(f'Unknown domain strategy: {self.config.RoguePath_DomainStrategy}')
|
||||
|
||||
logger.error('No domain was selected, return the first instead')
|
||||
logger.info(f'Goto next domain: {results[0]}')
|
||||
return directions[0]
|
||||
|
Loading…
Reference in New Issue
Block a user