StarRailCopilot/tasks/map/control/control.py

430 lines
16 KiB
Python
Raw Normal View History

from collections import deque
2023-08-06 14:19:10 +00:00
from functools import cached_property
import numpy as np
2023-08-06 14:19:10 +00:00
from module.base.timer import Timer
from module.logger import logger
from tasks.base.assets.assets_base_page import CLOSE
2023-09-21 16:23:00 +00:00
from tasks.combat.combat import Combat
2023-08-06 14:19:10 +00:00
from tasks.map.assets.assets_map_control import ROTATION_SWIPE_AREA
2023-09-21 16:23:00 +00:00
from tasks.map.control.joystick import JoystickContact
from tasks.map.control.waypoint import Waypoint, ensure_waypoints
from tasks.map.interact.aim import AimDetectorMixin
2023-08-06 14:19:10 +00:00
from tasks.map.minimap.minimap import Minimap
2023-08-20 17:28:30 +00:00
from tasks.map.resource.const import diff_to_180_180
2023-08-06 14:19:10 +00:00
2023-09-21 16:23:00 +00:00
class MapControl(Combat, AimDetectorMixin):
waypoint: Waypoint
2023-08-06 14:19:10 +00:00
@cached_property
def minimap(self) -> Minimap:
return Minimap()
def handle_rotation_set(self, target, threshold=15):
"""
Set rotation while running.
self.minimap.update_rotation() must be called first.
Args:
target: Target degree (0~360)
threshold:
Returns:
bool: If swiped rotation
"""
if self.minimap.is_rotation_near(target, threshold=threshold):
return False
2023-08-20 17:28:30 +00:00
# if abs(self.minimap.rotation_diff(target)) > 60:
# self.device.image_save()
# exit(1)
2023-08-06 14:19:10 +00:00
logger.info(f'Rotation set: {target}')
diff = self.minimap.rotation_diff(target) * self.minimap.ROTATION_SWIPE_MULTIPLY
diff = min(diff, self.minimap.ROTATION_SWIPE_MAX_DISTANCE)
diff = max(diff, -self.minimap.ROTATION_SWIPE_MAX_DISTANCE)
self.device.swipe_vector((-diff, 0), box=ROTATION_SWIPE_AREA.area, duration=(0.2, 0.5))
return True
def rotation_set(self, target, threshold=15, skip_first_screenshot=False):
"""
Set rotation while standing.
Args:
target: Target degree (0~360)
threshold:
skip_first_screenshot:
Returns:
bool: If swiped rotation
"""
2023-08-20 17:28:30 +00:00
interval = Timer(1, count=2)
2023-08-06 14:19:10 +00:00
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
self.minimap.update_rotation(self.device.image)
self.minimap.log_minimap()
# End
if self.minimap.is_rotation_near(target, threshold=threshold):
logger.info(f'Rotation is now at: {target}')
break
2023-08-20 17:28:30 +00:00
if interval.reached():
if self.handle_rotation_set(target, threshold=threshold):
interval.reset()
continue
def walk_additional(self) -> bool:
"""
Handle popups during walk
Returns:
bool: If handled
"""
if self.appear_then_click(CLOSE):
return True
return False
2023-08-20 17:28:30 +00:00
def _goto(
self,
contact: JoystickContact,
waypoint: Waypoint,
end_opt=True,
2023-08-20 17:28:30 +00:00
skip_first_screenshot=False
2023-09-21 16:23:00 +00:00
) -> list[str]:
2023-08-20 17:28:30 +00:00
"""
Point to point walk.
Args:
contact:
JoystickContact, must be wrapped with:
`with JoystickContact(self) as contact:`
waypoint:
Position to goto, (x, y)
end_opt:
2023-08-20 17:28:30 +00:00
True to enable endpoint optimizations,
character will smoothly approach target position
skip_first_screenshot:
2023-09-21 16:23:00 +00:00
Returns:
list[str]: A list of walk result
2023-08-20 17:28:30 +00:00
"""
logger.hr('Goto', level=2)
logger.info(f'Goto {waypoint}')
self.waypoint = waypoint
2023-08-20 17:28:30 +00:00
self.device.stuck_record_clear()
self.device.click_record_clear()
end_opt = end_opt and waypoint.end_opt
2023-09-21 16:23:00 +00:00
allow_run_2x = waypoint.speed in ['run_2x']
allow_straight_run = waypoint.speed in ['run_2x', 'straight_run']
allow_run = waypoint.speed in ['run_2x', 'straight_run', 'run']
allow_walk = True
2023-08-20 17:28:30 +00:00
allow_rotation_set = True
last_rotation = 0
2023-09-21 16:23:00 +00:00
result = []
2023-08-20 17:28:30 +00:00
direction_interval = Timer(0.5, count=1)
rotation_interval = Timer(0.3, count=1)
2023-09-21 16:23:00 +00:00
aim_interval = Timer(0.3, count=1)
attacked_enemy = Timer(1.2, count=4)
attacked_item = Timer(0.6, count=2)
near_queue = deque(maxlen=waypoint.unexpected_confirm.count)
2023-08-20 17:28:30 +00:00
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
2023-09-21 16:23:00 +00:00
# End
for expected in waypoint.expected_end:
if callable(expected):
if expected():
logger.info(f'Walk result add: {expected.__name__}')
result.append(expected.__name__)
return result
if self.is_combat_executing():
logger.info('Walk result add: enemy')
result.append('enemy')
contact.up()
logger.hr('Combat', level=2)
2023-09-21 16:23:00 +00:00
self.combat_execute()
if waypoint.early_stop:
return result
if self.walk_additional():
attacked_enemy.clear()
attacked_item.clear()
continue
2023-09-21 16:23:00 +00:00
# The following detection require page_main
if not self.is_in_main():
attacked_enemy.clear()
attacked_item.clear()
continue
2023-08-20 17:28:30 +00:00
# Update
self.minimap.update(self.device.image)
2023-09-21 16:23:00 +00:00
if aim_interval.reached_and_reset():
self.aim.predict(self.device.image)
diff = self.minimap.position_diff(waypoint.position)
direction = self.minimap.position2direction(waypoint.position)
2023-10-02 09:17:06 +00:00
rotation_diff = self.minimap.rotation_diff(direction)
logger.info(f'Pdiff: {diff}, Ddiff: {direction}, Rdiff: {rotation_diff}')
2023-09-21 16:23:00 +00:00
2023-10-04 17:58:14 +00:00
def contact_direction():
if waypoint.lock_direction is not None:
return waypoint.lock_direction
return diff_to_180_180(direction - last_rotation)
2023-09-21 16:23:00 +00:00
# Interact
if self.aim.aimed_enemy:
if 'enemy' in waypoint.expected_end:
if self.handle_map_A():
allow_run_2x = allow_straight_run = False
2023-09-21 16:23:00 +00:00
attacked_enemy.reset()
direction_interval.reset()
rotation_interval.reset()
if attacked_enemy.started():
attacked_enemy.reset()
if self.aim.aimed_item:
if 'item' in waypoint.expected_end:
if self.handle_map_A():
allow_run_2x = allow_straight_run = False
2023-09-21 16:23:00 +00:00
attacked_item.reset()
direction_interval.reset()
rotation_interval.reset()
elif 'item' in waypoint.expected_enroute:
if self.handle_map_A():
direction_interval.reset()
rotation_interval.reset()
2023-09-21 16:23:00 +00:00
if attacked_item.started():
attacked_item.reset()
else:
if attacked_item.started() and attacked_item.reached():
logger.info('Walk result add: item')
result.append('item')
if waypoint.early_stop:
return result
2023-10-04 17:58:14 +00:00
if waypoint.interact_radius > 0:
if diff < waypoint.interact_radius:
2023-10-19 02:06:31 +00:00
if self.handle_combat_interact(interval=1):
contact.up()
2023-08-20 17:28:30 +00:00
# Arrive
if near := self.minimap.is_position_near(waypoint.position, threshold=waypoint.get_threshold(end_opt)):
near_queue.append(near)
if not waypoint.expected_end or waypoint.match_results(result):
logger.info(f'Arrive waypoint: {waypoint}')
return result
2023-09-21 16:23:00 +00:00
else:
if waypoint.unexpected_confirm.reached():
logger.info(f'Arrive waypoint with unexpected result: {waypoint}')
return result
else:
near_queue.append(near)
if np.mean(near_queue) < 0.6:
2023-09-21 16:23:00 +00:00
waypoint.unexpected_confirm.reset()
2023-08-20 17:28:30 +00:00
# Switch run case
if end_opt:
2023-09-21 16:23:00 +00:00
if allow_run_2x and diff < 20:
logger.info(f'Approaching target, diff={round(diff, 1)}, disallow run_2x')
allow_run_2x = False
if allow_straight_run and diff < 15 and not allow_rotation_set:
2023-08-20 17:28:30 +00:00
logger.info(f'Approaching target, diff={round(diff, 1)}, disallow straight_run')
direction_interval = Timer(0.2)
2023-09-21 16:23:00 +00:00
aim_interval = Timer(0.1)
self.map_run_2x_timer.reset()
2023-08-20 17:28:30 +00:00
allow_straight_run = False
2023-10-04 17:58:14 +00:00
if allow_run and diff < 7 and waypoint.min_speed == 'walk':
2023-08-20 17:28:30 +00:00
logger.info(f'Approaching target, diff={round(diff, 1)}, disallow run')
direction_interval = Timer(0.2)
2023-09-21 16:23:00 +00:00
aim_interval = Timer(0.2)
2023-08-20 17:28:30 +00:00
allow_run = False
# Control
2023-09-21 16:23:00 +00:00
if allow_run_2x:
# Run with run_2x button
2023-08-20 17:28:30 +00:00
# - Set rotation once
# - Continuous fine-tuning direction
2023-09-21 16:23:00 +00:00
# - Enable run_2x
2023-08-20 17:28:30 +00:00
if allow_rotation_set:
# Cache rotation cause rotation detection has a higher error rate
last_rotation = self.minimap.rotation
if self.minimap.is_rotation_near(direction, threshold=10):
logger.info(f'Already at target rotation, '
f'current={last_rotation}, target={direction}, disallow rotation_set')
allow_rotation_set = False
if not allow_rotation_set and rotation_interval.reached_and_reset():
last_rotation = self.minimap.rotation
2023-08-20 17:28:30 +00:00
if allow_rotation_set and rotation_interval.reached():
if self.handle_rotation_set(direction, threshold=10):
rotation_interval.reset()
direction_interval.reset()
if direction_interval.reached():
2023-10-04 17:58:14 +00:00
contact.set(direction=contact_direction(), run=True)
2023-08-20 17:28:30 +00:00
direction_interval.reset()
2023-09-21 16:23:00 +00:00
self.handle_map_run_2x(run=True)
2023-08-20 17:28:30 +00:00
elif allow_straight_run:
# Run straight forward
2023-08-20 17:28:30 +00:00
# - Set rotation once
# - Continuous fine-tuning direction
2023-09-21 16:23:00 +00:00
# - Disable run_2x
2023-08-20 17:28:30 +00:00
if allow_rotation_set:
# Cache rotation cause rotation detection has a higher error rate
last_rotation = self.minimap.rotation
if self.minimap.is_rotation_near(direction, threshold=10):
logger.info(f'Already at target rotation, '
f'current={last_rotation}, target={direction}, disallow rotation_set')
allow_rotation_set = False
if not allow_rotation_set and rotation_interval.reached_and_reset():
last_rotation = self.minimap.rotation
2023-08-20 17:28:30 +00:00
if allow_rotation_set and rotation_interval.reached():
if self.handle_rotation_set(direction, threshold=10):
rotation_interval.reset()
direction_interval.reset()
if direction_interval.reached():
2023-10-04 17:58:14 +00:00
contact.set(direction=contact_direction(), run=True)
2023-08-20 17:28:30 +00:00
direction_interval.reset()
2023-09-21 16:23:00 +00:00
self.handle_map_run_2x(run=False)
2023-08-20 17:28:30 +00:00
elif allow_run:
# Run
# - No rotation set
# - Continuous fine-tuning direction
2023-09-21 16:23:00 +00:00
# - Disable run_2x
2023-08-20 17:28:30 +00:00
if allow_rotation_set:
last_rotation = self.minimap.rotation
allow_rotation_set = False
if not allow_rotation_set and rotation_interval.reached_and_reset():
last_rotation = self.minimap.rotation
2023-08-20 17:28:30 +00:00
if direction_interval.reached():
2023-10-04 17:58:14 +00:00
contact.set(direction=contact_direction(), run=True)
2023-09-21 16:23:00 +00:00
direction_interval.reset()
self.handle_map_run_2x(run=False)
elif allow_walk:
2023-08-20 17:28:30 +00:00
# Walk
# - Continuous fine-tuning direction
2023-09-21 16:23:00 +00:00
# - Disable run_2x
2023-08-20 17:28:30 +00:00
if allow_rotation_set:
last_rotation = self.minimap.rotation
allow_rotation_set = False
if not allow_rotation_set and rotation_interval.reached_and_reset():
last_rotation = self.minimap.rotation
2023-08-20 17:28:30 +00:00
if direction_interval.reached():
2023-10-04 17:58:14 +00:00
contact.set(direction=contact_direction(), run=False)
2023-08-20 17:28:30 +00:00
direction_interval.reset()
2023-09-21 16:23:00 +00:00
self.handle_map_run_2x(run=False)
else:
contact.up()
2023-08-20 17:28:30 +00:00
2023-09-21 16:23:00 +00:00
def goto(self, *waypoints):
2023-08-20 17:28:30 +00:00
"""
2023-09-21 16:23:00 +00:00
Go along a list of position, or goto target position.
2023-08-20 17:28:30 +00:00
Args:
2023-09-21 16:23:00 +00:00
waypoints: position (x, y), a list of position to go along,
or a list of Waypoint objects to go along.
Returns:
list[str]: A list of walk result
2023-08-20 17:28:30 +00:00
"""
logger.hr('Goto', level=1)
2023-09-21 16:23:00 +00:00
self.map_A_timer.clear()
self.map_E_timer.clear()
self.map_run_2x_timer.clear()
waypoints = ensure_waypoints(waypoints)
2023-08-20 17:28:30 +00:00
logger.info(f'Go along {len(waypoints)} waypoints')
end_list = [False for _ in waypoints]
end_list[-1] = True
results = []
2023-08-20 17:28:30 +00:00
with JoystickContact(self) as contact:
2023-09-21 16:23:00 +00:00
for waypoint, end in zip(waypoints, end_list):
waypoint: Waypoint
result = self._goto(
2023-08-20 17:28:30 +00:00
contact=contact,
2023-09-21 16:23:00 +00:00
waypoint=waypoint,
end_opt=end,
2023-09-21 16:23:00 +00:00
skip_first_screenshot=True,
2023-08-20 17:28:30 +00:00
)
2023-09-21 16:23:00 +00:00
expected = waypoint.expected_to_str(waypoint.expected_end)
logger.info(f'Arrive waypoint, expected: {expected}, result: {result}')
results += result
2023-09-21 16:23:00 +00:00
matched = waypoint.match_results(result)
2023-10-07 13:26:24 +00:00
if not waypoint.expected_end:
logger.info(f'Arrive waypoint: {matched}')
elif matched:
2023-09-21 16:23:00 +00:00
logger.info(f'Arrive waypoint with expected result: {matched}')
2023-10-07 13:26:24 +00:00
break
2023-09-21 16:23:00 +00:00
else:
logger.warning(f'Arrive waypoint with unexpected result: {result}')
2023-08-20 17:28:30 +00:00
return results
2023-08-20 17:28:30 +00:00
2023-09-21 16:23:00 +00:00
def clear_item(self, *waypoints):
"""
Go along a list of position and clear destructive object at last.
Args:
waypoints: position (x, y), a list of position to go along.
or a list of Waypoint objects to go along.
"""
logger.hr('Clear item', level=1)
waypoints = ensure_waypoints(waypoints)
for point in waypoints[:-1]:
point.expected_end = []
2023-09-21 16:23:00 +00:00
end_point = waypoints[-1]
end_point.expected_end.append('item')
self.goto(*waypoints)
def clear_enemy(self, *waypoints):
"""
Go along a list of position and enemy at last.
Args:
waypoints: position (x, y), a list of position to go along.
or a list of Waypoint objects to go along.
"""
logger.hr('Clear enemy', level=1)
2023-09-21 16:23:00 +00:00
waypoints = ensure_waypoints(waypoints)
for point in waypoints[:-1]:
point.expected_end = []
2023-09-21 16:23:00 +00:00
end_point = waypoints[-1]
end_point.expected_end.append('enemy')
self.goto(*waypoints)
2023-08-20 17:28:30 +00:00
if __name__ == '__main__':
# Control test in Himeko trial
# Must manually enter Himeko trial first and dismiss popup
2023-09-19 17:20:52 +00:00
self = MapControl('src')
2023-08-20 17:28:30 +00:00
self.minimap.set_plane('Jarilo_BackwaterPass', floor='F1')
self.device.screenshot()
self.minimap.init_position((519, 359))
# Visit 3 items
2023-09-21 16:23:00 +00:00
self.clear_item(
Waypoint((587.6, 366.9)).run_2x(),
)
self.clear_item(
Waypoint((575.5, 377.4)).straight_run(),
)
2023-09-21 16:23:00 +00:00
self.clear_item(
# Go through arched door
Waypoint((581.5, 383.3)).set_threshold(3),
Waypoint((575.7, 417.2)),
)
2023-08-20 17:28:30 +00:00
# Goto boss
self.clear_enemy(
Waypoint((613.5, 427.3)).straight_run(),
)