mirror of
https://github.com/LmeSzinc/StarRailCopilot.git
synced 2024-11-25 01:49:41 +00:00
263 lines
8.3 KiB
Python
263 lines
8.3 KiB
Python
import math
|
|
from functools import cached_property
|
|
|
|
import cv2
|
|
import numpy as np
|
|
|
|
from module.base.timer import Timer
|
|
from module.device.method.maatouch import MaatouchBuilder
|
|
from module.device.method.minitouch import CommandBuilder, insert_swipe, random_normal_distribution
|
|
from module.exception import ScriptError
|
|
from module.logger import logger
|
|
from tasks.base.ui import UI
|
|
from tasks.map.assets.assets_map_control import *
|
|
|
|
|
|
class JoystickContact:
|
|
CENTER = (JOYSTICK.area[0] + JOYSTICK.area[2]) / 2, (JOYSTICK.area[1] + JOYSTICK.area[3]) / 2
|
|
# Minimum radius 49px
|
|
RADIUS_WALK = (25, 40)
|
|
# Minimum radius 103px
|
|
RADIUS_RUN = (105, 115)
|
|
|
|
def __init__(self, main):
|
|
"""
|
|
Args:
|
|
main (MapControlJoystick):
|
|
"""
|
|
self.main = main
|
|
self.prev_point = None
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
"""
|
|
Lift finger when:
|
|
- Walk event ends, JoystickContact ends
|
|
- Any error is raised
|
|
Can not lift finger when:
|
|
- Process is force terminated
|
|
"""
|
|
if self.is_downed:
|
|
self.up()
|
|
logger.info('JoystickContact ends')
|
|
else:
|
|
logger.info('JoystickContact ends but it was never downed')
|
|
|
|
@property
|
|
def is_downed(self):
|
|
return self.prev_point is not None
|
|
|
|
@cached_property
|
|
def builder(self):
|
|
"""
|
|
Initialize a command builder
|
|
"""
|
|
method = self.main.config.Emulator_ControlMethod
|
|
if method == 'MaaTouch':
|
|
# Get the very first builder to initialize MaaTouch
|
|
_ = self.main.device.maatouch_builder
|
|
builder = MaatouchBuilder(self.main.device, contact=1)
|
|
elif method == 'minitouch':
|
|
# Get the very first builder to initialize minitouch
|
|
_ = self.main.device.minitouch_builder
|
|
builder = CommandBuilder(self.main.device, contact=1)
|
|
else:
|
|
raise ScriptError(f'Control method {method} does not support multi-finger, '
|
|
f'please use MaaTouch or minitouch instead')
|
|
|
|
# def empty_func():
|
|
# pass
|
|
#
|
|
# # No clear()
|
|
# builder.clear = empty_func
|
|
# No delay
|
|
builder.DEFAULT_DELAY = 0.
|
|
|
|
return builder
|
|
|
|
@classmethod
|
|
def direction2screen(cls, direction, run=True):
|
|
"""
|
|
Args:
|
|
direction (int, float): Direction to goto (-180~180)
|
|
run: True for character running, False for walking
|
|
|
|
Returns:
|
|
tuple[int, int]: Position on screen to control joystick
|
|
"""
|
|
direction += random_normal_distribution(-5, 5, n=5)
|
|
radius = cls.RADIUS_RUN if run else cls.RADIUS_WALK
|
|
radius = random_normal_distribution(*radius, n=5)
|
|
direction = math.radians(direction)
|
|
|
|
# Contact at the lower is limited within `cls.CENTER[1] - half_run_radius`
|
|
# or will exceed the joystick area
|
|
# Random radius * multiplier makes the point randomly approaching the lower bound
|
|
for multiplier in [1.0, 0.95, 0.90, 0.85, 0.80, 0.75]:
|
|
point = (
|
|
cls.CENTER[0] + radius * multiplier * math.sin(direction),
|
|
cls.CENTER[1] - radius * multiplier * math.cos(direction),
|
|
)
|
|
point = (int(round(point[0])), int(round(point[1])))
|
|
if point[1] <= cls.CENTER[1] - 101:
|
|
return point
|
|
return point
|
|
|
|
def up(self):
|
|
builder = self.builder
|
|
builder.up().commit()
|
|
builder.send()
|
|
self.prev_point = None
|
|
|
|
def set(self, direction, run=True):
|
|
"""
|
|
Set joystick to given position
|
|
|
|
Args:
|
|
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}')
|
|
point = JoystickContact.direction2screen(direction, run=run)
|
|
builder = self.builder
|
|
|
|
if self.is_downed and not self.main.joystick_speed():
|
|
if self.main.joystick_lost_timer.reached():
|
|
logger.warning(f'Joystick contact lost: {self.main.joystick_lost_timer}, re-down')
|
|
self.up()
|
|
else:
|
|
self.main.joystick_lost_timer.reset()
|
|
|
|
if self.is_downed:
|
|
points = insert_swipe(p0=self.prev_point, p3=point, speed=20)
|
|
for point in points[1:]:
|
|
builder.move(*point).commit().wait(10)
|
|
builder.send()
|
|
else:
|
|
builder.down(*point).commit()
|
|
builder.send()
|
|
# Character starts moving, RUN button is still unavailable in a short time.
|
|
# Assume available in 0.3s
|
|
# We still have reties if 0.3s is incorrect.
|
|
self.main.map_run_2x_timer.set_current(0.7)
|
|
self.main.joystick_lost_timer.reset()
|
|
|
|
self.prev_point = point
|
|
|
|
|
|
class MapControlJoystick(UI):
|
|
map_A_timer = Timer(1)
|
|
map_E_timer = Timer(1)
|
|
map_run_2x_timer = Timer(1)
|
|
|
|
joystick_lost_timer = Timer(1, count=1)
|
|
|
|
@cached_property
|
|
def joystick_center(self) -> tuple[int, int]:
|
|
x1, y1, x2, y2 = JOYSTICK.area
|
|
return int((x1 + x2) // 2), int((y1 + y2) // 2)
|
|
|
|
@cached_property
|
|
def DirectionRemapData(self):
|
|
d = JoystickContact.RADIUS_RUN[1] * 2
|
|
mx = np.zeros((d, d), dtype=np.float32)
|
|
my = np.zeros((d, d), dtype=np.float32)
|
|
for i in range(d):
|
|
for j in range(d):
|
|
mx[i, j] = d / 2 + i / 2 * np.cos(2 * np.pi * j / d)
|
|
my[i, j] = d / 2 + i / 2 * np.sin(2 * np.pi * j / d)
|
|
return mx, my
|
|
|
|
def joystick_speed(self) -> str:
|
|
"""
|
|
Returns:
|
|
str: 'run', 'walk', ''
|
|
"""
|
|
# About 1.5ms
|
|
x, y = self.joystick_center
|
|
radius = JoystickContact.RADIUS_RUN[1]
|
|
image = self.image_crop((x - radius, y - radius, x + radius, y + radius), copy=False)
|
|
image = cv2.remap(image, *self.DirectionRemapData, cv2.INTER_CUBIC)
|
|
|
|
# 190~205
|
|
run = image[185:210, :]
|
|
if self.image_color_count(run, color=(223, 199, 145), threshold=221, count=100):
|
|
return 'run'
|
|
# 90~100
|
|
walk = image[85:105, :]
|
|
if self.image_color_count(walk, color=(235, 235, 235), threshold=221, count=50):
|
|
return 'walk'
|
|
|
|
return ''
|
|
|
|
def map_get_technique_points(self):
|
|
"""
|
|
Returns:
|
|
int: 0 to 5.
|
|
"""
|
|
points = [
|
|
self.image_color_count(button, color=(255, 255, 255), threshold=221, count=20)
|
|
for button in [
|
|
TECHNIQUE_POINT_1,
|
|
TECHNIQUE_POINT_2,
|
|
TECHNIQUE_POINT_3,
|
|
TECHNIQUE_POINT_4,
|
|
TECHNIQUE_POINT_5,
|
|
]
|
|
]
|
|
count = sum(points)
|
|
logger.attr('TechniquePoints', count)
|
|
return count
|
|
|
|
def handle_map_A(self):
|
|
"""
|
|
Simply clicking A with an interval of 1s, no guarantee of success.
|
|
|
|
Returns:
|
|
bool: If clicked.
|
|
"""
|
|
if self.map_A_timer.reached():
|
|
self.device.click(A_BUTTON)
|
|
self.map_A_timer.reset()
|
|
return True
|
|
|
|
return False
|
|
|
|
def handle_map_E(self):
|
|
"""
|
|
Simply clicking E with an interval of 1s, no guarantee of success.
|
|
Note that E cannot be released if technique points ran out.
|
|
|
|
Returns:
|
|
bool: If clicked.
|
|
"""
|
|
if self.map_E_timer.reached():
|
|
self.device.click(E_BUTTON)
|
|
self.map_E_timer.reset()
|
|
return True
|
|
|
|
return False
|
|
|
|
def handle_map_run_2x(self, run=True):
|
|
"""
|
|
Keep character running.
|
|
Note that RUN button can only be clicked when character is moving.
|
|
|
|
Returns:
|
|
bool: If clicked.
|
|
"""
|
|
is_running = self.image_color_count(RUN_BUTTON, color=(208, 183, 138), threshold=221, count=100)
|
|
|
|
if run and not is_running and self.map_run_2x_timer.reached():
|
|
self.device.click(RUN_BUTTON)
|
|
self.map_run_2x_timer.reset()
|
|
return True
|
|
if not run and is_running and self.map_run_2x_timer.reached():
|
|
self.device.click(RUN_BUTTON)
|
|
self.map_run_2x_timer.reset()
|
|
return True
|
|
|
|
return False
|