Add: Detect domain exit and go

This commit is contained in:
LmeSzinc 2023-10-05 01:58:14 +08:00
parent 62150e60b9
commit 312cc6f617
8 changed files with 238 additions and 14 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -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:

View File

@ -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:

View File

@ -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}')

View File

@ -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()

View 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),
),
)

View File

@ -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

View File

@ -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]