From 343d361967ab665ae6039a3542308dc26dc87eca Mon Sep 17 00:00:00 2001 From: ChengRanORZ Date: Fri, 10 Jan 2025 23:53:48 +0800 Subject: [PATCH 01/21] refactor(AbyssShadows):init --- tasks/AbyssShadows/config.py | 171 ++++++++++++++++++++++++++- tasks/AbyssShadows/script_task.py | 187 +++++++++++------------------- 2 files changed, 235 insertions(+), 123 deletions(-) diff --git a/tasks/AbyssShadows/config.py b/tasks/AbyssShadows/config.py index e4c32943b..6a9c8a60d 100644 --- a/tasks/AbyssShadows/config.py +++ b/tasks/AbyssShadows/config.py @@ -3,21 +3,188 @@ # @author jackyhwei # @note draft version without full test # github https://github.com/roarhill/oas +from datetime import date, datetime +from enum import Enum +from pathlib import Path from pydantic import BaseModel, Field + +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 +from cached_property import cached_property + + +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 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() + + def __str__(self): + return self.name + + __repr__ = __str__ + + +class EnemyType(Enum): + """ 敌人类型 """ + BOSS = 1 # 首领 + GENERAL = 2 # 副将 + ELITE = 3 # 精英 + 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)) + # 尝试主动开启狭间-区别于游戏中的自动开启狭间功能 + try_start_abyss_shadows: bool = Field(default=False, description='try_start_abyss_shadows_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='', description='attack_order_help') + # 标记主怪,与攻击顺序类似,可以根据类单独设置 + # 例如 A;2;3;C-4;D-6 + mark_main: str = Field(default='', description='mark_boss_help') + # 首领预设 + preset_boss: str = Field(default='', description='preset_boss_help') + # 副将预设 + preset_general: str = Field(default='', description='preset_general_help') + # 精英预设 + preset_elite: str = Field(default='', description='preset_elite_help') + # 小蛇预设 + preset_snake: str = Field(default='', description='preset_snake_help') + # 首领策略 等待打完/时间到了退出/伤害足够退出/秒退 + strategy_boss: str = Field(default='', description='strategy_boss_help') + # 副将策略 + strategy_general: str = Field(default='', description='strategy_general_help') + # 精英策略 + strategy_elite: str = Field(default='', description='strategy_elite_help') + + def parse_order_item(self, str): + area, num = str.split('-') + result = [str] + match area: + case 'A': + result.append(AbyssShadowsAssets.C_ABYSS_DRAGON) + case 'B': + result.append(AbyssShadowsAssets.C_ABYSS_PEACOCK) + case 'C': + result.append(AbyssShadowsAssets.C_ABYSS_FOX) + case 'D': + result.append(AbyssShadowsAssets.C_ABYSS_LEOPARD) + case _: + result.append(AbyssShadowsAssets.C_ABYSS_DRAGON) + match num: + case '1': + result.append(AbyssShadowsAssets.C_BOSS_CLICK_AREA) + case '2': + result.append(AbyssShadowsAssets.C_GENERAL_1_CLICK_AREA) + case '3': + result.append(AbyssShadowsAssets.C_GENERAL_2_CLICK_AREA) + case '4': + result.append(AbyssShadowsAssets.C_ELITE_1_CLICK_AREA) + case '5': + result.append(AbyssShadowsAssets.C_ELITE_2_CLICK_AREA) + case '6': + result.append(AbyssShadowsAssets.C_ELITE_3_CLICK_AREA) + case _: + result.append(AbyssShadowsAssets.C_ELITE_1_CLICK_AREA) + return tuple(result) + + def parse_order(self, value: str) -> list: + if value == '': + return [] + tmp = value.split(';') + for item in tmp: + result = self.parse_order_item(item) + yield result + + def is_need_mark_main(self, code): + def expand_mark_main(mark_main_str: str): + items = mark_main_str.split(';') + expanded = [] + for item in items: + if item == 'A' or item == 'B' or item == 'C' or item == 'D': + expanded.append(item + "-1") + expanded.append(item + "-2") + expanded.append(item + "-3") + expanded.append(item + "-4") + expanded.append(item + "-5") + expanded.append(item + "-6") + continue + if item == '1' or item == '2' or item == '3' or item == '4' or item == '5' or item == '6': + expanded.append('A-' + item) + expanded.append('B-' + item) + expanded.append('C-' + item) + expanded.append('D-' + item) + continue + expanded.append(item) + return expanded + + return code in expand_mark_main(self.mark_main) + + +class SavedParams(ConfigBase): + # 已完成 + done: str = Field(default='', description='done_help') + # 失败 + fail: str = Field(default='', description='fail_help') + # 已知的已经打完的 + unavailable: str = Field(default='', description='unavailable_help') + # 当前时间,用于判断存储参数有效性 + today: str = Field(default='2023-01-01', description='today_help') + + # def save(self): + # self.today = datetime.today().strftime('yyyy-mm-dd') + # self.config.save() + class AbyssShadows(ConfigBase): scheduler: Scheduler = Field(default_factory=Scheduler) abyss_shadows_time: AbyssShadowsTime = Field(default_factory=AbyssShadowsTime) general_battle_config: GeneralBattleConfig = Field(default_factory=GeneralBattleConfig) - switch_soul_config: SwitchSoulConfig = Field(default_factory=SwitchSoulConfig) \ No newline at end of file + switch_soul_config: SwitchSoulConfig = Field(default_factory=SwitchSoulConfig) diff --git a/tasks/AbyssShadows/script_task.py b/tasks/AbyssShadows/script_task.py index e62de0270..57fb074d6 100644 --- a/tasks/AbyssShadows/script_task.py +++ b/tasks/AbyssShadows/script_task.py @@ -4,89 +4,24 @@ # @note draft version without full test # github https://github.com/roarhill/oas -from datetime import datetime, timedelta -import random -import numpy as np -from enum import Enum -from cached_property import cached_property +from datetime import datetime 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 module.logger import logger from module.exception import TaskEnd -from module.atom.image_grid import ImageGrid -from module.base.utils import point2str -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 tasks.AbyssShadows.assets import AbyssShadowsAssets - -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() - - def __str__(self): - return self.name - - __repr__ = __str__ +from tasks.AbyssShadows.config import AbyssShadows, EnemyType, AreaType, CilckArea +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_shikigami_records, page_guild class ScriptTask(GeneralBattle, GameUi, SwitchSoul, AbyssShadowsAssets): - - boss_fight_count = 0 # 首领战斗次数 general_fight_count = 0 # 副将战斗次数 elite_fight_count = 0 # 精英战斗次数 - + def run(self): """ 狭间暗域主函数 @@ -106,7 +41,8 @@ def run(self): 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.custom_next_run(task='AbyssShadows', custom_time=cfg.abyss_shadows_time.custom_run_time_friday, + time_delta=4 - today) raise TaskEnd success = True # 进入狭间 @@ -117,7 +53,7 @@ def run(self): 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') # 集结中图片 @@ -127,13 +63,14 @@ def run(self): # 准备攻打精英、副将、首领 while 1: # 点击战报按钮 - find_list = [EmemyType.BOSS, EmemyType.GENERAL, EmemyType.ELITE] + find_list = [EnemyType.BOSS, EnemyType.GENERAL, EnemyType.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.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 @@ -147,13 +84,12 @@ def run(self): elif current_area == AreaType.PEACOCK: self.change_area(AreaType.FOX) continue - elif current_area == AreaType.FOX: + 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 - # 保持好习惯,一个任务结束了就返回到庭院,方便下一任务的开始 self.goto_main() @@ -163,15 +99,18 @@ def run(self): 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) + 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) + 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) + 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 @@ -207,14 +146,14 @@ def change_area(self, area_name: AreaType) -> bool: 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): + continue + # 点击战报按钮 + if self.appear_then_click(self.I_CHANGE_AREA, interval=4): logger.info(f"Click {self.I_CHANGE_AREA.name}") continue - + return True - + def goto_main(self): ''' 保持好习惯,一个任务结束了就返回庭院,方便下一任务的开始或者是出错重启 ''' @@ -222,8 +161,6 @@ def goto_main(self): logger.info("Exiting abyss_shadows") self.ui_goto(page_main) - - def goto_abyss_shadows(self) -> bool: ''' 进入狭间 :return bool @@ -231,23 +168,23 @@ def goto_abyss_shadows(self) -> 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 @@ -256,13 +193,17 @@ def select_boss(self, area_name: AreaType) -> bool: while 1: self.screenshot() # 区域图片与入口图片不一致,使用点击进去 - + if self.appear(self.I_ABYSS_DRAGON): 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") @@ -274,7 +215,7 @@ def select_boss(self, area_name: AreaType) -> bool: break return True - def find_enemy(self, enemy_type: EmemyType) -> bool: + def find_enemy(self, enemy_type: EnemyType) -> bool: ''' 寻找敌人,并开始寻路进入战斗 :return 是否找到敌人,若目标已死亡则返回False,否则返回True True 找到敌人,并已经战斗完成 @@ -285,15 +226,18 @@ def find_enemy(self, enemy_type: EmemyType) -> bool: # 点击战报按钮 if self.appear(self.I_ABYSS_MAP): break - if self.appear_then_click(self.I_ABYSS_NAVIGATION,interval=1): + 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 + case EnemyType.BOSS: + success = self.run_boss_fight() + case EnemyType.GENERAL: + success = self.run_general_fight() + case EnemyType.ELITE: + success = self.run_elite_fight() + + return success def run_boss_fight(self) -> bool: ''' 首领战斗 @@ -304,7 +248,7 @@ def run_boss_fight(self) -> bool: logger.info(f"boss fight count {self.boss_fight_count} times, skip") return True success = True - logger.info(f"Run boss fight") + 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() @@ -319,7 +263,7 @@ def run_general_fight(self) -> bool: :return ''' general_list = [CilckArea.GENERAL_1, CilckArea.GENERAL_2] - logger.info(f"Run general fight") + logger.info(f"Run general fight") for general in general_list: # 副将战斗次数达到4个时,退出循环 if self.general_fight_count >= 4: @@ -332,13 +276,12 @@ def run_general_fight(self) -> bool: 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") + logger.info(f"Run elite fight") for elite in elite_list: # 精英战斗次数达到6个时,退出循环 if self.elite_fight_count >= 6: @@ -366,7 +309,7 @@ def click_emeny_area(self, click_area: CilckArea) -> bool: continue if self.appear(self.I_ABYSS_MAP): logger.info("Find abyss map, exit") - + click_times = 0 # 点击攻打区域 while 1: @@ -378,16 +321,16 @@ def click_emeny_area(self, click_area: CilckArea) -> bool: # 出现前往按钮就退出 if self.appear(self.I_ABYSS_GOTO_ENEMY): 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): + 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): @@ -397,25 +340,25 @@ def click_emeny_area(self, click_area: CilckArea) -> bool: continue else: break - + # 如果遇到点击前往按钮后不动的 bug,则再次尝试进入 if self.wait_until_appear(self.I_ABYSS_FIRE, wait_time=20): break logger.warning("Failed to enter fire") - + # 点击战斗按钮 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_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}") continue if self.appear(self.I_PREPARE_HIGHLIGHT): break - + return suceess def run_general_battle_back(self) -> bool: @@ -455,6 +398,8 @@ def run_general_battle_back(self) -> bool: return True + def start_abyss_shadows(self): + self.ui_goto(page_guild) if __name__ == "__main__": From fd15b280c2c655d60d782d82489857cb00208e7c Mon Sep 17 00:00:00 2001 From: ChengRanORZ Date: Wed, 15 Jan 2025 19:39:15 +0800 Subject: [PATCH 02/21] tmp --- tasks/AbyssShadows/assets.py | 14 +++ tasks/AbyssShadows/config.py | 104 ++++++++++++++------- tasks/AbyssShadows/res/click.json | 6 ++ tasks/AbyssShadows/res/image.json | 54 +++++++++++ tasks/AbyssShadows/script_task.py | 147 ++++++++++++++++++++++++++++-- 5 files changed, 284 insertions(+), 41 deletions(-) diff --git a/tasks/AbyssShadows/assets.py b/tasks/AbyssShadows/assets.py index 64a1b4c53..b7eab63e7 100644 --- a/tasks/AbyssShadows/assets.py +++ b/tasks/AbyssShadows/assets.py @@ -41,6 +41,8 @@ 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") # Image Rule Assets @@ -90,6 +92,18 @@ 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") + # 选择难度按钮 + I_SELECT_DIFFICULTY = RuleImage(roi_front=(710,650,50,50), roi_back=(710,650,50,50), 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") # List Rule Assets diff --git a/tasks/AbyssShadows/config.py b/tasks/AbyssShadows/config.py index 6a9c8a60d..c7665f0e4 100644 --- a/tasks/AbyssShadows/config.py +++ b/tasks/AbyssShadows/config.py @@ -7,8 +7,10 @@ from enum import Enum from pathlib import Path +from exceptiongroup import catch from pydantic import BaseModel, Field +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 @@ -61,11 +63,17 @@ def __str__(self): __repr__ = __str__ -class EnemyType(Enum): +class EnemyType(str, Enum): """ 敌人类型 """ - BOSS = 1 # 首领 - GENERAL = 2 # 副将 - ELITE = 3 # 精英 + BOSS = "BOSS" # 首领 + GENERAL = "GENERAL" # 副将 + ELITE = "ELITE" # 精英 + + +class AbyssShadowsDifficulty(str, Enum): + EASY = "EASY" + NORMAL = "NORMAL" + HARD = "HARD" class AbyssShadowsTime(ConfigBase): @@ -75,6 +83,8 @@ class AbyssShadowsTime(ConfigBase): custom_run_time_sunday: Time = Field(default=Time(hour=19, minute=0, second=0)) # 尝试主动开启狭间-区别于游戏中的自动开启狭间功能 try_start_abyss_shadows: bool = Field(default=False, description='try_start_abyss_shadows_help') + # 难度 + difficulty: AbyssShadowsDifficulty = Field(default=AbyssShadowsDifficulty.EASY, description='difficulty_help') class ProcessManage(ConfigBase): @@ -86,8 +96,8 @@ class ProcessManage(ConfigBase): # 小蛇使用E,-后面表示打几只,例如E-2表示打两只小蛇 # 例如 A-1;B-2;C-3... attack_order: str = Field(default='', description='attack_order_help') - # 标记主怪,与攻击顺序类似,可以根据类单独设置 - # 例如 A;2;3;C-4;D-6 + # 标记主怪 + # EnemyType, 多个用;分隔 mark_main: str = Field(default='', description='mark_boss_help') # 首领预设 preset_boss: str = Field(default='', description='preset_boss_help') @@ -109,15 +119,15 @@ def parse_order_item(self, str): result = [str] match area: case 'A': - result.append(AbyssShadowsAssets.C_ABYSS_DRAGON) + result.append(AreaType.DRAGON) case 'B': - result.append(AbyssShadowsAssets.C_ABYSS_PEACOCK) + result.append(AreaType.PEACOCK) case 'C': - result.append(AbyssShadowsAssets.C_ABYSS_FOX) + result.append(AreaType.FOX) case 'D': - result.append(AbyssShadowsAssets.C_ABYSS_LEOPARD) + result.append(AreaType.LEOPARD) case _: - result.append(AbyssShadowsAssets.C_ABYSS_DRAGON) + result.append(AreaType.DRAGON) match num: case '1': result.append(AbyssShadowsAssets.C_BOSS_CLICK_AREA) @@ -135,7 +145,9 @@ def parse_order_item(self, str): result.append(AbyssShadowsAssets.C_ELITE_1_CLICK_AREA) return tuple(result) - def parse_order(self, value: str) -> list: + def parse_order(self, value: str = None) -> list: + if value is None or value == '': + value = self.attack_order if value == '': return [] tmp = value.split(';') @@ -143,29 +155,49 @@ def parse_order(self, value: str) -> list: result = self.parse_order_item(item) yield result - def is_need_mark_main(self, code): - def expand_mark_main(mark_main_str: str): - items = mark_main_str.split(';') - expanded = [] - for item in items: - if item == 'A' or item == 'B' or item == 'C' or item == 'D': - expanded.append(item + "-1") - expanded.append(item + "-2") - expanded.append(item + "-3") - expanded.append(item + "-4") - expanded.append(item + "-5") - expanded.append(item + "-6") - continue - if item == '1' or item == '2' or item == '3' or item == '4' or item == '5' or item == '6': - expanded.append('A-' + item) - expanded.append('B-' + item) - expanded.append('C-' + item) - expanded.append('D-' + item) - continue - expanded.append(item) - return expanded - - return code in expand_mark_main(self.mark_main) + def is_need_mark_main(self, enemy_type): + return str(enemy_type) in self.mark_main + + def parse_strategy(self, strategy: str): + class Condition: + _is_time_out: bool = False + _time = -1 + _timer: Timer = None + _is_damage_enough: bool = False + _damage: int = -1 + + _result: bool = False + + def __init__(self, value: str): + # 没有策略 + if value == "TRUE": + self._result = True + elif value == "FALSE": + self._result = 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: + _damage = int(value) + except ValueError: + self._damage = 999999999 + + def is_valid(self, damage: int = None): + if self._time >= 0: + return self._timer.reached() + if self._damage >= 0 and damage is not None: + return self._damage < damage + return self._result + + if strategy is None or strategy == '': + return False + return Condition(strategy) class SavedParams(ConfigBase): @@ -186,5 +218,7 @@ class SavedParams(ConfigBase): class AbyssShadows(ConfigBase): scheduler: Scheduler = Field(default_factory=Scheduler) abyss_shadows_time: AbyssShadowsTime = Field(default_factory=AbyssShadowsTime) + 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..0e892a57d 100644 --- a/tasks/AbyssShadows/res/click.json +++ b/tasks/AbyssShadows/res/click.json @@ -88,5 +88,11 @@ "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": "刚进入神社时,可点击进入狭间暗域的区域,用于开启狭间暗域" } ] \ No newline at end of file diff --git a/tasks/AbyssShadows/res/image.json b/tasks/AbyssShadows/res/image.json index 226b4dc5e..0def44653 100644 --- a/tasks/AbyssShadows/res/image.json +++ b/tasks/AbyssShadows/res/image.json @@ -205,5 +205,59 @@ "method": "Template matching", "threshold": 0.8, "description": "description" + }, + { + "itemName": "select_difficulty", + "imageName": "res_select_difficulty.png", + "roiFront": "710,650,50,50", + "roiBack": "710,650,50,50", + "method": "Template matching", + "threshold": 0.8, + "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": "开启确认按钮" } ] \ No newline at end of file diff --git a/tasks/AbyssShadows/script_task.py b/tasks/AbyssShadows/script_task.py index 57fb074d6..70d18f6a2 100644 --- a/tasks/AbyssShadows/script_task.py +++ b/tasks/AbyssShadows/script_task.py @@ -10,7 +10,7 @@ from module.exception import TaskEnd from module.logger import logger from tasks.AbyssShadows.assets import AbyssShadowsAssets -from tasks.AbyssShadows.config import AbyssShadows, EnemyType, AreaType, CilckArea +from tasks.AbyssShadows.config import AbyssShadows, EnemyType, AreaType, CilckArea, AbyssShadowsDifficulty from tasks.Component.GeneralBattle.general_battle import GeneralBattle from tasks.Component.SwitchSoul.switch_soul import SwitchSoul from tasks.GameUi.game_ui import GameUi @@ -22,6 +22,11 @@ class ScriptTask(GeneralBattle, GameUi, SwitchSoul, AbyssShadowsAssets): general_fight_count = 0 # 副将战斗次数 elite_fight_count = 0 # 精英战斗次数 + # + cur_area = None + # + cur_preset = None + def run(self): """ 狭间暗域主函数 @@ -295,7 +300,7 @@ def run_elite_fight(self) -> bool: return True def click_emeny_area(self, click_area: CilckArea) -> bool: - suceess = True + success = True ''' 点击敌人区域 :return @@ -317,7 +322,8 @@ def click_emeny_area(self, click_area: CilckArea) -> bool: # 如果点3次还没进去就表示目标已死亡,跳过 if click_times >= 3: logger.warning(f"Failed to click {click_area}") - return + success = False + return success # 出现前往按钮就退出 if self.appear(self.I_ABYSS_GOTO_ENEMY): break @@ -332,6 +338,7 @@ def click_emeny_area(self, click_area: CilckArea) -> bool: self.screenshot() if self.appear_then_click(self.I_ABYSS_GOTO_ENEMY, interval=1): logger.info(f"Click {self.I_ABYSS_GOTO_ENEMY.name}") + self.wait_until_appear(self.I_ENSURE_BUTTON, wait_time=1) # 点击敌人后,如果是不同区域会确认框,点击确认 if self.appear_then_click(self.I_ENSURE_BUTTON, interval=1): logger.info(f"Click {self.I_ENSURE_BUTTON.name}") @@ -359,7 +366,7 @@ def click_emeny_area(self, click_area: CilckArea) -> bool: if self.appear(self.I_PREPARE_HIGHLIGHT): break - return suceess + return success def run_general_battle_back(self) -> bool: """ @@ -399,14 +406,142 @@ def run_general_battle_back(self) -> bool: return True def start_abyss_shadows(self): + # 尝试开启狭间暗域 self.ui_goto(page_guild) + from tasks.Dokan.assets import DokanAssets as da + self.ui_click_until_disappear(da.I_RYOU_SHENSHE) + self.ui_click_until_smt_disappear(self.C_ABYSS_SHENSHE_ENTER_ABYSS, stop=da.I_RYOU_DOKAN, interval=1) + # 选择难度 + 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): + # + ps_list = self.config.model.abyss_shadows.process_manage.parse_order() + # + done_list = self.config.model.abyss_shadows.saved_params.done.split(";") + # + unavailable_list = self.config.model.abyss_shadows.saved_params.unavailable.split(";") + + def get_next(): + for ps in ps_list: + if ps not in done_list and ps not in unavailable_list: + return ps + return None + + def process_item(item): + + return + + while True: + next = get_next() + if next is None: + break + process_item(next) + + def open_navigation(self): + self.ui_click(self.I_ABYSS_NAVIGATION, self.I_ABYSS_MAP, interval=1) + + def goto(self, item): + self.open_navigation() + code, area, enemy = item + need_change_area = area == self.cur_area + if need_change_area: + self.change_area(area) + # + + self.open_navigation() + if not self.click_emeny_area(enemy): + # 该单位不可用 + self.config.model.abyss_shadows.saved_params.unavailable += f"{code};" + return False + # 战斗 + self.run_battle(enemy) + # 战后统计 + + def run_battle(self, enemy): + enemy_type = None + match enemy: + case AbyssShadowsAssets.C_BOSS_CLICK_AREA: + enemy_type = EnemyType.BOSS + case AbyssShadowsAssets.C_GENERAL_1_CLICK_AREA | AbyssShadowsAssets.C_GENERAL_2_CLICK_AREA: + enemy_type = EnemyType.GENERAL + case AbyssShadowsAssets.C_ELITE_1_CLICK_AREA | AbyssShadowsAssets.C_ELITE_2_CLICK_AREA | AbyssShadowsAssets.C_ELITE_3_CLICK_AREA: + enemy_type = EnemyType.ELITE + case _: + enemy_type = EnemyType.BOSS + + # 判断是否需要更换预设 + def get_preset(enemy_type): + match enemy_type: + case EnemyType.BOSS: + return self.config.model.abyss_shadows.process_manage.preset_boss + case EnemyType.ELITE: + return self.config.model.abyss_shadows.process_manage.preset_elite + case EnemyType.GENERAL: + return self.config.model.abyss_shadows.process_manage.preset_general + + preset = get_preset(enemy_type) + if preset != self.cur_preset: + self.switch_preset_team(preset) + + # 点击准备 + self.ui_click_until_disappear(self.I_PREPARE_HIGHLIGHT, interval=0.3) + + # 标记主怪 + is_need_mark_main=self.config.model.abyss_shadows.process_manage.is_need_mark_main() + if is_need_mark_main: + self.ui_click(self.I_MARK_MAIN, interval=0.3) + + # 生成退出条件 + def generate_quit_condition(enemy_type): + strategy = None + match enemy_type: + case EnemyType.BOSS: + strategy = self.config.model.abyss_shadows.process_manage.strategy_boss + case EnemyType.ELITE: + strategy = self.config.model.abyss_shadows.process_manage.strategy_elite + case EnemyType.GENERAL: + strategy = self.config.model.abyss_shadows.process_manage.strategy_general + return self.config.model.abyss_shadows.process_manage.parse_strategy(strategy) + + condition = generate_quit_condition(enemy_type) + cur_damage = 0 + self.device.screenshot_interval_set(1) + while True: + self.screenshot() + if condition.is_valid(cur_damage): + self.quit_battle() + break + cur_damage = self.O_DAMAGE.ocr_digit(self.device.image) + self.device.screenshot_interval_set() + + def quit_battle(self): + # TODO quit + pass + + def update_state(self, item, info): + pass if __name__ == "__main__": from module.config.config import Config from module.device.device import Device - config = Config('zhu') + config = Config('却把烟花嗅') device = Device(config) t = ScriptTask(config, device) - t.run() + t.screenshot() + t.start_abyss_shadows() From 849cd65140ed2a6757569912de7cfbf9dc65c6d7 Mon Sep 17 00:00:00 2001 From: ChengRanORZ Date: Mon, 2 Jun 2025 00:20:12 +0800 Subject: [PATCH 03/21] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev_tools/assets_extract.py | 4 +- tasks/AbyssShadows/assets.py | 15 + tasks/AbyssShadows/config.py | 279 +++++++----- tasks/AbyssShadows/res/click.json | 6 + tasks/AbyssShadows/res/image.json | 36 ++ tasks/AbyssShadows/res/ocr.json | 11 + tasks/AbyssShadows/res/res_mark_main.png | Bin 0 -> 1869 bytes tasks/AbyssShadows/script_task.py | 524 +++++++++++------------ 8 files changed, 487 insertions(+), 388 deletions(-) create mode 100644 tasks/AbyssShadows/res/ocr.json create mode 100644 tasks/AbyssShadows/res/res_mark_main.png diff --git a/dev_tools/assets_extract.py b/dev_tools/assets_extract.py index 07ca2deaf..85d21c13a 100644 --- a/dev_tools/assets_extract.py +++ b/dev_tools/assets_extract.py @@ -408,7 +408,9 @@ 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): diff --git a/tasks/AbyssShadows/assets.py b/tasks/AbyssShadows/assets.py index b7eab63e7..b47495a5a 100644 --- a/tasks/AbyssShadows/assets.py +++ b/tasks/AbyssShadows/assets.py @@ -43,6 +43,8 @@ class AbyssShadowsAssets: 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") # Image Rule Assets @@ -52,6 +54,8 @@ 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_OVER = RuleImage(roi_front=(253,185,33,100), roi_back=(253,185,33,100), 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") # 狭间_白藏主入口 @@ -70,6 +74,8 @@ 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") # 前往 @@ -104,6 +110,10 @@ class AbyssShadowsAssets: 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 @@ -112,6 +122,11 @@ class AbyssShadowsAssets: array=["道馆", "首领", "狭间"]) + # Ocr Rule Assets + # 伤害 + O_DAMAGE = RuleOcr(roi=(50,110,300,50), area=(50,110,300,50), mode="Digit", method="Default", keyword="", name="damage") + + # 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 c7665f0e4..2acb661b9 100644 --- a/tasks/AbyssShadows/config.py +++ b/tasks/AbyssShadows/config.py @@ -3,11 +3,10 @@ # @author jackyhwei # @note draft version without full test # github https://github.com/roarhill/oas -from datetime import date, datetime from enum import Enum -from pathlib import Path +from module.atom.click import RuleClick +from module.atom.image import RuleImage -from exceptiongroup import catch from pydantic import BaseModel, Field from module.base.timer import Timer @@ -16,31 +15,25 @@ from tasks.Component.SwitchSoul.switch_soul_config import SwitchSoulConfig from tasks.Component.config_base import ConfigBase, Time, DateTime from tasks.Component.config_scheduler import Scheduler -from cached_property import cached_property -class AreaType: +class AreaType(Enum): """ 暗域类型 """ 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: - """ + # @classmethod + # def __str__(cls, value): + # # 遍历类属性,找到匹配值对应的属性名 + # for name, attr_value in vars(cls).items(): + # if attr_value == value and not name.startswith('__'): + # return name + # return str(value) # 默认行为 - :return: - """ - return Path(self.file).stem.upper() - def __str__(self): - return self.name - - __repr__ = __str__ - - -class CilckArea: +class ClickArea(Enum): """ 点击区域 """ GENERAL_1 = AbyssShadowsAssets.C_GENERAL_1_CLICK_AREA GENERAL_2 = AbyssShadowsAssets.C_GENERAL_2_CLICK_AREA @@ -49,18 +42,18 @@ class CilckArea: 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() - - def __str__(self): - return self.name - - __repr__ = __str__ + # @cached_property + # def name(self) -> str: + # """ + # + # :return: + # """ + # return Path(self.file).stem.upper() + # + # def __str__(self): + # return self.name + # + # __repr__ = __str__ class EnemyType(str, Enum): @@ -76,6 +69,148 @@ class AbyssShadowsDifficulty(str, Enum): HARD = "HARD" +class Code(str): + def __init__(self, value: str): + self.value = value + + def get_areatype(self): + area, num = self.value.split('-') + match area: + case 'A': + return AreaType.DRAGON + case 'B': + return AreaType.PEACOCK + case 'C': + return AreaType.FOX + case 'D': + return AreaType.LEOPARD + case _: + return AreaType.DRAGON + + def get_enemy_click(self): + area, num = self.value.split('-') + match num: + case '1': + return AbyssShadowsAssets.C_BOSS_CLICK_AREA + case '2': + return AbyssShadowsAssets.C_GENERAL_1_CLICK_AREA + case '3': + return AbyssShadowsAssets.C_GENERAL_2_CLICK_AREA + case '4': + return AbyssShadowsAssets.C_ELITE_1_CLICK_AREA + case '5': + return AbyssShadowsAssets.C_ELITE_2_CLICK_AREA + case '6': + return AbyssShadowsAssets.C_ELITE_3_CLICK_AREA + case _: + return AbyssShadowsAssets.C_ELITE_1_CLICK_AREA + + def get_enemy_type(self): + area, num = self.value.split('-') + match num: + case '1': + return EnemyType.BOSS + case '2': + return EnemyType.GENERAL + case '3': + return EnemyType.GENERAL + case '4': + return EnemyType.ELITE + case '5': + return EnemyType.ELITE + case '6': + return EnemyType.ELITE + case _: + return EnemyType.ELITE + + +class CodeList(list[Code]): + + def __init__(self, v: str): + def expand_str(v: str): + if v.find('-') != -1: + return [v] + if v in ['A', 'B', 'C', 'D']: + return [f'{v}-4', f'{v}-5', f'{v}-6', f'{v}-2', f'{v}-3', f'{v}-1'] + if v in ['1', '2', '3', '4', '5', '6']: + return [f'A-{v}', f'B-{v}', f'C-{v}', f'D-{v}'] + + 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 save_to_obj(self, config_obj: str): + ret = "" + for item in self: + ret += ';' + ret += item.value + ret = ret[1:] + config_obj = ret + + +class Condition: + # _is_time_out: bool = False + _time = -1 + _timer: Timer = None + # _is_damage_enough: bool = False + _damage_max: int = -1 + + _dont_need_check: bool = False + + # 存储结果,用于后期查询 + _condition_result: bool = False + + def __init__(self, value: str): + # 为True时,相当于没有策略(所有情况都通过条件检查) + if value == "TRUE": + self._dont_need_check = True + elif value == "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) + else: + try: + _damage = int(value) + except ValueError: + self._damage_max = 999999999 + + # 检查条件 + def is_valid(self, damage: int = None): + + 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 + return False + + def is_need_damage_value(self): + return self._damage_max >= 0 + + def is_passed(self): + return self._condition_result + + class AbyssShadowsTime(ConfigBase): # 自定义运行时间 custom_run_time_friday: Time = Field(default=Time(hour=19, minute=0, second=0)) @@ -114,87 +249,10 @@ class ProcessManage(ConfigBase): # 精英策略 strategy_elite: str = Field(default='', description='strategy_elite_help') - def parse_order_item(self, str): - area, num = str.split('-') - result = [str] - match area: - case 'A': - result.append(AreaType.DRAGON) - case 'B': - result.append(AreaType.PEACOCK) - case 'C': - result.append(AreaType.FOX) - case 'D': - result.append(AreaType.LEOPARD) - case _: - result.append(AreaType.DRAGON) - match num: - case '1': - result.append(AbyssShadowsAssets.C_BOSS_CLICK_AREA) - case '2': - result.append(AbyssShadowsAssets.C_GENERAL_1_CLICK_AREA) - case '3': - result.append(AbyssShadowsAssets.C_GENERAL_2_CLICK_AREA) - case '4': - result.append(AbyssShadowsAssets.C_ELITE_1_CLICK_AREA) - case '5': - result.append(AbyssShadowsAssets.C_ELITE_2_CLICK_AREA) - case '6': - result.append(AbyssShadowsAssets.C_ELITE_3_CLICK_AREA) - case _: - result.append(AbyssShadowsAssets.C_ELITE_1_CLICK_AREA) - return tuple(result) - - def parse_order(self, value: str = None) -> list: - if value is None or value == '': - value = self.attack_order - if value == '': - return [] - tmp = value.split(';') - for item in tmp: - result = self.parse_order_item(item) - yield result - def is_need_mark_main(self, enemy_type): return str(enemy_type) in self.mark_main def parse_strategy(self, strategy: str): - class Condition: - _is_time_out: bool = False - _time = -1 - _timer: Timer = None - _is_damage_enough: bool = False - _damage: int = -1 - - _result: bool = False - - def __init__(self, value: str): - # 没有策略 - if value == "TRUE": - self._result = True - elif value == "FALSE": - self._result = 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: - _damage = int(value) - except ValueError: - self._damage = 999999999 - - def is_valid(self, damage: int = None): - if self._time >= 0: - return self._timer.reached() - if self._damage >= 0 and damage is not None: - return self._damage < damage - return self._result - if strategy is None or strategy == '': return False return Condition(strategy) @@ -208,12 +266,17 @@ class SavedParams(ConfigBase): # 已知的已经打完的 unavailable: str = Field(default='', description='unavailable_help') # 当前时间,用于判断存储参数有效性 - today: str = Field(default='2023-01-01', description='today_help') + save_time: str = Field(default='2023-01-01', description='today_help') # def save(self): # self.today = datetime.today().strftime('yyyy-mm-dd') # self.config.save() + def push_to(self, item, l): + if len(l) > 0: + l += ';' + l += item + class AbyssShadows(ConfigBase): scheduler: Scheduler = Field(default_factory=Scheduler) diff --git a/tasks/AbyssShadows/res/click.json b/tasks/AbyssShadows/res/click.json index 0e892a57d..734d1ef51 100644 --- a/tasks/AbyssShadows/res/click.json +++ b/tasks/AbyssShadows/res/click.json @@ -94,5 +94,11 @@ "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": "战斗时,左上角退出战斗按钮区域 下半部分" } ] \ No newline at end of file diff --git a/tasks/AbyssShadows/res/image.json b/tasks/AbyssShadows/res/image.json index 0def44653..9fbdd0542 100644 --- a/tasks/AbyssShadows/res/image.json +++ b/tasks/AbyssShadows/res/image.json @@ -26,6 +26,15 @@ "threshold": 0.8, "description": "狭间_神龙入口" }, + { + "itemName": "abyss_dragon_over", + "imageName": "res_abyss_dragon_over.png", + "roiFront": "253,185,33,100", + "roiBack": "253,185,33,100", + "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", @@ -259,5 +277,23 @@ "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..e74ce6dcf --- /dev/null +++ b/tasks/AbyssShadows/res/ocr.json @@ -0,0 +1,11 @@ +[ + { + "itemName": "damage", + "roiFront": "50,110,300,50", + "roiBack": "50,110,300,50", + "mode": "Digit", + "method": "Default", + "keyword": "", + "description": "伤害" + } +] \ No newline at end of file diff --git a/tasks/AbyssShadows/res/res_mark_main.png b/tasks/AbyssShadows/res/res_mark_main.png new file mode 100644 index 0000000000000000000000000000000000000000..21c31ab818debad1254862ffbec6ddef839fa150 GIT binary patch literal 1869 zcmaKsc{tnY7RM7K8Wba@rnU51TZ0f15vj3METO6*f>M=`6pf`asIBfGmMKHD+UnCV z)Gf6KZFQoip_!?@wNxYbA{4b&h@EzR?R}nk?)-EAIPdv<&wJi;p6@?z&bhPB3UX?4 zAP`6a?}8%$*%Sz6s0?tr`)phR5|}}7J`Jkx*O&*ySB%qmA`}W0iaaI(DSgS_-5vzu zRcJX6?K#DIu2e`;cQ5|@1LiNSd+}%NjKm!y60RvM20)OrYga6A?uK+d! zNdYAdAY8M;K+>PDfuMn5e=#6IAp6LWn5eK2EfUJi5>L>Iq0*^j1{HppngDEu6nyn% z$~Qn`RUzvOOQS3>pbD)otPj-N3Y5R_A%I1N(7#VNN0?h+5oj#JOv}s)i!{R`Fd&3w zFv1drw1S&aLNIWYg?TWXViin-ql0NEbEG8-gQ6foMhFZRfdIVy&EH?1z_9=I3T*zr zJ~DT)nShyMJkFlz;?_r6^ceCvdM)vO%Qte&z*guCOd7{~sD)`;Ug))<`%RVi>-XGR z8Y)cj-Sk)z+L#KOLiSqp@Akp3|H_w_qY#a?zO6KXMu49R2%&qz^^%VTs;fP>^ljdq zyOy;T-=9=6moVYgIa|kY9ce@UczR@W@^ZYKADV+Y17iw45!wfRWFk+7)FsO3ps24cIOWaQ*6Ef*w z0m-PTxtaX<4(C}Jm@{)kvY`|d7Z=E{of5y$3$Lyb&n`BQ!@^{TkjR%kJxt_44y;sZ zX2w4VrS|$iL+RXfhwKKu;UuRGe`k!w?;TS*9bQAw!-pq(1V)UutCj&Rmr?peRED?hF;vtKBU(=A z?wGH|FgSJX$A7-3ai9Q`@b?<5{p)UCgBu#-K22(oNTlI__MsdP+nuA|HU!wujhIx^ z&#e#G9n<&pYrFqUQ*b869FxNlU&-k3Y7{n^S=xFy1?M8QM^~Ox^tv~uccs+moAlUL zfxypkFtJrmZgrgdVKSdFw;CiVD@oTLLX%woJ=k<8J}Yyw-210zfj8GAvL+Zbx*uAC z)Qwj-p06GOhc^nBqcIw;R`^7gv9PsdV9_Dvf@VZPrEfEh`mpof&W5pdq*7N*S#2$m zg|?1S8vT{)UHc#`n@@?xuCEdpNLY?EUUp&cK>=?ctm?iuPQvLZ8{gQuYW=h9m|gfJ z>4WBZ%IW-o^sbP!qVfJN&h$1X7%JHieS9D43OQkW7NTabwNauDm2Bz4sOdqJCTIW4 z^2riOpZ^naOpI(2VKd0uxLH2h)3C+nilFBLv0};K zrOUX;x8~iPeRx?h+YZg*SHJb#TpDNcL}aQ8N4Pv5IL50%pJ$@}xk~iaY~x%xl`Eri zDR$@Ko!225S>o~llglSn{oMkR;ksGtc|x_Tm`az%cJH&iWfKaFlyU;Xm*M98R-ShM z-qw#7`>34l3R^erjlsyLxyo068Zg*_!BpS&V<^}3wrWUn)`;p_b0osir-3B6%7{Y1B6Q6#$f2v>^rJx-vz z^m;H>Pw+$bj14wc?peIl_Tr$vg-=({?0c4Owd1ujEGJ4cMACGLdCpH&ylr}xsxs8urTt?=mdO9d#G<6+gq=X y6iTmj%Hn6jd~D!_o?KJoTq2MBq`dxZdMaql%gcFYw`uM3M~-(qi>p5!l=dfO^)bEx literal 0 HcmV?d00001 diff --git a/tasks/AbyssShadows/script_task.py b/tasks/AbyssShadows/script_task.py index 70d18f6a2..9b5f64cea 100644 --- a/tasks/AbyssShadows/script_task.py +++ b/tasks/AbyssShadows/script_task.py @@ -7,17 +7,27 @@ from datetime import datetime from time import sleep +from future.backports.datetime import timedelta +from module.atom.click import RuleClick from module.exception import TaskEnd from module.logger import logger +from sympy.physics.units import hours +from sympy.plotting.intervalmath import interval +from sympy.strategies.core import switch from tasks.AbyssShadows.assets import AbyssShadowsAssets -from tasks.AbyssShadows.config import AbyssShadows, EnemyType, AreaType, CilckArea, AbyssShadowsDifficulty +from tasks.AbyssShadows.config import AbyssShadows, EnemyType, AreaType, ClickArea, Code, AbyssShadowsDifficulty, \ + CodeList 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_shikigami_records, page_guild +# 单个首领/副将/精英 一次无法完成目标(一般是一次没打掉) 的情况下,最大战斗次数 +MAX_BATTLE_COUNT = 2 + class ScriptTask(GeneralBattle, GameUi, SwitchSoul, AbyssShadowsAssets): + # TODO 完善战斗次数限制 boss_fight_count = 0 # 首领战斗次数 general_fight_count = 0 # 副将战斗次数 elite_fight_count = 0 # 精英战斗次数 @@ -26,6 +36,14 @@ class ScriptTask(GeneralBattle, GameUi, SwitchSoul, AbyssShadowsAssets): cur_area = None # cur_preset = None + # process list + ps_list = [] + # 已完成 列表 + done_list = [] + # 已知的 已经被打完的 列表 + unavailable_list = [] + # + switch_soul_done = False def run(self): """ 狭间暗域主函数 @@ -34,14 +52,6 @@ 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") @@ -49,11 +59,25 @@ def run(self): self.custom_next_run(task='AbyssShadows', custom_time=cfg.abyss_shadows_time.custom_run_time_friday, time_delta=4 - today) raise TaskEnd - success = True + server_time = datetime.combine(datetime.now().date(), cfg.scheduler.server_update) + if datetime.now() - server_time > timedelta(hours=2): + # 超时两小时未开始,直接退出 + logger.info("") + self.set_next_run(task='AbyssShadows', finish=False, server=True, success=True) + raise TaskEnd + # 进入狭间 self.goto_abyss_shadows() - # 第一次默认选择神龙暗域 - if not self.select_boss(AreaType.DRAGON): + + # 尝试开启狭间 + if cfg.abyss_shadows_time.try_start_abyss_shadows: + self.start_abyss_shadows() + + # + self.update_list() + _next = self.get_next() + # TODO 增加等待时长 + if not self.select_boss(_next.get_areatype()): logger.warning("Failed to enter abyss shadows") self.goto_main() self.set_next_run(task='AbyssShadows', finish=False, server=True, success=False) @@ -62,64 +86,43 @@ def run(self): # 等待可进攻时间 self.device.stuck_record_add('BATTLE_STATUS_S') # 集结中图片 + self.wait_until_appear(self.I_WAIT_TO_START, wait_time=2) + # 切换御魂 + self.switch_soul_in_as() + # self.wait_until_disappear(self.I_WAIT_TO_START) self.device.stuck_record_clear() - # 准备攻打精英、副将、首领 - while 1: - # 点击战报按钮 - find_list = [EnemyType.BOSS, EnemyType.GENERAL, EnemyType.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: - 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 + self.process() # 保持好习惯,一个任务结束了就返回到庭院,方便下一任务的开始 self.goto_main() # 设置下次运行时间 - if success: - 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) + # TODO 添加周四周五周六周天不同的处理方式 + self.set_next_run(task='AbyssShadows', finish=False, server=True, success=True) raise TaskEnd + def update_list(self): + # BUG 跨天可能有问题 + if self.config.model.abyss_shadows.saved_params.save_time != datetime.now().strftime("%Y-%m-%d"): + logger.warning("there isn`t params saved today") + return + # + 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) + + def flash_list(self): + self.ps_list.save_to_obj(self.config.model.abyss_shadows.process_manage.attack_order) + self.done_list.save_to_obj(self.config.model.abyss_shadows.saved_params.done) + self.unavailable_list.save_to_obj(self.config.model.abyss_shadows.saved_params.unavailable) + self.config.model.abyss_shadows.saved_params.save_time = datetime.now().strftime("%Y-%m-%d") + self.config.save() + logger.info("Flash list done") + def check_current_area(self) -> AreaType: ''' 获取当前区域 :return AreaType @@ -139,7 +142,7 @@ def check_current_area(self) -> AreaType: def change_area(self, area_name: AreaType) -> bool: ''' 切换到下个区域 - :return + :return ''' while 1: self.screenshot() @@ -148,11 +151,11 @@ def change_area(self, area_name: AreaType) -> bool: if current_area == area_name: break # 切换区域界面 - if self.appear(self.I_ABYSS_DRAGON): + if self.appear(self.I_ABYSS_DRAGON_OVER) or 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): logger.info(f"Click {self.I_CHANGE_AREA.name}") continue @@ -192,14 +195,14 @@ def goto_abyss_shadows(self) -> bool: def select_boss(self, area_name: AreaType) -> bool: ''' 选择暗域类型 - :return + :return ''' 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): match area_name: case AreaType.DRAGON: is_click = self.click(self.C_ABYSS_DRAGON, interval=2) @@ -218,112 +221,29 @@ def select_boss(self, area_name: AreaType) -> bool: continue if self.appear(self.I_ABYSS_NAVIGATION): break + self.cur_area = area_name return True - def find_enemy(self, enemy_type: EnemyType) -> 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 EnemyType.BOSS: - success = self.run_boss_fight() - case EnemyType.GENERAL: - success = self.run_general_fight() - case EnemyType.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() - 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() - 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() - logger.info(f'Fight, elite_fight_count {self.elite_fight_count} times') - return True - - def click_emeny_area(self, click_area: CilckArea) -> bool: - success = True - ''' 点击敌人区域 - - :return - ''' + def goto_enemy(self, item_code: Code) -> bool: + # 前往当前区域 的某个 敌人 + click_area = item_code.get_enemy_click() logger.info(f"Click emeny area: {click_area.name}") # 点击战报 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 + + self.open_navigation() click_times = 0 - # 点击攻打区域 + # 点击攻打区域,直到出现"前往"字样 while 1: self.screenshot() # 如果点3次还没进去就表示目标已死亡,跳过 if click_times >= 3: logger.warning(f"Failed to click {click_area}") - success = False - return success + return False # 出现前往按钮就退出 if self.appear(self.I_ABYSS_GOTO_ENEMY): break @@ -333,84 +253,46 @@ def click_emeny_area(self, click_area: CilckArea) -> bool: 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}") - self.wait_until_appear(self.I_ENSURE_BUTTON, wait_time=1) - # 点击敌人后,如果是不同区域会确认框,点击确认 - 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_ABYSS_FIRE): + break + if self.appear(self.I_ENSURE_BUTTON): + self.click(self.I_ENSURE_BUTTON, interval=1) continue - else: + if self.appear(self.I_ABYSS_GOTO_ENEMY): + self.click(self.I_ABYSS_GOTO_ENEMY, interval=1) + continue + if not self.wait_until_appear(self.I_ABYSS_FIRE, wait_time=10): break + return True - # 如果遇到点击前往按钮后不动的 bug,则再次尝试进入 - if self.wait_until_appear(self.I_ABYSS_FIRE, wait_time=20): - break - logger.warning("Failed to enter fire") - + def attack_enemy(self): # 点击战斗按钮 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}") - continue - if self.appear(self.I_PREPARE_HIGHLIGHT): - break - - return success - - def run_general_battle_back(self) -> bool: - """ - 重写父类方法,因为狭间暗域的准备和战斗流程不一样 - 进入挑战然后直接返回 - :param config: - :return: - """ - while 1: - self.screenshot() - if self.appear_then_click(self.I_PREPARE_HIGHLIGHT, interval=1.5): - continue - if not self.appear(self.I_PRESET): - break - logger.info(f"Click {self.I_PREPARE_HIGHLIGHT.name}") - - # 点击返回 - while 1: - self.screenshot() - if self.appear_then_click(self.I_EXIT, interval=1.5): + # + if self.appear(self.I_ABYSS_ENEMY_FIRE): + self.click(self.I_ABYSS_ENEMY_FIRE, interval=0.4) continue - if self.appear(self.I_EXIT_ENSURE): - break - logger.info(f"Click {self.I_EXIT.name}") - - # 点击返回确认 - while 1: - self.screenshot() - if self.appear_then_click(self.I_EXIT_ENSURE, interval=1.5): + # + if self.appear_then_click(self.I_ABYSS_FIRE, interval=1): continue - if self.appear_then_click(self.I_WIN, interval=1.5): + # 挑战敌人后,如果是奖励次数上限,会出现确认框 + if self.appear_then_click(self.I_ENSURE_BUTTON, interval=1): continue - if self.appear(self.I_ABYSS_NAVIGATION): + # + if self.appear(self.I_PREPARE_HIGHLIGHT): break - logger.info(f"Click {self.I_EXIT_ENSURE.name}") - - return True + return def start_abyss_shadows(self): # 尝试开启狭间暗域 - self.ui_goto(page_guild) - from tasks.Dokan.assets import DokanAssets as da - self.ui_click_until_disappear(da.I_RYOU_SHENSHE) - self.ui_click_until_smt_disappear(self.C_ABYSS_SHENSHE_ENTER_ABYSS, stop=da.I_RYOU_DOKAN, interval=1) + + if not self.appear(self.I_SELECT_DIFFICULTY): + logger.info("Failed to Open abyss_shadows ,cause not found I_SELECT_DIFFICULTY") + return # 选择难度 self.ui_click(self.I_SELECT_DIFFICULTY, stop=self.I_DIFFICULTY_EASY, interval=2) @@ -428,60 +310,64 @@ def start_abyss_shadows(self): self.ui_click_until_disappear(self.I_START_ENSURE, interval=2) def process(self): - # - ps_list = self.config.model.abyss_shadows.process_manage.parse_order() - # - done_list = self.config.model.abyss_shadows.saved_params.done.split(";") - # - unavailable_list = self.config.model.abyss_shadows.saved_params.unavailable.split(";") - - def get_next(): - for ps in ps_list: - if ps not in done_list and ps not in unavailable_list: - return ps - return None - - def process_item(item): - - return - while True: - next = get_next() + self.update_list() + next = self.get_next() if next is None: break - process_item(next) + self.execute(next) + self.flash_list() + + def get_next(self) -> Code: + # 获取下一个任务目标 + for ps in self.ps_list: + if ps not in self.done_list and ps not in self.unavailable_list: + return ps + return None def open_navigation(self): - self.ui_click(self.I_ABYSS_NAVIGATION, self.I_ABYSS_MAP, interval=1) + while True: + self.screenshot() + if self.appear(self.I_ABYSS_MAP): + break + 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 - def goto(self, item): - self.open_navigation() - code, area, enemy = item - need_change_area = area == self.cur_area + + def execute(self, item_code: Code): + area = item_code.get_areatype() + need_change_area = (self.cur_area is None) or (area == self.cur_area) if need_change_area: self.change_area(area) + self.cur_area = area + # 当前应当在正确的区域 # + # if not self.check_available(item_code): + # return - self.open_navigation() - if not self.click_emeny_area(enemy): - # 该单位不可用 - self.config.model.abyss_shadows.saved_params.unavailable += f"{code};" + if not self.goto_enemy(item_code): + # 前往失败,添加进unavailable_list + self.unavailable_list.append(item_code) return False - # 战斗 - self.run_battle(enemy) - # 战后统计 - - def run_battle(self, enemy): - enemy_type = None - match enemy: - case AbyssShadowsAssets.C_BOSS_CLICK_AREA: - enemy_type = EnemyType.BOSS - case AbyssShadowsAssets.C_GENERAL_1_CLICK_AREA | AbyssShadowsAssets.C_GENERAL_2_CLICK_AREA: - enemy_type = EnemyType.GENERAL - case AbyssShadowsAssets.C_ELITE_1_CLICK_AREA | AbyssShadowsAssets.C_ELITE_2_CLICK_AREA | AbyssShadowsAssets.C_ELITE_3_CLICK_AREA: - enemy_type = EnemyType.ELITE - case _: - enemy_type = EnemyType.BOSS + + battle_count = MAX_BATTLE_COUNT + while battle_count > 0: + self.attack_enemy() + # 战斗 + suc = self.run_battle(item_code) + if suc: + break + battle_count -= 1 + return True + + def run_battle(self, item_code: Code): + success = False + enemy = item_code.get_enemy_click() + enemy_type = enemy.get_enemy_type() # 判断是否需要更换预设 def get_preset(enemy_type): @@ -495,15 +381,16 @@ def get_preset(enemy_type): preset = get_preset(enemy_type) if preset != self.cur_preset: - self.switch_preset_team(preset) + self.switch_preset_team_with_str(preset) + self.cur_preset = preset # 点击准备 self.ui_click_until_disappear(self.I_PREPARE_HIGHLIGHT, interval=0.3) # 标记主怪 - is_need_mark_main=self.config.model.abyss_shadows.process_manage.is_need_mark_main() + is_need_mark_main = self.config.model.abyss_shadows.process_manage.is_need_mark_main() if is_need_mark_main: - self.ui_click(self.I_MARK_MAIN, interval=0.3) + self.ui_click(self.I_MARK_MAIN, interval=1) # 生成退出条件 def generate_quit_condition(enemy_type): @@ -518,30 +405,109 @@ def generate_quit_condition(enemy_type): return self.config.model.abyss_shadows.process_manage.parse_strategy(strategy) condition = generate_quit_condition(enemy_type) - cur_damage = 0 + _cur_damage = 0 self.device.screenshot_interval_set(1) while True: self.screenshot() - if condition.is_valid(cur_damage): + if condition.is_need_damage_value(): + _cur_damage = self.O_DAMAGE.ocr_digit(self.device.image) + logger.info(f"Damage Done: {_cur_damage}") + if condition.is_valid(_cur_damage): + self.device.screenshot_interval_set() self.quit_battle() break - cur_damage = self.O_DAMAGE.ocr_digit(self.device.image) - self.device.screenshot_interval_set() + # 战斗胜利标志 + if self.appear_then_click(self.I_WIN, interval=1): + self.device.screenshot_interval_set() + continue + # 战斗奖励标志 + if self.appear_then_click(self.I_REWARD, interval=1): + self.device.screenshot_interval_set() + continue + if self.appear(self.I_ABYSS_NAVIGATION): + self.device.screenshot_interval_set() + break + if condition.is_passed(): + # 通过条件结束的,视其为 完成,加入完成队列 + self.done_list.append(item_code) + success = True + + logger.info(f"{enemy_type.name} DONE") + return success def quit_battle(self): # TODO quit - pass + self.ui_click(self.C_QUIT_AREA, self.I_EXIT_ENSURE, interval=2) + self.ui_click_until_disappear(self.I_EXIT_ENSURE, interval=2) + 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.switch_soul_config.enable: + 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.") + return + self.switch_soul_one(int(l[0]), int(l[1])) - def update_state(self, item, info): - pass + self.ui_click_until_disappear(self.I_ABYSS_SHIKI, interval=2) + + switch_soul(self.config.model.abyss_shadows.process_manage.preset_boss) + switch_soul(self.config.model.abyss_shadows.process_manage.preset_general) + switch_soul(self.config.model.abyss_shadows.process_manage.preset_elite) + 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 + + return True if __name__ == "__main__": from module.config.config import Config from module.device.device import Device - config = Config('却把烟花嗅') - device = Device(config) - t = ScriptTask(config, device) - t.screenshot() - t.start_abyss_shadows() + import cv2, numpy as np + + # config = Config('却把烟花嗅') + # device = Device(config) + + image = cv2.imread('E:/5.png') + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + + hsv_image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) + lower_green = np.array([167, 100, 200]) + upper_green = np.array([180, 225, 225]) + mask = cv2.inRange(hsv_image, lower_green, upper_green) + res_img = cv2.bitwise_and(image, image, mask=mask) + cv2.imshow('res', res_img) + cv2.waitKey() + + # t = ScriptTask(config, device) + # t.screenshot() + # t.start_abyss_shadows() From 748b9bd5aa6da671df282245b41088c8b37eb474 Mon Sep 17 00:00:00 2001 From: ChengRanORZ Date: Mon, 2 Jun 2025 00:22:27 +0800 Subject: [PATCH 04/21] add pic --- .../AbyssShadows/res/res_abyss_dragon_over.png | Bin 0 -> 5702 bytes tasks/AbyssShadows/res/res_abyss_enemy_fire.png | Bin 0 -> 14799 bytes tasks/AbyssShadows/res/res_btn_start.png | Bin 0 -> 7750 bytes tasks/AbyssShadows/res/res_difficulty_easy.png | Bin 0 -> 2842 bytes tasks/AbyssShadows/res/res_difficulty_hard.png | Bin 0 -> 3235 bytes .../AbyssShadows/res/res_difficulty_normal.png | Bin 0 -> 2424 bytes .../AbyssShadows/res/res_select_difficulty.png | Bin 0 -> 1831 bytes tasks/AbyssShadows/res/res_start_ensure.png | Bin 0 -> 5045 bytes 8 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tasks/AbyssShadows/res/res_abyss_dragon_over.png create mode 100644 tasks/AbyssShadows/res/res_abyss_enemy_fire.png create mode 100644 tasks/AbyssShadows/res/res_btn_start.png create mode 100644 tasks/AbyssShadows/res/res_difficulty_easy.png create mode 100644 tasks/AbyssShadows/res/res_difficulty_hard.png create mode 100644 tasks/AbyssShadows/res/res_difficulty_normal.png create mode 100644 tasks/AbyssShadows/res/res_select_difficulty.png create mode 100644 tasks/AbyssShadows/res/res_start_ensure.png 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 0000000000000000000000000000000000000000..2affd0c6837cd06bc6a77b382d98fea3677832e0 GIT binary patch literal 5702 zcmV-M7P;w(P)V>U4h#&rc_l?b?oJ93kx>c^NFXt}xPSpSU}RwUb`3-_FkIFREMa58 zqZS+BECN~xq(1<$NIIC!2r@$?3Br~DvCB&eN`Pz~5Ieb`2*d`un1QD#B*+=a_5rdL z(vjFnNbHo<;$$E@2gr^`_$_}v1B1mE1_tJd2(hGT3=G@b85kt)AjDJ_GBEJBGB9kr zj1c?m5B4vk*rAftiV~2BpS~kQL1IxNSeh9WplN9g44;-WFz`k)FbH2@VBm^|xC0DW zi-7@6VmJ{30AwjPdeHYMD*ylh32;bRa{vGi!~g&e!~vBn4jTXf6$?p3K~!jgje1#^ zT*r~#7m<1It*uw|imd?xAV`291%d!UK_o`h(c#FHG$YSBewhcq_;2_Z_=kAWoEdwL z<&h{#v_y%kxL6$0RjM`VBq*6!Gnnm4AQ(CPwBO}336~Lh-vHqlZP3I!QM<^M(hBP%S>|9 zV5V_1G6TGsIiCOsK>#%XW@dZ$2jIbuTv{$^P=K5#asV->rRYseda6Q11TNXc+Yt>! zsy0wk0R$I60zA0{0NC^B4En1eN5t_*v#3uvUjeacK`Len^f~pc?^*6l@K6Oa zHyY$K0L%%TCvpZwDHca1S27i7*po$kCj!6>fC*%PDjAd=$E$h(l-9`Ci5w9?1T%vG zmUx%AQvJbD6k--^u&-)qilx{bpg^EJ z&hr4E2y*B4();gy?ikDrm_@alB6=M$h>0D@0r3Y`6;%P?*(D+5kN_fLPwZfTiU>wV zYFQ=i_VRf006@hwmY=~Q1yj=VWJ5R(6PT5xgbF7Y)Ak0Y9Buywjk0ENe zdx=BlX>ED!_rLw9x$`(nUAg$BJ$dhl2w4I^Rf{~_ zySvofZ4DUQ`10E`;@iIWShR(J94^4jXdAc7}}iN?ko;}fG>4|jHUnnjT7*X}f%oxlCNA2?-8#t58nGfe|-0|xW2jZ zsMpIYl}hO)W~JxEh)Kk|kDJWAwYAgj<^Z(+M5U5WPmfo!bikutuhr_+JU|VNMs={B z=RtxHp)~#eiY039%>kL`+QNoHsLOZnwLG@4Wfb%hNNH0PI?qD0PLdi89Ur6I^btMz&{YK+RrQPT2*wO{;?|D9&ZtC!E2KA|&F z1xzhKwVx=7*aHZ?jz9U#5h5DcK$<27h}rs^xHC!*1@M#&?DA_z0@ zKQP10H^17`N}d-)sAjGxilPXEbz&ywcw-xEP9-l1h3|HH)mmk8a*T=8Ow`(~jsbb0 zm5hgFRpepu-tyy}W@Dt@9P-&D@%4H&&m~5Y_4dASMFAU`(WH0NAI#)9C?7 z(`0ULD)P8-{m#DWi5u7N+`GFxEE7?aUJ-TH*EY?~Pkr~bvmpo*DY#!54ZeiX>-gg* z7D8dpx%mS#U){K;rjNFFfAP=1!~X}pc>dH&=TFz_wQd2W027p{9twghb12Wu?&}s( zh#ovNSF2Y3A3HK(W@fTh%K&6)^7P4tS1+H-YPC*JR0UF)hyg}y#5O2)D^>UE#h1lk zu+h;#F4C0Al1VxM>@z@{R^AV&%#A`qC#0PpuAXr-)6 z3FKmc?yHI8<@yIQVqZ2@aGazbNtz6tV`6d)F=wkpZN_5IOf2*e z&*(fbC|?Z#vGXv7b3`CVgKl~7Os!v2kB*HTJ8?+VRMkwWY#3FgfCMlTISi7&ne`h3 zlfsJ>48SoNoFju05g;OFaFhGS;jzzAeyL`>|=)HJ}XU!g>H z@McOxiKnuqB!jUTfm98ZY9&p4UW8L;j*DpA6vWg|*TeukF-4V_N|X;VN1%Xx>eIwk zD=Zp(;vBnP5Q8d=sjLLX#v6Gajy`puDB}0oVu4e3n?&qP#Z>#{2ocA8uFfP73 z?p?d~_0+zR7tcRCI@)M=yGBCrs%D163;Qe8Y`8v(7ChWFm_?8Ar3sk9LJV1^;??Za zkFU4d?alRvc@cj2*B8C#LWGD!O-(OcJX5J=BH|rqS(4{PQOJ;MA_jAE9*90Hh~**@ z08sUt8#|qDZbW8we{rSxxY-!1iz(PCtl7y&Yn9cN%}+kQYH9}$&z*YiShZ5=_IlcH z0%GeHz09N_W)PW*#o$QnP8~hIw6qo*MG0Yi-^kqleL!J$$%x z=l0V5d#gu|?yom$MNk7o0=dLEQ?(dljJ(D2Cnl?qbF>P`T^NCSH76=CR!<7|zPu{yU<4(I{6zgrRkJR?hPlrO)L{(G- zpuDi>&z}6zk6#_cmBo82MBvyg4}0t|5r~+GO^w^FP86yUW@jhrBlTVpF*%A@Subc2 z!ikedE?hhlLvM6UiQ+bpiAWf-x*_8;GZ8=B^vxfbAtF^|8D*I#Bx(>vuP1pP&YpV~ z!1XV_{?6HF00a?cZ{#TEr9pXU26C`IC`IIltDF4?rkT*FrR@02YhOQDTs^!nfBfmg zje51+mc-M=@4oQLrSF(or`t0FJJM)cL?FTdZiy6uAyZ!zB~+0(9vP|aZ0|nY+P?DV z8~5+6sM!}+ZjFxAe)6-|r)DObt#)1%iBH6=?1@TL(U_eX0S4l|>MDjvBA@rld$4nF z{mXj~A8zk9o5MQX-EH6e>fY?!BmsF5LJ)F}hMtH(Q;g%oIBHCk?7bg!%Duc;yuW4| z3za7n=p2_z28Fp2fhl>Y$xzN1wk!ZJ(I-FqmTjboKYV2Wsnf@b5KOg%sD^XQ-fmBl zm2iNm{007zCMHHtKY#q$XP;_})YPoiZUdlF-0`J|#ZIgi%5)(x#46N606@%pJeVk} zRqBoEp~EvLo>`ci+XtA4=Dobr%bA&(R1HiZ*4Q7D0tAWMOGK?eAPMNh2@)Yc0CRKu zo;o%^J2$y+dc0aq0R#!1ZqG!?Ie;t%308ouU!C-elsv^86Z?IG0XcRNEN3Q~KQIm8(RNe%u`5lJ z@$nHN@{T|G`05{i|LNkrmG|EL+&e;fizs$66N6HDlRdL0fE`Cm;gQi=rIG&Eo5b7gUZ}Oga`+VYCyk~B4Q>6kwFz|MC_d-_G9Ctl}f6r ztNk{BL9cx8toQEz;>vH|{&3KW&&*B^`)JfIM|(3-`$ljWOe)kLI55NPdcEGxPAkfI zY+`g`V&v05U3>4{&x$@CG%+!9;pH(qkCfnD=Tqt7GX>E2M*2{OhuZ_ zvgMwgnRw^z55K&2TlxS}qf!0Qk6)`dYP-#DR5X+G-T@jU0vN0sOwFu}HH5PHBn0og zgPE#z+P%2r&9ClBe~RK9AARcJYkzfaWNc)&)hn+>l%=j(Nka%~)@*jzr)Fd@A}#S+ zGl11d1sp_Gt=G#30a%9cuk#n4d*k0+8X0Z0TU{vvOqt*vC5gLx=fQ8@dOuAZGn;B0 zgAsGYDM73y;N*M=nx%dZHz340Nxhc6{==6~oIH{T+3gf00y732KunJ5`qi&nt!|zN z=Y0r5{pr{f13rM5e3rOQyL;oyyMwbEY<_;~weO#sKQPno6?xuo>&=+SxE!;sE^qq8 z^}2bIWdo=X{h8aIajE%MtMk!^pRcU0mk+2t{q&I^{^*q?_03jKL`;naj;y8*iQ{gk zm*=6`Y~Q)PbmoN#cJr%xWKRE8xsxQ-FWnHSNiXP@f!+PA*C2SPW` z|MBO)Ido{|=&^%S(-Uc$m|3rz@9Z=mZSAbCY_1LgmH89Z^0zof(`DNBl-%1Am8dWOk<=4-jdErF2S4f<+#zrx2%@kn~A7N7_YIk}g_3E2HxqSWl zt)&O6Q&Z#2yuH10zo?vs0&!U3le8wOVO+dcC|bvxtcpQHk~L&9?hlMO1fN zosmZ6+)Jm=zx>?J_G1y9+&9*4b#C8&5DM{$uViVhUKt&2G)8I$ghF&# z9wm~y^wLZ0h|5Zbh>)gkY`lKw*8SKv>^^Rdj5cbu%KPtqUaeLpCK|+Cga9){^aEwr zy;_pfTag( zFP?kWF$aBO>ZYbv{@n$YSLtKiK1sYF0OY;py-;L5>6bC+Iw zvEHb*n(d%!2DOM3#jb$|mcad)T;!oy>T1<=YwPjCk^P4b&xlHvrK$=;nk4|19<2ZF zw}0yP@}2GG*!akWi)Zr0YaZfVVs;V*RLU_jVSrARteoT9UoZZ{fBDt@#Z~XU77TA1IXX{( zN)e!yTD8;a{Px!$ksv3gq?lhA$8EI5Sq}|Cw z2vo+y5>SgX7Y_o%nc?_}!`H6f6xBbz``N$y`#0*945q(%>x0F`RVF%cX!h)j&pdwI zTv^`O-EHk`?{029+J3a#7^%(9OQW8}o0+c*} zYUaHsBD(awvkMDztE-zo|IfeNSbsR!%K8IA$CRc?QuE}v$cs+97oQjztpi}^%Dy(@ z`mC|s^Q~rQ^I>~yb8BsFV{`LiyVHTHl9k9Qq0y*MPL1!I8k?9H9UW~{s#%hH$E>O= z%bVA(emy!`KYQ+36$S03X@W8OW5d@@Uu?B@dwJJ1C`3&okD2N5Q)iByJb5&pahSTj sTtuDY!639KWVh8B+?z`9E4mK;FU_x_aS3GXT>t<807*qoM6N<$g82CG`~Uy| literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..de48b10c966ab85a9979898ae487ac48e27063e3 GIT binary patch literal 14799 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 oVVqa+D%6FMBA2;mDgMO>YPx# literal 0 HcmV?d00001 diff --git a/tasks/AbyssShadows/res/res_btn_start.png b/tasks/AbyssShadows/res/res_btn_start.png new file mode 100644 index 0000000000000000000000000000000000000000..0101e113b3d4eb55208b88818d20fcb720496680 GIT binary patch literal 7750 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 Ij>F^s0hHaC3jhEB literal 0 HcmV?d00001 diff --git a/tasks/AbyssShadows/res/res_difficulty_easy.png b/tasks/AbyssShadows/res/res_difficulty_easy.png new file mode 100644 index 0000000000000000000000000000000000000000..5800badfb4eed15c3a2a3736a5cfb0904c49b001 GIT binary patch literal 2842 zcmV+#3+42QP)V>U4h#&rc_l?b?oJ93kx>c^NFXt}xPSpSU}RwUb`3-_FkIFREMa5E zqZS+BECN~xq(1<$NIIC!$O2*~LD&)?c6mub36QM=VkZ|Af!IJ7Gw>9J1UUoQK0vlY zIubhxiJg*KoD5{=0NL>fzva(oV6gbYz`#5aA(k|afnj?)1B1jJgqX@g1_u6C28L~y z5n`YH!Tx0wJ5-WdQ34Y2(|2SjNGwVOOEZH4G%byR;nQ*k2Hr>p2H^_~3|!F=cYpzF zF)*M>3@1VW0BDgmebKUdIRF3v32;bRa{vGi!~g&e!~vBn4jTXf3ExRXK~zYIWmZXV zT*ncv>hAYu=FPs591b^06e&>@6>Eze*jel(LgIrRBfbPV<(6BH`6D^Um%uQx0D)sA zFcL4ZBg>Y>L=q#B6e;erkh9O5w{~~sFf=VAb$Y0-_u=cRuc{gsfAmAj=JTmcv)2uk zG)#jVypT;#m&Ul@>Cpla0YEuhJU25lH!)L4q@BQ9ukJ0cJh``We|vXNNl9)u4pJB~ z>9j%mCI=w}=Y%g`z5e82cc)qnNFryK&&~bfwO0fa#Z(baj2XZ<09a=9;`G_c$uno? zPXG3IZ&hnGB2c6lX{`}ZXas=_&8YeFS6>;;<=?t>>(S1ZtAj!@@5(R}jUeCvQ;2gsQ*0gllAN^N&PuC(VIr0YC`g%i#APzCS%OHa?vjk7XyV-2R~TQFYz4OaQQDFlFU$jxSCn@@dmD z7zdys?s1|R5(0712~LWW6Nq6>KtrwBd{Wt)E0oKL%;|J-+irA2*OKsZY2i$=I1)+a zV+qTMoG{fyn}@a4+QGp0000ET7)QqS05k^yz)%Qa8WgVXA6%L0OpJ_vfBM2qrU(Ei z;8Jd~6w4Tlholr8(^Q}v!Ql4#>Q3X(4SZmNbd)fnNl6u=X$WE*3+9(P-QN5GP3pq};C7_ZtnRHA5r-l3L494SeL9v=V^Fz{zN=kj})+2mrV; zSZN*nVe|8f-I`3~e!2LPB_dxk)xMIIZtE|%Ka)x^WH|hvam^5rX)iz}1sYrfA;Rgg z@=ss*-udz*0Bv;Ze|&m(ty`V2GQYZXGbRj8H00%}aJzT(!OnyA$^m1XGsZbG#W@)u zIR-+7APg=TLyRI3tQ>9)T$}37*I^$(gH79o+!7{39O5=Td%aWPjE$rchTsTr(Ci6DNSYx^rGPX62w;qWlv0|s8~CA;Ulm=; z)!xzjmB*o0*NQW_ShD30{?piUq|X`4<>T$1M?{Vn8laRL_ya$dvYb{okcv-#_jz+9 zIh7yrP-%gI3GsNm5KB5Le9}95XZPW~!`(f*)$*KH;8dONAoRzq%zU;qpB=5mR+ zW}yru`k*xV%16|)?+IoK5s$on`Gu9E-R<_#!%kK0h=W1Lli{}Ad~kFCfC7XjjBzs? zwW5497m26Lm{Jk|I{sk1pWHSN8>}BHr42IEQMVltB5E*XKbT!wDow2(RUY^28-t^j zwF3e|WZ6h!Dw$)*E*Ga~GsU=R9*<+K8cq)YGLd*Daw=nGo+j#m*y(j#+s0@_By#Df z83CZ#+}Lbx>_WeJYxm*554U5&NE)%}RO!bvO9tZr5JD(H4VyU7l7QN-Ef}|qNIsoS zIK9*R_jbF@{hb7v!n@J;jr8rfx(!;IB2SUC$|6se~Ej$TTSc za00(M=mP>NEpo|}t0e&3scr1`n!xbd*lf;BG@QZ5mDRC$Di=+@Sel(^Ty03aew zB;LLE>3-C2c>Pi$`<gHef?i-AsNsqpE=K2X}sMLD5_NaAWFg|Lf7Dp#e zjvUWFY8y;xXJpS^X4b-1yX(~ zb|}PB7xI%c>5;E>3&6S5_)BAp3z>;$y9N>Ou5a!f9RUDC1f^x)1w>xDaV3bt!-MVp zW@ELs-{kSM^+S z0t#j5_~=PN0GjlAegClE{nnZD&rQxw7fYP6)n;Y4*W7A#x`EgA+=vj*m(R@RMo#E! zt(P~RZq@2u7?KmnmVr~*LP^<{HFLBK-^GK4S$BGO8f z%cZHgspl5X?$+y{+<)-2()J~}uHWf93@{#z9@kTni?Fp<+uE!8VR+Q;1_KuWWZRW} zpDPztz?1?&AdQTPOV^&88OwgLw)XDFw>$PghK5oc02v|@sDU@cmB`wlh$8?2B5G9K zfTRj-4>Um_N%_b*0x_*H78S{O`t84cxV*V8rDDjC0QK-`fj~fn2tYteYg7mf2@o}r s(x55qd!+SHbxg?!f|LO;tV_0002tX+uL$b5ch_ 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 Date: Sat, 7 Jun 2025 20:05:20 +0800 Subject: [PATCH 05/21] =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E6=88=90=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- module/atom/click.py | 8 +- module/atom/ocr.py | 3 + module/ocr/base_ocr.py | 37 +- tasks/AbyssShadows/assets.py | 16 +- tasks/AbyssShadows/config.py | 184 +++++----- tasks/AbyssShadows/res/click.json | 6 + tasks/AbyssShadows/res/image.json | 12 +- tasks/AbyssShadows/res/ocr.json | 36 ++ .../res/res_select_difficulty.png | Bin 1831 -> 1893 bytes tasks/AbyssShadows/script_task.py | 315 +++++++++++++----- 10 files changed, 439 insertions(+), 178 deletions(-) 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 21c6bfa91..00ea90677 100644 --- a/module/atom/ocr.py +++ b/module/atom/ocr.py @@ -17,6 +17,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + def after_process(self, result): match self.mode: case OcrMode.FULL: return Full.after_process(self, result) @@ -36,6 +37,8 @@ def ocr(self, image, keyword=None): case OcrMode.DURATION: return Duration.ocr_duration(self, image) case _: return None + + def coord(self) -> tuple: """ 获取一个区域,随机返回一个坐标 diff --git a/module/ocr/base_ocr.py b/module/ocr/base_ocr.py index 5c232ae97..af5b711f5 100644 --- a/module/ocr/base_ocr.py +++ b/module/ocr/base_ocr.py @@ -8,7 +8,6 @@ from ppocronnx.predict_system import BoxedResult from enum import Enum - from module.base.decorator import cached_property from module.base.utils import area_pad, crop, float2str from module.ocr.ppocr import TextSystem @@ -17,7 +16,6 @@ from module.logger import logger - def enlarge_canvas(image): """ copy from https://github.com/LmeSzinc/StarRailCopilot @@ -40,22 +38,40 @@ class OcrMode(Enum): DIGITCOUNTER = 4 # str: "DigitCounter" DURATION = 5 # str: "Duration" -class OcrMethod(Enum): - DEFAULT = 1 # str: "Default" -class BaseCor: +class OcrMethod(): + _reg = r"^([^()]+)(?:$(.*?)$)?$" + _METHODS = { + "Default": 1, + "ColorFilter": 2, + } + _method = "Default" + _val = None + + def __init__(self, val: str = None): + if val is None: + self._method = "Default" + return + import re + match = re.match(self._reg, val) + if not match: + self._method = "Default" + return + self._method = match.group(1) + self._val = match.group(2) + +class BaseCor: lang: str = "ch" score: float = 0.6 # 阈值默认为0.5 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 = "" # 默认为空 - def __init__(self, name: str, mode: str, @@ -78,7 +94,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) @@ -185,7 +201,7 @@ def detect_and_ocr(self, image) -> list[BoxedResult]: text=str([result.ocr_text for result in results])) return results - def match(self, result: str, included: bool=False) -> bool: + def match(self, result: str, included: bool = False) -> bool: """ 使用ocr获取结果后和keyword进行匹配 :param result: @@ -197,7 +213,7 @@ def match(self, result: str, included: bool=False) -> bool: else: return self.keyword == result - def filter(self, boxed_results: list[BoxedResult], keyword: str=None) -> list or None: + def filter(self, boxed_results: list[BoxedResult], keyword: str = None) -> list or None: """ 使用ocr获取结果后和keyword进行匹配. 返回匹配的index list :param keyword: 如果不指定默认适用对象的keyword @@ -263,7 +279,6 @@ def detect_text(self, image) -> str: text=f'[{results}]') return results - # def test(): # # strings = ["探", "索"] # # keyword = "探索" diff --git a/tasks/AbyssShadows/assets.py b/tasks/AbyssShadows/assets.py index b47495a5a..d173fa2ac 100644 --- a/tasks/AbyssShadows/assets.py +++ b/tasks/AbyssShadows/assets.py @@ -45,6 +45,8 @@ class AbyssShadowsAssets: 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 @@ -53,9 +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=(253,185,33,100), roi_back=(253,185,33,100), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_abyss_dragon_over.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") # 狭间_白藏主入口 @@ -99,7 +101,7 @@ class AbyssShadowsAssets: # 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") # 选择难度按钮 - I_SELECT_DIFFICULTY = RuleImage(roi_front=(710,650,50,50), roi_back=(710,650,50,50), threshold=0.8, method="Template matching", file="./tasks/AbyssShadows/res/res_select_difficulty.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") # 普通难度 @@ -125,6 +127,14 @@ class AbyssShadowsAssets: # Ocr Rule Assets # 伤害 O_DAMAGE = RuleOcr(roi=(50,110,300,50), area=(50,110,300,50), mode="Digit", method="Default", keyword="", name="damage") + # 神龙暗域已完成 + O_DRAGON_DONE = RuleOcr(roi=(130,160,180,360), area=(130,160,180,360), mode="Single", method="Default", keyword="封印", name="dragon_done") + # 孔雀暗域已完成 + O_PEACOCK_DONE = RuleOcr(roi=(420,160,180,360), area=(420,160,180,360), mode="Single", method="Default", keyword="封印", name="peacock_done") + # 白藏主暗域已完成 + O_FOX_DONE = RuleOcr(roi=(680,160,180,360), area=(680,160,180,360), mode="Single", method="Default", keyword="封印", name="fox_done") + # 黑豹暗域已完成 + O_LEOPARD_DONE = RuleOcr(roi=(1000,160,180,360), area=(1000,160,180,360), mode="Single", method="Default", keyword="封印", name="leopard_done") # Swipe Rule Assets diff --git a/tasks/AbyssShadows/config.py b/tasks/AbyssShadows/config.py index 2acb661b9..f620e9076 100644 --- a/tasks/AbyssShadows/config.py +++ b/tasks/AbyssShadows/config.py @@ -24,36 +24,29 @@ class AreaType(Enum): FOX = AbyssShadowsAssets.I_ABYSS_FOX # 白藏主暗域 LEOPARD = AbyssShadowsAssets.I_ABYSS_LEOPARD # 黑豹暗域 - # @classmethod - # def __str__(cls, value): - # # 遍历类属性,找到匹配值对应的属性名 - # for name, attr_value in vars(cls).items(): - # if attr_value == value and not name.startswith('__'): - # return name - # return str(value) # 默认行为 - 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 - BOSS = AbyssShadowsAssets.C_BOSS_CLICK_AREA - # @cached_property - # def name(self) -> str: - # """ - # - # :return: - # """ - # return Path(self.file).stem.upper() - # - # def __str__(self): - # return self.name - # - # __repr__ = __str__ + +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): @@ -69,59 +62,61 @@ class AbyssShadowsDifficulty(str, Enum): 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('-') - match area: - case 'A': - return AreaType.DRAGON - case 'B': - return AreaType.PEACOCK - case 'C': - return AreaType.FOX - case 'D': - return AreaType.LEOPARD - case _: - return AreaType.DRAGON - def get_enemy_click(self): - area, num = self.value.split('-') - match num: - case '1': - return AbyssShadowsAssets.C_BOSS_CLICK_AREA - case '2': - return AbyssShadowsAssets.C_GENERAL_1_CLICK_AREA - case '3': - return AbyssShadowsAssets.C_GENERAL_2_CLICK_AREA - case '4': - return AbyssShadowsAssets.C_ELITE_1_CLICK_AREA - case '5': - return AbyssShadowsAssets.C_ELITE_2_CLICK_AREA - case '6': - return AbyssShadowsAssets.C_ELITE_3_CLICK_AREA - case _: - return AbyssShadowsAssets.C_ELITE_1_CLICK_AREA + 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('-') - match num: - case '1': - return EnemyType.BOSS - case '2': - return EnemyType.GENERAL - case '3': - return EnemyType.GENERAL - case '4': - return EnemyType.ELITE - case '5': - return EnemyType.ELITE - case '6': - return EnemyType.ELITE - case _: - return EnemyType.ELITE + # 查找 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]): @@ -130,10 +125,18 @@ def __init__(self, v: str): def expand_str(v: str): if v.find('-') != -1: return [v] - if v in ['A', 'B', 'C', 'D']: + + 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 ['1', '2', '3', '4', '5', '6']: - return [f'A-{v}', f'B-{v}', f'C-{v}', f'D-{v}'] + + 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 == '': @@ -145,13 +148,12 @@ def parse_order(value: str = None) -> list: super().__init__(parse_order(v)) - def save_to_obj(self, config_obj: str): + def parse2str(self): ret = "" for item in self: ret += ';' ret += item.value - ret = ret[1:] - config_obj = ret + return ret[1:] class Condition: @@ -160,7 +162,8 @@ class Condition: _timer: Timer = None # _is_damage_enough: bool = False _damage_max: int = -1 - + # True 立即退出 + # False 任何情况下,该条件检查不通过 _dont_need_check: bool = False # 存储结果,用于后期查询 @@ -180,9 +183,10 @@ def __init__(self, value: str): except ValueError: self._time = 180 self._timer = Timer(self._time) + self._timer.start() else: try: - _damage = int(value) + self._damage_max = int(value) except ValueError: self._damage_max = 999999999 @@ -190,8 +194,8 @@ def __init__(self, value: str): def is_valid(self, damage: int = None): if self._time >= 0: - if not self._timer.started(): - self._timer.start() + # if not self._timer.started(): + # self._timer.start() if self._timer.started() and self._timer.reached(): self._condition_result = True return True @@ -202,6 +206,7 @@ def is_valid(self, damage: int = None): if self._dont_need_check: self._condition_result = True return True + self._condition_result = False return False def is_need_damage_value(self): @@ -210,6 +215,9 @@ def is_need_damage_value(self): def is_passed(self): return self._condition_result + def __repr__(self): + return f"Condition(time={self._time},damage_max={self._damage_max},dont_need_check={self._dont_need_check})" + class AbyssShadowsTime(ConfigBase): # 自定义运行时间 @@ -220,6 +228,8 @@ class AbyssShadowsTime(ConfigBase): 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): @@ -233,7 +243,7 @@ class ProcessManage(ConfigBase): attack_order: str = Field(default='', description='attack_order_help') # 标记主怪 # EnemyType, 多个用;分隔 - mark_main: str = Field(default='', description='mark_boss_help') + mark_main: MarkMainConfig = Field(default=MarkMainConfig.BOSS_ONLY, description='mark_main_help') # 首领预设 preset_boss: str = Field(default='', description='preset_boss_help') # 副将预设 @@ -249,8 +259,26 @@ class ProcessManage(ConfigBase): # 精英策略 strategy_elite: str = Field(default='', description='strategy_elite_help') - def is_need_mark_main(self, enemy_type): - return str(enemy_type) in self.mark_main + 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 parse_strategy(self, strategy: str): if strategy is None or strategy == '': @@ -265,8 +293,6 @@ class SavedParams(ConfigBase): fail: str = Field(default='', description='fail_help') # 已知的已经打完的 unavailable: str = Field(default='', description='unavailable_help') - # 当前时间,用于判断存储参数有效性 - save_time: str = Field(default='2023-01-01', description='today_help') # def save(self): # self.today = datetime.today().strftime('yyyy-mm-dd') diff --git a/tasks/AbyssShadows/res/click.json b/tasks/AbyssShadows/res/click.json index 734d1ef51..2638262ce 100644 --- a/tasks/AbyssShadows/res/click.json +++ b/tasks/AbyssShadows/res/click.json @@ -100,5 +100,11 @@ "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 9fbdd0542..b8ec4c139 100644 --- a/tasks/AbyssShadows/res/image.json +++ b/tasks/AbyssShadows/res/image.json @@ -20,8 +20,8 @@ { "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": "狭间_神龙入口" @@ -29,8 +29,8 @@ { "itemName": "abyss_dragon_over", "imageName": "res_abyss_dragon_over.png", - "roiFront": "253,185,33,100", - "roiBack": "253,185,33,100", + "roiFront": "200,150,110,270", + "roiBack": "200,150,110,270", "method": "Template matching", "threshold": 0.8, "description": "狭间_神龙入口_已封印" @@ -227,8 +227,8 @@ { "itemName": "select_difficulty", "imageName": "res_select_difficulty.png", - "roiFront": "710,650,50,50", - "roiBack": "710,650,50,50", + "roiFront": "703,645,50,55", + "roiBack": "703,645,50,55", "method": "Template matching", "threshold": 0.8, "description": "选择难度按钮" diff --git a/tasks/AbyssShadows/res/ocr.json b/tasks/AbyssShadows/res/ocr.json index e74ce6dcf..1bdac0cbd 100644 --- a/tasks/AbyssShadows/res/ocr.json +++ b/tasks/AbyssShadows/res/ocr.json @@ -7,5 +7,41 @@ "method": "Default", "keyword": "", "description": "伤害" + }, + { + "itemName": "dragon_done", + "roiFront": "130,160,180,360", + "roiBack": "130,160,180,360", + "mode": "Single", + "method": "Default", + "keyword": "封印", + "description": "神龙暗域已完成" + }, + { + "itemName": "peacock_done", + "roiFront": "420,160,180,360", + "roiBack": "420,160,180,360", + "mode": "Single", + "method": "Default", + "keyword": "封印", + "description": "孔雀暗域已完成" + }, + { + "itemName": "fox_done", + "roiFront": "680,160,180,360", + "roiBack": "680,160,180,360", + "mode": "Single", + "method": "Default", + "keyword": "封印", + "description": "白藏主暗域已完成" + }, + { + "itemName": "leopard_done", + "roiFront": "1000,160,180,360", + "roiBack": "1000,160,180,360", + "mode": "Single", + "method": "Default", + "keyword": "封印", + "description": "黑豹暗域已完成" } ] \ No newline at end of file diff --git a/tasks/AbyssShadows/res/res_select_difficulty.png b/tasks/AbyssShadows/res/res_select_difficulty.png index f1d266e57b6f4ef3a23e423d8980e884e9330afd..ce294f0bb9278a5846812e3ce38aac49355ff474 100644 GIT binary patch delta 1849 zcmV-92gdlP4&@GAiBL{Q4GJ0x0000DNk~Le0000M0000P2nGNE05#&??f?J)AY({U zO#lFg3jhF&4*&rDQ2+peKmY)Jb^rkBjsO5W<^TX0%>oR+c>n+a;b}udP;*j8LI40L z&yg`Ne_1pMk;R3j+i5M1)wnwTMQ-P!t7l1NffLwOIeHDkwWPmRHwMwk&Mll7SB$N=LdYh(sa?0-p;5c|d1p zGg{LrOij&UabX4TojH!f`+Hz=&MueWIgWJ94RC!GGjldRJ^u}EkBq~qRuPZIp@u>s zM(HA8q`liRIQPL@IQ#yaXh_EJ@WC{ofANop!nWnG?EuSpMp$s*_#sq06{EQv#wMl^ zVy3Z(iK=Zu73m8beB%(#yn7Oj%?W&ac?egp40B3>j!cCm3`RJ-G*#7habMbiQ-xXs;a`VJq+C(e`SiU zbSyHn8BwXY2CG~q908Jv8VsI#0lT+%z|e_+r$9Y*;#r&=*pH63dSvNf`1)NeE#=n> zL-Ps4a-oQ^smHMFsyxgzOoT&WK5K9py=fTeY;VHJM%7EtN8-df51guECP*1G|7_sWF1UhL$OpwG#bL5-JNL9G$YKY z)s}_?+-iWig#sSWucBCS;VK3l>+qFz#~S}H&#yuCJuEHe@!Q=87@M3WkO5*bMy@Fc zhjoNP3c5R55sAe(C6J~)SzN*Dnnn7^pZ_zlW_)}OmQ}@%*Z;u9OE+-&e`*%D?o7&p zwKgYZ(G-5RcGM$Y!-!QyWd&6>gh{N#^ZG%;0`Z|!!SQ0*#`L2V`VO|IkkC>}f$P>LV8R5#bOfA`uKwh`Vj8uqqm= zWeqb^EBN`RdkC>cCdcB;A|k9wNTXvxr}nl67}SQYOcNSYb&}yC>bB-MPQU#;`g_|6 zlP3BFo`gm&LAm5&db+^E=S6B9M41yFLttHb8l7r_pZwpycPsYvf3-^p>T1Jy=|~Tr zJGxD32ov2X5Nk z2g!I8aSkZl%yptHthwAMu=ga5#0(_m7MF@Buv=9x@JKO=5M!_KB$ut**_OhqFYd;{ zXS!h#yGC-|FvHOGe+a6YPzSJm6gEp3navG|v84kx3UmY6(I;40DM8KN`4eTTmY|61 zIeeLiN9U8JlsOmaWjkVbOR0g$mHSh`LozXezIA5f? zUHJMs9?vaP)QOresO3w+DV1!L%hX*F9=|rXCJ|-<1=EBae??6VTQVv1^|hf|^(mY4 z`24FI5()v7I&y!O>UAFr%SGWq7ExzJ0ySqtDuso@T&%0`Sfv;{tG6eO!-T3K5yQQ) zS)Bju8gBm1>?ukrVbPizn?JleGK*v~iY@IaY@$#{qXyH9{KH>SoPbwQE;olf<#gcK zF1#~%6zSGFe_XtB8|Oax4v*&Zys{Vw_xQ|S4eMc;?3AWt3O=tHK`3#hl}IDOS@wjy zMLcRV+o6gRXfh z-wIJzC7Tp#vCG?oh2sSq3R6;9i&C)yG7AA>~G%4-|-x=hp2*&fPYz~!8!aI;bXoO6s1Ks4a n=91fvAB@!JzB02~t@!v0RB*N3&NLvu00000NkvXXu0mjfhw?sd delta 1806 zcmV+p2l4pj4yO(=iBL{Q4GJ0x0000DNk~Le0000P0000Q2m$~A0L>p@ng9R*;b}ud zP;*j8LI405m62U9f5)R18{jMgS_q^+0I^6qn9axnVkbe^5+HVYNkIvatpj2w7Zicm zKo>Lc6omvi1KB=6wn91*I|+%Ml3JV$Waj|c@d&@=&u3t;_`<-zJP{$5G>w5_dpiSz z#2ti~%0dPP{#FKtZI=;ZpZ&r9WfVJ9l3Gy$67kb_WGF~1e@X;PGlK#&EscTU({cs| z-be-p;R_55T+tABfB|bUFrY~cCqe)KXpuI3(Xx6u00009a7bBm000ie000ie0hKEb z8vp`6pHR5*=QmCJ9OXBEbubKdv6%#6Re+K%mUe95J$OKt>`MNC8y6jF6js1PVL z8zeR${smU-f7zpMkXW!lAc2ISk+>&q0;y>sb$qFVQ(xlyc*Zl|e3$oq4~tkxIlHqs z&rdq~Ne^9q>9QqTEEk#B>9q@TpySVTaO&zHy?O>x^EQCY^BhESiL*JslE4b zAMVqzu^eBbw&=~#M!h=_>m#_8H-Yw^kE{^8N$^2Xf<3rUiT9D6(3 ztVIL9hUowEt#gyjq0`OU!Y7+aD-{UIaoh*#w{rGa40L6uLrqOlDRDou-`hRUWJL)z&}WdmQBA=d!oH zS703|&p|T*1UPY%<3paWPfwf}Zd4#bgz=H;55N2Bweyp{r-WcdL?D8=XV%s_+q-QD zAVeY-BmfKa#(GO2zONOt@9S%ye?4>Q!lXbdysaK2DY7yiJ@WBLRa8AS; z2M~ZT)O-8gPHF|DkGk>WC(o={=bW(glN2J;CmYR?a;e}axeO20JU`T0f9J5CF&?NC zg%c--qS}CH1<>5WR;$$u7@VZ(>gN93qo;A2Mv-r9)@pTZW*U`ZwOmMy8y>1ojSNP< z_DPK5T%G&`&|Z$I2MsC%jcKZ?>+ov_n-?vF>A#H zWSM=gUJx>CwnWiDaDJva`}wm&wdnEE#ydZ|`|#gS<0NCPn3WnC8UX{qqxp?`y>x1# zUMl#-qF1gIo|^@bFfA@_w~zWazxB#E z{RDuu_C&qz91D|m==CyU)&U@}a5HC{gXK!ITDy1e$=&xBS2p&A*b4Z0&xm+oNH<@5 zeRp?Pv4WspD+QinCIUHuIL8hkN>Y>b#26EVo)g#W_EU?5`1uDEBv85#hPvBnPc{eF zH(Siy=_E|-#EFP=R1j-n0a5^o1t3Xg09av102mMgl;T36L?qnrXT3O`Xf_lR6Uz(T wNDzTD5O=(S0CJ=)d4`k_2MS@}3#>Kve@$UJ`$akxdH?_b07*qoM6N<$f?*n9aR2}S diff --git a/tasks/AbyssShadows/script_task.py b/tasks/AbyssShadows/script_task.py index 9b5f64cea..00bccaeb2 100644 --- a/tasks/AbyssShadows/script_task.py +++ b/tasks/AbyssShadows/script_task.py @@ -5,43 +5,44 @@ # github https://github.com/roarhill/oas from datetime import datetime -from time import sleep from future.backports.datetime import timedelta -from module.atom.click import RuleClick from module.exception import TaskEnd +from module.base.timer import Timer from module.logger import logger -from sympy.physics.units import hours from sympy.plotting.intervalmath import interval -from sympy.strategies.core import switch from tasks.AbyssShadows.assets import AbyssShadowsAssets -from tasks.AbyssShadows.config import AbyssShadows, EnemyType, AreaType, ClickArea, Code, AbyssShadowsDifficulty, \ - CodeList +from tasks.AbyssShadows.config import AbyssShadows, EnemyType, AreaType, Code, AbyssShadowsDifficulty, \ + CodeList, IndexMap +import numpy as np +import cv2 +from tasks.Component.GeneralBattle.config_general_battle import GreenMarkType 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_shikigami_records, page_guild +from tasks.GameUi.page import page_main, page_guild # 单个首领/副将/精英 一次无法完成目标(一般是一次没打掉) 的情况下,最大战斗次数 MAX_BATTLE_COUNT = 2 class ScriptTask(GeneralBattle, GameUi, SwitchSoul, AbyssShadowsAssets): - # TODO 完善战斗次数限制 - boss_fight_count = 0 # 首领战斗次数 - general_fight_count = 0 # 副将战斗次数 - elite_fight_count = 0 # 精英战斗次数 - + # + min_count = { + EnemyType.BOSS: 2, # 最少首领战斗次数 + EnemyType.GENERAL: 4, # 最少副将战斗次数 + EnemyType.ELITE: 6 # 最少精英战斗次数 + } # cur_area = None # cur_preset = None # process list - ps_list = [] + ps_list: CodeList = CodeList('') # 已完成 列表 - done_list = [] + done_list: CodeList = CodeList('') # 已知的 已经被打完的 列表 - unavailable_list = [] + unavailable_list: CodeList = CodeList('') # switch_soul_done = False @@ -73,11 +74,22 @@ def run(self): if cfg.abyss_shadows_time.try_start_abyss_shadows: self.start_abyss_shadows() - # + # 判断各个区域是否可用 + area_available = None + for area in AreaType: + if self.is_area_done(area): + self.unavailable_list.append(IndexMap[area.name].value) + continue + area_available = area + self.update_list() + _next = self.get_next() - # TODO 增加等待时长 - if not self.select_boss(_next.get_areatype()): + if _next is not None: + area_available = _next.get_areatype() + + # 通过能否进入,检测狭间是否开启 + if not self.select_boss(area_available): logger.warning("Failed to enter abyss shadows") self.goto_main() self.set_next_run(task='AbyssShadows', finish=False, server=True, success=False) @@ -99,16 +111,13 @@ def run(self): self.goto_main() # 设置下次运行时间 - # TODO 添加周四周五周六周天不同的处理方式 - self.set_next_run(task='AbyssShadows', finish=False, server=True, success=True) + self.set_next_run(task='AbyssShadows', finish=True, server=True, success=True) + + self.clear_saved_params() + raise TaskEnd def update_list(self): - # BUG 跨天可能有问题 - if self.config.model.abyss_shadows.saved_params.save_time != datetime.now().strftime("%Y-%m-%d"): - logger.warning("there isn`t params saved today") - return - # 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) @@ -116,19 +125,30 @@ def update_list(self): self.unavailable_list = CodeList(self.config.model.abyss_shadows.saved_params.unavailable) def flash_list(self): - self.ps_list.save_to_obj(self.config.model.abyss_shadows.process_manage.attack_order) - self.done_list.save_to_obj(self.config.model.abyss_shadows.saved_params.done) - self.unavailable_list.save_to_obj(self.config.model.abyss_shadows.saved_params.unavailable) - self.config.model.abyss_shadows.saved_params.save_time = datetime.now().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("Flash list done") + 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 ''' 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 self.appear(self.I_PEACOCK_AREA): return AreaType.PEACOCK elif self.appear(self.I_DRAGON_AREA): @@ -203,6 +223,7 @@ def select_boss(self, area_name: AreaType) -> bool: # 区域图片与入口图片不一致,使用点击进去 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) @@ -289,10 +310,13 @@ def attack_enemy(self): 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) @@ -312,17 +336,59 @@ def start_abyss_shadows(self): def process(self): while True: self.update_list() - next = self.get_next() - if next is None: + _next = self.get_next() + if _next is None: break - self.execute(next) + self.execute(_next) self.flash_list() - def get_next(self) -> Code: + 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 + + 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): @@ -331,16 +397,15 @@ def open_navigation(self): if self.appear(self.I_ABYSS_MAP): break if self.appear(self.I_ABYSS_NAVIGATION): - self.click(self.I_ABYSS_NAVIGATION,interval=1) + 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) + self.click(self.I_ABYSS_ENEMY_INFO_EXIT, interval=2) continue - def execute(self, item_code: Code): area = item_code.get_areatype() - need_change_area = (self.cur_area is None) or (area == self.cur_area) + need_change_area = (self.cur_area is None) or (area != self.cur_area) if need_change_area: self.change_area(area) self.cur_area = area @@ -359,38 +424,40 @@ def execute(self, item_code: Code): self.attack_enemy() # 战斗 suc = self.run_battle(item_code) + self.device.stuck_record_clear() if suc: break battle_count -= 1 + logger.info(f"{item_code} push into done_list") + self.done_list.append(item_code) return True def run_battle(self, item_code: Code): success = False enemy = item_code.get_enemy_click() - enemy_type = enemy.get_enemy_type() + enemy_type = item_code.get_enemy_type() # 判断是否需要更换预设 - def get_preset(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.ELITE: - return self.config.model.abyss_shadows.process_manage.preset_elite 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 # 点击准备 - self.ui_click_until_disappear(self.I_PREPARE_HIGHLIGHT, interval=0.3) - - # 标记主怪 - is_need_mark_main = self.config.model.abyss_shadows.process_manage.is_need_mark_main() - if is_need_mark_main: - self.ui_click(self.I_MARK_MAIN, interval=1) + _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() # 生成退出条件 def generate_quit_condition(enemy_type): @@ -404,41 +471,73 @@ def generate_quit_condition(enemy_type): strategy = self.config.model.abyss_shadows.process_manage.strategy_general return self.config.model.abyss_shadows.process_manage.parse_strategy(strategy) + # 因为条件中存在时间相关,所以在点击准备按钮后直接生成 condition = 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") + self.ui_click(self.C_MARK_MAIN, stop=self.I_MARK_MAIN, interval=1.5) + + # 绿标 + # 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 condition.is_need_damage_value(): - _cur_damage = self.O_DAMAGE.ocr_digit(self.device.image) - logger.info(f"Damage Done: {_cur_damage}") + if need_check_damage: + _cur_damage = self.get_damage(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): + # 正常来讲,此处不应该出现准备按钮,以防万一 + continue # 战斗胜利标志 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 - if condition.is_passed(): - # 通过条件结束的,视其为 完成,加入完成队列 - self.done_list.append(item_code) + if condition.is_passed() or (not _timer_battle.reached()): + # 通过条件结束的,视其为完成 success = True logger.info(f"{enemy_type.name} DONE") return success def quit_battle(self): - # TODO quit - self.ui_click(self.C_QUIT_AREA, self.I_EXIT_ENSURE, interval=2) - self.ui_click_until_disappear(self.I_EXIT_ENSURE, interval=2) + logger.info("Quitting battle") + while True: + self.screenshot() + if self.appear(self.I_EXIT_ENSURE): + self.click(self.I_EXIT_ENSURE, interval=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(self.I_EXIT): + self.click(self.I_EXIT, interval=1) + continue return def switch_preset_team_with_str(self, v: str): @@ -462,13 +561,17 @@ def switch_soul(v: str): if len(l) != 2: logger.error(f"Due to a configuration error (value: {v}), an error occurred while switch soul.") return - self.switch_soul_one(int(l[0]), int(l[1])) + 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) - switch_soul(self.config.model.abyss_shadows.process_manage.preset_boss) - switch_soul(self.config.model.abyss_shadows.process_manage.preset_general) - switch_soul(self.config.model.abyss_shadows.process_manage.preset_elite) self.switch_soul_done = True # 退出式神录 from tasks.GameUi.assets import GameUiAssets as gua @@ -487,27 +590,85 @@ def check_available(self, item_code: Code): return True + def is_area_done(self, area_type: AreaType): + self.screenshot() + # 去除背景杂色 + lower_green = np.array([240, 240, 240]) + upper_green = np.array([255, 255, 255]) + mask = cv2.inRange(self.device.image, lower_green, upper_green) + res_img = cv2.bitwise_and(self.device.image, self.device.image, mask=mask) + res_img = cv2.cvtColor(res_img, cv2.COLOR_RGB2BGR) + + 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 get_damage(self, image): + hsv_image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) + # Note 默认战斗场景伤害数字颜色 + 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) + + damage = self.O_DAMAGE.ocr_digit(res_img) + return damage + if __name__ == "__main__": + import cv2, numpy as np from module.config.config import Config from module.device.device import Device - import cv2, numpy as np - - # config = Config('却把烟花嗅') - # device = Device(config) - - image = cv2.imread('E:/5.png') - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) - - hsv_image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) - lower_green = np.array([167, 100, 200]) - upper_green = np.array([180, 225, 225]) - mask = cv2.inRange(hsv_image, lower_green, upper_green) - res_img = cv2.bitwise_and(image, image, mask=mask) - cv2.imshow('res', res_img) - cv2.waitKey() + config = Config('oas1') + device = Device(config) - # t = ScriptTask(config, device) + # image = cv2.imread('E:/5.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) + # 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) From e25539ba17b7dfeea4fb714166d2d88ba1bbc903 Mon Sep 17 00:00:00 2001 From: ChengRanORZ Date: Sun, 8 Jun 2025 07:55:09 +0800 Subject: [PATCH 06/21] add pic --- .../res/res_abyss_enemy_info_exit.png | Bin 0 -> 2367 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tasks/AbyssShadows/res/res_abyss_enemy_info_exit.png 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 0000000000000000000000000000000000000000..b8884f8ec03265525f8f786f0c8579798cc19bd5 GIT binary patch literal 2367 zcmV-F3BdM=P)V>U4h#&rc_l?b?oJ93kx>c^NFXt}xPSpSU}RwUb`3-_FkIFREMa58 zqZS+BECN~xq(1<$NIIC!2r@$?3Br~DvCB&eN`Pz~5Ieb`2*d`un1QD#B*+=a_5rdL z(vjFnNbHo<;$$E@2gr^`_$_}v1B1mE1_tJd2(hGT3=G@b85kt)AjDJ_GBEJBGB9kr zj1c?m5B4vk*rAftiV~2BpS~kQL1IxNSeh9WplN9g44;-WFz`k)FbH2@VBm^|xC0DW zi-7@6VmJ{30AwjPdeHYMD*ylh32;bRa{vGi!~g&e!~vBn4jTXf2n0z)K~zYIg;vXM zT*nbTRbAb8?wuKONDjpz^`>N7wiGdz?HGX{$STOg2@oJofUL5}2jm0t3;BwyvPiPY z&W2Y;49AA!$cY`jNlBDR4~Lu?&dhyvcV$5tk_90-iv}89eYzf}3q>FP?)Ss%55@=6 zai~j$s>T?d6=se^#73+T5JL1mmQ@IK3?V|*h;cDhp+c<}3k0Hyz9A@?G)?jw`z-z2E zuxU18S<8Gjw{d3qdzU-Cu7F!Gi86r^0;&p80RS+Dh|wZzU@YuX);e+Gc<=H#Tv zb#=LS`8-})$73L2Fg@I;GE^TOTEl0$r+)Qjr!!|jEi!XtGbF8O(`nhR!s?0k zLZ@NHbZmCwQg7);&*92hd=p8pZ5L7iswj#me(#Xyo0KI$M2sNTUT5v?*TWy)o$Sxp zCPuhGP>o8Shm+P96B6pa{Zeb=6j_F0g_3^la$X}w%dBimP7E`LvZjVR3htp1vpK%7 z_VYK|&#Xm`WH2XoK@!C1|1~j9bL^MjX^?7e&Q=3l?qyE}B`tT+k7 ziLNeEV!sim7`MLCQWa)^k76t}-^&E&EPv_&vKiN9Cy_e^Or$|Jq8Y4+H zo8|x6A|KrP8g*NoCj77@P$o2l&n~(7EKVyqw;1S~#Hu*bppWH^o!$5DOhMx%? z*wY*w6UW?UnzM$5ClV=EOflABXSW=VvRPr~nr3;P%+s>)MAE(2XNkCwcgef*g!WtYy=T>4b@;*)=eoqk!A`@3GMFAjHga8IjJg$*-l zh%rVG#~5m#SM{W-MkXbJ6@WPeqhQCa_Cn)(m*wO!B<7*mt6KX*KN{PMX9>~Yy^k^{ z(qP0 zq_+eQt!lHfj1T~5SvBfOds0{zLX6RKRjCgFU?wr-Mcn*cDt?;tv`Cw2<6GzZEop-@MMChLnEPscu8Du&xdv45DZ+`pZpF*hU_w(!5>+S6@9?SeZXBjcG zYW>;P^rOw$q-<8usV`5Rq_lda0U~1YLZ2wR>q%C$qu*nN*~f z4MU=5X*NMrZg1Cv0f_8g++uY%}?F}phOU{PRNKE)jFC^hof+R7DozkYzzw%^Kd#1Ue!m* zy@^VRkTU?#AEMm^0gl+2YIjoHJupqveQ`~sX0ik)#*kJO8S>L{EOM2^qiq_7jM1<4 z6oEpKDD7YC&kk~Xr{9=Qjp2GcU4D~fW!}|rr@Bc+{l9PSesE(pD#WTG!$i;k4MqdX z*(A(HE?p&?#G|nRq#88_5LdpA5kYx!Z&YvY8Bzxyt&tG3)5~*bR;7_NPIdjH+p%5C8=b6jf9JHB0Vm3Uy0y`~4f)2e($g le Date: Sun, 8 Jun 2025 16:00:00 +0800 Subject: [PATCH 07/21] =?UTF-8?q?=E5=A2=9E=E5=8A=A0ocr=20method(CF=5FRGB,C?= =?UTF-8?q?F=5FHSV),=E7=94=A8=E4=BA=8Epre=5Fprocess.CF(Color=20Filter).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- module/atom/ocr.py | 68 ++++++++++++++++++++++++--------- module/ocr/base_ocr.py | 41 ++++++++++++++------ tasks/AbyssShadows/assets.py | 10 ++--- tasks/AbyssShadows/res/ocr.json | 10 ++--- tasks/base_task.py | 4 +- 5 files changed, 92 insertions(+), 41 deletions(-) diff --git a/module/atom/ocr.py b/module/atom/ocr.py index 00ea90677..fe83ab11e 100644 --- a/module/atom/ocr.py +++ b/module/atom/ocr.py @@ -5,10 +5,10 @@ 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 from module.logger import logger - +from sympy.codegen.ast import break_ class RuleOcr(Digit, DigitCounter, Duration, Single, Full): @@ -16,28 +16,60 @@ class RuleOcr(Digit, DigitCounter, Duration, Single, Full): 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_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)]) + 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 _: 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 _: + 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 _: 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 _: + return None def coord(self) -> tuple: """ @@ -56,7 +88,5 @@ def coord(self) -> tuple: return x, y - if __name__ == "__main__": pass - diff --git a/module/ocr/base_ocr.py b/module/ocr/base_ocr.py index af5b711f5..ab5130c58 100644 --- a/module/ocr/base_ocr.py +++ b/module/ocr/base_ocr.py @@ -39,27 +39,42 @@ class OcrMode(Enum): DURATION = 5 # str: "Duration" -class OcrMethod(): - _reg = r"^([^()]+)(?:$(.*?)$)?$" - _METHODS = { - "Default": 1, - "ColorFilter": 2, - } - _method = "Default" - _val = None +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: - self._method = "Default" return import re match = re.match(self._reg, val) if not match: - self._method = "Default" return - self._method = match.group(1) + 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: lang: str = "ch" @@ -279,6 +294,7 @@ def detect_text(self, image) -> str: text=f'[{results}]') return results + # def test(): # # strings = ["探", "索"] # # keyword = "探索" @@ -304,3 +320,6 @@ def detect_text(self, image) -> str: # else: # return None # print(test()) + +if __name__ == '__main__': + print(OcrMethod.DEFAULT) diff --git a/tasks/AbyssShadows/assets.py b/tasks/AbyssShadows/assets.py index d173fa2ac..cdd0ce632 100644 --- a/tasks/AbyssShadows/assets.py +++ b/tasks/AbyssShadows/assets.py @@ -126,15 +126,15 @@ class AbyssShadowsAssets: # Ocr Rule Assets # 伤害 - O_DAMAGE = RuleOcr(roi=(50,110,300,50), area=(50,110,300,50), mode="Digit", method="Default", keyword="", name="damage") + 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="Default", keyword="封印", name="dragon_done") + 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="Default", keyword="封印", name="peacock_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="Default", keyword="封印", name="fox_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="Default", keyword="封印", name="leopard_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 diff --git a/tasks/AbyssShadows/res/ocr.json b/tasks/AbyssShadows/res/ocr.json index 1bdac0cbd..5955099c5 100644 --- a/tasks/AbyssShadows/res/ocr.json +++ b/tasks/AbyssShadows/res/ocr.json @@ -4,7 +4,7 @@ "roiFront": "50,110,300,50", "roiBack": "50,110,300,50", "mode": "Digit", - "method": "Default", + "method": "CF_HSV(0980B4,1ED2FF)", "keyword": "", "description": "伤害" }, @@ -13,7 +13,7 @@ "roiFront": "130,160,180,360", "roiBack": "130,160,180,360", "mode": "Single", - "method": "Default", + "method": "CF_RGB(CCCCCC,FFFFFF)", "keyword": "封印", "description": "神龙暗域已完成" }, @@ -22,7 +22,7 @@ "roiFront": "420,160,180,360", "roiBack": "420,160,180,360", "mode": "Single", - "method": "Default", + "method": "CF_RGB(CCCCCC,FFFFFF)", "keyword": "封印", "description": "孔雀暗域已完成" }, @@ -31,7 +31,7 @@ "roiFront": "680,160,180,360", "roiBack": "680,160,180,360", "mode": "Single", - "method": "Default", + "method": "CF_RGB(CCCCCC,FFFFFF)", "keyword": "封印", "description": "白藏主暗域已完成" }, @@ -40,7 +40,7 @@ "roiFront": "1000,160,180,360", "roiBack": "1000,160,180,360", "mode": "Single", - "method": "Default", + "method": "CF_RGB(CCCCCC,FFFFFF)", "keyword": "封印", "description": "黑豹暗域已完成" } 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 From 8080a5c6df00421df4b9cb984078478197f386f3 Mon Sep 17 00:00:00 2001 From: ChengRanORZ Date: Sun, 8 Jun 2025 18:27:28 +0800 Subject: [PATCH 08/21] =?UTF-8?q?=E5=B0=8F=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tasks/AbyssShadows/config.py | 97 +++++++++++++++------ tasks/AbyssShadows/script_task.py | 134 +++++++++++++++++------------- 2 files changed, 147 insertions(+), 84 deletions(-) diff --git a/tasks/AbyssShadows/config.py b/tasks/AbyssShadows/config.py index f620e9076..11dc6198d 100644 --- a/tasks/AbyssShadows/config.py +++ b/tasks/AbyssShadows/config.py @@ -149,31 +149,39 @@ def parse_order(value: str = None) -> list: super().__init__(parse_order(v)) def parse2str(self): - ret = "" - for item in self: - ret += ';' - ret += item.value - return ret[1:] + return ';'.join(item.value for item in self) class Condition: - # _is_time_out: bool = False - _time = -1 - _timer: Timer = None - # _is_damage_enough: bool = False - _damage_max: int = -1 - # True 立即退出 - # False 任何情况下,该条件检查不通过 - _dont_need_check: bool = False - - # 存储结果,用于后期查询 - _condition_result: bool = False + """ + Condition类用于管理一个条件对象,该对象可以基于时间或伤害值来判断条件是否满足。 + 它支持立即满足条件、基于时间满足条件和基于伤害值满足条件三种方式。 + """ def __init__(self, value: str): - # 为True时,相当于没有策略(所有情况都通过条件检查) - if value == "TRUE": + """ + 初始化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 == "FALSE": + elif value.upper() == "FALSE": # 任何情况都不通过条件检查 self._dont_need_check = False elif len(value) <= 3: @@ -190,40 +198,65 @@ def __init__(self, value: str): 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)) # 尝试主动开启狭间-区别于游戏中的自动开启狭间功能 try_start_abyss_shadows: bool = Field(default=False, description='try_start_abyss_shadows_help') # 难度 @@ -238,12 +271,14 @@ class ProcessManage(ConfigBase): # 2 3 # 4 5 6 # 之间用-分隔,不同怪物用;分隔 - # 小蛇使用E,-后面表示打几只,例如E-2表示打两只小蛇 + # 未实现-->小蛇使用E,-后面表示打几只,例如E-2表示打两只小蛇 # 例如 A-1;B-2;C-3... attack_order: str = Field(default='', description='attack_order_help') # 标记主怪 # EnemyType, 多个用;分隔 mark_main: MarkMainConfig = Field(default=MarkMainConfig.BOSS_ONLY, description='mark_main_help') + # 是否启用切换御魂 + switch_group_team: bool = Field(default=False, description='switch_group_team_help') # 首领预设 preset_boss: str = Field(default='', description='preset_boss_help') # 副将预设 @@ -251,7 +286,7 @@ class ProcessManage(ConfigBase): # 精英预设 preset_elite: str = Field(default='', description='preset_elite_help') # 小蛇预设 - preset_snake: str = Field(default='', description='preset_snake_help') + # preset_snake: str = Field(default='', description='preset_snake_help') # 首领策略 等待打完/时间到了退出/伤害足够退出/秒退 strategy_boss: str = Field(default='', description='strategy_boss_help') # 副将策略 @@ -280,7 +315,15 @@ def is_need_mark_main(self, enemy_type: EnemyType) -> bool: case _: return False - def parse_strategy(self, strategy: str): + 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) diff --git a/tasks/AbyssShadows/script_task.py b/tasks/AbyssShadows/script_task.py index 00bccaeb2..7d891e297 100644 --- a/tasks/AbyssShadows/script_task.py +++ b/tasks/AbyssShadows/script_task.py @@ -7,15 +7,12 @@ from datetime import datetime from future.backports.datetime import timedelta -from module.exception import TaskEnd +from module.exception import TaskEnd, RequestHumanTakeover from module.base.timer import Timer from module.logger import logger -from sympy.plotting.intervalmath import interval from tasks.AbyssShadows.assets import AbyssShadowsAssets from tasks.AbyssShadows.config import AbyssShadows, EnemyType, AreaType, Code, AbyssShadowsDifficulty, \ CodeList, IndexMap -import numpy as np -import cv2 from tasks.Component.GeneralBattle.config_general_battle import GreenMarkType from tasks.Component.GeneralBattle.general_battle import GeneralBattle from tasks.Component.SwitchSoul.switch_soul import SwitchSoul @@ -55,15 +52,14 @@ def run(self): 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("") + logger.info("Timeout threshold: 2h (force quit if not started)") self.set_next_run(task='AbyssShadows', finish=False, server=True, success=True) raise TaskEnd @@ -76,11 +72,14 @@ def run(self): # 判断各个区域是否可用 area_available = None + self.screenshot() for area in AreaType: if self.is_area_done(area): - self.unavailable_list.append(IndexMap[area.name].value) + self.unavailable_list += CodeList(IndexMap[area.name].value) + logger.info(f"{area.name} unavailable") continue area_available = area + logger.info(f"{area.name} available") self.update_list() @@ -95,16 +94,16 @@ def run(self): 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_appear(self.I_WAIT_TO_START, wait_time=2) # 切换御魂 self.switch_soul_in_as() - # - self.wait_until_disappear(self.I_WAIT_TO_START) + # 等待可进攻时间 + 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() # 保持好习惯,一个任务结束了就返回到庭院,方便下一任务的开始 @@ -123,12 +122,13 @@ def update_list(self): 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): 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("Flash list done") + 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 = '' @@ -170,6 +170,11 @@ def change_area(self, area_name: AreaType) -> bool: current_area = self.check_current_area() if current_area == area_name: break + + if self.is_area_done(area_name): + logger.info(f"change area:{area_name.name} is done") + self.unavailable_list += CodeList(IndexMap[area_name.name].value) + return False # 切换区域界面 if self.appear(self.I_ABYSS_DRAGON_OVER) or self.appear(self.I_ABYSS_DRAGON): self.select_boss(area_name) @@ -179,6 +184,14 @@ def change_area(self, area_name: AreaType) -> bool: if self.appear_then_click(self.I_CHANGE_AREA, interval=4): logger.info(f"Click {self.I_CHANGE_AREA.name}") continue + # 大概率没用,保险点加上吧 + 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 return True @@ -221,7 +234,6 @@ def select_boss(self, area_name: AreaType) -> bool: while 1: self.screenshot() # 区域图片与入口图片不一致,使用点击进去 - if self.appear(self.I_ABYSS_DRAGON_OVER) or self.appear(self.I_ABYSS_DRAGON): is_click = False match area_name: @@ -460,19 +472,8 @@ def get_preset(enemy_type: EnemyType): _timer_battle.start() # 生成退出条件 - def generate_quit_condition(enemy_type): - strategy = None - match enemy_type: - case EnemyType.BOSS: - strategy = self.config.model.abyss_shadows.process_manage.strategy_boss - case EnemyType.ELITE: - strategy = self.config.model.abyss_shadows.process_manage.strategy_elite - case EnemyType.GENERAL: - strategy = self.config.model.abyss_shadows.process_manage.strategy_general - return self.config.model.abyss_shadows.process_manage.parse_strategy(strategy) - - # 因为条件中存在时间相关,所以在点击准备按钮后直接生成 - condition = generate_quit_condition(enemy_type) + # 因为条件中可能是时间相关,所以在点击准备按钮后直接生成,尽量减小误差 + condition = self.config.model.abyss_shadows.process_manage.generate_quit_condition(enemy_type) logger.info(f"enemyType{enemy_type}--{condition}") # 标记主怪 @@ -491,7 +492,7 @@ def generate_quit_condition(enemy_type): while True: self.screenshot() if need_check_damage: - _cur_damage = self.get_damage(self.device.image) + _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() @@ -499,6 +500,8 @@ def generate_quit_condition(enemy_type): 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_then_click(self.I_WIN, interval=1): @@ -515,6 +518,7 @@ def generate_quit_condition(enemy_type): break if condition.is_passed() or (not _timer_battle.reached()): # 通过条件结束的,视其为完成 + # 条件未通过且战斗时间不足3分钟的,极大可能是打死了,视之为完成 success = True logger.info(f"{enemy_type.name} DONE") @@ -550,7 +554,7 @@ def switch_preset_team_with_str(self, v: str): def switch_soul_in_as(self): if self.switch_soul_done: return - if not self.config.model.abyss_shadows.switch_soul_config.enable: + if not self.config.model.abyss_shadows.process_manage.switch_group_team: self.switch_soul_done = True return @@ -560,7 +564,7 @@ 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.") - return + raise RequestHumanTakeover self.run_switch_soul((int(l[0]), int(l[1]))) self.ui_click_until_disappear(self.I_ABYSS_SHIKI, interval=2) @@ -578,6 +582,7 @@ def switch_soul(v: str): 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()) @@ -591,13 +596,17 @@ def check_available(self, item_code: Code): return True def is_area_done(self, area_type: AreaType): - self.screenshot() + # 不再切换区域界面直接返回 + if not self.appear(self.I_ABYSS_DRAGON) and not self.appear(self.I_ABYSS_DRAGON_OVER): + return False + # + res_img = self.device.image # 去除背景杂色 - lower_green = np.array([240, 240, 240]) - upper_green = np.array([255, 255, 255]) - mask = cv2.inRange(self.device.image, lower_green, upper_green) - res_img = cv2.bitwise_and(self.device.image, self.device.image, mask=mask) - res_img = cv2.cvtColor(res_img, cv2.COLOR_RGB2BGR) + # lower_green = np.array([240, 240, 240]) + # upper_green = np.array([255, 255, 255]) + # mask = cv2.inRange(self.device.image, lower_green, upper_green) + # res_img = cv2.bitwise_and(self.device.image, self.device.image, mask=mask) + # res_img = cv2.cvtColor(res_img, cv2.COLOR_RGB2BGR) match area_type: case AreaType.DRAGON: @@ -615,16 +624,16 @@ def is_area_done(self, area_type: AreaType): return False - def get_damage(self, image): - hsv_image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) - # Note 默认战斗场景伤害数字颜色 - 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) - - damage = self.O_DAMAGE.ocr_digit(res_img) - return damage + # def get_damage(self, image): + # hsv_image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) + # # Note 默认战斗场景伤害数字颜色 + # 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) + # + # damage =self.O_DAMAGE.ocr(res_img) + # return damage if __name__ == "__main__": @@ -635,8 +644,8 @@ def get_damage(self, image): config = Config('oas1') device = Device(config) - # image = cv2.imread('E:/5.png') - # image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + image = cv2.imread('E:/f.png') + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # # hsv_image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) # @@ -649,6 +658,17 @@ def get_damage(self, image): # cv2.waitKey() t = ScriptTask(config, device) + + 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) @@ -656,13 +676,13 @@ def get_damage(self, image): # 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) + # 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) From 888699e8643449cc6743a6cb21b66904f1644918 Mon Sep 17 00:00:00 2001 From: ChengRanORZ Date: Sun, 8 Jun 2025 19:50:39 +0800 Subject: [PATCH 09/21] fix --- tasks/AbyssShadows/res/image.json | 4 ++-- tasks/AbyssShadows/script_task.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tasks/AbyssShadows/res/image.json b/tasks/AbyssShadows/res/image.json index b8ec4c139..32d7876b3 100644 --- a/tasks/AbyssShadows/res/image.json +++ b/tasks/AbyssShadows/res/image.json @@ -164,8 +164,8 @@ { "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": "进攻中" diff --git a/tasks/AbyssShadows/script_task.py b/tasks/AbyssShadows/script_task.py index 7d891e297..6fd36d6c4 100644 --- a/tasks/AbyssShadows/script_task.py +++ b/tasks/AbyssShadows/script_task.py @@ -91,7 +91,7 @@ def run(self): if not self.select_boss(area_available): logger.warning("Failed to enter abyss shadows") self.goto_main() - self.set_next_run(task='AbyssShadows', finish=False, server=True, success=False) + self.set_next_run(task='AbyssShadows', finish=False, server=False, success=False) raise TaskEnd # 集结中图片 @@ -660,7 +660,7 @@ def is_area_done(self, area_type: AreaType): t = ScriptTask(config, device) area_type = AreaType.DRAGON - t.unavailable_list+=CodeList(IndexMap[area_type.name].value) + t.unavailable_list += CodeList(IndexMap[area_type.name].value) print(f"{t.unavailable_list=}") # t.screenshot() From 2582f5334395cd7f7e8cff4a2ec1841907d57778 Mon Sep 17 00:00:00 2001 From: ChengRanORZ Date: Sun, 8 Jun 2025 19:58:26 +0800 Subject: [PATCH 10/21] fix --- module/ocr/base_ocr.py | 1 + 1 file changed, 1 insertion(+) diff --git a/module/ocr/base_ocr.py b/module/ocr/base_ocr.py index ab5130c58..1858dca4d 100644 --- a/module/ocr/base_ocr.py +++ b/module/ocr/base_ocr.py @@ -37,6 +37,7 @@ class OcrMode(Enum): DIGIT = 3 # str: "Digit" DIGITCOUNTER = 4 # str: "DigitCounter" DURATION = 5 # str: "Duration" + QUANTITY = 6 # str: "Quantity" class OcrMethodType(Enum): From 0fb635544195fe34a710f4cf0d54127eaed9aa72 Mon Sep 17 00:00:00 2001 From: ChengRanORZ Date: Sun, 8 Jun 2025 20:11:54 +0800 Subject: [PATCH 11/21] fix --- tasks/AbyssShadows/assets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/AbyssShadows/assets.py b/tasks/AbyssShadows/assets.py index cdd0ce632..ab3a7e655 100644 --- a/tasks/AbyssShadows/assets.py +++ b/tasks/AbyssShadows/assets.py @@ -87,7 +87,7 @@ class AbyssShadowsAssets: # 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") # 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") # 黑豹领域 From 8e83739e56fbec046543522bba15f84fcc8b3d0e Mon Sep 17 00:00:00 2001 From: ChengRanORZ Date: Mon, 9 Jun 2025 10:43:07 +0800 Subject: [PATCH 12/21] Complete missing updates --- module/atom/ocr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/atom/ocr.py b/module/atom/ocr.py index bf275de42..ba140165e 100644 --- a/module/atom/ocr.py +++ b/module/atom/ocr.py @@ -5,7 +5,7 @@ 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 From e0ef8a25cc99a792c00cbbc8de3f518b58a2f7f5 Mon Sep 17 00:00:00 2001 From: ChengRanORZ Date: Mon, 9 Jun 2025 18:27:08 +0800 Subject: [PATCH 13/21] remove useless --- tasks/AbyssShadows/config.py | 31 ++++++++------------ tasks/AbyssShadows/script_task.py | 48 ++++++++++++------------------- 2 files changed, 30 insertions(+), 49 deletions(-) diff --git a/tasks/AbyssShadows/config.py b/tasks/AbyssShadows/config.py index 11dc6198d..12ae26ab2 100644 --- a/tasks/AbyssShadows/config.py +++ b/tasks/AbyssShadows/config.py @@ -273,26 +273,28 @@ class ProcessManage(ConfigBase): # 之间用-分隔,不同怪物用;分隔 # 未实现-->小蛇使用E,-后面表示打几只,例如E-2表示打两只小蛇 # 例如 A-1;B-2;C-3... - attack_order: str = Field(default='', description='attack_order_help') + 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') # 是否启用切换御魂 - switch_group_team: bool = Field(default=False, description='switch_group_team_help') + enable_switch_soul_in_as: bool = Field(default=False, description='enable_switch_soul_in_as_help') # 首领预设 - preset_boss: str = Field(default='', description='preset_boss_help') + preset_boss: str = Field(default='6,1', description='preset_boss_help') # 副将预设 - preset_general: str = Field(default='', description='preset_general_help') + preset_general: str = Field(default='6,2', description='preset_general_help') # 精英预设 - preset_elite: str = Field(default='', description='preset_elite_help') + preset_elite: str = Field(default='6,3', description='preset_elite_help') # 小蛇预设 # preset_snake: str = Field(default='', description='preset_snake_help') # 首领策略 等待打完/时间到了退出/伤害足够退出/秒退 - strategy_boss: str = Field(default='', description='strategy_boss_help') + # 可用值: 'TRUE', 'FALSE', 时间(秒),或最大伤害值 + # 详见类 {Condition} + strategy_boss: str = Field(default='FALSE', description='strategy_boss_help') # 副将策略 - strategy_general: str = Field(default='', description='strategy_general_help') + strategy_general: str = Field(default='30', description='strategy_general_help') # 精英策略 - strategy_elite: str = Field(default='', description='strategy_elite_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 枚举值 @@ -332,19 +334,10 @@ def generate_quit_condition(self, enemy_type: EnemyType): class SavedParams(ConfigBase): # 已完成 done: str = Field(default='', description='done_help') - # 失败 - fail: str = Field(default='', description='fail_help') # 已知的已经打完的 unavailable: str = Field(default='', description='unavailable_help') - # def save(self): - # self.today = datetime.today().strftime('yyyy-mm-dd') - # self.config.save() - def push_to(self, item, l): - if len(l) > 0: - l += ';' - l += item class AbyssShadows(ConfigBase): @@ -352,5 +345,5 @@ class AbyssShadows(ConfigBase): abyss_shadows_time: AbyssShadowsTime = Field(default_factory=AbyssShadowsTime) 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) + # general_battle_config: GeneralBattleConfig = Field(default_factory=GeneralBattleConfig) + # switch_soul_config: SwitchSoulConfig = Field(default_factory=SwitchSoulConfig) diff --git a/tasks/AbyssShadows/script_task.py b/tasks/AbyssShadows/script_task.py index 6fd36d6c4..9ee72df17 100644 --- a/tasks/AbyssShadows/script_task.py +++ b/tasks/AbyssShadows/script_task.py @@ -30,18 +30,20 @@ class ScriptTask(GeneralBattle, GameUi, SwitchSoul, AbyssShadowsAssets): EnemyType.GENERAL: 4, # 最少副将战斗次数 EnemyType.ELITE: 6 # 最少精英战斗次数 } - # - cur_area = None - # - cur_preset = None - # process list - ps_list: CodeList = CodeList('') - # 已完成 列表 - done_list: CodeList = CodeList('') - # 已知的 已经被打完的 列表 - unavailable_list: CodeList = CodeList('') - # - switch_soul_done = False + def __init__(self): + super().__init__() + # + self.cur_area = None + # + 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 def run(self): """ 狭间暗域主函数 @@ -98,7 +100,7 @@ def run(self): self.wait_until_appear(self.I_WAIT_TO_START, wait_time=2) # 切换御魂 self.switch_soul_in_as() - # 等待可进攻时间 + # self.device.stuck_record_add('BATTLE_STATUS_S') # 等待战斗开始 self.wait_until_appear(self.I_IS_ATTACK, wait_time=180) @@ -554,7 +556,7 @@ def switch_preset_team_with_str(self, v: str): def switch_soul_in_as(self): if self.switch_soul_done: return - if not self.config.model.abyss_shadows.process_manage.switch_group_team: + if not self.config.model.abyss_shadows.process_manage.enable_switch_soul_in_as: self.switch_soul_done = True return @@ -601,12 +603,6 @@ def is_area_done(self, area_type: AreaType): return False # res_img = self.device.image - # 去除背景杂色 - # lower_green = np.array([240, 240, 240]) - # upper_green = np.array([255, 255, 255]) - # mask = cv2.inRange(self.device.image, lower_green, upper_green) - # res_img = cv2.bitwise_and(self.device.image, self.device.image, mask=mask) - # res_img = cv2.cvtColor(res_img, cv2.COLOR_RGB2BGR) match area_type: case AreaType.DRAGON: @@ -624,16 +620,8 @@ def is_area_done(self, area_type: AreaType): return False - # def get_damage(self, image): - # hsv_image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) - # # Note 默认战斗场景伤害数字颜色 - # 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) - # - # damage =self.O_DAMAGE.ocr(res_img) - # return damage + + if __name__ == "__main__": From e01c269cb39fec5fc9745927cf318ac296c73a9c Mon Sep 17 00:00:00 2001 From: ChengRanORZ Date: Thu, 12 Jun 2025 22:04:13 +0800 Subject: [PATCH 14/21] add missed params --- tasks/AbyssShadows/script_task.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tasks/AbyssShadows/script_task.py b/tasks/AbyssShadows/script_task.py index 9ee72df17..e6c0a82fb 100644 --- a/tasks/AbyssShadows/script_task.py +++ b/tasks/AbyssShadows/script_task.py @@ -10,6 +10,8 @@ from module.exception import TaskEnd, RequestHumanTakeover from module.base.timer import Timer 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 @@ -30,8 +32,9 @@ class ScriptTask(GeneralBattle, GameUi, SwitchSoul, AbyssShadowsAssets): EnemyType.GENERAL: 4, # 最少副将战斗次数 EnemyType.ELITE: 6 # 最少精英战斗次数 } - def __init__(self): - super().__init__() + + def __init__(self, config: Config, device: Device): + super().__init__(config, device) # self.cur_area = None # @@ -621,9 +624,6 @@ def is_area_done(self, area_type: AreaType): return False - - - if __name__ == "__main__": import cv2, numpy as np from module.config.config import Config From 4891580b3337f25de2c5be263f3582fe9c5e7710 Mon Sep 17 00:00:00 2001 From: ChengRanORZ Date: Fri, 13 Jun 2025 20:23:46 +0800 Subject: [PATCH 15/21] fix error --- module/atom/ocr.py | 47 +++++++++++++++++++------------ tasks/AbyssShadows/script_task.py | 22 +++++++++++++-- 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/module/atom/ocr.py b/module/atom/ocr.py index ba140165e..4c4a630d6 100644 --- a/module/atom/ocr.py +++ b/module/atom/ocr.py @@ -10,7 +10,6 @@ from module.logger import logger - class RuleOcr(Digit, DigitCounter, Duration, Single, Full, Quantity): def __init__(self, *args, **kwargs): @@ -28,7 +27,7 @@ def pre_process(self, image): mask = cv2.inRange(image, lower, upper) res_img = cv2.bitwise_and(image, image, mask=mask) return res_img - case OcrMethodType.CF_RGB: + 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)]) @@ -42,24 +41,38 @@ def pre_process(self, 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: """ @@ -78,7 +91,5 @@ def coord(self) -> tuple: return x, y - if __name__ == "__main__": pass - diff --git a/tasks/AbyssShadows/script_task.py b/tasks/AbyssShadows/script_task.py index e6c0a82fb..7349130ff 100644 --- a/tasks/AbyssShadows/script_task.py +++ b/tasks/AbyssShadows/script_task.py @@ -203,7 +203,23 @@ def change_area(self, area_name: AreaType) -> bool: def goto_main(self): ''' 保持好习惯,一个任务结束了就返回庭院,方便下一任务的开始或者是出错重启 ''' - self.ui_get_current_page() + + # 保证在狭间暗域界面 + while 1: + self.screenshot() + if self.appear(self.I_ABYSS_NAVIGATION): + 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, timeout=2) + continue + if self.appear_then_click(self.I_ABYSS_MAP_EXIT, interval=2): + continue + if self.appear_then_click(self.I_ABYSS_ENEMY_INFO_EXIT, interval=2): + continue + + # logger.info("Exiting abyss_shadows") self.ui_goto(page_main) @@ -423,8 +439,10 @@ def open_navigation(self): def execute(self, item_code: Code): area = item_code.get_areatype() need_change_area = (self.cur_area is None) or (area != self.cur_area) + if need_change_area: - self.change_area(area) + if not self.change_area(area): + return False self.cur_area = area # 当前应当在正确的区域 # From 28503404ce4ddaca5eb9b9b4f107b2afc55bd194 Mon Sep 17 00:00:00 2001 From: ChengRanORZ Date: Sat, 14 Jun 2025 20:07:31 +0800 Subject: [PATCH 16/21] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=8C=91=E6=88=98?= =?UTF-8?q?=E7=BB=93=E6=9D=9F=E6=A3=80=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tasks/AbyssShadows/assets.py | 2 ++ tasks/AbyssShadows/res/image.json | 9 +++++++++ tasks/AbyssShadows/script_task.py | 20 +++++++++++++++++--- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/tasks/AbyssShadows/assets.py b/tasks/AbyssShadows/assets.py index ab3a7e655..f5fc1cbfd 100644 --- a/tasks/AbyssShadows/assets.py +++ b/tasks/AbyssShadows/assets.py @@ -88,6 +88,8 @@ class AbyssShadowsAssets: 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=(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") # 黑豹领域 diff --git a/tasks/AbyssShadows/res/image.json b/tasks/AbyssShadows/res/image.json index 32d7876b3..acf888d53 100644 --- a/tasks/AbyssShadows/res/image.json +++ b/tasks/AbyssShadows/res/image.json @@ -170,6 +170,15 @@ "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", diff --git a/tasks/AbyssShadows/script_task.py b/tasks/AbyssShadows/script_task.py index 7349130ff..0089820da 100644 --- a/tasks/AbyssShadows/script_task.py +++ b/tasks/AbyssShadows/script_task.py @@ -15,7 +15,6 @@ from tasks.AbyssShadows.assets import AbyssShadowsAssets from tasks.AbyssShadows.config import AbyssShadows, EnemyType, AreaType, Code, AbyssShadowsDifficulty, \ CodeList, IndexMap -from tasks.Component.GeneralBattle.config_general_battle import GreenMarkType from tasks.Component.GeneralBattle.general_battle import GeneralBattle from tasks.Component.SwitchSoul.switch_soul import SwitchSoul from tasks.GameUi.game_ui import GameUi @@ -25,6 +24,10 @@ MAX_BATTLE_COUNT = 2 +class AbyssShadowsFinished(Exception): + pass + + class ScriptTask(GeneralBattle, GameUi, SwitchSoul, AbyssShadowsAssets): # min_count = { @@ -77,8 +80,8 @@ def run(self): # 判断各个区域是否可用 area_available = None - self.screenshot() for area in AreaType: + self.screenshot() if self.is_area_done(area): self.unavailable_list += CodeList(IndexMap[area.name].value) logger.info(f"{area.name} unavailable") @@ -109,7 +112,10 @@ def run(self): self.wait_until_appear(self.I_IS_ATTACK, wait_time=180) self.device.stuck_record_clear() # - self.process() + try: + self.process() + except AbyssShadowsFinished: + pass # 保持好习惯,一个任务结束了就返回到庭院,方便下一任务的开始 self.goto_main() @@ -171,6 +177,8 @@ def change_area(self, area_name: AreaType) -> bool: ''' while 1: self.screenshot() + if self.appear(self.I_CHECK_FINISH): + raise AbyssShadowsFinished # 判断当前区域是否正确 current_area = self.check_current_area() if current_area == area_name: @@ -310,6 +318,8 @@ def goto_enemy(self, item_code: Code) -> bool: # 点击前往按钮,知道该按钮消失或出现"挑战"字样 while 1: self.screenshot() + if self.appear(self.I_CHECK_FINISH): + raise AbyssShadowsFinished if self.appear(self.I_ABYSS_FIRE): break if self.appear(self.I_ENSURE_BUTTON): @@ -326,6 +336,8 @@ def attack_enemy(self): # 点击战斗按钮 while 1: self.screenshot() + if self.appear(self.I_CHECK_FINISH): + raise AbyssShadowsFinished # if self.appear(self.I_ABYSS_ENEMY_FIRE): self.click(self.I_ABYSS_ENEMY_FIRE, interval=0.4) @@ -427,6 +439,8 @@ def get_next(self) -> [Code, None]: def open_navigation(self): while True: self.screenshot() + if self.appear(self.I_CHECK_FINISH): + raise AbyssShadowsFinished if self.appear(self.I_ABYSS_MAP): break if self.appear(self.I_ABYSS_NAVIGATION): From 6faac510d4508f5ffaca6184fd1f98dd8aa51af3 Mon Sep 17 00:00:00 2001 From: ChengRanORZ Date: Sat, 14 Jun 2025 20:12:26 +0800 Subject: [PATCH 17/21] fix --- tasks/AbyssShadows/res/res_check_finish.png | Bin 0 -> 4785 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tasks/AbyssShadows/res/res_check_finish.png diff --git a/tasks/AbyssShadows/res/res_check_finish.png b/tasks/AbyssShadows/res/res_check_finish.png new file mode 100644 index 0000000000000000000000000000000000000000..84cdd0f8de9b615297f696d947bdcb245a88927e GIT binary patch literal 4785 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 ChN5i% literal 0 HcmV?d00001 From 19719c3196f51391a6c6422c8ce909c715288ead Mon Sep 17 00:00:00 2001 From: ChengRanORZ Date: Sun, 15 Jun 2025 09:59:20 +0800 Subject: [PATCH 18/21] =?UTF-8?q?1.=20=E9=85=8D=E7=BD=AE=E5=86=85=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E4=BF=9D=E5=AD=98=E6=97=B6=E9=97=B4,=E9=98=B2?= =?UTF-8?q?=E6=AD=A2=E9=94=99=E8=AF=AF=E9=80=80=E5=87=BA=E6=9C=AA=E6=B8=85?= =?UTF-8?q?=E7=A9=BA=E5=8F=82=E6=95=B0=E5=AF=BC=E8=87=B4=E4=B8=8B=E6=AC=A1?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E5=87=BA=E9=94=99=202.=20=E6=8C=81=E7=BB=AD?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=B4=BB=E5=8A=A8=E7=BB=93=E6=9D=9F=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=203.=20=E4=BF=AE=E5=A4=8Dgotomain,=E6=B2=A1=E6=83=B3?= =?UTF-8?q?=E5=88=B0=E5=95=8A=E6=B2=A1=E6=83=B3=E5=88=B0,=E5=B0=9D?= =?UTF-8?q?=E8=AF=95=E7=82=B9=E5=87=BB=E8=BF=94=E5=9B=9E=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E7=AB=9F=E7=84=B6=E6=98=AF=E5=9C=A8ui=5Fget=5Fcurrent=5Fpage?= =?UTF-8?q?=E9=87=8C=204.=20=E5=B0=9D=E8=AF=95=E4=BD=BF=E7=94=A8=E5=B7=A6?= =?UTF-8?q?=E4=B8=8B=E6=96=B9=E6=91=87=E6=9D=86=E7=A7=BB=E5=8A=A8,?= =?UTF-8?q?=E9=98=B2=E6=AD=A2=E9=98=B4=E9=98=B3=E5=B8=88=E5=8D=A1=E6=AD=BB?= =?UTF-8?q?BUG(=E6=9C=AA=E9=AA=8C=E8=AF=81=E5=85=B6=E6=9C=89=E6=95=88?= =?UTF-8?q?=E6=80=A7)=205.=20=E5=85=B6=E4=BB=96=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tasks/AbyssShadows/config.py | 4 +- tasks/AbyssShadows/script_task.py | 139 ++++++++++++++++++++---------- 2 files changed, 97 insertions(+), 46 deletions(-) diff --git a/tasks/AbyssShadows/config.py b/tasks/AbyssShadows/config.py index 12ae26ab2..ed5a9b8e0 100644 --- a/tasks/AbyssShadows/config.py +++ b/tasks/AbyssShadows/config.py @@ -332,14 +332,14 @@ def generate_quit_condition(self, enemy_type: EnemyType): 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) diff --git a/tasks/AbyssShadows/script_task.py b/tasks/AbyssShadows/script_task.py index 0089820da..7bcd205f1 100644 --- a/tasks/AbyssShadows/script_task.py +++ b/tasks/AbyssShadows/script_task.py @@ -1,8 +1,9 @@ # 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 time import sleep from datetime import datetime @@ -12,6 +13,7 @@ from module.logger import logger from module.config.config import Config from module.device.device import Device +from random import random from tasks.AbyssShadows.assets import AbyssShadowsAssets from tasks.AbyssShadows.config import AbyssShadows, EnemyType, AreaType, Code, AbyssShadowsDifficulty, \ CodeList, IndexMap @@ -38,9 +40,9 @@ class ScriptTask(GeneralBattle, GameUi, SwitchSoul, AbyssShadowsAssets): def __init__(self, config: Config, device: Device): super().__init__(config, device) - # + # 当前所处区域 self.cur_area = None - # + # 当前所用队伍预设 self.cur_preset = None # process list self.ps_list: CodeList = CodeList('') @@ -48,7 +50,7 @@ def __init__(self, config: Config, device: Device): self.done_list: CodeList = CodeList('') # 已知的 已经被打完的 列表 self.unavailable_list: CodeList = CodeList('') - # + # 是否已经切换过御魂 self.switch_soul_done = False def run(self): @@ -78,6 +80,7 @@ def run(self): if cfg.abyss_shadows_time.try_start_abyss_shadows: self.start_abyss_shadows() + self.init_list_from_cfg() # 判断各个区域是否可用 area_available = None for area in AreaType: @@ -89,7 +92,6 @@ def run(self): area_available = area logger.info(f"{area.name} available") - self.update_list() _next = self.get_next() if _next is not None: @@ -102,22 +104,30 @@ def run(self): 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) - # 切换御魂 - 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() - # try: + # 集结中图片 + 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.ui_get_current_page() self.goto_main() # 设置下次运行时间 @@ -127,7 +137,11 @@ def run(self): raise TaskEnd - def update_list(self): + def init_list_from_cfg(self): + if datetime.today().strftime('%Y-%m-%d') != self.config.model.abyss_shadows.saved_params.save_time: + 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) @@ -136,8 +150,15 @@ def update_list(self): 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_time = 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=}") @@ -148,9 +169,9 @@ def clear_saved_params(self): logger.info("Clear saved params done") def check_current_area(self) -> AreaType: - ''' 获取当前区域 + """ 获取当前区域 :return AreaType - ''' + """ while 1: self.screenshot() # 关闭战报界面 @@ -172,9 +193,9 @@ def check_current_area(self) -> AreaType: continue def change_area(self, area_name: AreaType) -> bool: - ''' 切换到下个区域 + """ 切换到下个区域 :return - ''' + """ while 1: self.screenshot() if self.appear(self.I_CHECK_FINISH): @@ -209,18 +230,23 @@ def change_area(self, area_name: AreaType) -> bool: return True def goto_main(self): - ''' 保持好习惯,一个任务结束了就返回庭院,方便下一任务的开始或者是出错重启 - ''' + """ 保持好习惯,一个任务结束了就返回庭院,方便下一任务的开始或者是出错重启 + """ - # 保证在狭间暗域界面 + # 可能在狭间,也可能在其他界面 + timer_quit_abyss_shadows = Timer(30) + timer_quit_abyss_shadows.start() while 1: self.screenshot() - if self.appear(self.I_ABYSS_NAVIGATION): + if timer_quit_abyss_shadows.reached(): + logger.info("timer_quit_abyss_shadows reached,") + break + if self.appear(self.I_ABYSS_NAVIGATION) or self.appear(self.I_CHECK_FINISH): 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, timeout=2) + self.wait_until_appear(self.I_ABYSS_NAVIGATION, wait_time=2) continue if self.appear_then_click(self.I_ABYSS_MAP_EXIT, interval=2): continue @@ -232,9 +258,9 @@ def goto_main(self): 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) @@ -256,9 +282,9 @@ def goto_abyss_shadows(self) -> bool: return True def select_boss(self, area_name: AreaType) -> bool: - ''' 选择暗域类型 + """ 选择暗域类型 :return - ''' + """ click_times = 0 while 1: self.screenshot() @@ -290,12 +316,18 @@ def goto_enemy(self, item_code: Code) -> bool: # 前往当前区域 的某个 敌人 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(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 @@ -316,6 +348,7 @@ def goto_enemy(self, item_code: Code) -> bool: continue # 点击前往按钮,知道该按钮消失或出现"挑战"字样 + while 1: self.screenshot() if self.appear(self.I_CHECK_FINISH): @@ -327,6 +360,7 @@ def goto_enemy(self, item_code: Code) -> bool: continue if self.appear(self.I_ABYSS_GOTO_ENEMY): self.click(self.I_ABYSS_GOTO_ENEMY, interval=1) + count_click_goto_enemy += 1 continue if not self.wait_until_appear(self.I_ABYSS_FIRE, wait_time=10): break @@ -380,7 +414,7 @@ def start_abyss_shadows(self): def process(self): while True: - self.update_list() + self.init_list_from_cfg() _next = self.get_next() if _next is None: break @@ -483,12 +517,11 @@ def execute(self, item_code: Code): def run_battle(self, item_code: Code): success = False - enemy = item_code.get_enemy_click() enemy_type = item_code.get_enemy_type() # 判断是否需要更换预设 - def get_preset(enemy_type: EnemyType): - match 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: @@ -597,10 +630,10 @@ def switch_soul_in_as(self): logger.info("start switch soul...") - def switch_soul(v: str): - l = v.split(',') + 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.") + 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]))) @@ -655,17 +688,26 @@ def is_area_done(self, area_type: AreaType): 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('oas1') + config = Config('oas') device = Device(config) - image = cv2.imread('E:/f.png') - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + # image = cv2.imread('E:/f.png') + # image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # # hsv_image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) # @@ -678,10 +720,19 @@ def is_area_done(self, area_type: AreaType): # cv2.waitKey() t = ScriptTask(config, device) - - area_type = AreaType.DRAGON - t.unavailable_list += CodeList(IndexMap[area_type.name].value) - print(f"{t.unavailable_list=}") + 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) From 1a2e456d201dceae1090537ab638310778900f43 Mon Sep 17 00:00:00 2001 From: ChengRanORZ Date: Sun, 15 Jun 2025 21:53:10 +0800 Subject: [PATCH 19/21] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E5=8C=BA=E5=9F=9F=E5=A4=B1=E8=B4=A5=E5=90=8E=EF=BC=8C=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E5=81=9C=E7=95=99=E5=9C=A8=E5=88=87=E6=8D=A2=E5=8C=BA?= =?UTF-8?q?=E5=9F=9F=E7=95=8C=E9=9D=A2=EF=BC=8C=E5=90=8E=E7=BB=AD=E5=87=BA?= =?UTF-8?q?=E9=94=99=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tasks/AbyssShadows/script_task.py | 164 +++++++++++++++++++----------- 1 file changed, 105 insertions(+), 59 deletions(-) diff --git a/tasks/AbyssShadows/script_task.py b/tasks/AbyssShadows/script_task.py index 7bcd205f1..d1d45b57c 100644 --- a/tasks/AbyssShadows/script_task.py +++ b/tasks/AbyssShadows/script_task.py @@ -13,7 +13,6 @@ from module.logger import logger from module.config.config import Config from module.device.device import Device -from random import random from tasks.AbyssShadows.assets import AbyssShadowsAssets from tasks.AbyssShadows.config import AbyssShadows, EnemyType, AreaType, Code, AbyssShadowsDifficulty, \ CodeList, IndexMap @@ -40,8 +39,6 @@ class ScriptTask(GeneralBattle, GameUi, SwitchSoul, AbyssShadowsAssets): def __init__(self, config: Config, device: Device): super().__init__(config, device) - # 当前所处区域 - self.cur_area = None # 当前所用队伍预设 self.cur_preset = None # process list @@ -80,31 +77,28 @@ def run(self): if cfg.abyss_shadows_time.try_start_abyss_shadows: self.start_abyss_shadows() - self.init_list_from_cfg() - # 判断各个区域是否可用 - area_available = None - for area in AreaType: - self.screenshot() - if self.is_area_done(area): + 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) - logger.info(f"{area.name} unavailable") - continue - area_available = area - logger.info(f"{area.name} available") + if unavailable_areas: + self.flash_list() + # 获取需要进入的区域类型 + _next = self.get_next() + if _next is None: + raise AbyssShadowsFinished + area_enter = _next.get_areatype() - _next = self.get_next() - if _next is not None: - area_available = _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 - # 通过能否进入,检测狭间是否开启 - if not self.select_boss(area_available): - logger.warning("Failed to enter abyss shadows") - self.goto_main() - self.set_next_run(task='AbyssShadows', finish=False, server=False, success=False) - raise TaskEnd - - try: # 集结中图片 self.wait_until_appear(self.I_WAIT_TO_START, wait_time=2) @@ -127,7 +121,6 @@ def run(self): logger.info("Abyss shadows process done") # 保持好习惯,一个任务结束了就返回到庭院,方便下一任务的开始 - self.ui_get_current_page() self.goto_main() # 设置下次运行时间 @@ -138,7 +131,7 @@ def run(self): raise TaskEnd def init_list_from_cfg(self): - if datetime.today().strftime('%Y-%m-%d') != self.config.model.abyss_shadows.saved_params.save_time: + 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() # @@ -155,7 +148,7 @@ def flash_list(self): @return: """ # BUG 跨天会出问题 - self.config.model.abyss_shadows.saved_params.save_time = datetime.today().strftime('%Y-%m-%d') + 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() @@ -181,6 +174,9 @@ def check_current_area(self) -> AreaType: 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): @@ -189,52 +185,75 @@ 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 """ + # 确保进入区域,有 切换区域 按钮 while 1: self.screenshot() + # 如果出现挑战完成,直接退出 if self.appear(self.I_CHECK_FINISH): raise AbyssShadowsFinished - # 判断当前区域是否正确 - current_area = self.check_current_area() - if current_area == area_name: - break - if self.is_area_done(area_name): - logger.info(f"change area:{area_name.name} is done") - self.unavailable_list += CodeList(IndexMap[area_name.name].value) - return False - # 切换区域界面 - if self.appear(self.I_ABYSS_DRAGON_OVER) or 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): - logger.info(f"Click {self.I_CHANGE_AREA.name}") - continue - # 大概率没用,保险点加上吧 + 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 - return True + # 判断当前区域是否正确 + current_area = self.check_current_area() + if current_area == area_name: + return True + + # 切换到选择区域界面 + while 1: + self.screenshot() + # 如果出现挑战完成,直接退出 + 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_then_click(self.I_CHANGE_AREA, interval=4): + logger.info(f"Click {self.I_CHANGE_AREA.name}") + continue + + # 判断区域是否可用,并进入一个区域 + 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): """ 保持好习惯,一个任务结束了就返回庭院,方便下一任务的开始或者是出错重启 """ - # 可能在狭间,也可能在其他界面 - timer_quit_abyss_shadows = Timer(30) + timer_quit_abyss_shadows = Timer(16) timer_quit_abyss_shadows.start() while 1: self.screenshot() @@ -243,6 +262,8 @@ def goto_main(self): break if self.appear(self.I_ABYSS_NAVIGATION) or self.appear(self.I_CHECK_FINISH): break + if self.appear(self.I_CHECK_SUMMON): + break if self.appear(self.I_ABYSS_DRAGON) or self.appear(self.I_ABYSS_DRAGON_OVER): # 在切换区域界面 self.device.click(x=600, y=600) @@ -252,9 +273,14 @@ def goto_main(self): continue if self.appear_then_click(self.I_ABYSS_ENEMY_INFO_EXIT, interval=2): continue + if self.appear_then_click(self.I_UI_BACK_BLUE, interval=2): + continue + if self.appear_then_click(self.I_UI_BACK_YELLOW, interval=2): + continue # logger.info("Exiting abyss_shadows") + self.ui_get_current_page() self.ui_goto(page_main) def goto_abyss_shadows(self) -> bool: @@ -309,7 +335,6 @@ def select_boss(self, area_name: AreaType) -> bool: continue if self.appear(self.I_ABYSS_NAVIGATION): break - self.cur_area = area_name return True def goto_enemy(self, item_code: Code) -> bool: @@ -486,12 +511,9 @@ def open_navigation(self): def execute(self, item_code: Code): area = item_code.get_areatype() - need_change_area = (self.cur_area is None) or (area != self.cur_area) - if need_change_area: - if not self.change_area(area): - return False - self.cur_area = area + if not self.change_area(area): + return False # 当前应当在正确的区域 # # if not self.check_available(item_code): @@ -550,7 +572,15 @@ def get_preset(_enemy_type: EnemyType): 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") - self.ui_click(self.C_MARK_MAIN, stop=self.I_MARK_MAIN, interval=1.5) + # 需要处理主怪没了的情况,增加最大次数 + 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) @@ -665,6 +695,22 @@ def check_available(self, item_code: Code): 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): From 9446fc7fd1589f25f6ff112282c5ef3a8741fa10 Mon Sep 17 00:00:00 2001 From: ChengRanORZ Date: Sun, 29 Jun 2025 23:04:06 +0800 Subject: [PATCH 20/21] fix --- tasks/AbyssShadows/config.py | 7 ++-- tasks/AbyssShadows/script_task.py | 54 +++++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/tasks/AbyssShadows/config.py b/tasks/AbyssShadows/config.py index ed5a9b8e0..4e0a756db 100644 --- a/tasks/AbyssShadows/config.py +++ b/tasks/AbyssShadows/config.py @@ -287,9 +287,10 @@ class ProcessManage(ConfigBase): preset_elite: str = Field(default='6,3', description='preset_elite_help') # 小蛇预设 # preset_snake: str = Field(default='', description='preset_snake_help') - # 首领策略 等待打完/时间到了退出/伤害足够退出/秒退 - # 可用值: 'TRUE', 'FALSE', 时间(秒),或最大伤害值 - # 详见类 {Condition} + + # 首领策略 秒退/直到消灭/时间到了退出/伤害足够退出 + # 可用值: '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') diff --git a/tasks/AbyssShadows/script_task.py b/tasks/AbyssShadows/script_task.py index d1d45b57c..a0bfd8775 100644 --- a/tasks/AbyssShadows/script_task.py +++ b/tasks/AbyssShadows/script_task.py @@ -260,23 +260,27 @@ def goto_main(self): if timer_quit_abyss_shadows.reached(): logger.info("timer_quit_abyss_shadows reached,") break - if self.appear(self.I_ABYSS_NAVIGATION) or self.appear(self.I_CHECK_FINISH): - break - if self.appear(self.I_CHECK_SUMMON): - 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") @@ -397,20 +401,32 @@ def attack_enemy(self): self.screenshot() 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_then_click(self.I_ABYSS_FIRE, interval=1): - continue - # 挑战敌人后,如果是奖励次数上限,会出现确认框 - if self.appear_then_click(self.I_ENSURE_BUTTON, interval=1): + if self.appear(self.I_ABYSS_FIRE): + self.click(self.I_ABYSS_FIRE, interval=0.4) continue # if self.appear(self.I_PREPARE_HIGHLIGHT): - break - return + return True + 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): # 尝试开启狭间暗域 @@ -526,7 +542,16 @@ def execute(self, item_code: Code): battle_count = MAX_BATTLE_COUNT while battle_count > 0: - self.attack_enemy() + 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() @@ -619,6 +644,7 @@ def get_preset(_enemy_type: EnemyType): if condition.is_passed() or (not _timer_battle.reached()): # 通过条件结束的,视其为完成 # 条件未通过且战斗时间不足3分钟的,极大可能是打死了,视之为完成 + logger.info(f"{enemy_type.name} battle result SUCCESS") success = True logger.info(f"{enemy_type.name} DONE") @@ -629,7 +655,8 @@ def quit_battle(self): while True: self.screenshot() if self.appear(self.I_EXIT_ENSURE): - self.click(self.I_EXIT_ENSURE, interval=1) + 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 @@ -640,7 +667,8 @@ def quit_battle(self): self.click(self.I_REWARD, interval=1) continue if self.appear(self.I_EXIT): - self.click(self.I_EXIT, interval=1) + if self.click(self.I_EXIT, interval=2): + self.wait_until_appear(self.I_EXIT_ENSURE, wait_time=1) continue return From bb5a0b7f23eed8b20dd3058d0e7bd4e297edccac Mon Sep 17 00:00:00 2001 From: ChengRanORZ Date: Mon, 4 Aug 2025 23:42:16 +0800 Subject: [PATCH 21/21] update --- tasks/AbyssShadows/res/res_btn_start.png | Bin 7750 -> 5264 bytes tasks/AbyssShadows/res/res_is_attack.png | Bin 3540 -> 3386 bytes tasks/AbyssShadows/script_task.py | 19 +++++++++++++++++++ 3 files changed, 19 insertions(+) diff --git a/tasks/AbyssShadows/res/res_btn_start.png b/tasks/AbyssShadows/res/res_btn_start.png index 0101e113b3d4eb55208b88818d20fcb720496680..31bb2c02461c8bad0da613a5960182e35690f94d 100644 GIT binary patch literal 5264 zcmaJ_Wmpqz+a3%UsW6akh7t}2jLwbj76FxTj8u_<(%sGI(UPKobO_SY-H1vlg3_Zw zKAz`0j`#T9Ki`k@zRvTyuei^@ca(viIu$uHIRF5l($r8fx`~Jz@RE|;oNU-ovYSAF zF;YhYDo0s2Zv+s7)HER_CH=CZzkDN!>=C**4b=%0mo~&Vn#@(h90LH{>iGu*se-py zZu*7n9%`a>bpZl51*8CCf}1HfjNqSo0)qeLgyT0{1VDI`V!eS@gd>3HpX(c-3Bdp1 zH}d9z&rxnK98sJP;3ATmMx1VTE_OB;JE*gr*Ue_)MBu+u=xz$juu1=NA~6w3KoRG^ zocAW*nn?362i@=$Ea`tw7lnz6%fKXML}8pTX&Esw8F4WHS{f#alD30EZAC<-p>S~< zaj2BIjTqEc3~noJBl;ALg24fTFew=r?8e)F`TLJ20`UL!db9ce^|4swl?(uYmo!xn zCJxFu=47s>eW`7`SJ(Bs9|06Zws)*+H6vAiylU-zwJOD~&udKU zlkGNWoa&hY5nuRhC;M{sSu3vyq?efxEuLY@34w4ylw=pvufJzwF6!|~=}kZV>%aM2 zwY)g_y1Tt$vc0s{EW$-IM$ zEKyY>Z1RW_$0M@X(RtLNG>l&hmqV zZZECzm<9ls3GZpFYZy#g>qG4pD7)?T(>OXhCpXPMH1}DHb35Fc5%pSyu#JU<++x)E z@Efy2iu)S+T``SpIoOMUw1osTUu7n`eiZ^*mweGG6!4|dEF6|_5gi_}^GW@A+vZ)S zyu1XvH&VCp-61w)q5TeWwvS7HUMXACki=C=lV^-ieHDyL#}!qsv}Vg%3D4BmyM5XZ zuB&f!cHaGy0)s3|#p`6rsqXxp%yApSiSryj2BU&}YN)pik&#~*K;Zg|ZB=l|~Y9SBYEY1(Q-2SvGo(EtnaT2a0D+3;%pqQ%56l z-^&Y|p8Rey5B5B}r?+?O;ZD%SL^Hbtz&-wsoU5!jNCEb6zx}`^we?^&%b?nCQgK|^ ztfi%>s{NX${P-nxFxzOBn7YK0f5c_?(VR1f?kbPY>BBvgCqTzQbf2R~7EhtTGeERj z@Z!TVmXvaLXWK9?HYzG97Ek&3nS-Z>kYr>3_;@0!q_j}+alOjMgt~dYbYO$wT|j)E z3~#&VKC#z}-}LPpwcShRwJ0_d*B=LN%_K(4p9kc$l6%USjr*##6%s^2@5#Gat&PrA zN-2&!vN@T3Vu>8dm-sd&=(vArMh@PvnTTjVZ)_l> zFm|J{nOk*_$4&zN9u(^AEsVSGV_Vxc<p&}%zO(NN&f*2sdZs(iGKP$jsT=v zZLLSQm$dZ3-rOf4VVBFO%boTBA(lKZwVm%D5D!vPHJ#kB!uQ0?+9ZoAn3u)UUS{R{ zhb0K6x8M7Dv);xTKdvZcPRrXj|6 zJ-2G;@cPWEhFpPPT);{*Py*FuMg4x^t=FnLi0oBi#K?vLK!TQm_FYoYtJei@HAz;Y zXp^za&(@S8xuRJ}!)#Niky>uiJ)I_x)>$Xwa3}=BF%x3hB&%ajD-RJzN*oL}S!Ei! zIvEeuliKR;i+AebPCPXgtW4*SKdQStZ;q?+AFc9l zsTH2Vp|n6UZ8ancK7Zw_;=Vo#piSq?kM8ZXF*2vBidA#9p}4vNTC*EjNpZ;(vU_f8 zkEH+j)66XGcl=%<+0fUX$;Ez3NK>*_{dg;($?r16T1mY1I%+Ca6hM_DWfSa#eDW&( zQz=Hl)I-fbp{)939I*)$V{ChV--_$HsfI9~&0_goOEe&fJZm|p^67lHq-VaCC^0(= z`&38h>AdGxaWb3J=0K^XFLSb8krJWrD?YyBs#mBg>50QH`w1hNqjF`uYVKRMGKM_} zO3H30BV^-fD-gezO0{of1ziIhtAdt#btw^

    2jeaEQ(?F>-sZ+4f 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 Ij>F^s0hHaC3jhEB diff --git a/tasks/AbyssShadows/res/res_is_attack.png b/tasks/AbyssShadows/res/res_is_attack.png index cd0f25cef6f24c2d5f4f4e66d5489052a4120fd3..60687568f368ad462443cdd616b1456f69a9297b 100644 GIT binary patch literal 3386 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 GUi=?FXc+GR literal 3540 zcmV;_4J-1AP)V>U4h#&rc_l?b?oJ93kx>c^NFXt}xPSpSU}RwUb`3-_FkIFREMa58 zqZS+BECN~xq(1<$NIIC!2r@$?3Br~DvCB&eN`Pz~5Ieb`2*d`un1QD#B*+=a_5rdL z(vjFnNbHo<;$$E@2gr^`_$_}v1B1mE1_tJd2(hGT3=G@b85kt)AjDJ_GBEJBGB9kr zj1c?m5B4vk*rAftiV~2BpS~kQL1IxNSeh9WplN9g44;-WFz`k)FbH2@VBm^|xC0DW zi-7@6VmJ{30AwjPdeHYMD*ylh32;bRa{vGi!~g&e!~vBn4jTXf44FwpK~z|UomlCQ zTSs!Q>hAY=lQSI7kVDSNxurRDdL3Doy;>i#122-maTW_~unDpsc7gmcNq_{&hh(#e zu|a|aP@ve3EDO7qY+15yjYh}ZoZ&INhQsrn-TA=xNY0GxY}_P>$M^bG*ROun)m4QH z$1ZSg0tf(XeE<*<0DvGNgdh?)Gz0+JzhM6g>mg$M+IlMByA2_}XtVgO3jTkF06+u*fWMQJHZTBSP1O;bUWiSdkq~^LwJp10=bx58g8=vj zc;7slFN)^sLYM#qv3b#c+*--&b;%NIC|aYTew$1H0HFQRiRK0a#HI&g%PnmZ52!0U zq%$uc{od&r!!T<#)i6zK9`2Kl?Jph}WASLP+h^#eVQ}nN_rQfMvt@ng3Hr~SoqzB5 z&-C?mJ$s%wv=#TMN^H&!4gl!zF@XrZp}>*p;r>VuK@0}{@4WR=UvC!@JRor&DoZpp z?PT2nICXOBgZIDxdoRzr-Od-RjkZo;*|sH6QMKMs*SWLvM`uRqaLi+(%glrVg7T!LiZk z!fZSe=|bGcFJN6kLETGXO9RvyD!Oj$qRwvOPR*r`PLB zY-g92HtO|;6()oYLx^vQNT$i_b#-sATB|9|RRTV@?2s#!nxbe2u?gGKEnPI-NTu`F zZ!G@&XYb7(9ews}t5m82fU0P_yH%y31$>@>&kYFElY=gYyt%cLPUQ^?p3PNLO92Ft zGUAYBBv`FB0AP4H?De{yK1*zFWrYxJRBZ^*+DJ~PLkOX1Iv^q;j1d4(Rb7@@w71ws1X?jZ(jYg;1Cb!8qO`gy1-Msz8?RG3a-l)|TS(XL|!V}{$ zkH_KjxzC-QA03G-%*J0jJ*}zwMq;NGxHPrGO&!1>gfj|G2MPBE=I5t|2g8Z2oDgvS z_2Z)>k-PVnA1$siN*xZ#>69eKOvB`d0{zVNP$V4Gb;Ip)ghGL#!SLMd=;Xv;PtZR% z8~^bK=g+;maO%X=?2%Ew-(9UWs?~;WI}z&?)XFUofOB3bl>hOcKQ>K1IN0~f%d=T2rP{CK`lPNoXMF8|cj;E7}7M`nh1OVz*noBzn>N}TgfmB!++zx>4?>bgNG zB?JWkK{QMbP)cVCU))&SE7y-68JV0I_>=d)|N6OOAOFwIUw!zgX_`%Jts~nyAw3@F zK(yEAbLqOVoy_f(Yw1k!{-afYz@w_#^OfYqOJDRydd9~G);D*uxst@FBr!!%OkEX1 zAfT=r@%TU_9AcDex_;~S(l38?saUMm>c$V>J3BhsuPExz{`_58mQ+H(#F63+f|cu&c$E+^xgPqe{r|+ z>1TH?eRPBKeY{zlt|^f*Iy@Zy<3D;$V(cIO>C%ITYmJ7ozMhWtha!E!Lb0rBda&Ck zN%Wn!U%K$t>E50|sa(5u{r=Ufx0*?Wt{d0BxG&4nSUmRb+hYu_VQ=9RYfaT8l3Y3k)*g=No-}CAfGR9>AE>M(5Grft*#V` z<>nl(*F_2W{QAQyS8o@J<7zsKUyNVxmR z^sw9Q@VK2GkF!`TZzXf(a;;I5Q2;n(sa&af+%Cr0-0b9!fAoeVu}?p{ z`*>-?G|honud3=RYsqw`2!gkh;*MRXYer&gXJULnQw>GY0I(VQWr>A)0v?ak=XDRn zdQ<7bz5C1C$vp4avaAD?H8jmgr;86BuD*I^E}!4?``t&6jD&guBg0|F=<;eZn=5&} zuKC&c-FwSVo^2|MZs-T)*Bycv=R5Dr4u4OP`04w(zig-E3etE;KS$Lr~IVQ47q z^}2R)rR9|*0th4s{BX{1ax;@DF3gY3&&4B=ZjZ;cFgMm8>2W&bK)~a2I@~TtG#bk1 zOO%q%%m@J33(r1H)60 zSZ0i!I6hh0t4fTnDZ`xh}I2>|Um*3}gg+g7& zk55aow6wIzg?ROqxlm7FWH>@8dGb8bI1Fk_u{Bq8XKB}NG%R4cW_cJ}zO@vZHg!yzrqj|aPbU))#} zLddezAMT-ySn0H|D_0aqB*BfYF0bG3iN$&s=EesHA`XW%JQV)ulUwObA+ePag1_~H zQ==pOzxn+B+G?uJSNjle!_zeV*4IzG9@q1g?Mx=GX$GbA=*)|LjnPk#G1M4+FHu*_c)!>vH7w2xp*>}yZQA~0AQ335B5nCv*{LA!bk{3$u#i? z=THB^56{J-z4eBA>+7c%FI``Hk^sPF{eJEG;>iy)KEv*S5l1ycZ?XUY=Y<0vEM4*I7j50=>vFeTUCu6Y?H$^lWB7_VNhO5=ahrhm( z*xD%+Du_e~NM(wq$qA*DF#v3)R{%l?f=F-QTYcxkE6oNMl z`>#)q$CQS4;`oHqDgW|cE^jAu$hiQ5`;|L91OQ{yAxpADBJ>5Fj!w>*EHO$60-Bry zfZ)&@>H-0XP_HSGNbskBdI11Z>B7otYGZR}JDK0v*}Z(_uA=DKT$xe^R-!BHR96%V zwZHzmi}jjPD3%pPN1&7dBKmx;Kl{mBue^Np;p6qMZZ3_D#oj!BVj$N0`SnM?z4l-y zyK9=ANr@0`MMX<-S(e;xrz9~#iJc<~t03EN7={QSguwP@{^as4LI{9h5_LLckJr;5 z2@b@1$KuiNou1)bsH&#v24nR2!nmsHhH2L7YPno5?p6|s+_U9v0g%`#m?k$(j)Xd8 z7VhhspNpS8b7VXoefE6&%GLW%o+Xl*UBAzL{G(H}k zo*axugN9+62IoSUhPk^}OC++(tEp6~NGSzC!I2Yd1uXPw-^9OwWd~U`R(oEv*py2=gCKlo3U6=cbAt@s%m<<(%9Rp z6$+JHp`0sJDV18zB!CDIoQr(EeCOV(Y4CKWP%PC9gEJ->CTwmNsHRAP3q+nr5amEnSgNMo9An zN9a6<_u2m2o2cSYfPB$(BrEIrom?%@<(4G*1&BIMer8tyR%fFH5(u$D5}VQ{RMMi@ z_G3WXt1XMYNb?sRT`vYHtiGLHz1#Y=yh+tDS5T AreaType: """ 获取当前区域 :return AreaType """ + logger.info("Checking current area") while 1: self.screenshot() # 关闭战报界面 @@ -191,6 +192,7 @@ def change_area(self, area_name: AreaType) -> bool: :return """ # 确保进入区域,有 切换区域 按钮 + logger.info(f"Change area to {area_name}") while 1: self.screenshot() # 如果出现挑战完成,直接退出 @@ -211,6 +213,7 @@ def change_area(self, area_name: AreaType) -> bool: # 判断当前区域是否正确 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 # 切换到选择区域界面 @@ -228,6 +231,7 @@ def change_area(self, area_name: AreaType) -> bool: logger.info(f"Click {self.I_CHANGE_AREA.name}") continue + logger.info(f"enter change area page") # 判断区域是否可用,并进入一个区域 available_areas, unavailable_areas = self.detect_area_status() success = area_name in available_areas @@ -315,6 +319,7 @@ def select_boss(self, area_name: AreaType) -> bool: """ 选择暗域类型 :return """ + logger.info(f"Select boss: {area_name.name} start") click_times = 0 while 1: self.screenshot() @@ -339,10 +344,12 @@ 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 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:点击后不动, @@ -369,6 +376,7 @@ def goto_enemy(self, item_code: Code) -> bool: 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): click_times += 1 @@ -383,6 +391,7 @@ def goto_enemy(self, item_code: Code) -> bool: 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) @@ -396,7 +405,12 @@ def goto_enemy(self, item_code: Code) -> bool: 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(self.I_CHECK_FINISH): @@ -413,10 +427,13 @@ def attack_enemy(self): # 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") @@ -512,6 +529,7 @@ def get_next(self) -> [Code, None]: return None def open_navigation(self): + logger.info("Open navigation") while True: self.screenshot() if self.appear(self.I_CHECK_FINISH): @@ -526,6 +544,7 @@ def open_navigation(self): continue 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):