mirror of
https://github.com/ok-oldking/ok-wuthering-waves.git
synced 2025-04-24 08:25:16 +00:00
添加大世界刷图
This commit is contained in:
parent
09b0ab6694
commit
12aa21eeb5
Binary file not shown.
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 202 KiB |
BIN
assets/images/77.png
Normal file
BIN
assets/images/77.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
assets/images/78.png
Normal file
BIN
assets/images/78.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
File diff suppressed because it is too large
Load Diff
@ -160,8 +160,7 @@ config = {
|
||||
["src.task.TacetTask", "TacetTask"],
|
||||
["src.task.FarmEchoTask", "FarmEchoTask"],
|
||||
["src.task.FarmWorldBossTask", "FarmWorldBossTask"],
|
||||
# ["src.task.FarmMapTask", "FarmMapTask"],
|
||||
# ["src.task.Farm13CTask", "Farm13CTask"],
|
||||
["src.task.FarmMapTask", "FarmMapTask"],
|
||||
["ok", "DiagnosisTask"],
|
||||
], 'trigger_tasks': [
|
||||
["src.task.AutoCombatTask", "AutoCombatTask"],
|
||||
|
@ -56,7 +56,7 @@ numpy==2.2.4
|
||||
# shapely
|
||||
rapidocr==2.0.6
|
||||
# via -r .\requirements.in
|
||||
ok-script==0.0.522
|
||||
ok-script==0.0.523
|
||||
# via -r .\requirements.in
|
||||
omegaconf==2.3.0
|
||||
# via ok-rapidocr-dml
|
||||
|
@ -28,6 +28,7 @@ class CombatCheck(BaseWWTask):
|
||||
self.last_in_realm_not_combat = 0
|
||||
self._last_liberation = 0
|
||||
self.target_enemy_time_out = 3
|
||||
self.check_pick_echo = False
|
||||
|
||||
@property
|
||||
def in_liberation(self):
|
||||
@ -110,6 +111,8 @@ class CombatCheck(BaseWWTask):
|
||||
now = time.time()
|
||||
if now - self.last_combat_check > self.combat_check_interval:
|
||||
self.last_combat_check = now
|
||||
if self.check_pick_echo:
|
||||
self.incr_drop(self.pick_f())
|
||||
if self.has_target():
|
||||
self.last_in_realm_not_combat = 0
|
||||
return True
|
||||
|
@ -1,3 +1,5 @@
|
||||
import time
|
||||
|
||||
from qfluentwidgets import FluentIcon
|
||||
|
||||
from ok import FindFeature, Logger
|
||||
@ -35,8 +37,12 @@ class AutoPickTask(TriggerTask, BaseWWTask, FindFeature):
|
||||
def run(self):
|
||||
if not self.scene.in_team(self.in_team_and_world):
|
||||
return
|
||||
while f := self.find_one('pick_up_f_hcenter_vcenter', box=self.f_search_box,
|
||||
threshold=0.8):
|
||||
start = time.time()
|
||||
while time.time() - start < 1:
|
||||
f = self.find_one('pick_up_f_hcenter_vcenter', box=self.f_search_box,
|
||||
threshold=0.8)
|
||||
if not f:
|
||||
return
|
||||
percent = self.calculate_color_percentage(f_white_color, f)
|
||||
if percent < 0.5:
|
||||
self.log_debug(f'f white color percent: {percent} wait')
|
||||
|
@ -61,8 +61,7 @@ class BaseWWTask(BaseTask):
|
||||
self.log_info('zoom map to max')
|
||||
self.map_zoomed = True
|
||||
self.send_key('m', after_sleep=1)
|
||||
for i in range(11):
|
||||
self.click(0.95, 0.29, after_sleep=0.1)
|
||||
self.click_relative(0.94, 0.33, after_sleep=0.5)
|
||||
self.send_key('esc', after_sleep=1)
|
||||
|
||||
def validate(self, key, value):
|
||||
@ -140,8 +139,8 @@ class BaseWWTask(BaseTask):
|
||||
return None
|
||||
return f
|
||||
|
||||
def walk_to_box(self, find_function, time_out=30, end_condition=None):
|
||||
if not find_function():
|
||||
def walk_to_box(self, find_function, time_out=30, end_condition=None, y_offset=0.05):
|
||||
if not find_function:
|
||||
self.log_info('find_function not found, break')
|
||||
return False
|
||||
last_direction = None
|
||||
@ -168,7 +167,7 @@ class BaseWWTask(BaseTask):
|
||||
continue
|
||||
x, y = treasure_icon.center()
|
||||
|
||||
y = max(0, y - self.height_of_screen(0.05))
|
||||
y = max(0, y - self.height_of_screen(y_offset))
|
||||
next_direction = self.get_direction(x, y, self.width, self.height)
|
||||
if next_direction != last_direction:
|
||||
if last_direction:
|
||||
@ -414,7 +413,7 @@ class BaseWWTask(BaseTask):
|
||||
"""
|
||||
# Load the ONNX model
|
||||
boxes = og.my_app.yolo_detect(self.frame, threshold=threshold, label=12)
|
||||
ret = sorted(boxes, key=lambda detection: abs(detection.y - self.height/2), reverse=True)
|
||||
ret = sorted(boxes, key=lambda detection: detection.x, reverse=True)
|
||||
return ret
|
||||
|
||||
def yolo_find_all(self, threshold=0.3):
|
||||
@ -435,10 +434,17 @@ class BaseWWTask(BaseTask):
|
||||
|
||||
def pick_echo(self):
|
||||
if self.find_f_with_text(target_text=self.absorb_echo_text()):
|
||||
if self.debug:
|
||||
self.screenshot('pick_echo')
|
||||
self.send_key('f')
|
||||
if not self.handle_claim_button():
|
||||
return True
|
||||
|
||||
def pick_f(self):
|
||||
if self.find_one('pick_up_f_hcenter_vcenter', box=self.f_search_box, threshold=0.8):
|
||||
self.send_key('f')
|
||||
return True
|
||||
|
||||
def yolo_find_echo(self, use_color=True, walk=True, turn=True):
|
||||
max_echo_count = 0
|
||||
if self.pick_echo():
|
||||
@ -452,13 +458,13 @@ class BaseWWTask(BaseTask):
|
||||
echos = self.find_echo()
|
||||
if self.debug:
|
||||
self.draw_boxes('echo', echos)
|
||||
if echos:
|
||||
self.screenshot('echo')
|
||||
max_echo_count = max(max_echo_count, len(echos))
|
||||
self.log_debug(f'max_echo_count {max_echo_count}')
|
||||
if echos and echos[0].center()[1] < self.height_of_screen(0.45):
|
||||
if echos:
|
||||
self.log_info(f'yolo found echo {echos}')
|
||||
if self.debug:
|
||||
self.screenshot('echo_yolo_picked')
|
||||
return self.walk_to_box(self.find_echo, time_out=15, end_condition=self.pick_echo), max_echo_count > 1
|
||||
return self.walk_to_box(self.find_echo, time_out=15, end_condition=self.pick_echo, y_offset=0.2), max_echo_count > 1
|
||||
if use_color:
|
||||
color_percent = self.calculate_color_percentage(echo_color, front_box)
|
||||
self.log_debug(f'pick_echo color_percent:{color_percent}')
|
||||
@ -599,7 +605,7 @@ class BaseWWTask(BaseTask):
|
||||
return 'en_US'
|
||||
return 'unknown_lang'
|
||||
|
||||
def teleport_to_boss(self, boss_name, use_custom=False, dead=False):
|
||||
def teleport_to_boss(self, boss_name, use_custom=False):
|
||||
self.zoom_map()
|
||||
pos = self.bosses_pos.get(boss_name)
|
||||
page = pos[0]
|
||||
@ -637,13 +643,8 @@ class BaseWWTask(BaseTask):
|
||||
self.log_info(f'index after scrolling down {index}')
|
||||
self.click_relative(0.89, 0.91)
|
||||
self.sleep(1)
|
||||
# 判断是否是角色死亡,需要传送复活状态
|
||||
if not dead:
|
||||
self.wait_click_travel(use_custom=use_custom)
|
||||
else:
|
||||
self.click_relative(0.92, 0.91)
|
||||
self.sleep(1)
|
||||
self.click_relative(0.68, 0.6)
|
||||
|
||||
self.wait_click_travel(use_custom=use_custom)
|
||||
self.wait_in_team_and_world(time_out=120)
|
||||
self.sleep(1)
|
||||
|
||||
@ -674,11 +675,11 @@ class BaseWWTask(BaseTask):
|
||||
self.click_relative(0.74, 0.92, after_sleep=1)
|
||||
if self.wait_click_feature(['confirm_btn_hcenter_vcenter', 'confirm_btn_highlight_hcenter_vcenter'],
|
||||
relative_x=-1, raise_if_not_found=True,
|
||||
threshold=0.7,
|
||||
threshold=0.6,
|
||||
time_out=4):
|
||||
self.wait_click_feature(['confirm_btn_hcenter_vcenter', 'confirm_btn_highlight_hcenter_vcenter'],
|
||||
relative_x=-1, raise_if_not_found=False,
|
||||
threshold=0.7,
|
||||
threshold=0.6,
|
||||
time_out=1)
|
||||
return True
|
||||
elif btn := self.find_one('gray_teleport', threshold=0.7):
|
||||
|
@ -55,12 +55,12 @@ class DiagnosisTask(WWOneTimeTask, BaseCombatTask):
|
||||
self.click_relative(x, y + (start - 1) * distance)
|
||||
self.sleep(0.5)
|
||||
|
||||
self.wait_click_feature('gray_button_challenge', raise_if_not_found=True, use_gray_scale=True,
|
||||
self.wait_click_feature('gray_button_challenge', raise_if_not_found=True,
|
||||
click_after_delay=0.5)
|
||||
self.wait_click_feature('gray_confirm_exit_button', relative_x=-1, raise_if_not_found=False,
|
||||
use_gray_scale=True, time_out=3, click_after_delay=0.5, threshold=0.8)
|
||||
time_out=3, click_after_delay=0.5, threshold=0.8)
|
||||
self.wait_click_feature('gray_start_battle', relative_x=-1, raise_if_not_found=True,
|
||||
use_gray_scale=True, click_after_delay=0.5, threshold=0.8)
|
||||
click_after_delay=0.5, threshold=0.8)
|
||||
|
||||
|
||||
echo_color = {
|
||||
|
@ -44,7 +44,7 @@ class FarmEchoTask(WWOneTimeTask, BaseCombatTask):
|
||||
self.walk_until_f(time_out=10,
|
||||
raise_if_not_found=True)
|
||||
logger.info(f'enter success')
|
||||
challenge = self.wait_feature('gray_button_challenge', raise_if_not_found=True, use_gray_scale=True)
|
||||
challenge = self.wait_feature('gray_button_challenge', raise_if_not_found=True)
|
||||
logger.info(f'found challenge {challenge}')
|
||||
self.sleep(1)
|
||||
self.choose_level(self.config.get("Level"))
|
||||
@ -59,9 +59,10 @@ class FarmEchoTask(WWOneTimeTask, BaseCombatTask):
|
||||
dropped = self.yolo_find_echo()[0]
|
||||
self.incr_drop(dropped)
|
||||
self.sleep(0.5)
|
||||
self.send_key('esc')
|
||||
self.send_key('esc', after_sleep=0.5)
|
||||
self.wait_click_feature('confirm_btn_hcenter_vcenter', relative_x=-1, raise_if_not_found=True,
|
||||
use_gray_scale=True, settle_time=2)
|
||||
post_action=lambda: self.send_key('esc', after_sleep=1),
|
||||
settle_time=2)
|
||||
self.wait_in_team_and_world(time_out=120)
|
||||
self.sleep(2)
|
||||
|
||||
@ -74,12 +75,12 @@ class FarmEchoTask(WWOneTimeTask, BaseCombatTask):
|
||||
self.click_relative(x, y + (start - 1) * distance)
|
||||
self.sleep(0.5)
|
||||
|
||||
self.wait_click_feature('gray_button_challenge', raise_if_not_found=True, use_gray_scale=True,
|
||||
self.wait_click_feature('gray_button_challenge', raise_if_not_found=True,
|
||||
click_after_delay=0.5)
|
||||
self.wait_click_feature('gray_confirm_exit_button', relative_x=-1, raise_if_not_found=False,
|
||||
use_gray_scale=True, time_out=3, click_after_delay=0.5, threshold=0.8)
|
||||
time_out=3, click_after_delay=0.5, threshold=0.8)
|
||||
self.wait_click_feature('gray_start_battle', relative_x=-1, raise_if_not_found=True,
|
||||
use_gray_scale=True, click_after_delay=0.5, threshold=0.8)
|
||||
click_after_delay=0.5, threshold=0.8)
|
||||
|
||||
|
||||
echo_color = {
|
||||
|
@ -1,58 +1,60 @@
|
||||
import math
|
||||
import re
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from qfluentwidgets import FluentIcon
|
||||
|
||||
from ok import Logger
|
||||
from ok import Logger, Box, get_bounding_box
|
||||
from src.task.BaseCombatTask import BaseCombatTask
|
||||
from src.task.WWOneTimeTask import WWOneTimeTask
|
||||
|
||||
logger = Logger.get_logger(__name__)
|
||||
|
||||
|
||||
class FarmMapTask(WWOneTimeTask, BaseCombatTask):
|
||||
|
||||
class BigMap(WWOneTimeTask, BaseCombatTask):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.icon = FluentIcon.FLAG
|
||||
self.description = "Farm selected Tacet Suppression until out of stamina, will use the backup stamina, you need to be able to teleport from the menu(F2)"
|
||||
self.name = "Tacet Suppression (Must explore first to be able to teleport)"
|
||||
default_config = {
|
||||
'Which Tacet Suppression to Farm': 1 # starts with 1
|
||||
}
|
||||
self.row_per_page = 5
|
||||
self.total_number = 11
|
||||
default_config.update(self.default_config)
|
||||
self.config_description = {
|
||||
'Which Tacet Suppression to Farm': 'the Nth number in the Tacet Suppression list (F2)',
|
||||
}
|
||||
self.default_config = default_config
|
||||
self.max_star_distance = 1000
|
||||
self.last_star = None
|
||||
self.last_angle = None
|
||||
self.stuck_keys = [['space', 0.2], ['a',2], ['d',2]]
|
||||
self.stuck_index = 0
|
||||
self.last_cords = None
|
||||
self.cords_re = re.compile('^\d+,\d+,\d+$')
|
||||
self.big_map_frame = None
|
||||
self.stars = None
|
||||
self.bounding_box = None
|
||||
|
||||
@property
|
||||
def star_move_distance_threshold(self):
|
||||
return self.height_of_screen(0.02)
|
||||
|
||||
def run(self):
|
||||
self.last_star = None
|
||||
self.last_angle = None
|
||||
self.go_to_star()
|
||||
def reset(self):
|
||||
self.frame = None
|
||||
self.stars = None
|
||||
|
||||
def get_cords(self):
|
||||
cords = self.ocr(0.01, 0.95, 0.21, 1, match=self.cords_re, log=True)
|
||||
if cords:
|
||||
return cords[0].name
|
||||
def load_stars(self):
|
||||
self.click_relative(0.94, 556 / 1080, after_sleep=1)
|
||||
self.big_map_frame = self.frame
|
||||
self.stars = self.find_feature('big_map_star', threshold=0.7, frame=self.big_map_frame, box=Box(0,0,self.big_map_frame.shape[1],self.big_map_frame.shape[0]))
|
||||
all_star_len = len(self.stars)
|
||||
self.stars = group_boxes_by_center_distance(self.stars, self.height_of_screen(0.2))
|
||||
if len(self.stars) <= 2:
|
||||
raise Exception('Need be in the map screen and have a path of at least 3 stars!')
|
||||
self.log_info(f'Loaded {len(self.stars)} from {all_star_len} Stars', notify=True)
|
||||
self.bounding_box = get_bounding_box(self.stars)
|
||||
mini_map_box = self.get_box_by_name('box_minimap')
|
||||
self.bounding_box.width += mini_map_box.width * 2
|
||||
self.bounding_box.height += mini_map_box.height * 2
|
||||
self.bounding_box.x -= mini_map_box.width
|
||||
self.bounding_box.y -= mini_map_box.height
|
||||
self.info_set('Stars', len(self.stars))
|
||||
self.send_key('esc', after_sleep=1)
|
||||
|
||||
def get_angle(self):
|
||||
def get_angle_between(self, my_angle, angle):
|
||||
if my_angle > angle:
|
||||
to_turn = angle - my_angle
|
||||
else:
|
||||
to_turn = -(my_angle - angle)
|
||||
if to_turn > 180:
|
||||
to_turn -= 360
|
||||
elif to_turn < -180:
|
||||
to_turn += 360
|
||||
return to_turn
|
||||
|
||||
def get_my_angle(self):
|
||||
arrow_template = self.get_feature_by_name('arrow')
|
||||
original_mat = arrow_template.mat
|
||||
max_conf = 0
|
||||
@ -68,60 +70,141 @@ class FarmMapTask(WWOneTimeTask, BaseCombatTask):
|
||||
for angle in range(0, 360):
|
||||
# Rotate the template image
|
||||
rotation_matrix = cv2.getRotationMatrix2D(center, -angle, 1.0)
|
||||
arrow_template.mat = cv2.warpAffine(original_mat, rotation_matrix, (w, h))
|
||||
arrow_template.mask = np.where(np.all(arrow_template.mat == [0, 0, 0], axis=2), 0, 255).astype(np.uint8)
|
||||
template = cv2.warpAffine(original_mat, rotation_matrix, (w, h))
|
||||
# mask = np.where(np.all(template == [0, 0, 0], axis=2), 0, 255).astype(np.uint8)
|
||||
|
||||
target = self.find_one(f'arrow_{angle}', box=target_box,
|
||||
template=arrow_template, threshold=0.01)
|
||||
target = self.find_one(box=target_box,
|
||||
template=template, threshold=0.01)
|
||||
# if self.debug and angle % 90 == 0:
|
||||
# self.screenshot(f'arrow_rotated_{angle}', arrow_template.mat)
|
||||
if target and target.confidence > max_conf:
|
||||
max_conf = target.confidence
|
||||
max_angle = angle
|
||||
max_target = target
|
||||
max_mat = arrow_template.mat
|
||||
arrow_template.mat = original_mat
|
||||
# max_target = target
|
||||
# max_mat = template
|
||||
# arrow_template.mat = original_mat
|
||||
# arrow_template.mask = None
|
||||
# if self.debug and max_mat is not None:
|
||||
# self.screenshot('max_mat',frame=max_mat)
|
||||
# self.log_debug(f'turn_east max_conf: {max_conf} {max_angle}')
|
||||
return max_angle , max_target
|
||||
return max_angle
|
||||
|
||||
def set_last_angle(self, angle):
|
||||
self.last_angle = angle
|
||||
self.info_set('Last Angle', angle)
|
||||
|
||||
def find_next_star(self):
|
||||
stars = self.find_stars()
|
||||
min_distance = self.max_star_distance
|
||||
nearest_star = None
|
||||
x2, y2 = self.get_box_by_name('box_minimap').center()
|
||||
for star in stars:
|
||||
if not self.last_star:
|
||||
x1, y1 = star.center()
|
||||
angle = calculate_angle_clockwise(x1, y1, x2, y2)
|
||||
if self.last_angle is not None and abs(angle - self.last_angle) < 90:
|
||||
self.log_debug(f'old path continue {abs(angle - self.last_angle)} {star}')
|
||||
continue
|
||||
self.last_star = star
|
||||
self.log_debug(f'no last star return nearest {self.last_star} {self.last_angle}')
|
||||
return -1, star, 360 - angle
|
||||
distance = star.center_distance(self.last_star)
|
||||
def find_direction_angle(self):
|
||||
my_box = self.find_my_location()
|
||||
min_distance = 100000
|
||||
min_star = None
|
||||
if len(self.stars) == 0:
|
||||
return None, 0, 0
|
||||
for star in self.stars:
|
||||
distance = star.center_distance(my_box)
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
nearest_star = star
|
||||
if nearest_star:
|
||||
self.last_star = nearest_star
|
||||
self.log_debug(f'nearest_star: {min_distance} {nearest_star}')
|
||||
return min_distance, nearest_star, -1
|
||||
min_star = star
|
||||
self.draw_boxes('star', min_star, color='green')
|
||||
# if self.debug:
|
||||
# self.screenshot('parse_read_big_map', frame=self.big_map_frame, show_box=True)
|
||||
direction_angle = calculate_angle_clockwise(my_box, min_star)
|
||||
my_angle = self.get_my_angle()
|
||||
to_turn = self.get_angle_between(my_angle, direction_angle)
|
||||
self.log_debug(f'direction_angle {to_turn} {my_angle} {direction_angle} min_distance {min_distance} min_star {min_star} ')
|
||||
return min_star, min_distance, to_turn
|
||||
|
||||
def remove_star(self, star):
|
||||
before = len(self.stars)
|
||||
self.stars.remove(star)
|
||||
self.info_set('Stars', len(self.stars))
|
||||
self.log_debug(f'removed star {before} -> {len(self.stars)}')
|
||||
|
||||
|
||||
def find_my_location(self):
|
||||
frame = self.big_map_frame
|
||||
mat = self.get_box_by_name('box_minimap').crop_frame(self.frame)
|
||||
mat = keep_circle(mat)
|
||||
in_big_map = self.find_one(frame=frame, template=mat, threshold=0.01, box=self.bounding_box, mask_function=create_circle_mask_with_hole)
|
||||
# in_big_maps = self.find_feature(frame=frame, template=mat, threshold=0.01, box=self.bounding_box)
|
||||
if not in_big_map:
|
||||
raise RuntimeError('can not find my cords on big map!')
|
||||
self.log_debug(f'found big map: {in_big_map}')
|
||||
if self.debug and in_big_map:
|
||||
self.draw_boxes('stars', self.stars)
|
||||
self.draw_boxes('search_map', self.bounding_box)
|
||||
self.draw_boxes('in_big_map', in_big_map.scale(0.1), color='blue')
|
||||
# self.screenshot('box_minimap', frame=mat, show_box=True)
|
||||
return in_big_map
|
||||
|
||||
def keep_circle(img):
|
||||
height, width = img.shape[:2]
|
||||
# Create a black mask with the same dimensions
|
||||
mask = np.zeros((height, width), dtype=np.uint8)
|
||||
# Define circle parameters (center and radius)
|
||||
center_x, center_y = width // 2, height // 2
|
||||
radius = min(center_x, center_y) # Fit circle within image bounds
|
||||
# Draw a filled white circle on the mask
|
||||
cv2.circle(mask, (center_x, center_y), radius, (255), thickness=-1)
|
||||
# Apply the mask to the original image using bitwise AND
|
||||
result = cv2.bitwise_and(img, img, mask=mask)
|
||||
return result
|
||||
|
||||
def create_circle_mask_with_hole(image):
|
||||
"""
|
||||
Creates a binary circular mask with a rectangular hole in the center.
|
||||
The circle fills the mask dimensions, and the hole is 1/4 width and height.
|
||||
Args:
|
||||
shape (tuple): The (height, width) of the desired mask.
|
||||
Returns:
|
||||
numpy.ndarray: A uint8 NumPy array representing the mask
|
||||
(255 in the circle ring, 0 elsewhere and in the hole).
|
||||
"""
|
||||
h, w = image.shape[:2]
|
||||
mask = np.zeros((h, w), dtype=np.uint8)
|
||||
center_x, center_y = w // 2, h // 2
|
||||
radius = min(w, h) // 2
|
||||
# 1. Draw the outer filled white circle
|
||||
cv2.circle(mask, (center_x, center_y), radius, 255, -1)
|
||||
# 2. Calculate rectangle dimensions and corners for the hole
|
||||
rect_w = round(w / 4.4)
|
||||
rect_h = round(h / 4.4)
|
||||
# Calculate top-left corner centered
|
||||
rect_x1 = center_x - rect_w // 2
|
||||
rect_y1 = center_y - rect_h // 2
|
||||
# Calculate bottom-right corner
|
||||
rect_x2 = rect_x1 + rect_w
|
||||
rect_y2 = rect_y1 + rect_h
|
||||
# 3. Draw the inner filled black rectangle (the hole)
|
||||
# Use color=0 and thickness=-1 to fill with black
|
||||
cv2.rectangle(mask, (rect_x1, rect_y1), (rect_x2, rect_y2), 0, -1)
|
||||
return mask
|
||||
|
||||
class FarmMapTask(BigMap):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.icon = FluentIcon.GLOBE
|
||||
self.description = "Farm world map with a marked path of stars, start in the map screen"
|
||||
self.name = "Farm Map with Star Path"
|
||||
self.max_star_distance = 1000
|
||||
self.stuck_keys = [['space', 0.2], ['a',2], ['d',2]]
|
||||
self.stuck_index = 0
|
||||
self.last_distance = 0
|
||||
self.check_pick_echo = True
|
||||
|
||||
@property
|
||||
def star_move_distance_threshold(self):
|
||||
return self.height_of_screen(0.025)
|
||||
|
||||
def run(self):
|
||||
self.stuck_index = 0
|
||||
self.last_distance = 0
|
||||
self.load_stars()
|
||||
self.go_to_star()
|
||||
|
||||
def go_to_star(self):
|
||||
return self.yolo_find_echo(use_color=False, walk=False)
|
||||
current_direction = None
|
||||
self.center_camera()
|
||||
current_adjust = None
|
||||
while True:
|
||||
self.sleep(0.01)
|
||||
self.middle_click(interval=1, after_sleep=0.2)
|
||||
if self.in_combat():
|
||||
if current_direction is not None:
|
||||
self.mouse_up(key='right')
|
||||
@ -129,45 +212,47 @@ class FarmMapTask(WWOneTimeTask, BaseCombatTask):
|
||||
current_direction = None
|
||||
self.combat_once()
|
||||
while self.yolo_find_echo(use_color=False, walk=False)[1]:
|
||||
self.incr_drop(True)
|
||||
self.sleep(0.5)
|
||||
continue
|
||||
distance, star, last_angle = self.find_next_star()
|
||||
star, distance, angle = self.find_direction_angle()
|
||||
# self.draw_boxes('next_star', star, color='green')
|
||||
if not star:
|
||||
self.log_info('cannot find any stars, stop farming', notify=True)
|
||||
break
|
||||
if distance == 0:
|
||||
if distance <= self.star_move_distance_threshold:
|
||||
self.log_info(f'reached star {star} {distance} {self.star_move_distance_threshold}')
|
||||
self.remove_star(star)
|
||||
continue
|
||||
elif distance >= self.height_of_screen(0.35):
|
||||
self.log_error('too far from next star, stop farming', notify=True)
|
||||
if self.debug:
|
||||
self.screenshot('too_far',frame=self.big_map_frame,show_box=True)
|
||||
self.screenshot('far',frame=self.get_box_by_name('box_minimap').crop(self.frame))
|
||||
break
|
||||
elif distance == self.last_distance:
|
||||
logger.info(f'might be stuck, try {[self.stuck_index % 3]}')
|
||||
self.send_key(self.stuck_keys[self.stuck_index % 3][0], down_time=self.stuck_keys[self.stuck_index % 3][1], after_sleep=0.5)
|
||||
self.stuck_index += 1
|
||||
continue
|
||||
elif distance > self.star_move_distance_threshold:
|
||||
if self.debug:
|
||||
self.screenshot('star_moved')
|
||||
self.log_info(f'star moved continue forward {distance} {self.star_move_distance_threshold}')
|
||||
self.last_star = None
|
||||
self.sleep(4)
|
||||
continue
|
||||
if last_angle > 0:
|
||||
self.set_last_angle(last_angle)
|
||||
|
||||
angle = self.get_angle_to_star(star)
|
||||
self.last_distance = distance
|
||||
|
||||
if current_direction == 'w':
|
||||
if 4 <= angle <= 70:
|
||||
if 15 <= angle <= 75:
|
||||
minor_adjust = 'd'
|
||||
elif -70 <= angle <= -4:
|
||||
elif -75 <= angle <= -15:
|
||||
minor_adjust = 'a'
|
||||
else:
|
||||
minor_adjust = None
|
||||
if minor_adjust != current_adjust and minor_adjust is not None:
|
||||
if current_adjust:
|
||||
self.send_key_up(current_adjust)
|
||||
# if self.debug:
|
||||
# self.screenshot(f'minor_adjust_{minor_adjust}_{angle}')
|
||||
self.log_info(f'minor_adjust to {minor_adjust}_{angle}')
|
||||
|
||||
if minor_adjust:
|
||||
self.send_key_down(minor_adjust)
|
||||
# self.center_camera()
|
||||
self.sleep(0.5)
|
||||
current_adjust = minor_adjust
|
||||
self.sleep(0.1)
|
||||
self.middle_click(down_time=0.1)
|
||||
self.send_key_up(minor_adjust)
|
||||
self.sleep(0.2)
|
||||
continue
|
||||
if current_adjust:
|
||||
self.send_key_up(current_adjust)
|
||||
@ -198,42 +283,22 @@ class FarmMapTask(WWOneTimeTask, BaseCombatTask):
|
||||
self.mouse_up(key='right')
|
||||
self.send_key_up(current_direction)
|
||||
|
||||
def get_angle_to_star(self, star):
|
||||
x1, y1 = self.get_box_by_name('box_minimap').center()
|
||||
x2, y2 = star.center()
|
||||
target_angle = calculate_angle_clockwise(x1, y1, x2, y2)
|
||||
my_angle = self.get_angle()[0]
|
||||
if my_angle >= target_angle:
|
||||
turn_angle = -(my_angle - target_angle)
|
||||
else:
|
||||
turn_angle = target_angle - my_angle
|
||||
if turn_angle > 180:
|
||||
turn_angle = 360 - turn_angle
|
||||
if turn_angle < -180:
|
||||
turn_angle = 360 + turn_angle
|
||||
logger.debug(f'go to turn_angle {my_angle} {target_angle} {turn_angle}')
|
||||
return turn_angle
|
||||
|
||||
def find_stars(self):
|
||||
box_minimap = self.get_box_by_name('box_minimap')
|
||||
stars = self.find_feature('star', threshold=0.65, box=box_minimap)
|
||||
sorted_stars = sorted(stars, key=lambda star: - box_minimap.center_distance(star))
|
||||
# if self.debug:
|
||||
# self.screenshot('starts', show_box=True)
|
||||
# self.screenshot('stars_mask', frame=mask_star(self.frame), show_box=True)
|
||||
return sorted_stars
|
||||
|
||||
|
||||
def calculate_angle_clockwise(x1, y1, x2, y2):
|
||||
def calculate_angle_clockwise(box1, box2):
|
||||
"""
|
||||
Calculates angle (radians) from horizontal right to line (x1,y1)->(x2,y2).
|
||||
Positive clockwise, negative counter-clockwise.
|
||||
"""
|
||||
x1, y1 = box1.center()
|
||||
x2, y2 = box2.center()
|
||||
dx = x2 - x1
|
||||
dy = y2 - y1
|
||||
# math.atan2(dy, dx) gives angle from positive x-axis, positive CCW.
|
||||
# Negate for positive CW convention.
|
||||
return math.degrees(math.atan2(dy, dx))
|
||||
|
||||
degree = math.degrees(math.atan2(dy, dx))
|
||||
if degree < 0:
|
||||
degree += 360
|
||||
return degree
|
||||
|
||||
star_color = {
|
||||
'r': (190, 220), # Red range
|
||||
@ -241,6 +306,39 @@ star_color = {
|
||||
'b': (190, 220) # Blue range
|
||||
}
|
||||
|
||||
def group_boxes_by_center_distance(boxes: List[Box], distance_threshold: float) -> List[Box]:
|
||||
"""
|
||||
Groups boxes where any box is close (center_distance < threshold)
|
||||
to any other box in the group (connected components).
|
||||
Returns the largest group.
|
||||
"""
|
||||
if not boxes:
|
||||
return []
|
||||
n = len(boxes)
|
||||
visited = [False] * n
|
||||
all_groups = []
|
||||
# Find connected components using DFS
|
||||
for i in range(n):
|
||||
if not visited[i]:
|
||||
current_group = []
|
||||
stack = [i]
|
||||
visited[i] = True
|
||||
while stack:
|
||||
current_idx = stack.pop()
|
||||
current_group.append(boxes[current_idx])
|
||||
for j in range(n):
|
||||
if not visited[j]:
|
||||
# Use center_distance as requested
|
||||
dist = boxes[current_idx].center_distance(boxes[j])
|
||||
if dist < distance_threshold:
|
||||
visited[j] = True
|
||||
stack.append(j)
|
||||
all_groups.append(current_group)
|
||||
# Return the largest group found
|
||||
if not all_groups:
|
||||
return [] # Should not happen if boxes is not empty, but safe check
|
||||
return max(all_groups, key=len)
|
||||
|
||||
def mask_star(image):
|
||||
# return image
|
||||
return create_color_mask(image, star_color)
|
||||
|
@ -43,25 +43,25 @@ class SkipBaseTask(BaseWWTask):
|
||||
self.click_box(skip, move_back=True)
|
||||
return self.wait_until(self.skip_confirm, time_out=3, raise_if_not_found=False)
|
||||
if time.time() - self.has_eye_time < 2:
|
||||
btn_dialog_close = self.find_one('btn_dialog_close', use_gray_scale=True, threshold=0.8)
|
||||
btn_dialog_close = self.find_one('btn_dialog_close', threshold=0.8)
|
||||
if btn_dialog_close:
|
||||
self.click(btn_dialog_close, move_back=True)
|
||||
return True
|
||||
btn_dialog_eye = self.find_one('btn_dialog_eye', use_gray_scale=True, threshold=0.8)
|
||||
btn_dialog_eye = self.find_one('btn_dialog_eye', threshold=0.8)
|
||||
if btn_dialog_eye:
|
||||
self.has_eye_time = time.time()
|
||||
btn_auto_play_dialog = self.find_one('btn_auto_play_dialog', use_gray_scale=True)
|
||||
btn_auto_play_dialog = self.find_one('btn_auto_play_dialog')
|
||||
if btn_auto_play_dialog:
|
||||
self.click_box(btn_auto_play_dialog, move_back=True)
|
||||
logger.info('toggle auto play')
|
||||
self.sleep(0.2)
|
||||
if arrow := self.find_feature('btn_dialog_arrow', x=0.59, y=0.33, to_x=0.75, to_y=0.75,
|
||||
use_gray_scale=True, threshold=0.7):
|
||||
threshold=0.7):
|
||||
self.click(arrow[-1])
|
||||
logger.info('choose arrow')
|
||||
self.sleep(0.2)
|
||||
elif dots := self.find_feature('btn_dialog_3dots', x=0.59, y=0.33, to_x=0.75, to_y=0.75,
|
||||
use_gray_scale=True, threshold=0.7):
|
||||
threshold=0.7):
|
||||
if dots:
|
||||
self.click(dots[-1])
|
||||
logger.info('choose dot')
|
||||
|
@ -16,18 +16,18 @@ class TestTacet(TaskTestCase):
|
||||
self.logger.info(f'test_find_treasure_icon {angle, box}')
|
||||
self.assertTrue(100 <= angle <= 200)
|
||||
|
||||
def test_find_stars(self):
|
||||
self.set_image('tests/images/stars.png')
|
||||
stars = self.task.find_stars()
|
||||
self.logger.info(f'find_stars {stars}')
|
||||
self.assertEqual(3, len(stars))
|
||||
|
||||
angle1 = self.task.get_angle_to_star(stars[0])
|
||||
angle2 = self.task.get_angle_to_star(stars[1])
|
||||
angle3 = self.task.get_angle_to_star(stars[2])
|
||||
self.assertTrue(100 <= angle1 <= 180)
|
||||
self.assertTrue(0 <= angle2 <= 90)
|
||||
self.assertTrue(-45 <= angle3 <= 0)
|
||||
# def test_find_stars(self):
|
||||
# self.set_image('tests/images/stars.png')
|
||||
# stars = self.task.find_stars()
|
||||
# self.logger.info(f'find_stars {stars}')
|
||||
# self.assertEqual(3, len(stars))
|
||||
#
|
||||
# angle1 = self.task.get_angle_to_star(stars[0])
|
||||
# angle2 = self.task.get_angle_to_star(stars[1])
|
||||
# angle3 = self.task.get_angle_to_star(stars[2])
|
||||
# self.assertTrue(100 <= angle1 <= 180)
|
||||
# self.assertTrue(0 <= angle2 <= 90)
|
||||
# self.assertTrue(-45 <= angle3 <= 0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
BIN
tests/images/big_map.png
Normal file
BIN
tests/images/big_map.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 MiB |
Loading…
x
Reference in New Issue
Block a user