mirror of
https://github.com/LmeSzinc/StarRailCopilot.git
synced 2024-11-24 17:42:46 +00:00
261 lines
9.0 KiB
Python
261 lines
9.0 KiB
Python
import numpy as np
|
|
from scipy import signal
|
|
|
|
from module.base.base import ModuleBase
|
|
from module.base.button import Button, ButtonWrapper
|
|
from module.base.timer import Timer
|
|
from module.base.utils import color_similarity_2d, random_rectangle_point, rgb2gray
|
|
from module.logger import logger
|
|
|
|
|
|
class Scroll:
|
|
color_threshold = 221
|
|
drag_threshold = 0.05
|
|
edge_threshold = 0.05
|
|
edge_add = (0.3, 0.5)
|
|
|
|
def __init__(self, area, color, is_vertical=True, name='Scroll'):
|
|
"""
|
|
Args:
|
|
area (Button, tuple): A button or area of the whole scroll.
|
|
color (tuple): RGB of the scroll
|
|
is_vertical (bool): True if vertical, false if horizontal.
|
|
name (str):
|
|
"""
|
|
if isinstance(area, (Button, ButtonWrapper)):
|
|
# name = area.name
|
|
area = area.area
|
|
self.area = area
|
|
self.color = color
|
|
self.is_vertical = is_vertical
|
|
self.name = name
|
|
|
|
if self.is_vertical:
|
|
self.total = self.area[3] - self.area[1]
|
|
else:
|
|
self.total = self.area[2] - self.area[0]
|
|
# Just default value, will change in match_color()
|
|
self.length = self.total / 2
|
|
self.drag_interval = Timer(1, count=2)
|
|
self.drag_timeout = Timer(5, count=10)
|
|
|
|
def match_color(self, main):
|
|
"""
|
|
Args:
|
|
main (ModuleBase):
|
|
|
|
Returns:
|
|
np.ndarray: Shape (n,), dtype bool.
|
|
"""
|
|
image = main.image_crop(self.area, copy=False)
|
|
image = color_similarity_2d(image, color=self.color)
|
|
mask = np.max(image, axis=1 if self.is_vertical else 0) > self.color_threshold
|
|
self.length = np.sum(mask)
|
|
return mask
|
|
|
|
def cal_position(self, main):
|
|
"""
|
|
Args:
|
|
main (ModuleBase):
|
|
|
|
Returns:
|
|
float: 0 to 1.
|
|
"""
|
|
mask = self.match_color(main)
|
|
middle = np.mean(np.where(mask)[0])
|
|
|
|
position = (middle - self.length / 2) / (self.total - self.length)
|
|
position = position if position > 0 else 0.0
|
|
position = position if position < 1 else 1.0
|
|
logger.attr(self.name, f'{position:.2f} ({middle}-{self.length / 2})/({self.total}-{self.length})')
|
|
return position
|
|
|
|
def position_to_screen(self, position, random_range=(-0.05, 0.05)):
|
|
"""
|
|
Convert scroll position to screen coordinates.
|
|
Call cal_position() or match_color() to get length, before calling this.
|
|
|
|
Args:
|
|
position (int, float):
|
|
random_range (tuple):
|
|
|
|
Returns:
|
|
tuple[int]: (upper_left_x, upper_left_y, bottom_right_x, bottom_right_y)
|
|
"""
|
|
position = np.add(position, random_range)
|
|
middle = position * (self.total - self.length) + self.length / 2
|
|
middle = middle.astype(int)
|
|
if self.is_vertical:
|
|
middle += self.area[1]
|
|
while np.max(middle) >= 720:
|
|
middle -= 2
|
|
while np.min(middle) <= 0:
|
|
middle += 2
|
|
area = (self.area[0], middle[0], self.area[2], middle[1])
|
|
else:
|
|
middle += self.area[0]
|
|
while np.max(middle) >= 1280:
|
|
middle -= 2
|
|
while np.min(middle) <= 0:
|
|
middle += 2
|
|
area = (middle[0], self.area[1], middle[1], self.area[3])
|
|
return area
|
|
|
|
def appear(self, main):
|
|
"""
|
|
Args:
|
|
main (ModuleBase):
|
|
|
|
Returns:
|
|
bool
|
|
"""
|
|
return np.mean(self.match_color(main)) > 0.1
|
|
|
|
def is_draggable(self, main):
|
|
"""
|
|
If scroll `length` is just a little smaller than `total`,
|
|
game client may not respond to such a short swipe.
|
|
|
|
Args:
|
|
main (ModuleBase):
|
|
|
|
Returns:
|
|
bool:
|
|
"""
|
|
_ = self.cal_position(main)
|
|
return self.length / self.total < 0.95
|
|
|
|
def at_top(self, main):
|
|
return self.cal_position(main) < self.edge_threshold
|
|
|
|
def at_bottom(self, main):
|
|
return self.cal_position(main) > 1 - self.edge_threshold
|
|
|
|
def set(self, position, main, random_range=(-0.05, 0.05), distance_check=True, skip_first_screenshot=True):
|
|
"""
|
|
Set scroll to a specific position.
|
|
|
|
Args:
|
|
position (float, int): 0 to 1.
|
|
main (ModuleBase):
|
|
random_range (tuple(int, float)):
|
|
distance_check (bool): Whether to drop short swipes
|
|
skip_first_screenshot:
|
|
|
|
Returns:
|
|
bool: If dragged.
|
|
"""
|
|
logger.info(f'{self.name} set to {position}')
|
|
self.drag_interval.clear()
|
|
self.drag_timeout.reset()
|
|
dragged = 0
|
|
if position <= self.edge_threshold:
|
|
random_range = np.subtract(0, self.edge_add)
|
|
if position >= 1 - self.edge_threshold:
|
|
random_range = self.edge_add
|
|
|
|
while 1:
|
|
if skip_first_screenshot:
|
|
skip_first_screenshot = False
|
|
else:
|
|
main.device.screenshot()
|
|
|
|
current = self.cal_position(main)
|
|
if abs(position - current) < self.drag_threshold:
|
|
break
|
|
if self.length:
|
|
self.drag_timeout.reset()
|
|
else:
|
|
if self.drag_timeout.reached():
|
|
logger.warning('Scroll disappeared, assume scroll set')
|
|
break
|
|
else:
|
|
continue
|
|
|
|
if self.drag_interval.reached():
|
|
p1 = random_rectangle_point(self.position_to_screen(current), n=1)
|
|
p2 = random_rectangle_point(self.position_to_screen(position, random_range=random_range), n=1)
|
|
main.device.swipe(p1, p2, name=self.name, distance_check=distance_check)
|
|
self.drag_interval.reset()
|
|
dragged += 1
|
|
|
|
return dragged
|
|
|
|
def set_top(self, main, random_range=(-0.05, 0.05), skip_first_screenshot=True):
|
|
return self.set(0.00, main=main, random_range=random_range, skip_first_screenshot=skip_first_screenshot)
|
|
|
|
def set_bottom(self, main, random_range=(-0.05, 0.05), skip_first_screenshot=True):
|
|
return self.set(1.00, main=main, random_range=random_range, skip_first_screenshot=skip_first_screenshot)
|
|
|
|
def drag_page(self, page, main, random_range=(-0.05, 0.05), skip_first_screenshot=True):
|
|
"""
|
|
Drag scroll forward or backward.
|
|
|
|
Args:
|
|
page (int, float): Relative position to drag. 1.0 means next page, -1.0 means previous page.
|
|
main (ModuleBase):
|
|
random_range (tuple[float]):
|
|
skip_first_screenshot:
|
|
"""
|
|
if not skip_first_screenshot:
|
|
main.device.screenshot()
|
|
current = self.cal_position(main)
|
|
|
|
multiply = self.length / (self.total - self.length)
|
|
target = current + page * multiply
|
|
target = round(min(max(target, 0), 1), 3)
|
|
return self.set(target, main=main, random_range=random_range, skip_first_screenshot=True)
|
|
|
|
def next_page(self, main, page=0.8, random_range=(-0.01, 0.01), skip_first_screenshot=True):
|
|
return self.drag_page(page, main=main, random_range=random_range, skip_first_screenshot=skip_first_screenshot)
|
|
|
|
def prev_page(self, main, page=0.8, random_range=(-0.01, 0.01), skip_first_screenshot=True):
|
|
return self.drag_page(-page, main=main, random_range=random_range, skip_first_screenshot=skip_first_screenshot)
|
|
|
|
|
|
class AdaptiveScroll(Scroll):
|
|
def __init__(self, area, parameters: dict = None, background=5, is_vertical=True, name='Scroll'):
|
|
"""
|
|
Args:
|
|
area (Button, tuple): A button or area of the whole scroll.
|
|
parameters (dict): Parameters passing to scipy.find_peaks
|
|
background (int):
|
|
is_vertical (bool): True if vertical, false if horizontal.
|
|
name (str):
|
|
"""
|
|
if parameters is None:
|
|
parameters = {}
|
|
self.parameters = parameters
|
|
self.background = background
|
|
super().__init__(area, color=(255, 255, 255), is_vertical=is_vertical, name=name)
|
|
|
|
def match_color(self, main):
|
|
if self.is_vertical:
|
|
area = (self.area[0] - self.background, self.area[1], self.area[2] + self.background, self.area[3])
|
|
image = main.image_crop(area, copy=False)
|
|
image = rgb2gray(image)
|
|
image = image.flatten()
|
|
wlen = area[2] - area[0]
|
|
else:
|
|
area = (self.area[0], self.area[1] - self.background, self.area[2], self.area[3] + self.background)
|
|
image = main.image_crop(area, copy=False)
|
|
image = rgb2gray(image)
|
|
image = image.flatten('F')
|
|
wlen = area[3] - area[1]
|
|
|
|
parameters = {
|
|
'height': 128,
|
|
'prominence': 30,
|
|
'wlen': wlen,
|
|
'width': 2,
|
|
# 'distance': wlen / 2,
|
|
}
|
|
parameters.update(self.parameters)
|
|
peaks, _ = signal.find_peaks(image, **parameters)
|
|
peaks //= wlen
|
|
|
|
self.length = len(peaks)
|
|
mask = np.zeros((self.total,), dtype=np.bool_)
|
|
mask[peaks] = 1
|
|
return mask
|