diff --git a/dev_tools/assets_extract.py b/dev_tools/assets_extract.py index bf9906415..85d21c13a 100644 --- a/dev_tools/assets_extract.py +++ b/dev_tools/assets_extract.py @@ -408,12 +408,10 @@ def __init__(self): self.task_paths = [x for x in self.task_paths if 'Component' not in x] self.task_paths.extend([str(x) for x in (self.task_path / 'Component').iterdir() if x.is_dir()]) - # process_map(self.work, self.task_paths, max_workers=1) for task_path in self.task_paths: me = AssetsExtractor(task_path) me.extract() - @staticmethod def work(task_path: str): me = AssetsExtractor(task_path) diff --git a/module/atom/click.py b/module/atom/click.py index 5baa6250d..c837b4ec3 100644 --- a/module/atom/click.py +++ b/module/atom/click.py @@ -6,6 +6,7 @@ from module.base.decorator import cached_property from module.logger import logger + class RuleClick: def __init__(self, roi_front: tuple, roi_back: tuple, name: str = None) -> None: @@ -60,14 +61,17 @@ def move(self, x: int, y: int) -> None: x, y, w, h = self.roi_front x += x y += y - if x <= 0 : + if x <= 0: x = 0 elif x >= 1280: x = 1280 - if y <= 0 : + if y <= 0: y = 0 elif y >= 720: y = 720 self.roi_front = x, y, w, h + + def __repr__(self): + return self.name diff --git a/module/atom/ocr.py b/module/atom/ocr.py index 4a3389227..4c4a630d6 100644 --- a/module/atom/ocr.py +++ b/module/atom/ocr.py @@ -5,38 +5,74 @@ import numpy as np import cv2 -from module.ocr.base_ocr import BaseCor, OcrMode, OcrMethod +from module.ocr.base_ocr import BaseCor, OcrMode, OcrMethod, OcrMethodType from module.ocr.sub_ocr import Full, Single, Digit, DigitCounter, Duration, Quantity from module.logger import logger - class RuleOcr(Digit, DigitCounter, Duration, Single, Full, Quantity): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + def pre_process(self, image): + match self.method.get_method_type(): + case OcrMethodType.DEFAULT: + pass + case OcrMethodType.CF_RGB: + _val = self.method.get_val() + lower, upper = _val.split(',') + lower = np.array([int(lower[i:i + 2], 16) for i in (0, 2, 4)]) + upper = np.array([int(upper[i:i + 2], 16) for i in (0, 2, 4)]) + mask = cv2.inRange(image, lower, upper) + res_img = cv2.bitwise_and(image, image, mask=mask) + return res_img + case OcrMethodType.CF_HSV: + _val = self.method.get_val() + lower, upper = _val.split(',') + lower = np.array([int(lower[i:i + 2], 16) for i in (0, 2, 4)]) + upper = np.array([int(upper[i:i + 2], 16) for i in (0, 2, 4)]) + res_img = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) + mask = cv2.inRange(res_img, lower, upper) + res_img = cv2.bitwise_and(res_img, res_img, mask=mask) + res_img = cv2.cvtColor(res_img, cv2.COLOR_HSV2RGB) + return res_img + return image def after_process(self, result): match self.mode: - case OcrMode.FULL: return Full.after_process(self, result) - case OcrMode.SINGLE: return Single.after_process(self, result) - case OcrMode.DIGIT: return Digit.after_process(self, result) - case OcrMode.DIGITCOUNTER: return DigitCounter.after_process(self, result) - case OcrMode.DURATION: return Duration.after_process(self, result) - case OcrMode.QUANTITY: return Quantity.after_process(self, result) - case _: return result + case OcrMode.FULL: + return Full.after_process(self, result) + case OcrMode.SINGLE: + return Single.after_process(self, result) + case OcrMode.DIGIT: + return Digit.after_process(self, result) + case OcrMode.DIGITCOUNTER: + return DigitCounter.after_process(self, result) + case OcrMode.DURATION: + return Duration.after_process(self, result) + case OcrMode.QUANTITY: + return Quantity.after_process(self, result) + case _: + return result def ocr(self, image, keyword=None): match self.mode: - case OcrMode.FULL: return Full.ocr_full(self, image, keyword) - case OcrMode.SINGLE: return Single.ocr_single(self, image) - case OcrMode.DIGIT: return Digit.ocr_digit(self, image) - case OcrMode.DIGITCOUNTER: return DigitCounter.ocr_digit_counter(self, image) - case OcrMode.DURATION: return Duration.ocr_duration(self, image) - case OcrMode.QUANTITY: return Quantity.ocr_quantity(self, image) - case _: return None + case OcrMode.FULL: + return Full.ocr_full(self, image, keyword) + case OcrMode.SINGLE: + return Single.ocr_single(self, image) + case OcrMode.DIGIT: + return Digit.ocr_digit(self, image) + case OcrMode.DIGITCOUNTER: + return DigitCounter.ocr_digit_counter(self, image) + case OcrMode.DURATION: + return Duration.ocr_duration(self, image) + case OcrMode.QUANTITY: + return Quantity.ocr_quantity(self, image) + case _: + return None def coord(self) -> tuple: """ @@ -55,20 +91,5 @@ def coord(self) -> tuple: return x, y - if __name__ == "__main__": - O_MALL_RESOURCE_1 = RuleOcr(roi=(144, 7, 100, 43), area=(144, 7, 100, 43), mode="Quantity", method="Default", - keyword="", name="mall_resource_1") - O_MALL_RESOURCE_2 = RuleOcr(roi=(326, 8, 124, 39), area=(326, 8, 124, 39), mode="Quantity", method="Default", - keyword="", name="mall_resource_2") - O_MALL_RESOURCE_3 = RuleOcr(roi=(533, 9, 107, 38), area=(533, 9, 107, 38), mode="Quantity", method="Default", - keyword="", name="mall_resource_3") - O_MALL_RESOURCE_4 = RuleOcr(roi=(739, 8, 100, 39), area=(739, 8, 100, 39), mode="Quantity", method="Default", - keyword="", name="mall_resource_4") - O_MALL_RESOURCE_5 = RuleOcr(roi=(935, 11, 100, 37), area=(935, 11, 100, 37), mode="Quantity", method="Default", - keyword="", name="mall_resource_5") - O_MALL_RESOURCE_6 = RuleOcr(roi=(1129, 6, 100, 41), area=(1129, 6, 100, 41), mode="Quantity", method="Default", - keyword="", name="mall_resource_6") - image = cv2.imread(r"E:\2025-01-16225353.png") - print(O_MALL_RESOURCE_5.ocr_quantity(image)) - + pass diff --git a/module/ocr/base_ocr.py b/module/ocr/base_ocr.py index cda18bea9..f847d128b 100644 --- a/module/ocr/base_ocr.py +++ b/module/ocr/base_ocr.py @@ -41,8 +41,43 @@ class OcrMode(Enum): DURATION = 5 # str: "Duration" QUANTITY = 6 # str: "Quantity" -class OcrMethod(Enum): - DEFAULT = 1 # str: "Default" + +class OcrMethodType(Enum): + # 默认,不需要预处理 + DEFAULT = "DEFAULT" + # # 颜色过滤Color Filter + # 配置相关CF_RGB(lower, upper) + # lower ,upper 格式为6位16进制,例如FFFFFF + # 过滤图片中颜色,仅保留符合指定范围(lower到upper)的颜色 + CF_HSV = "CF_HSV" + # 与CF_HSV 相似 + CF_RGB = "CF_RGB" + + +class OcrMethod: + _reg = r"([^()]+)\((.*?)\)?$" + + def __init__(self, val: str = None): + self._method_type: OcrMethodType = OcrMethodType.DEFAULT + self._val: str = val + if val is None: + return + import re + match = re.match(self._reg, val) + if not match: + return + type_str = match.group(1).upper() + if type_str not in OcrMethodType.__members__: + return + self._method_type = OcrMethodType[type_str] + self._val = match.group(2) + + def get_method_type(self): + return self._method_type + + def get_val(self): + return self._val + class BaseCor: @@ -51,7 +86,7 @@ class BaseCor: name: str = "ocr" mode: OcrMode = OcrMode.FULL - method: OcrMethod = OcrMethod.DEFAULT # 占位符 + method: OcrMethod = OcrMethod() roi: list = [] # [x, y, width, height] area: list = [] # [x, y, width, height] keyword: str = "" # 默认为空 @@ -79,7 +114,7 @@ def __init__(self, elif isinstance(mode, OcrMode): self.mode = mode if isinstance(method, str): - self.method = OcrMethod[method.upper()] + self.method = OcrMethod(method.upper()) elif isinstance(method, OcrMethod): self.method = method self.roi: list = list(roi) diff --git a/tasks/AbyssShadows/assets.py b/tasks/AbyssShadows/assets.py index 130d7cb54..f5fc1cbfd 100644 --- a/tasks/AbyssShadows/assets.py +++ b/tasks/AbyssShadows/assets.py @@ -41,6 +41,12 @@ class AbyssShadowsAssets: C_ABYSS_FOX = RuleClick(roi_front=(822,184,49,144), roi_back=(789,130,148,249), name="abyss_fox") # 狭间_黑豹入口 C_ABYSS_LEOPARD = RuleClick(roi_front=(1140,190,50,162), roi_back=(1093,143,138,297), name="abyss_leopard") + # 刚进入神社时,可点击进入狭间暗域的区域,用于开启狭间暗域 + C_ABYSS_SHENSHE_ENTER_ABYSS = RuleClick(roi_front=(740,640,45,15), roi_back=(740,640,45,15), name="abyss_shenshe_enter_abyss") + # 战斗时,左上角退出战斗按钮区域 下半部分 + C_QUIT_AREA = RuleClick(roi_front=(20,36,30,14), roi_back=(20,36,30,14), name="quit_area") + # 红标主怪点击区域 + C_MARK_MAIN = RuleClick(roi_front=(380,15,45,30), roi_back=(380,15,45,30), name="mark_main") # Image Rule Assets @@ -49,7 +55,9 @@ class AbyssShadowsAssets: # 神社->狭间暗域 I_RYOU_ABYSS_SHADOWS = RuleImage(roi_front=(707,492,110,27), roi_back=(707,492,110,27), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_ryou_abyss_shadows.png") # 狭间_神龙入口 - I_ABYSS_DRAGON = RuleImage(roi_front=(227,211,55,151), roi_back=(190,147,140,283), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_abyss_dragon.png") + I_ABYSS_DRAGON = RuleImage(roi_front=(200,150,110,270), roi_back=(200,150,110,270), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_abyss_dragon.png") + # 狭间_神龙入口_已封印 + I_ABYSS_DRAGON_OVER = RuleImage(roi_front=(200,150,110,270), roi_back=(200,150,110,270), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_abyss_dragon_over.png") # 狭间_孔雀入口 I_ABYSS_PEACOCK = RuleImage(roi_front=(521,152,48,165), roi_back=(465,104,145,312), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_abyss_peacock.png") # 狭间_白藏主入口 @@ -68,14 +76,20 @@ class AbyssShadowsAssets: I_ABYSS_MAP = RuleImage(roi_front=(306,147,170,48), roi_back=(306,147,170,48), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_abyss_map.png") # 战报退出按钮 I_ABYSS_MAP_EXIT = RuleImage(roi_front=(1154,96,32,32), roi_back=(1154,96,32,32), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_abyss_map_exit.png") + # 怪物信息页面退出按钮 + I_ABYSS_ENEMY_INFO_EXIT = RuleImage(roi_front=(975,80,90,70), roi_back=(975,80,90,70), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_abyss_enemy_info_exit.png") # 挑战按钮 I_ABYSS_FIRE = RuleImage(roi_front=(1121,605,77,50), roi_back=(1121,605,77,50), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_abyss_fire.png") # 前往 I_ABYSS_GOTO_ENEMY = RuleImage(roi_front=(1120,610,75,45), roi_back=(1120,610,75,45), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_abyss_goto_enemy.png") # description + I_CHANGE_AREA = RuleImage(roi_front=(993,610,63,61), roi_back=(993,610,63,61), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_change_area.png") + # description I_ENSURE_BUTTON = RuleImage(roi_front=(672,405,169,55), roi_back=(672,405,169,55), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_ensure_button.png") # 进攻中 - I_IS_ATTACK = RuleImage(roi_front=(586,62,73,27), roi_back=(586,62,73,27), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_is_attack.png") + I_IS_ATTACK = RuleImage(roi_front=(576,54,91,45), roi_back=(576,54,91,45), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_is_attack.png") + # 狭间暗域 挑战结束 + I_CHECK_FINISH = RuleImage(roi_front=(570,50,120,60), roi_back=(570,50,120,60), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_check_finish.png") # description I_PEACOCK_AREA = RuleImage(roi_front=(577,14,127,36), roi_back=(577,14,127,36), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_peacock_area.png") # 黑豹领域 @@ -88,8 +102,22 @@ class AbyssShadowsAssets: I_DRAGON_AREA = RuleImage(roi_front=(584,15,111,34), roi_back=(584,15,111,34), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_dragon_area.png") # description I_WAIT_TO_START = RuleImage(roi_front=(588,64,70,26), roi_back=(588,64,70,26), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_wait_to_start.png") - # description - I_EQUIPPING = RuleImage(roi_front=(1126,545,100,83), roi_back=(1126,545,100,83), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_equipping.png") + # 选择难度按钮 + I_SELECT_DIFFICULTY = RuleImage(roi_front=(703,645,50,55), roi_back=(703,645,50,55), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_select_difficulty.png") + # 容易难度 + I_DIFFICULTY_EASY = RuleImage(roi_front=(620,445,90,210), roi_back=(620,445,90,210), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_difficulty_easy.png") + # 普通难度 + I_DIFFICULTY_NORMAL = RuleImage(roi_front=(620,445,90,210), roi_back=(620,445,90,210), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_difficulty_normal.png") + # 困难难度 + I_DIFFICULTY_HARD = RuleImage(roi_front=(620,445,90,210), roi_back=(620,445,90,210), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_difficulty_hard.png") + # 开启按钮 + I_BTN_START = RuleImage(roi_front=(1120,570,100,120), roi_back=(1120,570,100,120), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_btn_start.png") + # 开启确认按钮 + I_START_ENSURE = RuleImage(roi_front=(660,390,190,80), roi_back=(660,390,190,80), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_start_ensure.png") + # 当附近有可用怪物时,右下角出现的开始战斗按钮 + I_ABYSS_ENEMY_FIRE = RuleImage(roi_front=(1100,560,130,130), roi_back=(1100,560,130,130), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_abyss_enemy_fire.png") + # 红标主怪 + I_MARK_MAIN = RuleImage(roi_front=(375,40,60,30), roi_back=(375,40,60,30), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_mark_main.png") # List Rule Assets @@ -98,6 +126,19 @@ class AbyssShadowsAssets: array=["道馆", "首领", "狭间"]) + # Ocr Rule Assets + # 伤害 + O_DAMAGE = RuleOcr(roi=(50,110,300,50), area=(50,110,300,50), mode="Digit", method="CF_HSV(0980B4,1ED2FF)", keyword="", name="damage") + # 神龙暗域已完成 + O_DRAGON_DONE = RuleOcr(roi=(130,160,180,360), area=(130,160,180,360), mode="Single", method="CF_RGB(CCCCCC,FFFFFF)", keyword="封印", name="dragon_done") + # 孔雀暗域已完成 + O_PEACOCK_DONE = RuleOcr(roi=(420,160,180,360), area=(420,160,180,360), mode="Single", method="CF_RGB(CCCCCC,FFFFFF)", keyword="封印", name="peacock_done") + # 白藏主暗域已完成 + O_FOX_DONE = RuleOcr(roi=(680,160,180,360), area=(680,160,180,360), mode="Single", method="CF_RGB(CCCCCC,FFFFFF)", keyword="封印", name="fox_done") + # 黑豹暗域已完成 + O_LEOPARD_DONE = RuleOcr(roi=(1000,160,180,360), area=(1000,160,180,360), mode="Single", method="CF_RGB(CCCCCC,FFFFFF)", keyword="封印", name="leopard_done") + + # Swipe Rule Assets # 滑到狭间 S_TO_ABBSY_SHADOWS = RuleSwipe(roi_front=(752,395,62,66), roi_back=(758,193,62,48), mode="default", name="to_abbsy_shadows") diff --git a/tasks/AbyssShadows/config.py b/tasks/AbyssShadows/config.py index f11796104..4e0a756db 100644 --- a/tasks/AbyssShadows/config.py +++ b/tasks/AbyssShadows/config.py @@ -3,33 +3,348 @@ # @author jackyhwei # @note draft version without full test # github https://github.com/roarhill/oas +from enum import Enum +from module.atom.click import RuleClick +from module.atom.image import RuleImage from pydantic import BaseModel, Field -from pygments.lexer import default -from scripts.regsetup import description -from sympy.testing.pytest import Failed +from module.base.timer import Timer +from tasks.AbyssShadows.assets import AbyssShadowsAssets from tasks.Component.GeneralBattle.config_general_battle import GeneralBattleConfig from tasks.Component.SwitchSoul.switch_soul_config import SwitchSoulConfig -from tasks.Component.config_base import ConfigBase, Time +from tasks.Component.config_base import ConfigBase, Time, DateTime from tasks.Component.config_scheduler import Scheduler + +class AreaType(Enum): + """ 暗域类型 """ + DRAGON = AbyssShadowsAssets.I_ABYSS_DRAGON # 神龙暗域 + PEACOCK = AbyssShadowsAssets.I_ABYSS_PEACOCK # 孔雀暗域 + FOX = AbyssShadowsAssets.I_ABYSS_FOX # 白藏主暗域 + LEOPARD = AbyssShadowsAssets.I_ABYSS_LEOPARD # 黑豹暗域 + + +class ClickArea(Enum): + """ 点击区域 """ + BOSS = AbyssShadowsAssets.C_BOSS_CLICK_AREA + GENERAL_1 = AbyssShadowsAssets.C_GENERAL_1_CLICK_AREA + GENERAL_2 = AbyssShadowsAssets.C_GENERAL_2_CLICK_AREA + ELITE_1 = AbyssShadowsAssets.C_ELITE_1_CLICK_AREA + ELITE_2 = AbyssShadowsAssets.C_ELITE_2_CLICK_AREA + ELITE_3 = AbyssShadowsAssets.C_ELITE_3_CLICK_AREA + + +class IndexMap(str, Enum): + """ 索引映射 """ + DRAGON = "A" # 神龙暗域 + PEACOCK = "B" # 孔雀暗域 + FOX = "C" # 白藏主暗域 + LEOPARD = "D" # 黑豹暗域 + BOSS = "1" # BOSS + GENERAL_1 = "2" # 副将 + GENERAL_2 = "3" + ELITE_1 = "4" # 精英 + ELITE_2 = "5" + ELITE_3 = "6" + + +class EnemyType(str, Enum): + """ 敌人类型 """ + BOSS = "BOSS" # 首领 + GENERAL = "GENERAL" # 副将 + ELITE = "ELITE" # 精英 + + +class AbyssShadowsDifficulty(str, Enum): + EASY = "EASY" + NORMAL = "NORMAL" + HARD = "HARD" + + +class MarkMainConfig(str, Enum): + """ 标记主怪策略 """ + NONE = "NONE" # 不需要标记 + BOSS_ONLY = "BOSS_ONLY" # 仅标记首领 + GENERAL_ONLY = "GENERAL_ONLY" # 仅标记副将 + ELITE_ONLY = "ELITE_ONLY" # 仅标记精英怪 + + BOSS_AND_GENERAL = "BOSS_AND_GENERAL" # 标记首领和副将 + ELITE_AND_GENERAL = "ELITE_AND_GENERAL" # 标记精英怪和副将 + ELITE_AND_BOSS = "ELITE_AND_BOSS" # 标记精英怪和首领 + + ALL = "ALL" # 标记所有敌人 + + +class Code(str): + def __init__(self, value: str): + self.value = value + + def get_areatype(self): + area, num = self.value.split('-') + + area_name = "" + for item in IndexMap: + if item.value == area: + area_name = item.name + break + + return AreaType[area_name] + + def get_enemy_click(self) -> RuleClick: + _, num = self.value.split('-') + + # 查找 IndexMap 中 value == num 的项 + for item in IndexMap: + if item.value == num: + try: + # 返回 ClickArea 中同名项 + return ClickArea[item.name].value + except KeyError: + break # 没找到就走默认逻辑 + + # 默认返回精英怪1的点击区域 + return ClickArea.ELITE_1.value + + def get_enemy_type(self): + area, num = self.value.split('-') + # 查找 IndexMap 中 value == num 的枚举项 + for item in IndexMap: + if item.value == num: + try: + return EnemyType[item.name.split("_")[0]] + except KeyError: + return EnemyType.ELITE # 默认值 + + return EnemyType.ELITE # 未找到时默认返回 ELITE + + +class CodeList(list[Code]): + + def __init__(self, v: str): + def expand_str(v: str): + if v.find('-') != -1: + return [v] + + VALID_AREAS = [area.value for area in IndexMap if area.name in AreaType.__members__] + VALID_NUMBERS = [str(i) for i in range(1, 7)] + + if v in VALID_AREAS: + return [f'{v}-4', f'{v}-5', f'{v}-6', f'{v}-2', f'{v}-3', f'{v}-1'] + + if v in VALID_NUMBERS: + # areas = [area.value for area in IndexMap if area.name in AreaType.__members__] + return [f'{area}-{v}' for area in VALID_AREAS] + + return [] + + def parse_order(value: str = None) -> list: + if value == '': + return [] + item_or_list = value.split(';') + for src in item_or_list: + for result in expand_str(src): + yield Code(result) + + super().__init__(parse_order(v)) + + def parse2str(self): + return ';'.join(item.value for item in self) + + +class Condition: + """ + Condition类用于管理一个条件对象,该对象可以基于时间或伤害值来判断条件是否满足。 + 它支持立即满足条件、基于时间满足条件和基于伤害值满足条件三种方式。 + """ + + def __init__(self, value: str): + """ + 初始化Condition对象。 + + 参数: + value (str): 用于设置条件的字符串值,可以是'TRUE', 'FALSE', 时间(秒),或最大伤害值。 + """ + # True 立即退出 + # False 任何情况下,该条件检查不通过 + self._dont_need_check: bool = False + # 时间,单位秒.时间到达时,条件满足 + # Note 在对象生成时,_timer 即开始运行 + self._time = -1 + self._timer: Timer = None + # 最大伤害,伤害达到该数值时,条件满足 + self._damage_max: int = -1 + # 存储结果,用于后期查询 + self._condition_result: bool = False + + # 根据输入值设置条件 + if value.upper() == "TRUE": + # 为True时,相当于没有策略(所有情况都通过条件检查) + self._dont_need_check = True + elif value.upper() == "FALSE": + # 任何情况都不通过条件检查 + self._dont_need_check = False + elif len(value) <= 3: + # 3位数 当作时间 + try: + self._time = int(value) + except ValueError: + self._time = 180 + self._timer = Timer(self._time) + self._timer.start() + else: + try: + self._damage_max = int(value) + except ValueError: + self._damage_max = 999999999 + + def is_valid(self, damage: int = None): + """ + 检查当前条件是否满足。 + + 参数: + damage (int, 可选): 当前的伤害值。默认为None。 + + 返回: + bool: 如果条件满足则返回True,否则返回False。 + """ + # 检查时间条件 + if self._time >= 0: + # if not self._timer.started(): + # self._timer.start() + if self._timer.started() and self._timer.reached(): + self._condition_result = True + return True + # 检查伤害条件 + if self._damage_max >= 0 and damage is not None: + if self._damage_max < damage: + self._condition_result = True + return True + # 检查是否无需检查条件 + if self._dont_need_check: + self._condition_result = True + return True + # 如果以上条件都不满足 + self._condition_result = False + return False + + def is_need_damage_value(self): + """ + 检查是否需要伤害值来判断条件。 + + 返回: + bool: 如果需要伤害值则返回True,否则返回False。 + """ + return self._damage_max >= 0 + + def is_passed(self): + """ + 检查条件是否已满足。 + + 返回: + bool: 如果条件已满足则返回True,否则返回False。 + """ + return self._condition_result + + def __repr__(self): + """ + 返回Condition对象的字符串表示。 + + 返回: + str: Condition对象的字符串表示。 + """ + return f"Condition(time={self._time},damage_max={self._damage_max},dont_need_check={self._dont_need_check})" + + class AbyssShadowsTime(ConfigBase): - # 自定义运行时间 - custom_run_time_friday: Time = Field(default=Time(hour=19, minute=0, second=0)) - custom_run_time_saturday: Time = Field(default=Time(hour=19, minute=0, second=0)) - custom_run_time_sunday: Time = Field(default=Time(hour=19, minute=0, second=0)) - -class AbyssShadowsCombatTime(BaseModel): - # 设置战斗时间 - CombatTime_enable: bool = Field(default=False, description="开启智能伤害") - boss_combat_time: int = Field(default=60, description="以秒为单位设置首领战斗时间,请合理设置") - general_combat_time: int = Field(default=60, description="以秒为单位设置副将战斗时间,请合理设置") - elite_combat_time: int = Field(default=60, description="以秒为单位设置精英战斗时间,请合理设置") + # 尝试主动开启狭间-区别于游戏中的自动开启狭间功能 + try_start_abyss_shadows: bool = Field(default=False, description='try_start_abyss_shadows_help') + # 难度 + difficulty: AbyssShadowsDifficulty = Field(default=AbyssShadowsDifficulty.EASY, description='difficulty_help') + # 是否尝试补全首领副将精英 2/4/6 数量限制 + try_complete_enemy_count: bool = Field(default=False, description='try_complete_enemy_count_help') + + +class ProcessManage(ConfigBase): + # 攻击顺序 A,B,C,D 分别表示四个区域,123456表示区域内6个怪物,从上到下,从左到右的顺序 + # 1 + # 2 3 + # 4 5 6 + # 之间用-分隔,不同怪物用;分隔 + # 未实现-->小蛇使用E,-后面表示打几只,例如E-2表示打两只小蛇 + # 例如 A-1;B-2;C-3... + attack_order: str = Field(default='A-1;B-1;B;A-2;A-3;A-4;A-5;A-6', description='attack_order_help') + # 标记主怪 + # EnemyType, 多个用;分隔 + mark_main: MarkMainConfig = Field(default=MarkMainConfig.BOSS_ONLY, description='mark_main_help') + # 是否启用切换御魂 + enable_switch_soul_in_as: bool = Field(default=False, description='enable_switch_soul_in_as_help') + # 首领预设 + preset_boss: str = Field(default='6,1', description='preset_boss_help') + # 副将预设 + preset_general: str = Field(default='6,2', description='preset_general_help') + # 精英预设 + preset_elite: str = Field(default='6,3', description='preset_elite_help') + # 小蛇预设 + # preset_snake: str = Field(default='', description='preset_snake_help') + + # 首领策略 秒退/直到消灭/时间到了退出/伤害足够退出 + # 可用值: 'TRUE', 'FALSE', 时间(秒)(1-999),或最大伤害值(1000-) + # 详见类 :class:`~tasks.AbyssShadows.config.Condition + strategy_boss: str = Field(default='FALSE', description='strategy_boss_help') + # 副将策略 + strategy_general: str = Field(default='30', description='strategy_general_help') + # 精英策略 + strategy_elite: str = Field(default='4380000', description='strategy_elite_help') + + def is_need_mark_main(self, enemy_type: EnemyType) -> bool: + strategy = self.mark_main # 获取 MarkMainConfig 枚举值 + + match strategy: + case MarkMainConfig.BOSS_ONLY: + return enemy_type == EnemyType.BOSS + case MarkMainConfig.GENERAL_ONLY: + return enemy_type == EnemyType.GENERAL + case MarkMainConfig.ELITE_ONLY: + return enemy_type == EnemyType.ELITE + case MarkMainConfig.BOSS_AND_GENERAL: + return enemy_type in (EnemyType.BOSS, EnemyType.GENERAL) + case MarkMainConfig.ELITE_AND_GENERAL: + return enemy_type in (EnemyType.ELITE, EnemyType.GENERAL) + case MarkMainConfig.ELITE_AND_BOSS: + return enemy_type in (EnemyType.ELITE, EnemyType.BOSS) + case MarkMainConfig.ALL: + return True + case _: + return False + + def generate_quit_condition(self, enemy_type: EnemyType): + strategy = None + match enemy_type: + case EnemyType.BOSS: + strategy = self.strategy_boss + case EnemyType.ELITE: + strategy = self.strategy_elite + case EnemyType.GENERAL: + strategy = self.strategy_general + if strategy is None or strategy == '': + return False + return Condition(strategy) + + +class SavedParams(ConfigBase): + # 参数保存的时间,用于判断是不是当天的数据 + save_date: str = Field(default='', description='save_date_help') + # 已完成 + done: str = Field(default='', description='done_help') + # 已知的已经打完的 + unavailable: str = Field(default='', description='unavailable_help') + class AbyssShadows(ConfigBase): scheduler: Scheduler = Field(default_factory=Scheduler) abyss_shadows_time: AbyssShadowsTime = Field(default_factory=AbyssShadowsTime) - abyss_shadows_combat_time: AbyssShadowsCombatTime = Field(default_factory=AbyssShadowsCombatTime) - general_battle_config: GeneralBattleConfig = Field(default_factory=GeneralBattleConfig) - switch_soul_config: SwitchSoulConfig = Field(default_factory=SwitchSoulConfig) + process_manage: ProcessManage = Field(default_factory=ProcessManage) + saved_params: SavedParams = Field(default_factory=SavedParams) + # general_battle_config: GeneralBattleConfig = Field(default_factory=GeneralBattleConfig) + # switch_soul_config: SwitchSoulConfig = Field(default_factory=SwitchSoulConfig) diff --git a/tasks/AbyssShadows/res/click.json b/tasks/AbyssShadows/res/click.json index 245049bed..2638262ce 100644 --- a/tasks/AbyssShadows/res/click.json +++ b/tasks/AbyssShadows/res/click.json @@ -88,5 +88,23 @@ "roiFront": "1140,190,50,162", "roiBack": "1093,143,138,297", "description": "狭间_黑豹入口" + }, + { + "itemName": "abyss_shenshe_enter_abyss", + "roiFront": "740,640,45,15", + "roiBack": "740,640,45,15", + "description": "刚进入神社时,可点击进入狭间暗域的区域,用于开启狭间暗域" + }, + { + "itemName": "quit_area", + "roiFront": "20,36,30,14", + "roiBack": "20,36,30,14", + "description": "战斗时,左上角退出战斗按钮区域 下半部分" + }, + { + "itemName": "mark_main", + "roiFront": "380,15,45,30", + "roiBack": "380,15,45,30", + "description": "红标主怪点击区域" } ] \ No newline at end of file diff --git a/tasks/AbyssShadows/res/image.json b/tasks/AbyssShadows/res/image.json index 9beefd56c..acf888d53 100644 --- a/tasks/AbyssShadows/res/image.json +++ b/tasks/AbyssShadows/res/image.json @@ -20,12 +20,21 @@ { "itemName": "abyss_dragon", "imageName": "res_abyss_dragon.png", - "roiFront": "227,211,55,151", - "roiBack": "190,147,140,283", + "roiFront": "200,150,110,270", + "roiBack": "200,150,110,270", "method": "Template matching", "threshold": 0.8, "description": "狭间_神龙入口" }, + { + "itemName": "abyss_dragon_over", + "imageName": "res_abyss_dragon_over.png", + "roiFront": "200,150,110,270", + "roiBack": "200,150,110,270", + "method": "Template matching", + "threshold": 0.8, + "description": "狭间_神龙入口_已封印" + }, { "itemName": "abyss_peacock", "imageName": "res_abyss_peacock.png", @@ -107,6 +116,15 @@ "threshold": 0.8, "description": "战报退出按钮" }, + { + "itemName": "abyss_enemy_info_exit", + "imageName": "res_abyss_enemy_info_exit.png", + "roiFront": "975,80,90,70", + "roiBack": "975,80,90,70", + "method": "Template matching", + "threshold": 0.8, + "description": "怪物信息页面退出按钮" + }, { "itemName": "abyss_fire", "imageName": "res_abyss_fire.png", @@ -125,6 +143,15 @@ "threshold": 0.8, "description": "前往" }, + { + "itemName": "change_area", + "imageName": "res_change_area.png", + "roiFront": "993,610,63,61", + "roiBack": "993,610,63,61", + "method": "Template matching", + "threshold": 0.8, + "description": "description" + }, { "itemName": "ensure_button", "imageName": "res_ensure_button.png", @@ -137,12 +164,21 @@ { "itemName": "is_attack", "imageName": "res_is_attack.png", - "roiFront": "586,62,73,27", - "roiBack": "586,62,73,27", + "roiFront": "576,54,91,45", + "roiBack": "576,54,91,45", "method": "Template matching", "threshold": 0.8, "description": "进攻中" }, + { + "itemName": "check_finish", + "imageName": "res_check_finish.png", + "roiFront": "570,50,120,60", + "roiBack": "570,50,120,60", + "method": "Template matching", + "threshold": 0.8, + "description": "狭间暗域 挑战结束" + }, { "itemName": "peacock_area", "imageName": "res_peacock_area.png", @@ -198,12 +234,75 @@ "description": "description" }, { - "itemName": "equipping", - "imageName": "res_equipping.png", - "roiFront": "1126,545,100,83", - "roiBack": "1126,545,100,83", + "itemName": "select_difficulty", + "imageName": "res_select_difficulty.png", + "roiFront": "703,645,50,55", + "roiBack": "703,645,50,55", "method": "Template matching", "threshold": 0.8, - "description": "description" + "description": "选择难度按钮" + }, + { + "itemName": "difficulty_easy", + "imageName": "res_difficulty_easy.png", + "roiFront": "620,445,90,210", + "roiBack": "620,445,90,210", + "method": "Template matching", + "threshold": 0.8, + "description": "容易难度" + }, + { + "itemName": "difficulty_normal", + "imageName": "res_difficulty_normal.png", + "roiFront": "620,445,90,210", + "roiBack": "620,445,90,210", + "method": "Template matching", + "threshold": 0.8, + "description": "普通难度" + }, + { + "itemName": "difficulty_hard", + "imageName": "res_difficulty_hard.png", + "roiFront": "620,445,90,210", + "roiBack": "620,445,90,210", + "method": "Template matching", + "threshold": 0.8, + "description": "困难难度" + }, + { + "itemName": "btn_start", + "imageName": "res_btn_start.png", + "roiFront": "1120,570,100,120", + "roiBack": "1120,570,100,120", + "method": "Template matching", + "threshold": 0.8, + "description": "开启按钮" + }, + { + "itemName": "start_ensure", + "imageName": "res_start_ensure.png", + "roiFront": "660,390,190,80", + "roiBack": "660,390,190,80", + "method": "Template matching", + "threshold": 0.8, + "description": "开启确认按钮" + }, + { + "itemName": "abyss_enemy_fire", + "imageName": "res_abyss_enemy_fire.png", + "roiFront": "1100,560,130,130", + "roiBack": "1100,560,130,130", + "method": "Template matching", + "threshold": 0.8, + "description": "当附近有可用怪物时,右下角出现的开始战斗按钮" + }, + { + "itemName": "mark_main", + "imageName": "res_mark_main.png", + "roiFront": "375,40,60,30", + "roiBack": "375,40,60,30", + "method": "Template matching", + "threshold": 0.8, + "description": "红标主怪" } ] \ No newline at end of file diff --git a/tasks/AbyssShadows/res/ocr.json b/tasks/AbyssShadows/res/ocr.json new file mode 100644 index 000000000..5955099c5 --- /dev/null +++ b/tasks/AbyssShadows/res/ocr.json @@ -0,0 +1,47 @@ +[ + { + "itemName": "damage", + "roiFront": "50,110,300,50", + "roiBack": "50,110,300,50", + "mode": "Digit", + "method": "CF_HSV(0980B4,1ED2FF)", + "keyword": "", + "description": "伤害" + }, + { + "itemName": "dragon_done", + "roiFront": "130,160,180,360", + "roiBack": "130,160,180,360", + "mode": "Single", + "method": "CF_RGB(CCCCCC,FFFFFF)", + "keyword": "封印", + "description": "神龙暗域已完成" + }, + { + "itemName": "peacock_done", + "roiFront": "420,160,180,360", + "roiBack": "420,160,180,360", + "mode": "Single", + "method": "CF_RGB(CCCCCC,FFFFFF)", + "keyword": "封印", + "description": "孔雀暗域已完成" + }, + { + "itemName": "fox_done", + "roiFront": "680,160,180,360", + "roiBack": "680,160,180,360", + "mode": "Single", + "method": "CF_RGB(CCCCCC,FFFFFF)", + "keyword": "封印", + "description": "白藏主暗域已完成" + }, + { + "itemName": "leopard_done", + "roiFront": "1000,160,180,360", + "roiBack": "1000,160,180,360", + "mode": "Single", + "method": "CF_RGB(CCCCCC,FFFFFF)", + "keyword": "封印", + "description": "黑豹暗域已完成" + } +] \ No newline at end of file diff --git a/tasks/AbyssShadows/res/res_abyss_dragon_over.png b/tasks/AbyssShadows/res/res_abyss_dragon_over.png new file mode 100644 index 000000000..2affd0c68 Binary files /dev/null and b/tasks/AbyssShadows/res/res_abyss_dragon_over.png differ diff --git a/tasks/AbyssShadows/res/res_abyss_enemy_fire.png b/tasks/AbyssShadows/res/res_abyss_enemy_fire.png new file mode 100644 index 000000000..de48b10c9 Binary files /dev/null and b/tasks/AbyssShadows/res/res_abyss_enemy_fire.png differ diff --git a/tasks/AbyssShadows/res/res_abyss_enemy_info_exit.png b/tasks/AbyssShadows/res/res_abyss_enemy_info_exit.png new file mode 100644 index 000000000..b8884f8ec Binary files /dev/null and b/tasks/AbyssShadows/res/res_abyss_enemy_info_exit.png differ diff --git a/tasks/AbyssShadows/res/res_btn_start.png b/tasks/AbyssShadows/res/res_btn_start.png new file mode 100644 index 000000000..31bb2c024 Binary files /dev/null and b/tasks/AbyssShadows/res/res_btn_start.png differ diff --git a/tasks/AbyssShadows/res/res_check_finish.png b/tasks/AbyssShadows/res/res_check_finish.png new file mode 100644 index 000000000..84cdd0f8d Binary files /dev/null and b/tasks/AbyssShadows/res/res_check_finish.png differ diff --git a/tasks/AbyssShadows/res/res_difficulty_easy.png b/tasks/AbyssShadows/res/res_difficulty_easy.png new file mode 100644 index 000000000..5800badfb Binary files /dev/null and b/tasks/AbyssShadows/res/res_difficulty_easy.png differ diff --git a/tasks/AbyssShadows/res/res_difficulty_hard.png b/tasks/AbyssShadows/res/res_difficulty_hard.png new file mode 100644 index 000000000..69cecf038 Binary files /dev/null and b/tasks/AbyssShadows/res/res_difficulty_hard.png differ diff --git a/tasks/AbyssShadows/res/res_difficulty_normal.png b/tasks/AbyssShadows/res/res_difficulty_normal.png new file mode 100644 index 000000000..830cef003 Binary files /dev/null and b/tasks/AbyssShadows/res/res_difficulty_normal.png differ diff --git a/tasks/AbyssShadows/res/res_is_attack.png b/tasks/AbyssShadows/res/res_is_attack.png index cd0f25cef..60687568f 100644 Binary files a/tasks/AbyssShadows/res/res_is_attack.png and b/tasks/AbyssShadows/res/res_is_attack.png differ diff --git a/tasks/AbyssShadows/res/res_mark_main.png b/tasks/AbyssShadows/res/res_mark_main.png new file mode 100644 index 000000000..21c31ab81 Binary files /dev/null and b/tasks/AbyssShadows/res/res_mark_main.png differ diff --git a/tasks/AbyssShadows/res/res_select_difficulty.png b/tasks/AbyssShadows/res/res_select_difficulty.png new file mode 100644 index 000000000..ce294f0bb Binary files /dev/null and b/tasks/AbyssShadows/res/res_select_difficulty.png differ diff --git a/tasks/AbyssShadows/res/res_start_ensure.png b/tasks/AbyssShadows/res/res_start_ensure.png new file mode 100644 index 000000000..e633c8690 Binary files /dev/null and b/tasks/AbyssShadows/res/res_start_ensure.png differ diff --git a/tasks/AbyssShadows/script_task.py b/tasks/AbyssShadows/script_task.py index 546ca34ef..193a6e110 100644 --- a/tasks/AbyssShadows/script_task.py +++ b/tasks/AbyssShadows/script_task.py @@ -1,93 +1,55 @@ # This Python file uses the following encoding: utf-8 -# @brief Ryou Dokan Toppa (阴阳竂道馆突破功能) +# @brief AbyssShadows(阴阳竂狭间暗域功能) # @author jackyhwei # @note draft version without full test # github https://github.com/roarhill/oas - -from datetime import datetime, timedelta -import random -import numpy as np -import time -from enum import Enum -from cached_property import cached_property from time import sleep -from tasks.Component.GeneralBattle.config_general_battle import GeneralBattleConfig -from tasks.Component.SwitchSoul.switch_soul import SwitchSoul -from tasks.Component.GeneralBattle.general_battle import GeneralBattle -from tasks.Component.config_base import ConfigBase, Time -from tasks.GameUi.game_ui import GameUi -from tasks.GameUi.page import page_main, page_kekkai_toppa, page_shikigami_records, page_guild -from tasks.RealmRaid.assets import RealmRaidAssets +from datetime import datetime -from module.logger import logger -from module.exception import TaskEnd -from module.atom.image_grid import ImageGrid -from module.base.utils import point2str +from future.backports.datetime import timedelta +from module.exception import TaskEnd, RequestHumanTakeover from module.base.timer import Timer -from module.exception import GamePageUnknownError -from pathlib import Path -from tasks.AbyssShadows.config import AbyssShadows +from module.logger import logger +from module.config.config import Config +from module.device.device import Device from tasks.AbyssShadows.assets import AbyssShadowsAssets +from tasks.AbyssShadows.config import AbyssShadows, EnemyType, AreaType, Code, AbyssShadowsDifficulty, \ + CodeList, IndexMap +from tasks.Component.GeneralBattle.general_battle import GeneralBattle +from tasks.Component.SwitchSoul.switch_soul import SwitchSoul +from tasks.GameUi.game_ui import GameUi +from tasks.GameUi.page import page_main, page_guild -class AreaType: - """ 暗域类型 """ - DRAGON = AbyssShadowsAssets.I_ABYSS_DRAGON # 神龙暗域 - PEACOCK = AbyssShadowsAssets.I_ABYSS_PEACOCK # 孔雀暗域 - FOX = AbyssShadowsAssets.I_ABYSS_FOX # 白藏主暗域 - LEOPARD = AbyssShadowsAssets.I_ABYSS_LEOPARD # 黑豹暗域 - - @cached_property - def name(self) -> str: - """ - - :return: - """ - return Path(self.file).stem.upper() - - def __str__(self): - return self.name - - __repr__ = __str__ - -class EmemyType(Enum): - - """ 敌人类型 """ - BOSS = 1 # 首领 - GENERAL = 2 # 副将 - ELITE = 3 # 精英 - - -class CilckArea: - """ 点击区域 """ - GENERAL_1 = AbyssShadowsAssets.C_GENERAL_1_CLICK_AREA - GENERAL_2 = AbyssShadowsAssets.C_GENERAL_2_CLICK_AREA - ELITE_1 = AbyssShadowsAssets.C_ELITE_1_CLICK_AREA - ELITE_2 = AbyssShadowsAssets.C_ELITE_2_CLICK_AREA - ELITE_3 = AbyssShadowsAssets.C_ELITE_3_CLICK_AREA - BOSS= AbyssShadowsAssets.C_BOSS_CLICK_AREA - - @cached_property - def name(self) -> str: - """ - - :return: - """ - return Path(self.file).stem.upper() +# 单个首领/副将/精英 一次无法完成目标(一般是一次没打掉) 的情况下,最大战斗次数 +MAX_BATTLE_COUNT = 2 - def __str__(self): - return self.name - __repr__ = __str__ +class AbyssShadowsFinished(Exception): + pass class ScriptTask(GeneralBattle, GameUi, SwitchSoul, AbyssShadowsAssets): - + # + min_count = { + EnemyType.BOSS: 2, # 最少首领战斗次数 + EnemyType.GENERAL: 4, # 最少副将战斗次数 + EnemyType.ELITE: 6 # 最少精英战斗次数 + } + + def __init__(self, config: Config, device: Device): + super().__init__(config, device) + # 当前所用队伍预设 + self.cur_preset = None + # process list + self.ps_list: CodeList = CodeList('') + # 已完成 列表 + self.done_list: CodeList = CodeList('') + # 已知的 已经被打完的 列表 + self.unavailable_list: CodeList = CodeList('') + # 是否已经切换过御魂 + self.switch_soul_done = False - boss_fight_count = 0 # 首领战斗次数 - general_fight_count = 0 # 副将战斗次数 - elite_fight_count = 0 # 精英战斗次数 - def run(self): """ 狭间暗域主函数 @@ -95,175 +57,127 @@ def run(self): """ cfg: AbyssShadows = self.config.abyss_shadows - if cfg.switch_soul_config.enable: - self.ui_get_current_page() - self.ui_goto(page_shikigami_records) - self.run_switch_soul(cfg.switch_soul_config.switch_group_team) - if cfg.switch_soul_config.enable_switch_by_name: - self.ui_get_current_page() - self.ui_goto(page_shikigami_records) - self.run_switch_soul_by_name(cfg.switch_soul_config.group_name, cfg.switch_soul_config.team_name) today = datetime.now().weekday() if today not in [4, 5, 6]: + # 非周五六日,直接退出 logger.info(f"Today is not abyss shadows day, exit") - # 设置下次运行时间为本周五 - self.custom_next_run(task='AbyssShadows', custom_time=cfg.abyss_shadows_time.custom_run_time_friday, time_delta=4-today) + self.set_next_run(task='AbyssShadows', finish=False, server=True, success=True) + raise TaskEnd + server_time = datetime.combine(datetime.now().date(), cfg.scheduler.server_update) + if datetime.now() - server_time > timedelta(hours=2): + # 超时两小时未开始,直接退出 + logger.info("Timeout threshold: 2h (force quit if not started)") + self.set_next_run(task='AbyssShadows', finish=False, server=True, success=True) raise TaskEnd - success = True + # 进入狭间 self.goto_abyss_shadows() - # 第一次默认选择神龙暗域 - if not self.select_boss(AreaType.DRAGON): - logger.warning("Failed to enter abyss shadows") - self.goto_main() - self.set_next_run(task='AbyssShadows', finish=False, server=True, success=False) - raise TaskEnd - - # 等待可进攻时间 - self.device.stuck_record_add('BATTLE_STATUS_S') - # 集结中图片 - self.wait_until_disappear(self.I_WAIT_TO_START) - self.device.stuck_record_clear() - # 未开启智能伤害准备攻打精英、副将、首领 - if not cfg.abyss_shadows_combat_time.CombatTime_enable: - while 1: - # 点击战报按钮 - find_list = [EmemyType.BOSS, EmemyType.GENERAL, EmemyType.ELITE] - for enemy_type in find_list: - # 寻找敌人并开始战斗, - if not self.find_enemy(enemy_type): - logger.warning(f"Failed to find {enemy_type.name} enemy, exit") - break - logger.info(f"Current fight times: boss {self.boss_fight_count} times, general {self.general_fight_count} times, elite {self.elite_fight_count} times") - # 正常应该打完一个区域了,检查攻打次数,如没打够则切换到下一个区域,默认神龙 -> 孔雀 -> 白藏主 -> 黑豹 - if self.boss_fight_count >= 2 and self.general_fight_count >= 4 and self.elite_fight_count >= 6: - success = True - break - else: - #切换区域之前关闭战报 - self.appear_then_click(self.I_ABYSS_MAP_EXIT, interval=1) - current_area = self.check_current_area() - logger.info(f"Current area is {current_area}, switch to next area") - if current_area == AreaType.DRAGON: - self.change_area(AreaType.PEACOCK) - continue - elif current_area == AreaType.PEACOCK: - self.change_area(AreaType.FOX) - continue - elif current_area == AreaType.FOX: - self.change_area(AreaType.LEOPARD) - continue - else: - logger.warning("All enemy types have been defeated, but not enough emeny to fight, exit") - break - - # 开启智能伤害 - if cfg.abyss_shadows_combat_time.CombatTime_enable: - while True: - # 1. 先攻打 1 个 BOSS - if self.boss_fight_count < 2: - self.boss_fight_count = self.fight_and_switch(EmemyType.BOSS, 2, self.boss_fight_count, - lambda: self.switch_area()) - - # 2. 攻打 2 个 GENERAL - if self.general_fight_count < 4: - self.general_fight_count = self.fight_and_switch(EmemyType.GENERAL, 4, self.general_fight_count, - lambda: self.switch_area()) - - # 3. 攻打 3 个 ELITE - if self.elite_fight_count < 6: - self.elite_fight_count = self.fight_and_switch(EmemyType.ELITE, 6, self.elite_fight_count, - lambda: self.switch_area()) - - # 检查是否已完成所有任务 - print(f"Current fight times: boss {self.boss_fight_count} times, general {self.general_fight_count} times, elite {self.elite_fight_count} times") - if self.boss_fight_count >= 2 and self.general_fight_count >= 4 and self.elite_fight_count >= 6: - logger.info("All fights completed") - success = True - break - else: - #没打满我也没办法就最后一张图,看看有没有剩余的吧没有也不想跑了 - find_list = [EmemyType.BOSS, EmemyType.GENERAL, EmemyType.ELITE] - for enemy_type in find_list: - # 寻找敌人并开始战斗, - if not self.find_enemy(enemy_type): - logger.warning(f"Failed to find {enemy_type.name} enemy, exit") - break - logger.info(f"Current fight times: boss {self.boss_fight_count} times, general {self.general_fight_count} times, elite {self.elite_fight_count} times") - logger.warning("All enemy types have been defeated, but not enough emeny to fight, exit") - success = True - break + # 尝试开启狭间 + if cfg.abyss_shadows_time.try_start_abyss_shadows: + self.start_abyss_shadows() + + try: + self.init_list_from_cfg() + # 判断各个区域是否可用 + available_areas, unavailable_areas = self.detect_area_status() + for area in unavailable_areas: + self.unavailable_list += CodeList(IndexMap[area.name].value) + if unavailable_areas: + self.flash_list() + + # 获取需要进入的区域类型 + _next = self.get_next() + if _next is None: + raise AbyssShadowsFinished + area_enter = _next.get_areatype() + + # 通过能否进入,检测狭间是否开启 + if not self.select_boss(area_enter): + logger.warning("Failed to enter abyss shadows") + self.goto_main() + self.set_next_run(task='AbyssShadows', finish=False, server=False, success=False) + raise TaskEnd + + # 集结中图片 + self.wait_until_appear(self.I_WAIT_TO_START, wait_time=2) + + # 检查活动是否结束 + if self.appear(self.I_CHECK_FINISH): + logger.info(f"{self.I_CHECK_FINISH} appear,abyss shadows finished") + raise AbyssShadowsFinished + # 切换御魂 + self.switch_soul_in_as() + # + self.device.stuck_record_add('BATTLE_STATUS_S') + # 等待战斗开始 + self.wait_until_appear(self.I_IS_ATTACK, wait_time=180) + self.device.stuck_record_clear() + # + self.process() + except AbyssShadowsFinished: + logger.info("Abyss shadows finished with Exception AbyssShadowsFinished") + pass + logger.info("Abyss shadows process done") # 保持好习惯,一个任务结束了就返回到庭院,方便下一任务的开始 self.goto_main() # 设置下次运行时间 - if success: - print("我要重新设置时间了") - if today == 4: - # 周五推迟到周六 - logger.info(f"The next abyss shadows day is Saturday") - self.custom_next_run(task='AbyssShadows', custom_time=cfg.abyss_shadows_time.custom_run_time_saturday, time_delta=1) - elif today == 5: - # 周六推迟到周日 - logger.info(f"The next abyss shadows day is Sunday") - self.custom_next_run(task='AbyssShadows', custom_time=cfg.abyss_shadows_time.custom_run_time_sunday, time_delta=1) - elif today == 6: - # 周日推迟到下周五 - logger.info(f"The next abyss shadows day is Friday") - self.custom_next_run(task='AbyssShadows', custom_time=cfg.abyss_shadows_time.custom_run_time_friday, time_delta=5) - else: - self.set_next_run(task='AbyssShadows', finish=True, server=True, success=False) - raise TaskEnd + self.set_next_run(task='AbyssShadows', finish=True, server=True, success=True) + self.clear_saved_params() + raise TaskEnd - #攻击并进行区域切换 - def fight_and_switch(self, enemy_type, required_count, fight_count, next_area_func): - while fight_count < required_count: # 0-2 - if not self.find_enemy(enemy_type): - logger.warning(f"Failed to find {enemy_type.name} enemy, exit") - return fight_count - else: - if enemy_type == EmemyType.BOSS: - fight_count += 1 - elif enemy_type == EmemyType.GENERAL: - fight_count += 2 - elif enemy_type == EmemyType.ELITE: - fight_count += 3 - logger.info( - f"Current fight times: boss {self.boss_fight_count} times, general {self.general_fight_count} times, elite {self.elite_fight_count} times") - - # 完成攻打后切换区域 - current_area = self.check_current_area() - if fight_count < required_count and current_area != AreaType.LEOPARD: - next_area_func() - return fight_count - - def switch_area(self): - #确保没有战报页面 - self.appear_then_click(self.I_ABYSS_MAP_EXIT, interval=1) - current_area = self.check_current_area() - logger.info(f"Current area is {current_area}, switch to next area") - if current_area == AreaType.DRAGON: - self.change_area(AreaType.PEACOCK) - elif current_area == AreaType.PEACOCK: - self.change_area(AreaType.FOX) - elif current_area == AreaType.FOX: - self.change_area(AreaType.LEOPARD) - else: - logger.warning("All areas have been completed, exit") - raise StopIteration # 退出循环 + def init_list_from_cfg(self): + if datetime.today().strftime('%Y-%m-%d') != self.config.model.abyss_shadows.saved_params.save_date: + logger.info("Today is not saved date, clear saved params") + self.clear_saved_params() + # + self.ps_list = CodeList(self.config.model.abyss_shadows.process_manage.attack_order) + # + self.done_list = CodeList(self.config.model.abyss_shadows.saved_params.done) + # + self.unavailable_list = CodeList(self.config.model.abyss_shadows.saved_params.unavailable) + logger.info(f"update list done!{self.done_list=} {self.unavailable_list=}") + + def flash_list(self): + """ + NOTE 导致该任务运行过程中,从前端修改的配置将会丢失 + @return: + """ + # BUG 跨天会出问题 + self.config.model.abyss_shadows.saved_params.save_date = datetime.today().strftime('%Y-%m-%d') + self.config.model.abyss_shadows.saved_params.done = self.done_list.parse2str() + self.config.model.abyss_shadows.saved_params.unavailable = self.unavailable_list.parse2str() + self.config.save() + logger.info(f"Flash list done!{self.done_list=} {self.unavailable_list=}") + def clear_saved_params(self): + self.config.model.abyss_shadows.saved_params.done = '' + self.config.model.abyss_shadows.saved_params.unavailable = '' + self.config.save() + logger.info("Clear saved params done") def check_current_area(self) -> AreaType: - ''' 获取当前区域 + """ 获取当前区域 :return AreaType - ''' + """ + logger.info("Checking current area") while 1: self.screenshot() + # 关闭战报界面 + if self.appear(self.I_ABYSS_MAP_EXIT): + self.click(self.I_ABYSS_MAP_EXIT, interval=2) + continue + if self.appear(self.I_ABYSS_ENEMY_INFO_EXIT): + self.click(self.I_ABYSS_ENEMY_INFO_EXIT, interval=2) + continue + if not self.appear(self.I_ABYSS_NAVIGATION): + # 确定不在战报界面后依旧没有在某一区域,则返回None + return None if self.appear(self.I_PEACOCK_AREA): return AreaType.PEACOCK elif self.appear(self.I_DRAGON_AREA): @@ -272,80 +186,155 @@ def check_current_area(self) -> AreaType: return AreaType.FOX elif self.appear(self.I_LEOPARD_AREA): return AreaType.LEOPARD - else: - continue def change_area(self, area_name: AreaType) -> bool: - ''' 切换到下个区域 - :return - ''' + """ 切换到下个区域,不管成功与否,只要存在可用区域,就进入,不会停留在选择区域页面 + :return + """ + # 确保进入区域,有 切换区域 按钮 + logger.info(f"Change area to {area_name}") while 1: - # 确保切换区域前不在战报页面 - if self.appear_then_click(self.I_ABYSS_MAP_EXIT, interval=1): + self.screenshot() + # 如果出现挑战完成,直接退出 + if self.appear(self.I_CHECK_FINISH): + raise AbyssShadowsFinished + + if self.appear(self.I_ABYSS_NAVIGATION) or self.appear(self.I_CHANGE_AREA): + break + # + if self.appear(self.I_ABYSS_MAP_EXIT): + self.click(self.I_ABYSS_MAP_EXIT, interval=2) continue + # + if self.appear(self.I_ABYSS_ENEMY_INFO_EXIT): + self.click(self.I_ABYSS_ENEMY_INFO_EXIT, interval=2) + continue + + # 判断当前区域是否正确 + current_area = self.check_current_area() + if current_area == area_name: + logger.info(f"Current area is {current_area.name}, no need to change") + return True + + # 切换到选择区域界面 + while 1: self.screenshot() - # 判断当前区域是否正确 - current_area = self.check_current_area() - if current_area == area_name: + # 如果出现挑战完成,直接退出 + if self.appear(self.I_CHECK_FINISH): + raise AbyssShadowsFinished + + # 出现切换区域界面 + if self.appear(self.I_ABYSS_DRAGON_OVER) or self.appear(self.I_ABYSS_DRAGON): break - # 切换区域界面 - if self.appear(self.I_ABYSS_DRAGON): - self.select_boss(area_name) - logger.info(f"Switch to {area_name.name}") - continue - # 点击更换领域按钮 - if self.appear_then_click(self.I_CHANGE_AREA,interval=4): + # 点击切换区域按钮 + if self.appear_then_click(self.I_CHANGE_AREA, interval=4): logger.info(f"Click {self.I_CHANGE_AREA.name}") continue - - return True - + + logger.info(f"enter change area page") + # 判断区域是否可用,并进入一个区域 + available_areas, unavailable_areas = self.detect_area_status() + success = area_name in available_areas + if not success: + # 更新配置 + for area in unavailable_areas: + self.unavailable_list += CodeList(IndexMap[area.name].value) + + if available_areas is None or available_areas == []: + # 所有区域均不可用 + raise AbyssShadowsFinished + + if not success: + # 原参数表示的 区域 已完成,则选择第一个未完成的区域 + area_name = available_areas[0] + + self.select_boss(area_name) + logger.info(f"Switch to {area_name.name}") + + return success + def goto_main(self): - ''' 保持好习惯,一个任务结束了就返回庭院,方便下一任务的开始或者是出错重启 - ''' - self.ui_get_current_page() + """ 保持好习惯,一个任务结束了就返回庭院,方便下一任务的开始或者是出错重启 + """ + # 可能在狭间,也可能在其他界面 + timer_quit_abyss_shadows = Timer(16) + timer_quit_abyss_shadows.start() + while 1: + self.screenshot() + if timer_quit_abyss_shadows.reached(): + logger.info("timer_quit_abyss_shadows reached,") + break + + if self.appear(self.I_ABYSS_DRAGON) or self.appear(self.I_ABYSS_DRAGON_OVER): + # 在切换区域界面 + self.device.click(x=600, y=600) + self.wait_until_appear(self.I_ABYSS_NAVIGATION, wait_time=2) + continue + if self.appear_then_click(self.I_ABYSS_MAP_EXIT, interval=2): + self.wait_until_appear(self.I_ABYSS_NAVIGATION, wait_time=2) + continue + if self.appear_then_click(self.I_ABYSS_ENEMY_INFO_EXIT, interval=2): + self.wait_until_appear(self.I_ABYSS_MAP_EXIT, wait_time=2) + continue + if self.appear_then_click(self.I_UI_BACK_BLUE, interval=2): + self.wait_until_appear(self.I_ABYSS_NAVIGATION, wait_time=1) + continue + if self.appear_then_click(self.I_UI_BACK_YELLOW, interval=2): + continue + if self.appear(self.I_ABYSS_NAVIGATION, threshold=0.85) or self.appear(self.I_CHECK_FINISH, threshold=0.85): + break + if self.appear(self.I_CHECK_SUMMON): + break + + # logger.info("Exiting abyss_shadows") + self.ui_get_current_page() self.ui_goto(page_main) def goto_abyss_shadows(self) -> bool: - ''' 进入狭间 + """ 进入狭间 :return bool - ''' + """ self.ui_get_current_page() logger.info("Entering abyss_shadows") self.ui_goto(page_guild) - + while 1: self.screenshot() # 进入神社 - if self.appear_then_click(self.I_RYOU_SHENSHE,interval=1): + if self.appear_then_click(self.I_RYOU_SHENSHE, interval=1): logger.info("Enter Shenshe") continue # 查找狭间 if not self.appear(self.I_ABYSS_SHADOWS, threshold=0.8): - self.swipe(self.S_TO_ABBSY_SHADOWS,interval=3) + self.swipe(self.S_TO_ABBSY_SHADOWS, interval=3) continue # 进入狭间 if self.appear_then_click(self.I_ABYSS_SHADOWS): logger.info("Enter abyss_shadows") break return True - + def select_boss(self, area_name: AreaType) -> bool: - ''' 选择暗域类型 - :return - ''' + """ 选择暗域类型 + :return + """ + logger.info(f"Select boss: {area_name.name} start") click_times = 0 while 1: self.screenshot() # 区域图片与入口图片不一致,使用点击进去 - - if self.appear(self.I_ABYSS_DRAGON): + if self.appear(self.I_ABYSS_DRAGON_OVER) or self.appear(self.I_ABYSS_DRAGON): + is_click = False match area_name: - case AreaType.DRAGON: is_click = self.click(self.C_ABYSS_DRAGON,interval=2) - case AreaType.PEACOCK: is_click = self.click(self.C_ABYSS_PEACOCK,interval=2) - case AreaType.FOX: is_click = self.click(self.C_ABYSS_FOX,interval=2) - case AreaType.LEOPARD: is_click = self.click(self.C_ABYSS_LEOPARD,interval=2) + case AreaType.DRAGON: + is_click = self.click(self.C_ABYSS_DRAGON, interval=2) + case AreaType.PEACOCK: + is_click = self.click(self.C_ABYSS_PEACOCK, interval=2) + case AreaType.FOX: + is_click = self.click(self.C_ABYSS_FOX, interval=2) + case AreaType.LEOPARD: + is_click = self.click(self.C_ABYSS_LEOPARD, interval=2) if is_click: click_times += 1 logger.info(f"Click {area_name.name} {click_times} times") @@ -355,222 +344,515 @@ def select_boss(self, area_name: AreaType) -> bool: continue if self.appear(self.I_ABYSS_NAVIGATION): break + logger.info(f"select boss: {area_name.name} done") return True - def find_enemy(self, enemy_type: EmemyType) -> bool: - ''' 寻找敌人,并开始寻路进入战斗 - :return 是否找到敌人,若目标已死亡则返回False,否则返回True - True 找到敌人,并已经战斗完成 - ''' - print(f"Find enemy: {enemy_type}") - while 1: - self.screenshot() - # 点击战报按钮 - if self.appear(self.I_ABYSS_MAP): - break - if self.appear_then_click(self.I_ABYSS_NAVIGATION,interval=1): - continue - - match enemy_type: - case EmemyType.BOSS: success = self.run_boss_fight() - case EmemyType.GENERAL: success = self.run_general_fight() - case EmemyType.ELITE: success = self.run_elite_fight() - - return success - - def run_boss_fight(self) -> bool: - ''' 首领战斗 - 只要进入了战斗都返回成功 - :return - ''' - if self.boss_fight_count >= 2: - logger.info(f"boss fight count {self.boss_fight_count} times, skip") - return True - success = True - logger.info(f"Run boss fight") - if self.click_emeny_area(CilckArea.BOSS): - logger.info(f"Click {CilckArea.BOSS.name}") - self.run_general_battle_back(Monster_type="BOSS") - self.boss_fight_count += 1 - logger.info(f'Fight, boss_fight_count {self.boss_fight_count} times') - else: - success = False - return success - - def run_general_fight(self) -> bool: - ''' 副将战斗 - :return - ''' - general_list = [CilckArea.GENERAL_1, CilckArea.GENERAL_2] - logger.info(f"Run general fight") - for general in general_list: - # 副将战斗次数达到4个时,退出循环 - if self.general_fight_count >= 4: - logger.info(f"general fight count {self.general_fight_count} times, skip") - break - if self.click_emeny_area(general): - logger.info(f"Click {general.name}") - self.general_fight_count += 1 - self.run_general_battle_back(Monster_type="GENERAL") - logger.info(f'Fight, general_fight_count {self.general_fight_count} times') - return True - - - def run_elite_fight(self) -> bool: - ''' 精英战斗 - :return - ''' - elite_list = [CilckArea.ELITE_1, CilckArea.ELITE_2, CilckArea.ELITE_3] - logger.info(f"Run elite fight") - for elite in elite_list: - # 精英战斗次数达到6个时,退出循环 - if self.elite_fight_count >= 6: - logger.info(f"Elite fight count {self.elite_fight_count} times, skip") - break - if self.click_emeny_area(elite): - logger.info(f"Click {elite.name}") - self.elite_fight_count += 1 - self.run_general_battle_back(Monster_type="ELITE") - logger.info(f'Fight, elite_fight_count {self.elite_fight_count} times') - return True - - def click_emeny_area(self, click_area: CilckArea) -> bool: - suceess = True - ''' 点击敌人区域 - - :return - ''' + def goto_enemy(self, item_code: Code) -> bool: + # 前往当前区域 的某个 敌人 + logger.info(f"Goto enemy: {item_code}") + click_area = item_code.get_enemy_click() logger.info(f"Click emeny area: {click_area.name}") + # 点击前往按钮的次数,阴阳师BUG:点击后不动, + # 所以如果失败了,在点击前,尝试使用左下方的摇杆移动一点点 + count_click_goto_enemy = 0 # 点击战报 while 1: self.screenshot() - if self.appear_then_click(self.I_ABYSS_NAVIGATION, interval=1.5): - logger.info(f"Click {self.I_ABYSS_NAVIGATION.name}") - continue - if self.appear(self.I_ABYSS_MAP): - logger.info("Find abyss map, exit") - + if self.appear(self.I_ABYSS_FIRE): + break + # 尝试使用左下方摇杆移动 + if count_click_goto_enemy > 0 and self.appear(self.I_ABYSS_NAVIGATION): + self.move_a_little() + # 打开导航页面 + self.open_navigation() + click_times = 0 - # 点击攻打区域 + # 点击攻打区域,直到出现"前往"字样 while 1: self.screenshot() # 如果点3次还没进去就表示目标已死亡,跳过 if click_times >= 3: logger.warning(f"Failed to click {click_area}") - return + return False # 出现前往按钮就退出 if self.appear(self.I_ABYSS_GOTO_ENEMY): + logger.info(f"{self.I_ABYSS_GOTO_ENEMY} appear") break - if self.click(click_area,interval=1.5): + if self.click(click_area, interval=1.5): click_times += 1 continue - if self.appear_then_click(self.I_ENSURE_BUTTON,interval=1): + if self.appear_then_click(self.I_ENSURE_BUTTON, interval=1): continue - - # 点击前往按钮 + + # 点击前往按钮,知道该按钮消失或出现"挑战"字样 + while 1: self.screenshot() - if self.appear_then_click(self.I_ABYSS_GOTO_ENEMY,interval=1): - logger.info(f"Click {self.I_ABYSS_GOTO_ENEMY.name}") - # 点击敌人后,如果是不同区域会确认框,点击确认 - if self.appear_then_click(self.I_ENSURE_BUTTON, interval=1): - logger.info(f"Click {self.I_ENSURE_BUTTON.name}") - # 跑动画比较花时间 - sleep(3) + if self.appear(self.I_CHECK_FINISH): + raise AbyssShadowsFinished + if self.appear(self.I_ABYSS_FIRE): + logger.info(f"{self.I_ABYSS_FIRE} appear") + break + if self.appear(self.I_ENSURE_BUTTON): + self.click(self.I_ENSURE_BUTTON, interval=1) + continue + if self.appear(self.I_ABYSS_GOTO_ENEMY): + self.click(self.I_ABYSS_GOTO_ENEMY, interval=1) + count_click_goto_enemy += 1 continue - else: + if not self.wait_until_appear(self.I_ABYSS_FIRE, wait_time=10): break - - # 如果遇到点击前往按钮后不动的 bug,则再次尝试进入 - if self.wait_until_appear(self.I_ABYSS_FIRE, wait_time=20): - break - logger.warning("Failed to enter fire") - + return True + + def attack_enemy(self): + logger.info("Attack enemy") # 点击战斗按钮 + # NOTE: 以下暂时为猜测,待验证 + # 同一敌人,需要第二次攻击时,此时刚刚退出战斗,先出现大地图的帧,然后才会出现战斗按钮,故延迟几秒检测 + timer_animation = Timer(2) + timer_animation.start() while 1: self.screenshot() - if self.appear_then_click(self.I_ABYSS_FIRE,interval=1): - - logger.info(f"Click {self.I_ABYSS_FIRE.name}") - # 挑战敌人后,如果是奖励次数上限,会出现确认框 - if self.appear_then_click(self.I_ENSURE_BUTTON, interval=1): - logger.info(f"Click {self.I_ENSURE_BUTTON.name}") + if self.appear(self.I_CHECK_FINISH): + raise AbyssShadowsFinished + # 挑战敌人后,如果是奖励次数上限,会出现确认框 + if self.appear(self.I_ENSURE_BUTTON): + self.click(self.I_ENSURE_BUTTON, interval=2) + continue + # + if self.appear(self.I_ABYSS_ENEMY_FIRE): + self.click(self.I_ABYSS_ENEMY_FIRE, interval=0.4) + self.wait_until_appear(self.I_ABYSS_FIRE, wait_time=1) continue + # + if self.appear(self.I_ABYSS_FIRE): + self.click(self.I_ABYSS_FIRE, interval=0.4) + self.wait_until_appear(self.I_PREPARE_HIGHLIGHT, wait_time=2) + continue + # if self.appear(self.I_PREPARE_HIGHLIGHT): + return True + if not timer_animation.reached(): + continue + if self.appear(self.I_ABYSS_NAVIGATION, threshold=0.85): + # 已返回主界面 + logger.info("Return to main page while try to attack enemy") + return False + if self.appear(self.I_ABYSS_GOTO_ENEMY): + # 为了修复问题:开始从一个怪物跑到另一个怪物时,还是可以打的,等小人到了之后,发现已经打死了 + # 就会出现这个前往按钮 + logger.info("Found goto enemy button while try to attack enemy") + return False + return True + + def start_abyss_shadows(self): + # 尝试开启狭间暗域 + self.wait_until_appear(self.I_SELECT_DIFFICULTY, wait_time=2) + if not self.appear(self.I_SELECT_DIFFICULTY): + logger.info("Failed to Open abyss_shadows ,cause not found I_SELECT_DIFFICULTY") + return + if not self.appear(self.I_BTN_START): + logger.info("Failed to Open abyss_shadows ,cause not found I_BTN_START") + return + # 选择难度 + self.ui_click(self.I_SELECT_DIFFICULTY, stop=self.I_DIFFICULTY_EASY, interval=2) + + difficulty_btn = None + match self.config.model.abyss_shadows.abyss_shadows_time.difficulty: + case AbyssShadowsDifficulty.EASY: + difficulty_btn = self.I_DIFFICULTY_EASY + case AbyssShadowsDifficulty.HARD: + difficulty_btn = self.I_DIFFICULTY_HARD + case AbyssShadowsDifficulty.NORMAL: + difficulty_btn = self.I_DIFFICULTY_NORMAL + self.ui_click_until_disappear(difficulty_btn, interval=2) + # 开始 + self.ui_click(self.I_BTN_START, stop=self.I_START_ENSURE, interval=2) + self.ui_click_until_disappear(self.I_START_ENSURE, interval=2) + + def process(self): + while True: + self.init_list_from_cfg() + _next = self.get_next() + if _next is None: break - - return suceess + self.execute(_next) + self.flash_list() + + def get_next(self) -> [Code, None]: + # 获取下一个任务目标 + for ps in self.ps_list: + if ps not in self.done_list and ps not in self.unavailable_list: + return ps + + if not self.config.model.abyss_shadows.abyss_shadows_time.try_complete_enemy_count: + # + logger.info("All done, don`t need to fix 246") + return None + + # 已配置的已完成,若未打满奖励,尝试补全 + # 统计已完成的各类型数量 + done_counts = { + EnemyType.BOSS: 0, + EnemyType.GENERAL: 0, + EnemyType.ELITE: 0 + } + + for code in self.done_list: + enemy_type = code.get_enemy_type() + done_counts[enemy_type] += 1 + + need_boss = done_counts[EnemyType.BOSS] < self.min_count[EnemyType.BOSS] + need_general = done_counts[EnemyType.GENERAL] < self.min_count[EnemyType.GENERAL] + need_elite = done_counts[EnemyType.ELITE] < self.min_count[EnemyType.ELITE] + + logger.info(f"Need boss: {need_boss}, need general: {need_general}, need elite: {need_elite}") + all_possible_codes = [] + for area in AreaType: + area_code = IndexMap[area.name].value # 如 DRAGON -> 'A' + for num in ['1', '2', '3', '4', '5', '6']: + all_possible_codes.append(Code(f"{area_code}-{num}")) + + for code in all_possible_codes: + if code in self.done_list or code in self.unavailable_list: + continue - def run_general_battle_back(self, Monster_type: str) -> bool: - """ - 重写父类方法,因为狭间暗域的准备和战斗流程不一样 - 进入挑战然后直接返回 - :param config: - :return: - """ - cfg: AbyssShadows = self.config.abyss_shadows - while 1: - #确保进入战斗 + enemy_type = code.get_enemy_type() + + if enemy_type == EnemyType.BOSS and need_boss: + return code + elif enemy_type == EnemyType.GENERAL and need_general: + return code + elif enemy_type == EnemyType.ELITE and need_elite: + return code + + return None + + def open_navigation(self): + logger.info("Open navigation") + while True: self.screenshot() - if self.wait_until_appear(self.I_EQUIPPING, wait_time=4): - self.click(self.I_EQUIPPING, interval=1.5) - if not self.appear(self.I_EQUIPPING): + if self.appear(self.I_CHECK_FINISH): + raise AbyssShadowsFinished + if self.appear(self.I_ABYSS_MAP): break - logger.info(f"Click {self.I_EQUIPPING.name}") - logger.info(f"点击准备了") + if self.appear(self.I_ABYSS_NAVIGATION): + self.click(self.I_ABYSS_NAVIGATION, interval=1) + continue + if self.appear(self.I_ABYSS_FIRE) or self.appear(self.I_ABYSS_GOTO_ENEMY): + self.click(self.I_ABYSS_ENEMY_INFO_EXIT, interval=2) + continue - # 进入战斗后,开始计时 - start_time = time.time() - if cfg.abyss_shadows_combat_time.CombatTime_enable: - self.device.stuck_record_add('BATTLE_STATUS_S') - if Monster_type == "BOSS": # BOSS战斗 - combat_time = cfg.abyss_shadows_combat_time.boss_combat_time - elif Monster_type == "GENERAL": # 是副将战斗 - combat_time = cfg.abyss_shadows_combat_time.general_combat_time - elif Monster_type == "ELITE": # 精英战斗 - combat_time = cfg.abyss_shadows_combat_time.elite_combat_time - else: - combat_time = 60 # 默认为 60 秒 - # 等待设定的战斗时间 - while time.time() - start_time < combat_time: - self.screenshot() - if self.appear_then_click(self.I_WIN, interval=1.5): - break - logger.info("Combat time ended, proceeding to exit.") + def execute(self, item_code: Code): + logger.info(f"Start to execute code {item_code}") + area = item_code.get_areatype() + + if not self.change_area(area): + return False + # 当前应当在正确的区域 + # + # if not self.check_available(item_code): + # return + + if not self.goto_enemy(item_code): + # 前往失败,添加进unavailable_list + self.unavailable_list.append(item_code) + return False + + battle_count = MAX_BATTLE_COUNT + while battle_count > 0: + self.screenshot() + + if not self.attack_enemy(): + # 根据战斗次数判断该 item_code 是否已被消灭 + if battle_count == MAX_BATTLE_COUNT: + # 没战斗过直接返回重试 + return + # 如果曾经战斗过,则认为该 item_code 已完成 + logger.info(f"{item_code} has been killed") + break + # 战斗 + suc = self.run_battle(item_code) self.device.stuck_record_clear() - # 战斗提前结束这时没有返回按钮 - if self.appear_then_click(self.I_WIN, interval=1.5): - return True + if suc: + break + battle_count -= 1 + logger.info(f"{item_code} push into done_list") + self.done_list.append(item_code) + return True - # 点击返回 - while 1: + def run_battle(self, item_code: Code): + success = False + enemy_type = item_code.get_enemy_type() + + # 判断是否需要更换预设 + def get_preset(_enemy_type: EnemyType): + match _enemy_type: + case EnemyType.BOSS: + return self.config.model.abyss_shadows.process_manage.preset_boss + case EnemyType.GENERAL: + return self.config.model.abyss_shadows.process_manage.preset_general + case EnemyType.ELITE: + return self.config.model.abyss_shadows.process_manage.preset_elite + + preset = get_preset(enemy_type) + if preset != self.cur_preset: + logger.info(f"enemyType{enemy_type}--Switch preset to {preset} and {self.cur_preset=}") + self.switch_preset_team_with_str(preset) + self.cur_preset = preset + + # 点击准备 + _timer_battle = Timer(180) + self.wait_until_appear(self.I_PREPARE_HIGHLIGHT, wait_time=3) + self.ui_click_until_disappear(self.I_PREPARE_HIGHLIGHT, interval=0.6) + _timer_battle.start() + + # 生成退出条件 + # 因为条件中可能是时间相关,所以在点击准备按钮后直接生成,尽量减小误差 + condition = self.config.model.abyss_shadows.process_manage.generate_quit_condition(enemy_type) + logger.info(f"enemyType{enemy_type}--{condition}") + + # 标记主怪 + is_need_mark_main = self.config.model.abyss_shadows.process_manage.is_need_mark_main(enemy_type) + if is_need_mark_main: + logger.info(f"enemyType{enemy_type}--Mark main") + # 需要处理主怪没了的情况,增加最大次数 + count_click_mark_main = 0 + while count_click_mark_main < 5: + if self.appear(self.I_MARK_MAIN): + break + if self.click(self.C_MARK_MAIN, interval=1): + count_click_mark_main += 1 + self.wait_until_appear(self.I_MARK_MAIN, wait_time=1) + continue + + # 绿标 + # self.green_mark(True,GreenMarkType.GREEN_LEFT1) + + _cur_damage = 0 + need_check_damage = condition.is_need_damage_value() + self.device.screenshot_interval_set(1) + self.device.stuck_record_add('BATTLE_STATUS_S') + while True: self.screenshot() - if self.appear_then_click(self.I_EXIT, interval=1.5): + if need_check_damage: + _cur_damage = self.O_DAMAGE.ocr_digit(self.device.image) + if condition.is_valid(_cur_damage): + logger.info(f"Condition Validated,try to quit battle") + self.device.screenshot_interval_set() + self.quit_battle() + break + if self.appear_then_click(self.I_PREPARE_HIGHLIGHT, interval=3): + # 正常来讲,此处不应该出现准备按钮,以防万一 + self.device.stuck_record_add("BATTLE_STATUS_S") + _timer_battle.reset() continue - if self.appear(self.I_EXIT_ENSURE): + # 战斗胜利标志 + if self.appear_then_click(self.I_WIN, interval=1): + self.device.screenshot_interval_set() + need_check_damage = False + continue + # 战斗奖励标志 + if self.appear_then_click(self.I_REWARD, interval=1): + self.device.screenshot_interval_set() + need_check_damage = False + continue + if self.appear(self.I_ABYSS_NAVIGATION): + self.device.screenshot_interval_set() break - logger.info(f"Click {self.I_EXIT.name}") + if condition.is_passed() or (not _timer_battle.reached()): + # 通过条件结束的,视其为完成 + # 条件未通过且战斗时间不足3分钟的,极大可能是打死了,视之为完成 + logger.info(f"{enemy_type.name} battle result SUCCESS") + success = True - # 点击返回确认 - while 1: + logger.info(f"{enemy_type.name} DONE") + return success + + def quit_battle(self): + logger.info("Quitting battle") + while True: self.screenshot() - if self.appear_then_click(self.I_EXIT_ENSURE, interval=1.5): + if self.appear(self.I_EXIT_ENSURE): + if self.click(self.I_EXIT_ENSURE, interval=1): + self.wait_until_appear(self.I_ABYSS_NAVIGATION, wait_time=1) + continue + if self.appear(self.I_ABYSS_NAVIGATION): + break + if self.appear(self.I_WIN): + self.click(self.I_WIN, interval=1) + continue + if self.appear(self.I_REWARD): + self.click(self.I_REWARD, interval=1) continue - if self.appear_then_click(self.I_WIN, interval=1.5): + if self.appear(self.I_EXIT): + if self.click(self.I_EXIT, interval=2): + self.wait_until_appear(self.I_EXIT_ENSURE, wait_time=1) continue + return + + def switch_preset_team_with_str(self, v: str): + tmp = v.split(',') + if not tmp or len(tmp) != 2: + logger.error(f"Due to a configuration error (value: {v}), an error occurred while switch preset team.") + return + self.switch_preset_team(True, int(tmp[0]), int(tmp[1])) + + def switch_soul_in_as(self): + if self.switch_soul_done: + return + if not self.config.model.abyss_shadows.process_manage.enable_switch_soul_in_as: + self.switch_soul_done = True + return + + logger.info("start switch soul...") + + def switch_soul(_v: str): + l = _v.split(',') + if len(l) != 2: + logger.error(f"Due to a configuration error (value: {_v}), an error occurred while switch soul.") + raise RequestHumanTakeover + self.run_switch_soul((int(l[0]), int(l[1]))) + + self.ui_click_until_disappear(self.I_ABYSS_SHIKI, interval=2) + soul_set: set[str] = set() + soul_set.add(self.config.model.abyss_shadows.process_manage.preset_boss) + soul_set.add(self.config.model.abyss_shadows.process_manage.preset_general) + soul_set.add(self.config.model.abyss_shadows.process_manage.preset_elite) + + for v in soul_set: + switch_soul(v) + + self.switch_soul_done = True + # 退出式神录 + from tasks.GameUi.assets import GameUiAssets as gua + self.ui_click_until_disappear(gua.I_BACK_Y, interval=2) + + def check_available(self, item_code: Code): + # 判断该怪物是否可用 + # TODO 设想使用平均亮度分辨 是否可用 + self.change_area(item_code.get_areatype()) + + while True: if self.appear(self.I_ABYSS_NAVIGATION): + self.click(self.I_ABYSS_NAVIGATION, interval=2) + continue + if self.appear(self.I_ABYSS_MAP): break - logger.info(f"Click {self.I_EXIT_ENSURE.name}") return True + def detect_area_status(self): + # 在切换区域界面检查各个区域是否可用 + # + available_areas = [] + unavailable_areas = [] + self.screenshot() + for area in AreaType: + if self.is_area_done(area): + unavailable_areas.append(area) + # self.unavailable_list += CodeList(IndexMap[area.name].value) + logger.info(f"{area.name} unavailable") + continue + available_areas.append(area) + logger.info(f"{area.name} available") + return available_areas, unavailable_areas + + def is_area_done(self, area_type: AreaType): + # 不再切换区域界面直接返回 + if not self.appear(self.I_ABYSS_DRAGON) and not self.appear(self.I_ABYSS_DRAGON_OVER): + return False + # + res_img = self.device.image + + match area_type: + case AreaType.DRAGON: + ocr_res = self.O_DRAGON_DONE.ocr(res_img) + return ocr_res.find('封印') != -1 + case AreaType.FOX: + ocr_res = self.O_FOX_DONE.ocr(res_img) + return ocr_res.find('封印') != -1 + case AreaType.LEOPARD: + ocr_res = self.O_LEOPARD_DONE.ocr(res_img) + return ocr_res.find('封印') != -1 + case AreaType.PEACOCK: + ocr_res = self.O_PEACOCK_DONE.ocr(res_img) + return ocr_res.find('封印') != -1 + + return False + + def move_a_little(self): + radius = 150 + # 寮里面摇杆的中心点 + p1 = (197, 568) + import random + dx, dy = random.randint(-radius, radius), random.randint(-radius, radius) + self.device.swipe_adb(p1, (p1[0] + dx, p1[1] + dy), duration=0.5) + logger.info(f"Swipe {p1} to {(p1[0] + dx, p1[1] + dy)}") if __name__ == "__main__": + import cv2, numpy as np from module.config.config import Config from module.device.device import Device - config = Config('zhu') + config = Config('oas') device = Device(config) + + # image = cv2.imread('E:/f.png') + # image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + # + # hsv_image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) + # + # lower_green = np.array([9, 128, 180]) + # upper_green = np.array([30, 210, 255]) + # mask = cv2.inRange(hsv_image, lower_green, upper_green) + # res_img = cv2.bitwise_and(image, image, mask=mask) + # res_img = cv2.cvtColor(res_img, cv2.COLOR_RGB2BGR) + # cv2.imshow('res', res_img) + # cv2.waitKey() + t = ScriptTask(config, device) - t.run() + radius = 150 + p1 = (197, 568) + import random + + while True: + dx, dy = random.randint(-radius, radius), random.randint(-radius, radius) + t.device.swipe_adb(p1, (p1[0] + dx, p1[1] + dy), duration=0.5) + logger.info(f"Swipe {p1} to {(p1[0] + dx, p1[1] + dy)}") + sleep(5) + + # area_type = AreaType.DRAGON + # t.unavailable_list += CodeList(IndexMap[area_type.name].value) + # print(f"{t.unavailable_list=}") + # t.screenshot() + + # cv2.imshow("origin", t.device.image) + # cv2.waitKey() + + # res = t.O_TEST_PRE.ocr(image) + # print(res) + # damage = t.O_DAMAGE.ocr(res_img) + # print(damage) + + # t.done_list = CodeList('A-4') + # t.unavailable_list = CodeList('D-3') + # t.flash_list() + + # code = Code('D-1') + # a = code.get_enemy_type() + # b = code.get_enemy_click() + # c = code.get_areatype() + # print(a, b, c) + # + # t.is_area_done(AreaType.DRAGON) + # t.screenshot() + # t.start_abyss_shadows() + # hsv_image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) + # + # lower_green = np.array([9, 128, 180]) + # upper_green = np.array([30, 210, 255]) + # mask = cv2.inRange(hsv_image, lower_green, upper_green) + # res_img = cv2.bitwise_and(image, image, mask=mask) + # res_img = cv2.cvtColor(res_img, cv2.COLOR_RGB2BGR) diff --git a/tasks/base_task.py b/tasks/base_task.py index 753eeaf7a..10a5d6d5b 100644 --- a/tasks/base_task.py +++ b/tasks/base_task.py @@ -125,6 +125,8 @@ def screenshot(self): 截图 引入中间函数的目的是 为了解决如协作的这类突发的事件 :return: """ + # nemu_ipc 返回为RGB + # 其他方式未知 self.device.screenshot() # 判断勾协 self._burst() @@ -628,4 +630,4 @@ def ui_click_until_smt_disappear(self, click, stop, interval: float = 1): continue if isinstance(click, RuleOcr): self.click(click) - continue \ No newline at end of file + continue