mirror of
https://github.com/LmeSzinc/StarRailCopilot.git
synced 2024-11-26 10:16:34 +00:00
commit
8f2bfa625d
BIN
assets/character/FuXuan.png
Normal file
BIN
assets/character/FuXuan.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
BIN
assets/character/Lynx.png
Normal file
BIN
assets/character/Lynx.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
BIN
assets/mask/MASK_MAP_INTERACT.png
Normal file
BIN
assets/mask/MASK_MAP_INTERACT.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
BIN
assets/share/daily/reward/MASK_DAILY_QUEST.png
Normal file
BIN
assets/share/daily/reward/MASK_DAILY_QUEST.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 226 KiB |
@ -152,20 +152,21 @@ class ModuleBase:
|
||||
prev_image = image
|
||||
timer.reset()
|
||||
|
||||
def image_crop(self, button):
|
||||
def image_crop(self, button, copy=True):
|
||||
"""Extract the area from image.
|
||||
|
||||
Args:
|
||||
button(Button, tuple): Button instance or area tuple.
|
||||
copy:
|
||||
"""
|
||||
if isinstance(button, Button):
|
||||
return crop(self.device.image, button.area)
|
||||
return crop(self.device.image, button.area, copy=copy)
|
||||
elif isinstance(button, ButtonWrapper):
|
||||
return crop(self.device.image, button.area)
|
||||
return crop(self.device.image, button.area, copy=copy)
|
||||
elif hasattr(button, 'area'):
|
||||
return crop(self.device.image, button.area)
|
||||
return crop(self.device.image, button.area, copy=copy)
|
||||
else:
|
||||
return crop(self.device.image, button)
|
||||
return crop(self.device.image, button, copy=copy)
|
||||
|
||||
def image_color_count(self, button, color, threshold=221, count=50):
|
||||
"""
|
||||
@ -178,9 +179,14 @@ class ModuleBase:
|
||||
Returns:
|
||||
bool:
|
||||
"""
|
||||
image = self.image_crop(button)
|
||||
mask = color_similarity_2d(image, color=color) > threshold
|
||||
return np.sum(mask) > count
|
||||
if isinstance(button, np.ndarray):
|
||||
image = button
|
||||
else:
|
||||
image = self.image_crop(button, copy=False)
|
||||
mask = color_similarity_2d(image, color=color)
|
||||
cv2.inRange(mask, threshold, 255, dst=mask)
|
||||
sum_ = cv2.countNonZero(mask)
|
||||
return sum_ > count
|
||||
|
||||
def image_color_button(self, area, color, color_threshold=250, encourage=5, name='COLOR_BUTTON'):
|
||||
"""
|
||||
|
@ -350,6 +350,7 @@
|
||||
"Clara",
|
||||
"DanHeng",
|
||||
"DanHengImbibitorLunae",
|
||||
"FuXuan",
|
||||
"Gepard",
|
||||
"Herta",
|
||||
"Himeko",
|
||||
@ -358,6 +359,7 @@
|
||||
"Kafka",
|
||||
"Luka",
|
||||
"Luocha",
|
||||
"Lynx",
|
||||
"March7th",
|
||||
"Natasha",
|
||||
"Pela",
|
||||
|
@ -53,7 +53,7 @@ class GeneratedConfig:
|
||||
|
||||
# Group `DungeonSupport`
|
||||
DungeonSupport_Use = 'when_daily' # always_use, when_daily, do_not_use
|
||||
DungeonSupport_Character = 'FirstCharacter' # FirstCharacter, Arlan, Asta, Bailu, Blade, Bronya, Clara, DanHeng, DanHengImbibitorLunae, Gepard, Herta, Himeko, Hook, JingYuan, Kafka, Luka, Luocha, March7th, Natasha, Pela, Qingque, Sampo, Seele, Serval, SilverWolf, Sushang, Tingyun, TrailblazerDestruction, TrailblazerPreservation, Welt, Yanqing, Yukong
|
||||
DungeonSupport_Character = 'FirstCharacter' # FirstCharacter, Arlan, Asta, Bailu, Blade, Bronya, Clara, DanHeng, DanHengImbibitorLunae, FuXuan, Gepard, Herta, Himeko, Hook, JingYuan, Kafka, Luka, Luocha, Lynx, March7th, Natasha, Pela, Qingque, Sampo, Seele, Serval, SilverWolf, Sushang, Tingyun, TrailblazerDestruction, TrailblazerPreservation, Welt, Yanqing, Yukong
|
||||
|
||||
# Group `DungeonStorage`
|
||||
DungeonStorage_TrailblazePower = {}
|
||||
|
@ -87,7 +87,7 @@ class ConfigGenerator:
|
||||
options=[dungeon.name for dungeon in DungeonList.instances.values() if dungeon.is_Cavern_of_Corrosion])
|
||||
# Insert characters
|
||||
from tasks.character.keywords import CharacterList
|
||||
unsupported_characters = ["FuXuan", "Lynx"]
|
||||
unsupported_characters = []
|
||||
characters = [character.name for character in CharacterList.instances.values()
|
||||
if character.name not in unsupported_characters]
|
||||
option_add(keys='DungeonSupport.Character.option', options=characters)
|
||||
|
@ -350,6 +350,7 @@
|
||||
"Clara": "Clara",
|
||||
"DanHeng": "Dan Heng",
|
||||
"DanHengImbibitorLunae": "Dan Heng • Imbibitor Lunae",
|
||||
"FuXuan": "Fu Xuan",
|
||||
"Gepard": "Gepard",
|
||||
"Herta": "Herta",
|
||||
"Himeko": "Himeko",
|
||||
@ -358,6 +359,7 @@
|
||||
"Kafka": "Kafka",
|
||||
"Luka": "Luka",
|
||||
"Luocha": "Luocha",
|
||||
"Lynx": "Lynx",
|
||||
"March7th": "March 7th",
|
||||
"Natasha": "Natasha",
|
||||
"Pela": "Pela",
|
||||
|
@ -350,6 +350,7 @@
|
||||
"Clara": "クラーラ",
|
||||
"DanHeng": "丹恒",
|
||||
"DanHengImbibitorLunae": "丹恒・飲月",
|
||||
"FuXuan": "符玄",
|
||||
"Gepard": "ジェパード",
|
||||
"Herta": "ヘルタ",
|
||||
"Himeko": "姫子",
|
||||
@ -358,6 +359,7 @@
|
||||
"Kafka": "カフカ",
|
||||
"Luka": "ルカ",
|
||||
"Luocha": "羅刹",
|
||||
"Lynx": "リンクス",
|
||||
"March7th": "三月なのか",
|
||||
"Natasha": "ナターシャ",
|
||||
"Pela": "ペラ",
|
||||
|
@ -350,6 +350,7 @@
|
||||
"Clara": "克拉拉",
|
||||
"DanHeng": "丹恒",
|
||||
"DanHengImbibitorLunae": "丹恒•饮月",
|
||||
"FuXuan": "符玄",
|
||||
"Gepard": "杰帕德",
|
||||
"Herta": "黑塔",
|
||||
"Himeko": "姬子",
|
||||
@ -358,6 +359,7 @@
|
||||
"Kafka": "卡芙卡",
|
||||
"Luka": "卢卡",
|
||||
"Luocha": "罗刹",
|
||||
"Lynx": "玲可",
|
||||
"March7th": "三月七",
|
||||
"Natasha": "娜塔莎",
|
||||
"Pela": "佩拉",
|
||||
|
@ -350,6 +350,7 @@
|
||||
"Clara": "克拉拉",
|
||||
"DanHeng": "丹恆",
|
||||
"DanHengImbibitorLunae": "丹恆•飲月",
|
||||
"FuXuan": "符玄",
|
||||
"Gepard": "傑帕德",
|
||||
"Herta": "黑塔",
|
||||
"Himeko": "姬子",
|
||||
@ -358,6 +359,7 @@
|
||||
"Kafka": "卡芙卡",
|
||||
"Luka": "盧卡",
|
||||
"Luocha": "羅剎",
|
||||
"Lynx": "玲可",
|
||||
"March7th": "三月七",
|
||||
"Natasha": "娜塔莎",
|
||||
"Pela": "佩拉",
|
||||
|
@ -389,6 +389,11 @@ class Duration(Ocr):
|
||||
}[lang]
|
||||
return re.compile(regex_str)
|
||||
|
||||
def after_process(self, result):
|
||||
result = super().after_process(result)
|
||||
result = result.strip('.,。,')
|
||||
return result
|
||||
|
||||
def format_result(self, result: str) -> timedelta:
|
||||
"""
|
||||
Do OCR on a duration, such as `18d 2h 13m 30s`, `2h`, `13m 30s`, `9s`
|
||||
|
@ -106,7 +106,7 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo
|
||||
continue
|
||||
if self.appear(COMBAT_TEAM_PREPARE):
|
||||
self.interval_reset(COMBAT_PREPARE)
|
||||
self._map_A_timer.reset()
|
||||
self.map_A_timer.reset()
|
||||
if self.appear(COMBAT_PREPARE, interval=2):
|
||||
if not self.handle_combat_prepare():
|
||||
return False
|
||||
|
@ -210,13 +210,23 @@ DAILY_QUEST_RIGHT_END = ButtonWrapper(
|
||||
button=(401, 259, 411, 632),
|
||||
),
|
||||
)
|
||||
MASK_DAILY_QUEST = ButtonWrapper(
|
||||
name='MASK_DAILY_QUEST',
|
||||
share=Button(
|
||||
file='./assets/share/daily/reward/MASK_DAILY_QUEST.png',
|
||||
area=(117, 308, 1173, 630),
|
||||
search=(97, 288, 1193, 650),
|
||||
color=(208, 208, 208),
|
||||
button=(117, 308, 1173, 630),
|
||||
),
|
||||
)
|
||||
OCR_DAILY_QUEST = ButtonWrapper(
|
||||
name='OCR_DAILY_QUEST',
|
||||
share=Button(
|
||||
file='./assets/share/daily/reward/OCR_DAILY_QUEST.png',
|
||||
area=(117, 257, 1173, 630),
|
||||
search=(97, 237, 1193, 650),
|
||||
color=(208, 206, 202),
|
||||
button=(117, 257, 1173, 630),
|
||||
area=(117, 308, 1173, 630),
|
||||
search=(97, 288, 1193, 650),
|
||||
color=(204, 202, 199),
|
||||
button=(117, 308, 1173, 630),
|
||||
),
|
||||
)
|
||||
|
@ -1,9 +1,12 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from module.base.timer import Timer
|
||||
from module.logger import *
|
||||
from module.base.utils import crop
|
||||
from module.logger import logger
|
||||
from module.ocr.ocr import Ocr, OcrResultButton
|
||||
from module.ocr.utils import split_and_pair_buttons
|
||||
from tasks.battle_pass.keywords import KEYWORD_BATTLE_PASS_QUEST
|
||||
from tasks.daily.assets.assets_daily_reward import *
|
||||
from tasks.daily.camera import CameraUI
|
||||
from tasks.daily.keywords import (
|
||||
@ -19,12 +22,18 @@ from tasks.dungeon.keywords import KEYWORDS_DUNGEON_TAB
|
||||
from tasks.dungeon.ui import DungeonUI
|
||||
from tasks.item.consumable_usage import ConsumableUsageUI
|
||||
from tasks.item.relics import RelicsUI
|
||||
from tasks.battle_pass.keywords import KEYWORD_BATTLE_PASS_QUEST
|
||||
|
||||
|
||||
class DailyQuestOcr(Ocr):
|
||||
def __init__(self, button: ButtonWrapper, lang=None, name=None):
|
||||
super().__init__(button, lang, name)
|
||||
merge_thres_y = 20
|
||||
|
||||
def pre_process(self, image):
|
||||
image = super().pre_process(image)
|
||||
image = crop(image, OCR_DAILY_QUEST.area)
|
||||
mask = MASK_DAILY_QUEST.matched_button.image
|
||||
# Remove "+200Activity"
|
||||
cv2.bitwise_and(image, mask, dst=image)
|
||||
return image
|
||||
|
||||
def after_process(self, result):
|
||||
result = super().after_process(result)
|
||||
@ -91,8 +100,7 @@ class DailyQuestUI(DungeonUI):
|
||||
|
||||
def _ocr_single_page(self) -> list[OcrResultButton]:
|
||||
ocr = DailyQuestOcr(OCR_DAILY_QUEST)
|
||||
ocr.merge_thres_y = 20
|
||||
results = ocr.matched_ocr(self.device.image, [DailyQuestState, DailyQuest])
|
||||
results = ocr.matched_ocr(self.device.image, [DailyQuestState, DailyQuest], direct_ocr=True)
|
||||
if len(results) < 8:
|
||||
logger.warning(f"Recognition failed at {8 - len(results)} quests on one page")
|
||||
|
||||
|
@ -248,7 +248,7 @@ class Dungeon(DungeonUI, DungeonEvent, Combat):
|
||||
area = area_offset((-50, -150, 0, 0), offset=self.config.ASSETS_RESOLUTION)
|
||||
|
||||
skip_first_screenshot = True
|
||||
self._map_A_timer.reset()
|
||||
self.map_A_timer.reset()
|
||||
handled = False
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
@ -265,7 +265,7 @@ class Dungeon(DungeonUI, DungeonEvent, Combat):
|
||||
logger.info(f'No destructible object')
|
||||
if not handled:
|
||||
break
|
||||
if self._map_A_timer.reached():
|
||||
if self.map_A_timer.reached():
|
||||
break
|
||||
|
||||
return handled
|
||||
|
@ -4,7 +4,7 @@ from module.base.timer import Timer
|
||||
from module.logger import logger
|
||||
from tasks.map.assets.assets_map_control import ROTATION_SWIPE_AREA
|
||||
from tasks.map.control.joystick import JoystickContact, MapControlJoystick
|
||||
from tasks.map.control.waypoint import Waypoint, WaypointRun, WaypointStraightRun, ensure_waypoint
|
||||
from tasks.map.control.waypoint import Waypoint, ensure_waypoint
|
||||
from tasks.map.minimap.minimap import Minimap
|
||||
from tasks.map.resource.const import diff_to_180_180
|
||||
|
||||
@ -76,7 +76,7 @@ class MapControl(MapControlJoystick):
|
||||
self,
|
||||
contact: JoystickContact,
|
||||
waypoint: Waypoint,
|
||||
end_point_opt=True,
|
||||
end_opt=True,
|
||||
skip_first_screenshot=False
|
||||
):
|
||||
"""
|
||||
@ -88,7 +88,7 @@ class MapControl(MapControlJoystick):
|
||||
`with JoystickContact(self) as contact:`
|
||||
waypoint:
|
||||
Position to goto, (x, y)
|
||||
end_point_opt:
|
||||
end_opt:
|
||||
True to enable endpoint optimizations,
|
||||
character will smoothly approach target position
|
||||
skip_first_screenshot:
|
||||
@ -98,7 +98,7 @@ class MapControl(MapControlJoystick):
|
||||
self.device.stuck_record_clear()
|
||||
self.device.click_record_clear()
|
||||
|
||||
end_point_opt = end_point_opt and waypoint.end_point_opt
|
||||
end_opt = end_opt and waypoint.end_opt
|
||||
allow_2x_run = waypoint.speed in ['2x_run']
|
||||
allow_straight_run = waypoint.speed in ['2x_run', 'straight_run']
|
||||
allow_run = waypoint.speed in ['2x_run', 'straight_run', 'run']
|
||||
@ -117,20 +117,20 @@ class MapControl(MapControlJoystick):
|
||||
self.minimap.update(self.device.image)
|
||||
|
||||
# Arrive
|
||||
if self.minimap.is_position_near(waypoint.position, threshold=waypoint.get_threshold(end_point_opt)):
|
||||
if self.minimap.is_position_near(waypoint.position, threshold=waypoint.get_threshold(end_opt)):
|
||||
logger.info(f'Arrive {waypoint}')
|
||||
break
|
||||
|
||||
# Switch run case
|
||||
diff = self.minimap.position_diff(waypoint.position)
|
||||
if end_point_opt:
|
||||
if end_opt:
|
||||
if allow_2x_run and diff < 20:
|
||||
logger.info(f'Approaching target, diff={round(diff, 1)}, disallow 2x_run')
|
||||
allow_2x_run = False
|
||||
if allow_straight_run and diff < 15:
|
||||
logger.info(f'Approaching target, diff={round(diff, 1)}, disallow straight_run')
|
||||
direction_interval = Timer(0.2)
|
||||
self._map_2x_run_timer.reset()
|
||||
self.map_2x_run_timer.reset()
|
||||
allow_straight_run = False
|
||||
if allow_run and diff < 7:
|
||||
logger.info(f'Approaching target, diff={round(diff, 1)}, disallow run')
|
||||
@ -160,7 +160,7 @@ class MapControl(MapControlJoystick):
|
||||
direction_interval.reset()
|
||||
self.handle_map_2x_run(run=True)
|
||||
elif allow_straight_run:
|
||||
# Run with 2x_run button
|
||||
# Run straight forward
|
||||
# - Set rotation once
|
||||
# - Continuous fine-tuning direction
|
||||
# - Disable 2x_run
|
||||
@ -204,7 +204,7 @@ class MapControl(MapControlJoystick):
|
||||
|
||||
def goto(
|
||||
self,
|
||||
waypoints,
|
||||
*waypoints,
|
||||
skip_first_screenshot=True
|
||||
):
|
||||
"""
|
||||
@ -218,8 +218,6 @@ class MapControl(MapControlJoystick):
|
||||
skip_first_screenshot:
|
||||
"""
|
||||
logger.hr('Goto', level=1)
|
||||
if not isinstance(waypoints, list):
|
||||
waypoints = [waypoints]
|
||||
waypoints = [ensure_waypoint(point) for point in waypoints]
|
||||
logger.info(f'Go along {len(waypoints)} waypoints')
|
||||
end_list = [False for _ in waypoints]
|
||||
@ -231,35 +229,36 @@ class MapControl(MapControlJoystick):
|
||||
self._goto(
|
||||
contact=contact,
|
||||
waypoint=point,
|
||||
end_point_opt=end,
|
||||
end_opt=end,
|
||||
skip_first_screenshot=skip_first_screenshot
|
||||
)
|
||||
skip_first_screenshot = True
|
||||
|
||||
end_point = waypoints[-1]
|
||||
if end_point.end_point_rotation is not None:
|
||||
self.rotation_set(end_point.end_point_rotation, threshold=end_point.end_point_rotation_threshold)
|
||||
if end_point.end_rotation is not None:
|
||||
logger.hr('End rotation', level=1)
|
||||
self.rotation_set(end_point.end_rotation, threshold=end_point.end_rotation_threshold)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Control test in Himeko trail
|
||||
# Must manually enter Himeko trail first and dismiss popup
|
||||
self = MapControl('alas')
|
||||
# Control test in Himeko trial
|
||||
# Must manually enter Himeko trial first and dismiss popup
|
||||
self = MapControl('src')
|
||||
self.minimap.set_plane('Jarilo_BackwaterPass', floor='F1')
|
||||
self.device.screenshot()
|
||||
self.minimap.init_position((519, 359))
|
||||
# Visit 3 items
|
||||
self.goto([
|
||||
WaypointRun((577.6, 363.4)),
|
||||
])
|
||||
self.goto([
|
||||
WaypointStraightRun((577.5, 369.4), end_point_rotation=200),
|
||||
])
|
||||
self.goto([
|
||||
WaypointRun((581.5, 387.3)),
|
||||
WaypointRun((577.4, 411.5)),
|
||||
])
|
||||
self.goto(
|
||||
Waypoint((577.6, 363.4)),
|
||||
)
|
||||
self.goto(
|
||||
Waypoint((577.5, 369.4), end_rotation=200),
|
||||
)
|
||||
self.goto(
|
||||
Waypoint((581.5, 387.3)),
|
||||
Waypoint((577.4, 411.5)),
|
||||
)
|
||||
# Goto boss
|
||||
self.goto([
|
||||
WaypointStraightRun((607.6, 425.3)),
|
||||
])
|
||||
self.goto(
|
||||
Waypoint((607.6, 425.3)),
|
||||
)
|
||||
|
@ -1,6 +1,9 @@
|
||||
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
|
||||
@ -36,10 +39,8 @@ class JoystickContact:
|
||||
Can not lift finger when:
|
||||
- Process is force terminated
|
||||
"""
|
||||
builder = self.builder
|
||||
if self.is_downed:
|
||||
builder.up().commit()
|
||||
builder.send()
|
||||
self.up()
|
||||
logger.info('JoystickContact ends')
|
||||
else:
|
||||
logger.info('JoystickContact ends but it was never downed')
|
||||
@ -98,6 +99,13 @@ class JoystickContact:
|
||||
point = (int(round(point[0])), int(round(point[1])))
|
||||
return point
|
||||
|
||||
def up(self):
|
||||
builder = self.builder
|
||||
if self.is_downed:
|
||||
builder.up().commit()
|
||||
builder.send()
|
||||
self.prev_point = None
|
||||
|
||||
def set(self, direction, run=True):
|
||||
"""
|
||||
Set joystick to given position
|
||||
@ -110,6 +118,13 @@ class JoystickContact:
|
||||
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:]:
|
||||
@ -121,20 +136,56 @@ class JoystickContact:
|
||||
# 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_2x_run_timer.set_current(0.7)
|
||||
self.main.map_2x_run_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_2x_run_timer = Timer(1)
|
||||
map_A_timer = Timer(1)
|
||||
map_E_timer = Timer(1)
|
||||
map_2x_run_timer = Timer(1)
|
||||
|
||||
joystick_lost_timer = Timer(1, count=2)
|
||||
|
||||
@cached_property
|
||||
def joystick_center(self) -> tuple[float, float]:
|
||||
def joystick_center(self) -> tuple[int, int]:
|
||||
x1, y1, x2, y2 = JOYSTICK.area
|
||||
return (x1 + x2) / 2, (y1 + y2) / 2
|
||||
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):
|
||||
"""
|
||||
@ -162,9 +213,9 @@ class MapControlJoystick(UI):
|
||||
Returns:
|
||||
bool: If clicked.
|
||||
"""
|
||||
if self._map_A_timer.reached():
|
||||
if self.map_A_timer.reached():
|
||||
self.device.click(A_BUTTON)
|
||||
self._map_A_timer.reset()
|
||||
self.map_A_timer.reset()
|
||||
return True
|
||||
|
||||
return False
|
||||
@ -177,9 +228,9 @@ class MapControlJoystick(UI):
|
||||
Returns:
|
||||
bool: If clicked.
|
||||
"""
|
||||
if self._map_E_timer.reached():
|
||||
if self.map_E_timer.reached():
|
||||
self.device.click(E_BUTTON)
|
||||
self._map_E_timer.reset()
|
||||
self.map_E_timer.reset()
|
||||
return True
|
||||
|
||||
return False
|
||||
@ -194,13 +245,13 @@ class MapControlJoystick(UI):
|
||||
"""
|
||||
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_2x_run_timer.reached():
|
||||
if run and not is_running and self.map_2x_run_timer.reached():
|
||||
self.device.click(RUN_BUTTON)
|
||||
self._map_2x_run_timer.reset()
|
||||
self.map_2x_run_timer.reset()
|
||||
return True
|
||||
if not run and is_running and self._map_2x_run_timer.reached():
|
||||
if not run and is_running and self.map_2x_run_timer.reached():
|
||||
self.device.click(RUN_BUTTON)
|
||||
self._map_2x_run_timer.reset()
|
||||
self.map_2x_run_timer.reset()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
@ -13,17 +13,17 @@ class Waypoint:
|
||||
endpoint_threshold: int = 3
|
||||
# Max move speed, '2x_run', 'straight_run', 'run', 'walk'
|
||||
# See MapControl._goto() for details of each speed level
|
||||
speed: str = '2x_run'
|
||||
speed: str = 'straight_run'
|
||||
|
||||
"""
|
||||
The following attributes are only be used if this waypoint is the end point of goto()
|
||||
"""
|
||||
# True to enable endpoint optimizations, character will smoothly approach target position
|
||||
# False to stop all controls at arrive
|
||||
end_point_opt: bool = True
|
||||
end_opt: bool = True
|
||||
# Set rotation after arrive, 0~360
|
||||
end_point_rotation: int = None
|
||||
end_point_rotation_threshold: int = 15
|
||||
end_rotation: int = None
|
||||
end_rotation_threshold: int = 15
|
||||
|
||||
def __str__(self):
|
||||
return f'Waypoint({self.position})'
|
||||
|
347
tasks/map/interact/aim.py
Normal file
347
tasks/map/interact/aim.py
Normal file
@ -0,0 +1,347 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from module.base.decorator import cached_property, del_cached_property
|
||||
from module.base.utils import Points, image_size, load_image
|
||||
from module.config.utils import dict_to_kv
|
||||
from module.logger import logger
|
||||
from tasks.base.ui import UI
|
||||
|
||||
|
||||
def inrange(image, lower=0, upper=255):
|
||||
"""
|
||||
Get the coordinates of pixels in range.
|
||||
Equivalent to `np.array(np.where(lower <= image <= upper))` but faster.
|
||||
Note that this method will change `image`.
|
||||
|
||||
`cv2.findNonZero()` is faster than `np.where`
|
||||
points = np.array(np.where(y > 24)).T[:, ::-1]
|
||||
points = np.array(cv2.findNonZero((y > 24).astype(np.uint8)))[:, 0, :]
|
||||
|
||||
`cv2.inRange(y, 24)` is faster than `y > 24`
|
||||
cv2.inRange(y, 24, 255, dst=y)
|
||||
y = y > 24
|
||||
|
||||
Returns:
|
||||
np.ndarray: Shape (N, 2)
|
||||
E.g. [[x1, y1], [x2, y2], ...]
|
||||
"""
|
||||
cv2.inRange(image, lower, upper, dst=image)
|
||||
try:
|
||||
return np.array(cv2.findNonZero(image))[:, 0, :]
|
||||
except IndexError:
|
||||
# Empty result
|
||||
# IndexError: too many indices for array: array is 0-dimensional, but 3 were indexed
|
||||
return np.array([])
|
||||
|
||||
|
||||
def subtract_blur(image, radius=3, negative=False):
|
||||
"""
|
||||
If you care performance more than quality:
|
||||
- radius=3, use medianBlur
|
||||
- radius=5,7,9,11, use GaussianBlur
|
||||
- radius>11, use stackBlur (requires opencv >= 4.7.0)
|
||||
|
||||
Args:
|
||||
image:
|
||||
radius:
|
||||
negative:
|
||||
|
||||
Returns:
|
||||
np.ndarray:
|
||||
"""
|
||||
if radius <= 3:
|
||||
blur = cv2.medianBlur(image, radius)
|
||||
elif radius <= 11:
|
||||
blur = cv2.GaussianBlur(image, (radius, radius), 0)
|
||||
else:
|
||||
blur = cv2.stackBlur(image, (radius, radius), 0)
|
||||
|
||||
if negative:
|
||||
cv2.subtract(blur, image, dst=blur)
|
||||
else:
|
||||
cv2.subtract(image, blur, dst=blur)
|
||||
return blur
|
||||
|
||||
|
||||
def remove_border(image, radius):
|
||||
"""
|
||||
Paint edge pixels black.
|
||||
No returns, changes are written to `image`
|
||||
|
||||
Args:
|
||||
image:
|
||||
radius:
|
||||
"""
|
||||
width, height = image_size(image)
|
||||
image[:, :radius + 1] = 0
|
||||
image[:, width - radius:] = 0
|
||||
image[:radius + 1, :] = 0
|
||||
image[height - radius:, :] = 0
|
||||
|
||||
|
||||
def create_circle(min_radius, max_radius):
|
||||
"""
|
||||
Create a circle with min_radius <= R <= max_radius.
|
||||
1 represents circle, 0 represents background
|
||||
|
||||
Args:
|
||||
min_radius:
|
||||
max_radius:
|
||||
|
||||
Returns:
|
||||
np.ndarray:
|
||||
"""
|
||||
circle = np.ones((max_radius * 2 + 1, max_radius * 2 + 1), dtype=np.uint8)
|
||||
center = np.array((max_radius, max_radius))
|
||||
points = np.array(np.meshgrid(np.arange(circle.shape[0]), np.arange(circle.shape[1]))).T
|
||||
distance = np.linalg.norm(points - center, axis=2)
|
||||
circle[distance < min_radius] = 0
|
||||
circle[distance > max_radius] = 0
|
||||
return circle
|
||||
|
||||
|
||||
def draw_circle(image, circle, points):
|
||||
"""
|
||||
Add a circle onto image.
|
||||
No returns, changes are written to `image`
|
||||
|
||||
Args:
|
||||
image:
|
||||
circle: Created from create_circle()
|
||||
points: (x, y), center of the circle to draw
|
||||
"""
|
||||
width, height = image_size(circle)
|
||||
x1 = -int(width // 2)
|
||||
y1 = -int(height // 2)
|
||||
x2 = width + x1
|
||||
y2 = height + y1
|
||||
for point in points:
|
||||
x, y = point
|
||||
# Fancy index is faster
|
||||
index = image[y + y1:y + y2, x + x1:x + x2]
|
||||
# print(index.shape)
|
||||
cv2.add(index, circle, dst=index)
|
||||
|
||||
|
||||
class Aim:
|
||||
radius_enemy = (24, 25)
|
||||
radius_item = (8, 10)
|
||||
|
||||
def __init__(self):
|
||||
self.debug = False
|
||||
|
||||
self.draw_item = None
|
||||
self.draw_enemy = None
|
||||
self.points_item = None
|
||||
self.points_enemy = None
|
||||
|
||||
def clear_image_cache(self):
|
||||
self.draw_item = None
|
||||
self.draw_enemy = None
|
||||
self.points_item = None
|
||||
self.points_enemy = None
|
||||
del_cached_property(self, 'aimed_enemy')
|
||||
del_cached_property(self, 'aimed_item')
|
||||
|
||||
@cached_property
|
||||
def mask_interact(self):
|
||||
return load_image('./assets/mask/MASK_MAP_INTERACT.png')
|
||||
|
||||
@cached_property
|
||||
def circle_enemy(self):
|
||||
return create_circle(*self.radius_enemy)
|
||||
|
||||
@cached_property
|
||||
def circle_item(self):
|
||||
return create_circle(*self.radius_item)
|
||||
|
||||
# @timer
|
||||
def predict_enemy(self, h, v):
|
||||
min_radius, max_radius = self.radius_enemy
|
||||
width, height = image_size(v)
|
||||
|
||||
# Get white circle `y`
|
||||
y = subtract_blur(h, 3, negative=False)
|
||||
cv2.inRange(h, 168, 255, h)
|
||||
cv2.bitwise_and(y, h, dst=y)
|
||||
# Get red glow `v`
|
||||
cv2.inRange(v, 168, 255, dst=v)
|
||||
|
||||
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
||||
cv2.dilate(v, kernel, dst=v)
|
||||
# Remove noise and leave red circle only
|
||||
cv2.bitwise_and(y, v, dst=y)
|
||||
# cv2.imshow('predict_enemy', y)
|
||||
# Remove game UI
|
||||
cv2.bitwise_and(y, self.mask_interact, dst=y)
|
||||
# Remove points on the edge, or draw_circle() will overflow
|
||||
remove_border(y, max_radius)
|
||||
|
||||
# Get all pixels
|
||||
points = inrange(y, lower=18)
|
||||
if points.shape[0] > 1000:
|
||||
logger.warning(f'AimDetector.predict_enemy() too many points to draw: {points.shape}')
|
||||
# Draw circles
|
||||
draw = np.zeros((height, width), dtype=np.uint8)
|
||||
draw_circle(draw, self.circle_enemy, points)
|
||||
if self.debug:
|
||||
self.draw_enemy = cv2.multiply(draw, 4)
|
||||
subtract_blur(draw, 3)
|
||||
|
||||
# Find peaks
|
||||
points = inrange(draw, lower=36)
|
||||
points = Points(points).group(threshold=10)
|
||||
if points.shape[0] > 3:
|
||||
logger.warning(f'AimDetector.predict_enemy() too many peaks: {points.shape}')
|
||||
self.points_enemy = points
|
||||
# print(points)
|
||||
return points
|
||||
|
||||
# @timer
|
||||
def predict_item(self, v):
|
||||
min_radius, max_radius = self.radius_item
|
||||
width, height = image_size(v)
|
||||
|
||||
# Get white circle `y`
|
||||
y = subtract_blur(v, 9)
|
||||
white = cv2.inRange(v, 112, 144)
|
||||
cv2.bitwise_and(y, white, dst=y)
|
||||
# Get cyan glow `v`
|
||||
cv2.inRange(v, 0, 84, dst=v)
|
||||
|
||||
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
||||
cv2.dilate(v, kernel, dst=v)
|
||||
|
||||
# Remove noise and leave cyan circle only
|
||||
cv2.bitwise_and(y, v, dst=y)
|
||||
# Remove game UI
|
||||
cv2.bitwise_and(y, self.mask_interact, dst=y)
|
||||
# Remove points on the edge, or draw_circle() will overflow
|
||||
remove_border(y, max_radius)
|
||||
|
||||
# Get all pixels
|
||||
points = inrange(y, lower=18)
|
||||
# print(points.shape)
|
||||
if points.shape[0] > 1000:
|
||||
logger.warning(f'AimDetector.predict_item() too many points to draw: {points.shape}')
|
||||
# Draw circles
|
||||
draw = np.zeros((height, width), dtype=np.uint8)
|
||||
draw_circle(draw, self.circle_item, points)
|
||||
if self.debug:
|
||||
self.draw_item = cv2.multiply(draw, 2)
|
||||
subtract_blur(draw, 7)
|
||||
|
||||
# Find peaks
|
||||
points = inrange(draw, lower=64)
|
||||
points = Points(points).group(threshold=10)
|
||||
if points.shape[0] > 3:
|
||||
logger.warning(f'AimDetector.predict_item() too many peaks: {points.shape}')
|
||||
self.points_item = points
|
||||
# print(points)
|
||||
return points
|
||||
|
||||
# @timer
|
||||
def predict(self, image, enemy=True, item=True, show_log=True, debug=False):
|
||||
"""
|
||||
Predict `aim` on image, costs about 10.0~10.5ms.
|
||||
|
||||
Args:
|
||||
image:
|
||||
enemy: True to predict enemy
|
||||
item: True to predict item
|
||||
show_log:
|
||||
debug: True to show AimDetector image
|
||||
"""
|
||||
self.debug = debug
|
||||
self.clear_image_cache()
|
||||
if isinstance(image, str):
|
||||
image = load_image(image)
|
||||
|
||||
# 1.5~2.0ms
|
||||
yuv = cv2.cvtColor(image, cv2.COLOR_RGB2YUV)
|
||||
v = yuv[:, :, 2]
|
||||
h = yuv[:, :, 0]
|
||||
# 4.0~4.5ms
|
||||
if enemy:
|
||||
self.predict_enemy(h.copy(), v.copy())
|
||||
# 3.0~3.5ms
|
||||
if item:
|
||||
self.predict_item(v.copy())
|
||||
|
||||
if show_log:
|
||||
kv = {}
|
||||
if self.aimed_enemy:
|
||||
kv['enemy'] = self.aimed_enemy
|
||||
if self.aimed_item:
|
||||
kv['item'] = self.aimed_item
|
||||
if kv:
|
||||
logger.info(f'Aimed: {dict_to_kv(kv)}')
|
||||
if debug:
|
||||
self.show_aim()
|
||||
|
||||
def show_aim(self):
|
||||
if self.draw_enemy is None:
|
||||
if self.draw_item is None:
|
||||
return
|
||||
else:
|
||||
r = g = b = self.draw_item
|
||||
else:
|
||||
if self.draw_item is None:
|
||||
r = g = b = self.draw_enemy
|
||||
else:
|
||||
r = self.draw_enemy
|
||||
g = b = self.draw_item
|
||||
|
||||
image = cv2.merge([b, g, r])
|
||||
|
||||
cv2.imshow('AimDetector', image)
|
||||
cv2.waitKey(1)
|
||||
|
||||
@cached_property
|
||||
def aimed_enemy(self) -> tuple[int, int] | None:
|
||||
if self.points_enemy is None:
|
||||
return None
|
||||
try:
|
||||
_ = self.points_enemy[1]
|
||||
logger.warning(f'Multiple aimed enemy found, using first point of {self.points_enemy}')
|
||||
except IndexError:
|
||||
pass
|
||||
try:
|
||||
point = self.points_enemy[0]
|
||||
return tuple(point)
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def aimed_item(self) -> tuple[int, int] | None:
|
||||
if self.points_item is None:
|
||||
return None
|
||||
try:
|
||||
_ = self.points_item[1]
|
||||
logger.warning(f'Multiple aimed item found, using first point of {self.points_item}')
|
||||
except IndexError:
|
||||
pass
|
||||
try:
|
||||
point = self.points_item[0]
|
||||
return tuple(point)
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
|
||||
class AimDetectorMixin(UI):
|
||||
@cached_property
|
||||
def aim(self):
|
||||
return Aim()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
"""
|
||||
Test
|
||||
"""
|
||||
self = AimDetectorMixin('src')
|
||||
self.device.disable_stuck_detection()
|
||||
|
||||
while 1:
|
||||
self.device.screenshot()
|
||||
self.aim.predict(self.device.image, debug=True)
|
Loading…
Reference in New Issue
Block a user