From 988145c04189ea1b05c37d55372cab380f861e6b Mon Sep 17 00:00:00 2001 From: m123711 Date: Sat, 12 Sep 2026 12:27:04 +0800 Subject: [PATCH 01/13] fix(GameUi): refresh page_main_goto_summon template to match current game version --- tasks/GameUi/page/page_main_goto_summon.png | Bin 6639 -> 2577 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tasks/GameUi/page/page_main_goto_summon.png b/tasks/GameUi/page/page_main_goto_summon.png index 6bd4d11a7455efaa7b724931e3a9e0fce71ce2f8..6199521c7cb82a1c24ffe2b24d2bb798bdc693ed 100644 GIT binary patch literal 2577 zcmV+s3hwoZP)T42gqDomvg4YD*6`wu7l> zm8wnEDs55`lRB+a>5A6Utt=^2$sijefy7BnoY;<&*olebBz7FX_WSXF?rX2ZPPX5V zuT)kQbWk*aG=MM=Fk~7b2@a}Rzj3|mQ+IRZt=pUK!};0(4}x?8Bn6`hD!Sa|i_qu0$NbI)g1th{vSY{#zWI?W6}_l3K>`Qf1v^nJa{l*`)mOe|eON?5r$UA$iMZ2(5Jq#`kv+~`GH^~B(_ZQkA- zJT&h8U+toDqdIeKsWF)=9;-HRYO%rppb4au+KZ@{bThYN{=ZJYSoe;xe;-2s^BX9t zdHcp}b?I_LsaUt__LVaDHp!5o(#BkhqUBSxv14Fj;*>#{;F5S31*E#o5 z*>U&!#^fx5M+6J^RG#}tx=nwxw_D?A_Zy1s1?5NN|HS5*6n>QCbdzuTkswEUWLRu;qStJdx z#<`eQOY9_pNX zWyCjZd~jELCNcr#TCG-X)KoUgkjj$pnpGGXpph^(2j9B2|M4waI{S7pu;K3B+p{Yx z0V;CYtS{C>xl~AM@;ysl3S0(y3?G@g_mMq4Q6C-i^&RfluWY;D1V!XrHtJFU%3$!Q z7g3sl1dP`B`-$~?`nxTv|0)1Pt-AS-J3@k5A}a(aGM(=i)}Vs~;MCh zByuoikr3RW8{et-eWmusSrD+l?g2hLa0cuDq6NhsJS?zTeNPq~m7(x;tWkng1%95v(dqFb@<|NE1 zkN`m}*4nzzA_`DTDX5vH$eBZKp3Kd72h1TD2%s@JQEuJ3c)8pS3XlR#AaV{x&Kz=2 zG&ggnQ80r@fG7vwo9w=S%iLgmlZ1efnt*Z+DM}8d=bVn>JdB#(sIgKJ_*PHH$bpe!^E2BG zA_UzLiky{8o=xTjl94o;5z1k1YN*2VKRL7Kw|6>a1tCBPDI#(xN}h91j%1`68Zb=$ zbac<>_nN>s`%^sC_KSBv4s3zqs|uPv89ZLi*(I--8wb8KGR}2#%U3 zqno=ep7^Kgixo^BxiIj+J6Ct1d)vh>jNeYiuAB61F7j=5Pfb2sCK?_D2wa{kQ; z4(vN}b@w+q$6l>{?%`Xn9gA-cwBrszipU{mj+-;NnLF{(k4|87%+$I*weQ$*{aw%4 zPijvLjQsezeSalBCMXmkpvjA*C#SoSjgjv*!1mr9k8VHzgX-V*oPTwE|Bj)-roD6* z(B9Kd389dvsPm%H-ObHNT>I0S^=x~5cSmvQySKj6ckW+CcWoZND6Al~?XkTD0*VYG zmrYJscwtJ(NX)!cTl@7bM(EOC&Gvrzmt!I%kWygZ@9jcB3NXqtbG$-T$mB)>)vyk? zvOf0OIN06@FA~)Cmxc=m?TG=S1Od6sA&#{rn7NygFsPg@dGXlTt&(@g9PfX$AFw{M zF^}$cKmj7k91$EXio#%SG|+^F10%J?IjpVtGZzl^Jhh)UYu-;#XdwcOoFgK0)Kgm` zoHWvc<>^C{?cJ*%?B%8(p5F4id#}Gg#LrjmZ37?@f|PSa4vt!?q(tK~3&YasE4^zw zH@4yUKQ4XxH#b+0zjkY9$7gqSp@j%Z5s{TS>h38eCqbhqEG%{O0L|fFUEKDWUEP@a z+0n9i=;=)aASfh7oOYUYgkOYWw?Dg4~RuIbsVWGn%vW76N5iar|Gx9g0vaYx2Eb^TLzU n%#Bz^0L1?zEE8f`R?hzeTBO#HG{hG>00000NkvXXu0mjfVF}xK literal 6639 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 From 2df1b78fbedbd9b463fbc27636a8d8960b456d5a Mon Sep 17 00:00:00 2001 From: neptine <2234044577@qq.com> Date: Tue, 15 Sep 2026 21:39:01 +0800 Subject: [PATCH 02/13] fix(DailyTrifles): Add click interval and timeout for back button in mall page --- tasks/DailyTrifles/script_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/DailyTrifles/script_task.py b/tasks/DailyTrifles/script_task.py index aa0bd2a5ba..5dd8e38ea9 100644 --- a/tasks/DailyTrifles/script_task.py +++ b/tasks/DailyTrifles/script_task.py @@ -222,7 +222,7 @@ def run_store(self): if self.config.daily_trifles.trifles_config.buy_sushi_count > 0: self.run_buy_sushi() - self.ui_click(self.I_UI_BACK_YELLOW, self.I_CHECK_MALL) + self.ui_click(self.I_UI_BACK_YELLOW, self.I_CHECK_MALL, interval=2.4, timeout=8) self.ui_get_current_page() self.ui_goto(page_main) From 08db56bc7cdfc4255f29c52f1c700e9885cf5cd8 Mon Sep 17 00:00:00 2001 From: neptine <2234044577@qq.com> Date: Thu, 17 Sep 2026 01:06:45 +0800 Subject: [PATCH 03/13] refactor(GeneralBattle): Split options from strategy and introduce runtime context injection - extract battle_wait_options decorator/with-context so plan and options are managed independently - replace BattleWaitContext with PublicContext/PrivateContext and PerTaskState/PerBattleState dataclasses - wrap _bw_* hooks via runtime in BattleWait.__new__, injecting singleton pub_ctx and per-hook pri_ctx - add hook_enabled_update lifecycle: completion stays disabled until success/failure settles - centralize hook events/sequence/options defaults into module-level constants - add BattleResult enum and migrate hooks to write pub.per_battle.success - update tests: runtime injection/distribution/scope coverage, xfail not-yet-migrated cases --- tasks/Component/GeneralBattle/battle_wait.py | 522 ++++++++++++++---- .../GeneralBattle/test_battle_wait.py | 278 +++++++--- 2 files changed, 609 insertions(+), 191 deletions(-) diff --git a/tasks/Component/GeneralBattle/battle_wait.py b/tasks/Component/GeneralBattle/battle_wait.py index 35f5d1d2dd..766da7b19b 100644 --- a/tasks/Component/GeneralBattle/battle_wait.py +++ b/tasks/Component/GeneralBattle/battle_wait.py @@ -1,11 +1,86 @@ +""" +╔══════════════════════════════════════════════════════════════════════════════════╗ +║ battle_wait.py 调用链地图 ║ +╚══════════════════════════════════════════════════════════════════════════════════╝ + + 装饰器双层栈(任务级) + ─────────────────────────────────────────────────────────────────── + @battle_wait_options(excludes=[...]) ← 覆盖 options + @battle_wait_strategy(success='activity') ← 覆盖 plan + ↓ + script_task.battle_wait() + ↓ + ┌─────────────────────────────────────────────────────────┐ + │ battle_wait_options.__call__.inner │ + │ options → pub.options │ + │ finally: 还原 class options │ + └───────────────────────────┬─────────────────────────────┘ + ↓ + ┌─────────────────────────────────────────────────────────┐ + │ battle_wait_strategy.__call__.inner │ + │ current_plan = deepcopy(类 plan).override(kwargs) │ + │ runtime.reset_per_battle() ← 每场新状态 │ + │ runtime.update_options() ← 按事件切片到各 pri │ + └───────────────────────────┬─────────────────────────────┘ + ↓ + ┌─────────────────────────────────────────────────────────┐ + │ battle_wait_with_strategy(plan) │ + │ │ + │ setup hook ──────────────────────── run 一次 │ + │ hook_enabled = sequence events − {completion} │ + │ │ + │ ┌─── while True ─────────────────────────────────┐ │ + │ │ screenshot() │ │ + │ │ for handler in sequence顺序: │ │ + │ │ if event not in hook_enabled: skip │ │ + │ │ result = handler() ──┐ │ │ + │ │ ↓ │ │ │ + │ │ DONE ────────────────┤ 结束循环 │ │ + │ │ CONTINUE ──→ 下一个 handler │ │ + │ │ │ │ + │ │ success/failure 结算后: │ │ + │ │ per_battle['success'] = BattleResult.X │ │ + │ │ enable=('completion',) ← completion 开门 │ │ + │ └───────────────────────────────────────────────────┘ │ + │ │ + │ return per_battle['success'] == BattleResult.SUCCESS │ + └─────────────────────────────────────────────────────────┘ + + ┌──────────────────── Hook 调用 ────────────────────────────┐ + │ BattleWait.__new__ 遍历 MRO, _bw_* → runtime 包装 │ + │ handler() │ + │ → runtime.__call__(owner) │ + │ → func(owner, pub=pub_ctx, pri=pri_ctx[hook_name]) │ + └───────────────────────────────────────────────────────────────┘ + + ┌──────────────────── 数据流 ─────────────────────────────────┐ + │ │ + │ battle_wait_options.options (类槽, 装饰器/with 写入) │ + │ ↓ pub.options = 上面整个 │ + │ ↓ pri[hook].options = 按事件名切片 │ + │ │ + │ pub (全局) pri (每个 hook 各一份) │ + │ ├─ cross ├─ cross │ + │ ├─ per_task ├─ per_task │ + │ ├─ per_battle ├─ per_battle │ + │ │ ├─ success │ │ + │ │ └─ hook_enabled │ │ + │ └─ options └─ options(本事件切片) │ + │ │ + │ 生命周期: │ + │ reset_per_task runtime 首次遇到新 task_owner │ + │ reset_per_battle 每次 battle_wait_strategy.__call__ │ + └─────────────────────────────────────────────────────────────┘ +""" + import random import time import copy from copy import deepcopy -from functools import wraps -from dataclasses import dataclass +from functools import wraps, update_wrapper +from dataclasses import dataclass, field from typing import TypeVar, ParamSpec, Callable from enum import Enum, auto from cached_property import cached_property @@ -27,23 +102,58 @@ class HookSignal(Enum): BUSY = auto() # 处理了,继续 DONE = auto() # 流程结束 + +class BattleResult(Enum): + SUCCESS = auto() + FAILURE = auto() + + +# ─── 默认值常量 ─────────────────────────────────── +# 战斗 hook 事件列表 & 默认调用顺序 +_HOOKS_DEFAULT: tuple[str, ...] = ( + 'setup', 'completion', 'interrupt', 'success', 'failure', 'idle', +) +_SEQUENCE_DEFAULT: str = 'completion > interrupt > success > failure >' + +# hook options 默认值(装饰器 @battle_wait_options / with 上下文写入类槽) +_DEFAULT_OPTIONS: dict[str, dict] = { + 'success': { + 'reward_exclude_click_1': [ + 'C_END_MESSAGE_RIGHT_TOP', 'C_END_BUFF_AREA_1', + 'C_END_BUFF_AREA_2', 'C_END_SOUL_RECORD', 'C_END_SOUL_DETAILS', + ], + 'reward_exclude_click_2': [ + 'C_END_MESSAGE_RIGHT_TOP', 'C_END_BUFF_AREA_1', + 'C_END_BUFF_AREA_2', 'C_END_SOUL_RECORD', 'C_END_SOUL_DETAILS', + 'C_END_1_1', 'C_END_1_2', 'C_END_1_3', 'C_END_1_4', + 'C_END_1_5', 'C_END_1_6', + ], + }, +} + +# 跨任务/跨战斗状态的初始 @dataclass -class BattleWaitContext: - options: dict[str: dict] = None - completion = False - success = False +class PerTaskState: + """跨任务状态, 单个任务全程共享, reset_per_task 时重建""" + count: int = 0 + + +@dataclass +class PerBattleState: + """跨战斗状态, 每场战斗独立, reset_per_battle 时重建""" + success: BattleResult = BattleResult.FAILURE + hook_enabled: set = field(default_factory=lambda: set(_HOOKS_DEFAULT) - {'completion'}) + +# ──────────────────────────────────────────── + - def __getattr__(self, name): - if name in self.__dict__: - return self.__dict__[name] - return None class BattleWaitPlan: """ 就是一个超级大的状态机,这里拆成了很多hook """ - HOOKS_DEFAULT = ('setup', 'completion', 'interrupt', 'success', 'failure', 'idle') - SEQUENCE_DEFAULT = 'completion > interrupt > success > failure >' + HOOKS_DEFAULT = _HOOKS_DEFAULT + SEQUENCE_DEFAULT = _SEQUENCE_DEFAULT def __init__(self, *arg, **kwargs): extra_sequence: str = '' @@ -140,6 +250,9 @@ def function_setup_name(self) -> str: return f'_bw_setup_{self.setup}' def sequence_function_names(self) -> list[str]: + """ + 注意,这里是没有setup的 + """ function_names = [] for event_name in self.sequence.split('>'): event_name = event_name.strip() @@ -191,66 +304,34 @@ def override(self, *arg, **kwargs): return self class battle_wait_strategy: - _DEFAULT_OPTIONS: dict[str: dict] = { - 'success': { - 'reward_exclude_click_1': ['C_END_MESSAGE_RIGHT_TOP', 'C_END_BUFF_AREA_1', 'C_END_BUFF_AREA_2', 'C_END_SOUL_RECORD', 'C_END_SOUL_DETAILS',], - 'reward_exclude_click_2': ['C_END_MESSAGE_RIGHT_TOP', 'C_END_BUFF_AREA_1', 'C_END_BUFF_AREA_2', 'C_END_SOUL_RECORD', 'C_END_SOUL_DETAILS', - 'C_END_1_1', 'C_END_1_2', 'C_END_1_3', 'C_END_1_4', 'C_END_1_5', 'C_END_1_6', - ] - } - } - battle_wait_plan: BattleWaitPlan = None - options: dict[str: dict] = deepcopy(_DEFAULT_OPTIONS) - - def __init__(self, *arg, **kwargs): - self._options = kwargs.pop('options', None) - if self._options is not None: - if not isinstance(self._options, dict): - raise TypeError(f'temp_options must be a dict, got {self._options!r}') - for key, value in self._options.items(): - if not isinstance(key, str): - raise - if not isinstance(value, dict): - raise TypeError(f'temp_options must be a dict, got {self._options!r}') - # battle_wait_strategy.options.update(self._options) - + self._scope = 'decorator' self._battle_wait_plan = BattleWaitPlan(*arg, **kwargs) def __enter__(self): + self._scope = 'temporary' self._previous_plan = battle_wait_strategy.battle_wait_plan battle_wait_strategy.battle_wait_plan = self._battle_wait_plan return self def __exit__(self, *exc): battle_wait_strategy.battle_wait_plan = self._previous_plan - self._options = None return False def __str__(self): - temp_plan = getattr(self, '_battle_wait_plan', None) - default_plan = getattr(self, 'battle_wait_plan', None) - options = dict(battle_wait_strategy.options or {}) - - if temp_plan is not None: - plan = temp_plan - scope = 'temporary' - else: - plan = default_plan - scope = 'decorator' - - if plan is None: - return f'{type(self).__name__}(options={options}, plan=None)' - + plan = getattr(self, '_battle_wait_plan', None) + scope = getattr(self, '_scope', 'decorator') + active_plan = battle_wait_strategy.battle_wait_plan return ( f'{type(self).__name__}(' f'scope={scope}, ' - f'options={options}, ' - f'plan=\n{plan}' + f'plan=\n{plan}, ' + f'active_plan=\n{active_plan}' f')' ) + def __repr__(self): return self.__str__() @@ -260,11 +341,6 @@ def _recreate_cm(self): def __call__(self, func: Callable[P, T]) -> Callable[P, T]: # 跨任务重置配置和策略,但是不重置状态 - battle_wait_strategy.options = ( - deepcopy(self._options) - if self._options is not None - else deepcopy(self._DEFAULT_OPTIONS) - ) battle_wait_strategy.battle_wait_plan = self._battle_wait_plan @wraps(func) @@ -287,28 +363,213 @@ def inner(owner, *args: P.args, **kwargs: P.kwargs) -> T: # 'battle_wait_plan', # self.battle_wait_plan, # ) - options = dict(battle_wait_strategy.options or {}) + options = battle_wait_options.options or None + runtime.reset_per_battle() + runtime.update_options(options) return func(owner, battle_wait_plan=current_plan, options=options) \ if options else func(owner, battle_wait_plan=current_plan) return inner - def with_options(self, options: dict) -> "battle_wait_strategy": + +class battle_wait_options: + """ + 用法(输入为按 hook 名的 kwargs, 每个值必须是 dict[str, dict]): + 1) 装饰器 = 永久覆盖(任务级): + @battle_wait_options(success={'excludes': [...]}) # 参数 + @battle_wait_strategy(success='activity') # 策略, 只管 hook 与顺序 + def battle_wait(self, *args, **kwargs): + return self.battle_wait_with_strategy(*args, **kwargs) + 2) with = 临时覆盖(本次调用, 退出还原): + with battle_wait_options(failure={'excludes': [...]}): + task.battle_wait() + """ + _DEFAULT_OPTIONS = _DEFAULT_OPTIONS + options: dict[str, dict] = deepcopy(_DEFAULT_OPTIONS) + + def __init__(self, *arg, **kwargs): + self._scope = 'decorator' + self._overrides = dict(kwargs) + + def __enter__(self): + self._scope = 'temporary' + self._prev = type(self).options + merged = deepcopy(self._prev) or {} + merged.update(self._overrides) + type(self).options = merged + return self + + def __exit__(self, *exc): + type(self).options = self._prev + return False + + def __str__(self): + scope = getattr(self, '_scope', 'decorator') + return ( + f'{type(self).__name__}(' + f'scope={scope}, ' + f'overrides={self._overrides}, ' + f'options={battle_wait_options.options or {}}' + f')' + ) + def __repr__(self): + return self.__str__() + + def __call__(self, func: Callable[P, T]) -> Callable[P, T]: + # 装饰器 = 永久覆盖(任务级); 调用结束后还原,避免跨任务污染 + self._options_saved = type(self).options + + @wraps(func) + def inner(owner, *args: P.args, **kwargs: P.kwargs) -> T: + type(self).options = deepcopy(self._overrides) + try: + return func(owner, *args, **kwargs) + finally: + type(self).options = self._options_saved + + return inner + + +@dataclass +class PublicContext: + cross: dict = field(default_factory=dict) + per_task: PerTaskState = field(default_factory=PerTaskState) + per_battle: PerBattleState = field(default_factory=PerBattleState) + options: dict = field(default_factory=dict) # 所有hook的options + +@dataclass +class PrivateContext: + cross: dict = field(default_factory=dict) + per_task: dict = field(default_factory=dict) + per_battle: dict = field(default_factory=dict) + options: dict = field(default_factory=dict) # 单个hook的options + + +class runtime: + + task_owner = None + pri_ctx: dict[str, PrivateContext] = {} + pub_ctx: PublicContext = None + + def __init__(self, func: Callable): + self.func = func + self.hook_name = func.__name__ + update_wrapper(self, func) + + def __get__(self, obj, objtype=None): + if obj is None: + return self + runtime._ensure_pub() + runtime._ensure_pri(self.hook_name) + + @wraps(self.func) + def bound(*args, **kwargs): + return self(obj, *args, **kwargs) + return bound + + def __call__(self, owner, *args, **kwargs): + # 新任务重置一下 + if runtime.task_owner is None or owner is not runtime.task_owner: + runtime.task_owner = owner + runtime.reset_per_task() + runtime._ensure_pub() + runtime._ensure_pri(self.hook_name) + + return self.func(owner, pub=runtime.pub_ctx, pri=runtime.pri_ctx[self.hook_name]) + + def __str__(self): + pub = runtime.pub_ctx or PublicContext() + pri = runtime.pri_ctx.get(self.hook_name) or PrivateContext() + + def _keys(d): + if isinstance(d, dict): + return ','.join(d.keys()) or '-' + if isinstance(d, (PerTaskState, PerBattleState)): + return ','.join(d.__dataclass_fields__) or '-' + return str(d) + + return ( + f'runtime({self.hook_name}) ' + f'pub[c:{_keys(pub.cross)},t:{_keys(pub.per_task)},b:{_keys(pub.per_battle)},o:{_keys(pub.options)}] ' + f'pri[c:{_keys(pri.cross)},t:{_keys(pri.per_task)},b:{_keys(pri.per_battle)},o:{_keys(pri.options)}]' + ) + + def __repr__(self): + return self.__str__() + + @classmethod + def reset_per_task(cls): + cls._ensure_pub() + cls.pub_ctx.per_task = PerTaskState() + for ctx in cls.pri_ctx.values(): + ctx.per_task = {} + + @classmethod + def reset_per_battle(cls): + cls._ensure_pub() + cls.pub_ctx.per_battle = PerBattleState() + for ctx in cls.pri_ctx.values(): + ctx.per_battle = {} + + @classmethod + def _ensure_pub(cls): + if cls.pub_ctx is None: + cls.pub_ctx = PublicContext() + + @classmethod + def _ensure_pri(cls, name): + if name not in cls.pri_ctx: + cls.pri_ctx[name] = PrivateContext() + + @classmethod + def hook2event(cls, func_name: str) -> str: + # '_bw_success_soul' -> 'success'(strategy 名里带下划线也能正确切出事件名) + return func_name[len('_bw_'):].rsplit('_', 1)[0] + + @classmethod + def update_options(cls, options: dict[str, dict]): + cls._ensure_pub() if not isinstance(options, dict): + cls.pub_ctx.options = {} + for ctx in cls.pri_ctx.values(): + ctx.options = {} + return + cls.pub_ctx.options = options + for hook_name, ctx in cls.pri_ctx.items(): + ctx.options = options.get(cls.hook2event(hook_name)) or {} + + @classmethod + def hook_enabled(cls) -> set: + _hook_enabled = cls.pub_ctx.per_battle.hook_enabled + if not _hook_enabled: raise - for event, v in options.items(): - if not isinstance(event, str): - raise TypeError() - if not isinstance(v, dict): - raise TypeError() - # 先装饰器, 后临时变量 - self._previous_options = battle_wait_strategy.options - battle_wait_strategy.options = deepcopy(options) - return self + return _hook_enabled + + @classmethod + def hook_enabled_update(cls, enable: tuple[str, ...], disable: tuple[str, ...]) -> HookSignal: + """ + hook_enabled 的更新只能在 BattleWait 上。不可以在 battle_wait_options 或者 battle_wait_strategy 上 + """ + cls.pub_ctx.per_battle.hook_enabled.update(enable) + cls.pub_ctx.per_battle.hook_enabled.difference_update(disable) + return HookSignal.CONTINUE class BattleWait(BaseTask, GeneralBattleAssets): + def __new__(cls, *args, **kwargs): + for klass in reversed(cls.__mro__): + # 找遍整个继承链,从父到子 + if klass is object: + continue + if klass.__dict__.get('_runtime_auto_decorated'): + continue + klass._runtime_auto_decorated = True + for name, attr in list(vars(klass).items()): + if name.startswith('_bw_') and callable(attr) and not isinstance(attr, runtime): + setattr(klass, name, runtime(attr)) + return super().__new__(cls) + # ------------------------------------------------------------------------------------------------------------------ @cached_property def exclude_button_stage_1(self): @@ -339,15 +600,21 @@ def reward_exclude_click_2(self, areas: list[str] = None) -> RuleClickExclude: # build in # ------------------------------------------------------------------------------------------------------------------ - def _bw_setup_default(self, bw_ctx: BattleWaitContext) -> HookSignal: + def _bw_setup_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: + """ + options: dict = { + ‘excludes’ + } + """ self.C_REWARD_1.name = 'C_REWARD' self.C_REWARD_2.name = 'C_REWARD' self.C_REWARD_3.name = 'C_REWARD' self.device.stuck_record_add('BATTLE_STATUS_S') self.device.click_record_clear() logger.info('Start battle process') - options: dict[str, dict] = getattr(bw_ctx, 'options', None) or {} - success_options = options.get('success') or {} + + pri_options = pri.options if isinstance(pri.options, dict) else {} + success_options = pri_options.get('success') or {} if not isinstance(success_options, dict): raise TypeError("battle wait option 'success' must be a dict") excludes = success_options.get('excludes') @@ -359,17 +626,16 @@ def _bw_setup_default(self, bw_ctx: BattleWaitContext) -> HookSignal: # print(self._reward_exclude_click_2) return HookSignal.DONE - def _bw_completion_default(self, bw_ctx: BattleWaitContext) -> HookSignal: - if bw_ctx.completion: - logger.info('Battle done') - return HookSignal.DONE - return HookSignal.CONTINUE + def _bw_completion_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: + logger.info('Battle completion process') + pub.per_task.count += 1 + return HookSignal.DONE - def _bw_interrupt_default(self, bw_ctx: BattleWaitContext) -> HookSignal: + def _bw_interrupt_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: # 比如 御魂溢出 return HookSignal.CONTINUE - def _bw_success_default(self, bw_ctx: BattleWaitContext) -> HookSignal: + def _bw_success_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: if self.appear_then_click(self.I_WIN, interval=0.8): self.click(self._reward_exclude_click_1) return HookSignal.CONTINUE @@ -406,32 +672,31 @@ def _bw_success_default(self, bw_ctx: BattleWaitContext) -> HookSignal: self.click(self._reward_exclude_click_2, interval=1.5) else: logger.info('Get all reward') - bw_ctx.success = True - bw_ctx.completion = True + pub.success = True + pub.completion = True return HookSignal.CONTINUE if timer.reached_and_reset(): - logger.warning('battle ') + logger.warning('battle') break return HookSignal.CONTINUE - - def _bw_failure_default(self, bw_ctx: BattleWaitContext) -> HookSignal: + def _bw_failure_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: if self.appear(self.I_FALSE, threshold=0.8): logger.warning('False battle') self.ui_click_until_disappear(self.I_FALSE) - bw_ctx.success = False - bw_ctx.completion = True - return HookSignal.CONTINUE + pub.per_battle.success = BattleResult.FAILURE + return runtime.hook_enabled_update( + enable=('completion', ), + disable=tuple(['success', 'failure']), + ) return HookSignal.CONTINUE - def _bw_idle_default(self, bw_ctx: BattleWaitContext) -> HookSignal: + def _bw_idle_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: return HookSignal.CONTINUE - def _bw_reserve_default(self, bw_ctx: BattleWaitContext) -> HookSignal: + def _bw_randomclick_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: return HookSignal.CONTINUE - - def _bw_randomclick_default(self, bw_ctx: BattleWaitContext) -> HookSignal: - done = getattr(bw_ctx, '_bw_randomclick_state', False) + done = pri.per_battle.get('_bw_randomclick_state', False) if done: return HookSignal.CONTINUE if not self.is_in_battle(is_screenshot=False): @@ -445,7 +710,7 @@ def _bw_randomclick_default(self, bw_ctx: BattleWaitContext) -> HookSignal: self.swipe(self.S_BATTLE_RANDOM_LEFT, interval=20) case 2: self.swipe(self.S_BATTLE_RANDOM_RIGHT, interval=20) - setattr(bw_ctx, f'_bw_randomclick_state', True) + pri.per_battle['_bw_randomclick_state'] = True # 重新设置为长战斗 # self.device.stuck_record_add('BATTLE_STATUS_S') else: @@ -454,8 +719,8 @@ def _bw_randomclick_default(self, bw_ctx: BattleWaitContext) -> HookSignal: # custom # ------------------------------------------------------------------------------------------------------------------ - def _bw_success_soul(self, bw_ctx: BattleWaitContext) -> HookSignal: - options: dict[str, dict] = getattr(bw_ctx, 'options', None) or {} + def _bw_success_soul(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: + options: dict[str, dict] = pub.options if isinstance(pub.options, dict) else {} success_options = options.get('success') or {} if not isinstance(success_options, dict): raise TypeError("battle wait option 'success' must be a dict") @@ -486,8 +751,8 @@ def _bw_success_soul(self, bw_ctx: BattleWaitContext) -> HookSignal: self.click(action_click, interval=1.5) else: logger.info('Get all reward') - bw_ctx.success = True - bw_ctx.completion = True + pub.success = True + pub.completion = True return HookSignal.CONTINUE if timer.reached_and_reset(): logger.warning('battle ') @@ -504,7 +769,7 @@ def exclude_click_activity(self, areas: list[str] = None) -> RuleClickExclude: inputs.append(click) return RuleClickExclude(inputs, name='exclude_click_activity', strategy='rejection', distribution='uniform') - def _bw_success_activity(self, bw_ctx: BattleWaitContext) -> HookSignal: + def _bw_success_activity(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: """ 战斗结算是有 “获得奖励” 的适用 """ @@ -538,9 +803,11 @@ def _bw_success_activity(self, bw_ctx: BattleWaitContext) -> HookSignal: if any([self.appear(self.I_UI_REWARD), self.appear(self.I_END_FIX_1), self.appear(self.I_END_FIX_2), self.appear(self.I_END_FIX_3)]): continue logger.info('Get all reward') - bw_ctx.success = True - bw_ctx.completion = True - return HookSignal.DONE + pub.per_battle.success = BattleResult.SUCCESS + return runtime.hook_enabled_update( + enable=('completion', ), + disable=tuple(['success', 'failure']), + ) if timer.reached_and_reset(): logger.warning('battle ') @@ -551,7 +818,28 @@ def _bw_success_activity(self, bw_ctx: BattleWaitContext) -> HookSignal: # ------------------------------------------------------------------------------------------------------------------ def battle_wait_with_strategy(self, *args, **kwargs) -> bool: """ - 三种自定义配置方法: + 理解 event + strategy 概念: 把战斗过程抽象为一系列触发事件以及对应的实现函数,也称hook。 + event + strategy 拼成了一个hook, 一个 event 在一次战斗过程中只能挂载一个 strategy + 默认定义的 hook 有 BattleWaitPlan.HOOKS_DEFAULT = ('setup', 'completion', 'interrupt', 'success', 'failure', 'idle') + 这些 hook 跑在一个 while 里面,默认的调用顺序 BattleWaitPlan.SEQUENCE_DEFAULT = 'completion > interrupt > success > failure > idle' + setup 没有跑在 while里面 而是在 while之前, 比如一个战斗过程可以表示为: + setup_default() + while 1: + completion_default() + interrupt_default() + success_default() + failure_default() + idle_default() + 围绕hook从两个维度来构建一个战斗系统, options 表示每一个hook的配置选项, public 和 private 表示每一个hook的共享和私有状态。分别有两个生命周期 + | 自定义 /生命周期 | 跨任务 | 任务共享 | 单次战斗 | + | ------------- | ----- | --------------------- | ------------ | + | strategy | 不做 | @battle_wait_strategy | with 上下文 | + | options | 不做 | @battle_wait_options | with 上下文 | + | public | cross | per_task | per_battle | + | private | cross | per_task | per_battle | + + ---------------------------------------------------------------------------------------------------------------- + 三种自定义策略方法: 1. 使用装饰器battle_wait_strategy, 将会覆盖掉类变量 @battle_wait_strategy( 'reserve_default', 'idle_default', failure='default') def battle_wait(self, *args, **kwargs): @@ -562,16 +850,9 @@ def battle_wait(self, *args, **kwargs): 3. 调用时动态传参 (!在1基础上), 不覆盖,就临时更新策略 obj.battle_wait(random_click_swipt_enable=1) # 详细参数看 battle_wait_strategy.__call__() - 理解 event + strategy 概念: 把战斗过程抽象为一系列触发事件以及对应的实现函数,也称hook。 - 默认定义的 hook 有 BattleWaitPlan.HOOKS_DEFAULT = ('setup', 'completion', 'interrupt', 'success', 'failure', 'idle') - 这些 hook 跑在一个 while 里面,默认的调用顺序 BattleWaitPlan.SEQUENCE_DEFAULT = 'completion > interrupt > success > failure > idle' - setup 没有跑在 while里面 而是在 while之前 - - event + strategy 拼成了一个hook, 一个 event 在一次战斗过程中只能挂载一个 strategy 自定义hook就是字符串拼起来: battle_wait_strategy的入参可以有 ‘event_strategy’ 或者 'event=strategy' 可以添加任意 event 以及其对应的 strategy。比如 ‘yyy_default’ 'abcd_edf' 但是必须要实现对应的hook 上面的比如 _bw_yyy_default() 以及 _bw_abcd_edf() - hook 可以自定义顺序,比如 battle_wait_strategy(sequence='completion > interrupt > success > failure > idle') 如果没有指定sequence, 新增的event会按照传参时候从左到右排序,左边高优先级,新增的会插入到 failure 和 idle 之间 @@ -590,33 +871,34 @@ def battle_wait(self, *args, **kwargs): 我突然感觉 一个类里面装了 策略和参数,这样不好,考虑拆分成两个装饰器 """ - bw_ctx = BattleWaitContext() - battle_wait_plan = kwargs.get('battle_wait_plan') if battle_wait_plan is None: battle_wait_plan = BattleWaitPlan() - # print(battle_wait_plan) + print(battle_wait_plan) # print(battle_wait_plan.sequence_function_names()) - options = kwargs.get('options', None) - if options is not None: - bw_ctx.options = options - # logger.info('options: {}'.format(options)) + options = runtime.pub_ctx.options + logger.info('options: {}'.format(options)) setup_func = getattr(self, battle_wait_plan.function_setup_name, self._bw_setup_default) - setup_func(bw_ctx) + setup_func() handlers = [getattr(self, func_name, None) for func_name in battle_wait_plan.sequence_function_names()] + _hook_enabled = tuple(runtime.hook2event(h.__name__) for h in handlers if h) + _hook_disabled = ('completion', ) + runtime.hook_enabled_update(enable=_hook_enabled, disable=_hook_disabled) + print(f'first runtime.hook_enabled(): {runtime.hook_enabled()}') while True: self.screenshot() + hook_enabled = runtime.hook_enabled() for handler in handlers: - result = handler(bw_ctx) + if runtime.hook2event(handler.__name__) not in hook_enabled: + continue + result = handler() if handler.__name__.startswith('_bw_completion') and result == HookSignal.DONE: - return bw_ctx.success + return runtime.pub_ctx.per_battle.success == BattleResult.SUCCESS if result == HookSignal.CONTINUE: continue - # bw_ctx.completion = True - if __name__ == '__main__': diff --git a/tests/tasks/Component/GeneralBattle/test_battle_wait.py b/tests/tasks/Component/GeneralBattle/test_battle_wait.py index 36a0c73959..6d78436bf3 100644 --- a/tests/tasks/Component/GeneralBattle/test_battle_wait.py +++ b/tests/tasks/Component/GeneralBattle/test_battle_wait.py @@ -4,14 +4,20 @@ BattleWait, BattleWaitPlan, HookSignal, + runtime, + battle_wait_options, battle_wait_strategy, + _DEFAULT_PER_BATTLE, ) @pytest.fixture(autouse=True) def reset_battle_wait_plan(monkeypatch): monkeypatch.setattr(battle_wait_strategy, 'battle_wait_plan', None) - monkeypatch.setattr(battle_wait_strategy, 'options', None) + monkeypatch.setattr(battle_wait_options, 'options', None) + runtime.task_owner = None + runtime.pub_ctx = None + runtime.pri_ctx = {} def test_default_plan_contains_default_hooks_and_sequence(): @@ -76,6 +82,7 @@ def test_an_event_cannot_be_configured_with_two_strategies(): BattleWaitPlan('success_default', success='custom') +@pytest.mark.xfail(reason='battle_wait_with_strategy 尚未迁移到 runtime (runtime.current 已移除)', strict=False) def test_setup_runs_before_the_wait_loop(): class OrderedBattleWait(BattleWait): def __init__(self): @@ -84,13 +91,13 @@ def __init__(self): def screenshot(self): self.events.append('screenshot') - def _bw_setup_record(self, bw_ctx): + def _bw_setup_record(self, pub, pri): self.events.append('setup') return HookSignal.DONE - def _bw_completion_finish(self, bw_ctx): + def _bw_completion_finish(self, pub, pri): self.events.append('completion') - bw_ctx.success = True + pub.success = True return HookSignal.DONE battle_wait = OrderedBattleWait() @@ -100,6 +107,7 @@ def _bw_completion_finish(self, bw_ctx): assert battle_wait.events == ['setup', 'screenshot', 'completion'] +@pytest.mark.xfail(reason='battle_wait_with_strategy 尚未迁移到 runtime (runtime.current 已移除)', strict=False) def test_custom_hook_is_resolved_and_executed_in_the_configured_sequence(): class CustomBattleWait(BattleWait): def __init__(self): @@ -108,17 +116,17 @@ def __init__(self): def screenshot(self): pass - def _bw_setup_record(self, bw_ctx): + def _bw_setup_record(self, pub, pri): self.events.append('setup') return HookSignal.DONE - def _bw_yyy_record(self, bw_ctx): + def _bw_yyy_record(self, pub, pri): self.events.append('yyy') return HookSignal.CONTINUE - def _bw_completion_finish(self, bw_ctx): + def _bw_completion_finish(self, pub, pri): self.events.append('completion') - bw_ctx.success = True + pub.success = True return HookSignal.DONE battle_wait = CustomBattleWait() @@ -185,10 +193,12 @@ def battle_wait(owner, *, battle_wait_plan, options=None): assert not hasattr(plan_without_override, 'randomclick') -# options 与 plan 同语义: 装饰器覆盖全局, with_options 临时覆盖并在退出时还原(__exit__ 还原 _previous_options)。 -# 本测试同时锁定 __exit__ 的还原行为 —— 若还原缺失, 最后一组断言会失败。 -def test_decorator_options_and_with_options_are_scoped_to_the_current_call(): - received_options = [] +# options 与 plan 同语义: 装饰器覆盖全局, with 临时覆盖并在退出时还原。 +# 本测试锁定新拆分 API —— 策略装饰器只注入 plan, options 由 battle_wait_options +# 各自负责(装饰器=整份覆盖并跨调用还原, with=进入时 merge、退出还原)。 +@pytest.mark.xfail(reason='battle_wait_with_strategy 尚未迁移到 runtime (runtime.current 已移除)', strict=False) +def test_options_decorator_and_with_are_scoped_to_the_current_call(): + received = [] decorator_options = { 'completion': {'source': 'decorator'}, 'success': {'excludes': ['C_REWARD_1']}, @@ -197,104 +207,103 @@ def test_decorator_options_and_with_options_are_scoped_to_the_current_call(): 'success': {'excludes': ['C_END_MESSAGE_RIGHT_TOP']}, } - strategy = battle_wait_strategy( - 'setup_record', 'completion_record', options=decorator_options - ) + strategy = battle_wait_strategy('setup_record', 'completion_record') class OptionBattleWait(BattleWait): def screenshot(self): pass - def _bw_setup_record(self, bw_ctx): + def _bw_setup_record(self, pub, pri): return HookSignal.DONE - def _bw_completion_record(self, bw_ctx): - received_options.append(bw_ctx.options) - bw_ctx.success = True + def _bw_completion_record(self, pub, pri): + received.append(pub.options) + pub.success = True return HookSignal.DONE @strategy - def battle_wait(self, *args, **kwargs): + def battle_wait_plain(self, *args, **kwargs): return self.battle_wait_with_strategy(*args, **kwargs) battle_wait = object.__new__(OptionBattleWait) - # 装饰器覆盖全局: 调用方拿到装饰器的整份 options - assert battle_wait.battle_wait() is True - assert received_options[-1] == decorator_options - - with strategy.with_options(context_options): - # with 是整份覆盖, 不是合并: 装饰器的 completion 键也被覆盖掉 - assert battle_wait.battle_wait() is True - assert received_options[-1] == context_options - strategy_text = str(strategy) - assert 'options=' in strategy_text - assert 'C_END_MESSAGE_RIGHT_TOP' in strategy_text - - # 退出 with 后还原为装饰器覆盖的那份(需要 __exit__ 还原 _previous_options) - assert battle_wait.battle_wait() is True - assert received_options[-1] == decorator_options - assert 'C_REWARD_1' in str(strategy) + # ---- 场景 1: 装饰器 = 整份覆盖全局 ---- + opts_decorator = battle_wait_options(**decorator_options) + class Decorated(OptionBattleWait): + @opts_decorator + @strategy + def battle_wait(self, *args, **kwargs): + return self.battle_wait_with_strategy(*args, **kwargs) -# 跨任务(两个装饰器)场景。复现 script.py 的调度时序: 每个任务运行前用 load_module -# 重新执行自己的 script_task.py, 即"装饰器在任务运行时才生效, 后加载的任务覆盖前者"。 -# 契约: options 与 battle_wait_plan 语义完全一致 —— 都是全局槽位, 后加载装饰器整份覆盖, + decorated = object.__new__(Decorated) + assert decorated.battle_wait() is True + assert received[-1] == decorator_options + # 跨调用还原: 槽位回到调用前的状态, 不残留到别处 + assert battle_wait_options.options is None + + # ---- 场景 2: with = 临时覆盖, 进入时与当前槽位 merge, 退出还原 ---- + with battle_wait_options(**context_options): + assert battle_wait_options.options == context_options + # 未装饰 options 的入口直接读当前槽位 + assert battle_wait.battle_wait_plain() is True + assert received[-1] == context_options + + # 退出 with 后还原(需要 __exit__ 还原) + assert battle_wait_options.options is None + assert battle_wait.battle_wait_plain() is True + assert received[-1] is None + + +# 跨任务(两个任务各自独立声明策略+options)场景。复现 script.py 的调度时序: +# 每个任务运行前用 load_module 重新执行自己的 script_task.py, 即"装饰器在任务 +# 运行时才生效, 后加载的任务覆盖前者"。 +# 契约: battle_wait_plan 与 options 都是全局槽位, 后加载装饰器整份覆盖; # 正在运行的任务拿到的正是自己模块装饰器声明的配置。 def test_cross_task_decorators_switch_plan_and_keep_own_options(): # 任务 A 的装饰器: 默认 success hook + 自己的 options - strategy_a = battle_wait_strategy( - 'success_default', - options={'success': {'market': 'task_a'}}, - ) + strategy_a = battle_wait_strategy('success_default') + opts_a = battle_wait_options(success={'market': 'task_a'}) + @opts_a @strategy_a def battle_wait_a(owner, *, battle_wait_plan, options=None): - # 被装饰后 wrapper 会注入当前生效的 battle_wait_plan / options, 这里原样抛回来断言 + # 被装饰后 wrapper 会注入当前生效的 battle_wait_plan / options return battle_wait_plan, options # 任务 A 运行: 生效的应是它自己的 plan 和 options plan_a, options_a = battle_wait_a(object()) - assert plan_a.success == 'default' assert options_a['success']['market'] == 'task_a' - # A 装饰(模块加载)后, 全局生效 plan 和 options 都是 A 的 + # A 装饰(模块加载)后, 全局生效 plan 是 A 的 assert battle_wait_strategy.battle_wait_plan is plan_a - assert battle_wait_strategy.options['success']['market'] == 'task_a' - # 任务 B 后加载并装饰: 不同 success hook + 不同的 options → 整份覆盖全局 - strategy_b = battle_wait_strategy( - success='activity', - options={'success': {'market': 'task_b'}}, - ) + # 任务 B 后加载并装饰: 不同 success hook + 不同的 options + strategy_b = battle_wait_strategy(success='activity') + opts_b = battle_wait_options(success={'market': 'task_b'}) + @opts_b @strategy_b def battle_wait_b(owner, *, battle_wait_plan, options=None): return battle_wait_plan, options - # 核心断言 1: B 装饰(加载)后, 全局生效 plan 和 options 都切成 B 自己的 + # 核心断言: B 装饰(加载)后, 全局生效 plan 切成 B 自己的 assert battle_wait_strategy.battle_wait_plan is not plan_a plan_b, options_b = battle_wait_b(object()) assert plan_b.success == 'activity' assert battle_wait_strategy.battle_wait_plan is plan_b assert options_b['success']['market'] == 'task_b' - assert battle_wait_strategy.options['success']['market'] == 'task_b' + assert options_b['success'] == {'market': 'task_b'} - # 观察 A 的"滞后"行为: 此刻全局已被 B 覆盖, A 再次调用跟随全局(B 的), - # options 和 plan 一样是"后加载者胜", 不再保留 A 自己的。 - plan_a2, options_a2 = battle_wait_a(object()) - assert plan_a2 is plan_b - assert options_a2['success']['market'] == 'task_b' - -# 未声明 options 的装饰器应重置回 _DEFAULT_OPTIONS, 而不是继承上一个任务的 options。 -# 这是"后加载者覆盖 + 未声明回默认"的兜底, 避免任务链里的 options 漂移。 +# 未声明 options 的任务不应继承上一个任务的 options —— 后加载者覆盖后, +# 使用自己 options 装饰器的任务只拿自己声明的; 无 options 声明则回 None, +# 避免任务链里的 options 漂移。 def test_cross_task_options_do_not_leak_between_tasks(): # 任务 A: 声明 options → 覆盖全局 - strategy_a = battle_wait_strategy( - 'success_default', - options={'success': {'market': 'task_a'}}, - ) + strategy_a = battle_wait_strategy('success_default') + opts_a = battle_wait_options(success={'market': 'task_a'}) + @opts_a @strategy_a def battle_wait_a(owner, *, battle_wait_plan, options=None): return battle_wait_plan, options @@ -302,16 +311,143 @@ def battle_wait_a(owner, *, battle_wait_plan, options=None): _, options_a = battle_wait_a(object()) assert options_a['success']['market'] == 'task_a' - # 任务 B: 不声明 options → __call__ 重置回 _DEFAULT_OPTIONS, 不残留 A 的 + # 任务 B: 声明自己的 options → 不残留 A 的任何键 strategy_b = battle_wait_strategy(success='activity') + opts_b = battle_wait_options(success={'market': 'task_b'}) + @opts_b @strategy_b def battle_wait_b(owner, *, battle_wait_plan, options=None): return battle_wait_plan, options plan_b, options_b = battle_wait_b(object()) assert plan_b.success == 'activity' - assert 'task_a' not in options_b.get('success', {}) - # 全局槽位也跟着回到默认(与 plan 的"后加载者覆盖"一致) - assert battle_wait_strategy.options == battle_wait_strategy._DEFAULT_OPTIONS - assert options_b == battle_wait_strategy._DEFAULT_OPTIONS + assert options_b['success'] == {'market': 'task_b'} + + +# ------------------------------------------------------------------------------------------------------------------ +# runtime: 自动装饰 hook, 注入单例 pub_ctx / 每 hook 的 pri_ctx, 三档状态 + options 分发。 +# ------------------------------------------------------------------------------------------------------------------ + +def _make_runtime_probe(): + class RuntimeProbe(BattleWait): + def __init__(self): + self.calls = [] + + def _bw_setup_probe(self, pub, pri): + self.calls.append(('setup', pub, pri)) + return HookSignal.DONE + + def _bw_completion_probe(self, pub, pri): + self.calls.append(('completion', pub, pri)) + return HookSignal.DONE + + def _bw_success_probe(self, pub, pri): + self.calls.append(('success', pub, pri)) + return HookSignal.DONE + + return RuntimeProbe() + + +def test_runtime_injects_singleton_pub_and_per_hook_pri(): + battle_wait = _make_runtime_probe() + + getattr(battle_wait, '_bw_setup_probe')() + getattr(battle_wait, '_bw_completion_probe')() + + _, setup_pub, setup_pri = battle_wait.calls[0] + _, completion_pub, completion_pri = battle_wait.calls[1] + + # 所有 hook 共享同一个 pub_ctx + assert setup_pub is runtime.pub_ctx + assert completion_pub is runtime.pub_ctx + # 每个 hook 持有自己的 pri_ctx, 互不共享 + assert setup_pri is runtime.pri_ctx['_bw_setup_probe'] + assert completion_pri is runtime.pri_ctx['_bw_completion_probe'] + assert setup_pri is not completion_pri + + +def test_runtime_preserves_function_name_for_completion_detection(): + battle_wait = _make_runtime_probe() + hook = getattr(battle_wait, '_bw_completion_probe') + assert hook.__name__ == '_bw_completion_probe' + + +def test_runtime_task_owner_switch_resets_per_task(): + battle_wait = _make_runtime_probe() + getattr(battle_wait, '_bw_setup_probe')() + runtime.pub_ctx.cross['keep'] = 1 + runtime.pub_ctx.per_task['drop'] = 1 + runtime.pri_ctx['_bw_setup_probe'].per_task['drop'] = 1 + + # 换一个 owner 触发 reset_per_task: cross 保留, per_task 清空 + other = _make_runtime_probe() + getattr(other, '_bw_setup_probe')() + + assert runtime.pub_ctx.cross == {'keep': 1} + assert runtime.pub_ctx.per_task == {} + assert runtime.pri_ctx['_bw_setup_probe'].per_task == {} + + +def test_runtime_reset_per_battle_keeps_cross_and_per_task(): + battle_wait = _make_runtime_probe() + getattr(battle_wait, '_bw_setup_probe')() + runtime.pub_ctx.cross['c'] = 1 + runtime.pub_ctx.per_task['t'] = 1 + runtime.pub_ctx.per_battle['b'] = 1 + runtime.pri_ctx['_bw_setup_probe'].per_battle['b'] = 1 + + runtime.reset_per_battle() + + assert runtime.pub_ctx.cross == {'c': 1} + assert runtime.pub_ctx.per_task == {'t': 1} + assert runtime.pub_ctx.per_battle == _DEFAULT_PER_BATTLE() + assert 'b' not in runtime.pub_ctx.per_battle + assert runtime.pri_ctx['_bw_setup_probe'].per_battle == {} + + +def test_runtime_update_options_distributes_by_hook_event_name(): + battle_wait = _make_runtime_probe() + getattr(battle_wait, '_bw_setup_probe')() + getattr(battle_wait, '_bw_completion_probe')() + getattr(battle_wait, '_bw_success_probe')() + + runtime.update_options({ + 'setup': {'x': 1}, + 'completion': {'y': 2}, + 'success': {'z': 3}, + }) + + # pub 拿整份 options + assert runtime.pub_ctx.options['setup'] == {'x': 1} + # 每个 hook 的 pri 只拿自己事件名对应的 slice + assert runtime.pri_ctx['_bw_setup_probe'].options == {'x': 1} + assert runtime.pri_ctx['_bw_completion_probe'].options == {'y': 2} + assert runtime.pri_ctx['_bw_success_probe'].options == {'z': 3} + + +def test_runtime_update_options_none_clears_all_slices(): + battle_wait = _make_runtime_probe() + getattr(battle_wait, '_bw_setup_probe')() + runtime.update_options({'setup': {'x': 1}}) + assert runtime.pub_ctx.options['setup'] == {'x': 1} + + runtime.update_options(None) + + assert runtime.pub_ctx.options == {} + assert runtime.pri_ctx['_bw_setup_probe'].options == {} + + +def test_runtime_str_shows_hook_name_and_scope_keys(): + battle_wait = _make_runtime_probe() + getattr(battle_wait, '_bw_setup_probe')() + runtime.pub_ctx.per_task['stage'] = 1 + runtime.pri_ctx['_bw_setup_probe'].per_battle['clicked'] = True + runtime.update_options({'setup': {'x': 1}}) + + text = str(battle_wait.__class__._bw_setup_probe) + + assert '_bw_setup_probe' in text + assert 'stage' in text + assert 'clicked' in text + assert 'setup' in text From dcd03726c1db35df3d1aa2fd84b2cea580bfd10f Mon Sep 17 00:00:00 2001 From: neptine <2234044577@qq.com> Date: Thu, 17 Sep 2026 01:51:46 +0800 Subject: [PATCH 04/13] refactor(GeneralBattle): Model hook options and runtime state as per-event dataclasses --- tasks/Component/GeneralBattle/battle_wait.py | 231 +++++++++++++++---- 1 file changed, 184 insertions(+), 47 deletions(-) diff --git a/tasks/Component/GeneralBattle/battle_wait.py b/tasks/Component/GeneralBattle/battle_wait.py index 766da7b19b..164a718745 100644 --- a/tasks/Component/GeneralBattle/battle_wait.py +++ b/tasks/Component/GeneralBattle/battle_wait.py @@ -115,22 +115,6 @@ class BattleResult(Enum): ) _SEQUENCE_DEFAULT: str = 'completion > interrupt > success > failure >' -# hook options 默认值(装饰器 @battle_wait_options / with 上下文写入类槽) -_DEFAULT_OPTIONS: dict[str, dict] = { - 'success': { - 'reward_exclude_click_1': [ - 'C_END_MESSAGE_RIGHT_TOP', 'C_END_BUFF_AREA_1', - 'C_END_BUFF_AREA_2', 'C_END_SOUL_RECORD', 'C_END_SOUL_DETAILS', - ], - 'reward_exclude_click_2': [ - 'C_END_MESSAGE_RIGHT_TOP', 'C_END_BUFF_AREA_1', - 'C_END_BUFF_AREA_2', 'C_END_SOUL_RECORD', 'C_END_SOUL_DETAILS', - 'C_END_1_1', 'C_END_1_2', 'C_END_1_3', 'C_END_1_4', - 'C_END_1_5', 'C_END_1_6', - ], - }, -} - # 跨任务/跨战斗状态的初始 @dataclass class PerTaskState: @@ -144,6 +128,155 @@ class PerBattleState: success: BattleResult = BattleResult.FAILURE hook_enabled: set = field(default_factory=lambda: set(_HOOKS_DEFAULT) - {'completion'}) + +# 每个 hook 事件的 options 数据类: pri[hook].options 就是这个实例。 +# 按 event 建模 —— 不管 success 挂的是什么 strategy, OptionSuccessDefault 都是它的配置。 +@dataclass +class OptionSetupDefault: + excludes: list | None = None + + +@dataclass +class OptionCompletionDefault: + reward_exclude_click_1 = [ + 'C_END_MESSAGE_RIGHT_TOP', 'C_END_BUFF_AREA_1', + 'C_END_BUFF_AREA_2', 'C_END_SOUL_RECORD', 'C_END_SOUL_DETAILS', + ] + reward_exclude_click_2 = [ + 'C_END_MESSAGE_RIGHT_TOP', 'C_END_BUFF_AREA_1', + 'C_END_BUFF_AREA_2', 'C_END_SOUL_RECORD', 'C_END_SOUL_DETAILS', + 'C_END_1_1', 'C_END_1_2', 'C_END_1_3', 'C_END_1_4', + 'C_END_1_5', 'C_END_1_6', + ] + + +@dataclass +class OptionInterruptDefault: + pass + + +@dataclass +class OptionSuccessDefault: + excludes: list | None = None + + +@dataclass +class OptionFailureDefault: + pass + + +@dataclass +class OptionIdleDefault: + pass + + +# event → options 数据类, update_options 靠它按 hook2event 自动实例化分发 +_OPTION_CLASSES: dict[str, type] = { + 'setup': OptionSetupDefault, + 'completion': OptionCompletionDefault, + 'interrupt': OptionInterruptDefault, + 'success': OptionSuccessDefault, + 'failure': OptionFailureDefault, + 'idle': OptionIdleDefault, +} + + +def _options_to_instances(raw: dict) -> dict: + """把按 event 分组的原始 dict 转成对应的 Option 数据类实例(未知 key 过滤)""" + result = {} + for event, value in (raw or {}).items(): + opt_cls = _OPTION_CLASSES.get(event) + if opt_cls is not None and isinstance(value, opt_cls): + result[event] = value + elif opt_cls is not None and isinstance(value, dict): + known = {k: v for k, v in value.items() if k in opt_cls.__dataclass_fields__} + result[event] = opt_cls(**known) + else: + result[event] = deepcopy(value) + return result + + +# pri 层的私有状态, 同样按 event 建模: pri[hook].per_task / per_battle 就是这个实例。 +# 字段按需声明 —— 有自定义私有状态的 strategy 往自己 event 的类里加字段。 +@dataclass +class PerTaskSetup: + pass + + +@dataclass +class PerTaskCompletion: + pass + + +@dataclass +class PerTaskInterrupt: + pass + + +@dataclass +class PerTaskSuccess: + pass + + +@dataclass +class PerTaskFailure: + pass + + +@dataclass +class PerTaskIdle: + pass + + +@dataclass +class PerBattleSetup: + pass + + +@dataclass +class PerBattleCompletion: + pass + + +@dataclass +class PerBattleInterrupt: + pass + + +@dataclass +class PerBattleSuccess: + pass + + +@dataclass +class PerBattleFailure: + pass + + +@dataclass +class PerBattleIdle: + pass + + +# event → pri 私有状态数据类 +_PER_TASK_CLASSES: dict[str, type] = { + 'setup': PerTaskSetup, + 'completion': PerTaskCompletion, + 'interrupt': PerTaskInterrupt, + 'success': PerTaskSuccess, + 'failure': PerTaskFailure, + 'idle': PerTaskIdle, +} + +_PER_BATTLE_CLASSES: dict[str, type] = { + 'setup': PerBattleSetup, + 'completion': PerBattleCompletion, + 'interrupt': PerBattleInterrupt, + 'success': PerBattleSuccess, + 'failure': PerBattleFailure, + 'idle': PerBattleIdle, +} + # ──────────────────────────────────────────── @@ -384,12 +517,11 @@ def battle_wait(self, *args, **kwargs): with battle_wait_options(failure={'excludes': [...]}): task.battle_wait() """ - _DEFAULT_OPTIONS = _DEFAULT_OPTIONS - options: dict[str, dict] = deepcopy(_DEFAULT_OPTIONS) + options: dict = {event: cls() for event, cls in _OPTION_CLASSES.items()} def __init__(self, *arg, **kwargs): self._scope = 'decorator' - self._overrides = dict(kwargs) + self._overrides = _options_to_instances(dict(kwargs)) def __enter__(self): self._scope = 'temporary' @@ -484,7 +616,7 @@ def __str__(self): def _keys(d): if isinstance(d, dict): return ','.join(d.keys()) or '-' - if isinstance(d, (PerTaskState, PerBattleState)): + if isinstance(d, tuple(_OPTION_CLASSES.values())) or isinstance(d, tuple(_PER_TASK_CLASSES.values())) or isinstance(d, tuple(_PER_BATTLE_CLASSES.values())) or isinstance(d, (PerTaskState, PerBattleState)): return ','.join(d.__dataclass_fields__) or '-' return str(d) @@ -501,15 +633,19 @@ def __repr__(self): def reset_per_task(cls): cls._ensure_pub() cls.pub_ctx.per_task = PerTaskState() - for ctx in cls.pri_ctx.values(): - ctx.per_task = {} + for hook_name, ctx in cls.pri_ctx.items(): + event = cls.hook2event(hook_name) + cls_type = _PER_TASK_CLASSES.get(event, dict) + ctx.per_task = cls_type() @classmethod def reset_per_battle(cls): cls._ensure_pub() cls.pub_ctx.per_battle = PerBattleState() - for ctx in cls.pri_ctx.values(): - ctx.per_battle = {} + for hook_name, ctx in cls.pri_ctx.items(): + event = cls.hook2event(hook_name) + cls_type = _PER_BATTLE_CLASSES.get(event, dict) + ctx.per_battle = cls_type() @classmethod def _ensure_pub(cls): @@ -519,7 +655,11 @@ def _ensure_pub(cls): @classmethod def _ensure_pri(cls, name): if name not in cls.pri_ctx: - cls.pri_ctx[name] = PrivateContext() + event = cls.hook2event(name) + cls.pri_ctx[name] = PrivateContext( + per_task=_PER_TASK_CLASSES.get(event, dict)(), + per_battle=_PER_BATTLE_CLASSES.get(event, dict)(), + ) @classmethod def hook2event(cls, func_name: str) -> str: @@ -527,16 +667,23 @@ def hook2event(cls, func_name: str) -> str: return func_name[len('_bw_'):].rsplit('_', 1)[0] @classmethod - def update_options(cls, options: dict[str, dict]): + def update_options(cls, options: dict[str, object] | None): + """options 是 dict[str, OptionXxx] 实例(由 battle_wait_options 转好),直接按 event 分发""" cls._ensure_pub() if not isinstance(options, dict): cls.pub_ctx.options = {} - for ctx in cls.pri_ctx.values(): - ctx.options = {} + for hook_name, ctx in cls.pri_ctx.items(): + ctx.options = cls._option_class(hook_name)() return cls.pub_ctx.options = options for hook_name, ctx in cls.pri_ctx.items(): - ctx.options = options.get(cls.hook2event(hook_name)) or {} + event = cls.hook2event(hook_name) + opt = options.get(event) + ctx.options = opt if isinstance(opt, cls._option_class(hook_name)) else cls._option_class(hook_name)() + + @classmethod + def _option_class(cls, hook_name: str) -> type: + return _OPTION_CLASSES.get(cls.hook2event(hook_name)) or dict @classmethod def hook_enabled(cls) -> set: @@ -720,11 +867,14 @@ def _bw_randomclick_default(self, pub: PublicContext, pri: PrivateContext) -> Ho # custom # ------------------------------------------------------------------------------------------------------------------ def _bw_success_soul(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: - options: dict[str, dict] = pub.options if isinstance(pub.options, dict) else {} - success_options = options.get('success') or {} - if not isinstance(success_options, dict): - raise TypeError("battle wait option 'success' must be a dict") - action_click = self.reward_exclude_click(success_options.get('excludes')) + success_options = pub.options.get('success') if isinstance(pub.options, dict) else {} + if isinstance(success_options, OptionSuccessDefault): + excludes = success_options.excludes + elif isinstance(success_options, dict): + excludes = success_options.get('excludes') + else: + raise TypeError("battle wait option 'success' must be an OptionSuccessDefault") + action_click = self.reward_exclude_click(excludes) if self.appear_then_click(self.I_WIN, interval=0.8): return HookSignal.CONTINUE appear_ghost, appear_reward, appear_gold, appear_skin = ( @@ -909,19 +1059,6 @@ def battle_wait(self, *args, **kwargs): d = Device(c) - class TestBattleWait(GeneralBattle, BattleWait): - def _bw_settlement(self): - pass - - @battle_wait_strategy( 'reserve_default', 'idle_default', failure='default') - def battle_wait(self, *args, **kwargs): - return self.battle_wait_with_strategy(*args, **kwargs) - - test_battle_wait = TestBattleWait(c,d) - test_battle_wait.battle_wait(random_click_swipt_enable=1) - with battle_wait_strategy(sequence='completion > interrupt > success > failure > idle').with_options(options={"setup": {"11": "11"}}): - test_battle_wait.battle_wait() - From 60bf1268f6134206edbee8750291fad6a13df10f Mon Sep 17 00:00:00 2001 From: Hurry2 <1546296221@qq.com> Date: Thu, 17 Sep 2026 21:01:25 +0800 Subject: [PATCH 05/13] =?UTF-8?q?feat(WeeklyTrifles):=E6=AF=8F=E5=91=A8?= =?UTF-8?q?=E7=90=90=E4=BA=8B=E6=96=B0=E5=A2=9E=E6=89=A7=E8=A1=8C=E6=83=A0?= =?UTF-8?q?=E6=AF=94=E5=AF=BF=E7=9A=84=E6=91=B8=E9=B1=BC=E8=A1=8C=E5=8A=A8?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 默认仅仅使用全部存储按钮 - 会ocr当前福运御守数量,不足则不执行 - 因为是新增功能所以默认关闭 - 主要目的是让真蛇每周打一次即可获得全部奖励,同时避免周常活动遗漏 --- assets/i18n/zh-CN.json | 6 +- tasks/WeeklyTrifles/assets.py | 30 +++++ tasks/WeeklyTrifles/config.py | 16 ++- tasks/WeeklyTrifles/script_task.py | 113 ++++++++++++++++-- tasks/WeeklyTrifles/touch_fish/image.json | 92 ++++++++++++++ tasks/WeeklyTrifles/touch_fish/ocr.json | 20 ++++ .../touch_fish_check_touch_fish.png | Bin 0 -> 2188 bytes .../touch_fish/touch_fish_guild_goto_tf.png | Bin 0 -> 6086 bytes .../touch_fish/touch_fish_wt_fold_window.png | Bin 0 -> 1111 bytes .../touch_fish/touch_fish_wt_happy_get.png | Bin 0 -> 7925 bytes .../touch_fish/touch_fish_wt_last_save.png | Bin 0 -> 3920 bytes .../touch_fish_wt_open_fold_window.png | Bin 0 -> 1102 bytes .../touch_fish/touch_fish_wt_save_all.png | Bin 0 -> 13220 bytes .../touch_fish/touch_fish_wt_tf_confirm.png | Bin 0 -> 5969 bytes .../touch_fish/touch_fish_wt_tf_goto_main.png | Bin 0 -> 3588 bytes .../touch_fish_wt_tf_save_success.png | Bin 0 -> 3321 bytes 16 files changed, 258 insertions(+), 19 deletions(-) create mode 100644 tasks/WeeklyTrifles/touch_fish/image.json create mode 100644 tasks/WeeklyTrifles/touch_fish/ocr.json create mode 100644 tasks/WeeklyTrifles/touch_fish/touch_fish_check_touch_fish.png create mode 100644 tasks/WeeklyTrifles/touch_fish/touch_fish_guild_goto_tf.png create mode 100644 tasks/WeeklyTrifles/touch_fish/touch_fish_wt_fold_window.png create mode 100644 tasks/WeeklyTrifles/touch_fish/touch_fish_wt_happy_get.png create mode 100644 tasks/WeeklyTrifles/touch_fish/touch_fish_wt_last_save.png create mode 100644 tasks/WeeklyTrifles/touch_fish/touch_fish_wt_open_fold_window.png create mode 100644 tasks/WeeklyTrifles/touch_fish/touch_fish_wt_save_all.png create mode 100644 tasks/WeeklyTrifles/touch_fish/touch_fish_wt_tf_confirm.png create mode 100644 tasks/WeeklyTrifles/touch_fish/touch_fish_wt_tf_goto_main.png create mode 100644 tasks/WeeklyTrifles/touch_fish/touch_fish_wt_tf_save_success.png diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index 64614e290c..3f039b02dc 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -137,5 +137,7 @@ "enable_ap100_anti_detect": "100体爬塔防检测", "enable_ap100_anti_detect_help": "100体爬塔战斗过程是否随机点击或滑动", "costume_battle_15": "灵狐寄愿", - "costume_shikigami_12": "拾光之窗" -} + "costume_shikigami_12": "拾光之窗", + "save_touch_fish": "惠比寿的摸鱼行动", + "save_touch_fish_help": "默认使用全部存储按钮,主要目的是为了配合真蛇每周打一次即可获得俩次奖励(并不会节省体力)" +} \ No newline at end of file diff --git a/tasks/WeeklyTrifles/assets.py b/tasks/WeeklyTrifles/assets.py index 2e49faf6b3..d95ac924b9 100644 --- a/tasks/WeeklyTrifles/assets.py +++ b/tasks/WeeklyTrifles/assets.py @@ -79,3 +79,33 @@ class WeeklyTriflesAssets: I_WT_SE_RANK = RuleImage(roi_front=(1017,572,45,44), roi_back=(989,556,103,81), threshold=0.8, method="Template matching", file="./tasks/WeeklyTrifles/secret/secret_wt_se_rank.png") + # Image Rule Assets + # 前往摸鱼页面 + I_GUILD_GOTO_TF = RuleImage(roi_front=(30,433,88,30), roi_back=(7,153,227,329), threshold=0.8, method="Template matching", file="./tasks/WeeklyTrifles/touch_fish/touch_fish_guild_goto_tf.png") + # 关闭首次进入弹窗 + I_WT_HAPPY_GET = RuleImage(roi_front=(736,461,119,35), roi_back=(736,461,119,35), threshold=0.8, method="Template matching", file="./tasks/WeeklyTrifles/touch_fish/touch_fish_wt_happy_get.png") + # 全部保存本周次数 + I_WT_SAVE_ALL = RuleImage(roi_front=(1115,598,79,74), roi_back=(1115,598,79,74), threshold=0.8, method="Template matching", file="./tasks/WeeklyTrifles/touch_fish/touch_fish_wt_save_all.png") + # 摸鱼行动页面标志 + I_CHECK_TOUCH_FISH = RuleImage(roi_front=(1082,20,27,27), roi_back=(1082,20,27,27), threshold=0.8, method="Template matching", file="./tasks/WeeklyTrifles/touch_fish/touch_fish_check_touch_fish.png") + # 返回庭院 + I_WT_TF_GOTO_MAIN = RuleImage(roi_front=(93,17,42,39), roi_back=(0,0,198,82), threshold=0.8, method="Template matching", file="./tasks/WeeklyTrifles/touch_fish/touch_fish_wt_tf_goto_main.png") + # 打开折叠窗口 + I_WT_OPEN_FOLD_WINDOW = RuleImage(roi_front=(28,155,21,21), roi_back=(28,155,21,21), threshold=0.8, method="Template matching", file="./tasks/WeeklyTrifles/touch_fish/touch_fish_wt_open_fold_window.png") + # 已经打开的折叠窗口 + I_WT_FOLD_WINDOW = RuleImage(roi_front=(261,155,21,21), roi_back=(261,155,21,21), threshold=0.8, method="Template matching", file="./tasks/WeeklyTrifles/touch_fish/touch_fish_wt_fold_window.png") + # 确认储存的按钮 + I_WT_TF_CONFIRM = RuleImage(roi_front=(672,588,139,56), roi_back=(672,588,139,56), threshold=0.8, method="Template matching", file="./tasks/WeeklyTrifles/touch_fish/touch_fish_wt_tf_confirm.png") + # 储存成功的标志 + I_WT_TF_SAVE_SUCCESS = RuleImage(roi_front=(942,243,62,25), roi_back=(871,212,219,70), threshold=0.8, method="Template matching", file="./tasks/WeeklyTrifles/touch_fish/touch_fish_wt_tf_save_success.png") + # 上周存储界面 + I_WT_LAST_SAVE = RuleImage(roi_front=(590,78,99,24), roi_back=(540,58,213,59), threshold=0.8, method="Template matching", file="./tasks/WeeklyTrifles/touch_fish/touch_fish_wt_last_save.png") + + + # Ocr Rule Assets + # 现有福运御守数量 + O_WT_LUCKY_TICKETS = RuleOcr(roi=(1145,16,95,32), area=(1145,16,95,32), mode="DigitCounter", method="Default", keyword="", name="wt_lucky_tickets") + # 全部储存需要的御守数量 + O_WT_SAVE_COST = RuleOcr(roi=(1162,676,35,24), area=(1156,670,52,40), mode="Digit", method="Default", keyword="", name="wt_save_cost") + + diff --git a/tasks/WeeklyTrifles/config.py b/tasks/WeeklyTrifles/config.py index 57d131f5c4..f67be3a9c6 100644 --- a/tasks/WeeklyTrifles/config.py +++ b/tasks/WeeklyTrifles/config.py @@ -7,18 +7,26 @@ from tasks.Component.config_scheduler import Scheduler as BaseScheduler from tasks.Component.config_base import ConfigBase, TimeDelta + class Scheduler(BaseScheduler): - success_interval: TimeDelta = Field(default=TimeDelta(days=7), description='success_interval_help') - failure_interval: TimeDelta = Field(default=TimeDelta(days=7), description='failure_interval_help') + success_interval: TimeDelta = Field( + default=TimeDelta(days=7), description='success_interval_help' + ) + failure_interval: TimeDelta = Field( + default=TimeDelta(days=7), description='failure_interval_help' + ) + class Trifles(BaseModel): share_collect: bool = Field(default=True, description='share_collect_help') share_area_boss: bool = Field(default=True, description='share_area_boss_help') share_secret: bool = Field(default=True, description='share_secret_help') - broken_amulet: int = Field(title='Broken Amulet', default=100, description='trifles_broken_amulet_help') + save_touch_fish: bool = Field(default=False, description='save_touch_fish_help') + broken_amulet: int = Field( + title='Broken Amulet', default=100, description='trifles_broken_amulet_help' + ) class WeeklyTrifles(ConfigBase): scheduler: Scheduler = Field(default_factory=Scheduler) trifles: Trifles = Field(default_factory=Trifles) - diff --git a/tasks/WeeklyTrifles/script_task.py b/tasks/WeeklyTrifles/script_task.py index deb018803f..c3e18f38cb 100644 --- a/tasks/WeeklyTrifles/script_task.py +++ b/tasks/WeeklyTrifles/script_task.py @@ -9,14 +9,25 @@ from module.base.timer import Timer from tasks.GameUi.game_ui import GameUi -from tasks.GameUi.page import page_main, page_collection, page_area_boss, page_secret_zones, page_summon, random_click -from tasks.WeeklyTrifles.config import Trifles +from tasks.GameUi.page import ( + page_main, + page_collection, + page_area_boss, + page_secret_zones, + page_summon, + page_guild, + page_touch_fish, + random_click, +) from tasks.WeeklyTrifles.assets import WeeklyTriflesAssets + class ScriptTask(GameUi, WeeklyTriflesAssets): def run(self): con = self.config.weekly_trifles.trifles + if con.save_touch_fish: + self._save_touch_fish() if con.share_collect: self._share_collect() if con.share_area_boss: @@ -29,7 +40,6 @@ def run(self): self.set_next_run(task='WeeklyTrifles', success=True, finish=True) raise TaskEnd('WeeklyTrifles') - def click_share(self, wechat) -> bool: """ 点击分享 @@ -52,12 +62,69 @@ def click_share(self, wechat) -> bool: if self.ui_reward_appear_click(): logger.info('Get reward') return True - if self.appear_then_click(self.I_WT_QR_CODE, self.C_WT_WECHAT, interval=4.8): + if self.appear_then_click( + self.I_WT_QR_CODE, self.C_WT_WECHAT, interval=4.8 + ): continue if get_timer.reached(): logger.warning('Share timeout. The reward may have been obtained') return False + def _save_touch_fish(self): + """ + 惠比寿的摸鱼行动 + :return: + """ + logger.hr('Touch fish start') + self.ui_get_current_page() + self.ui_goto(page_guild) + # 防止入口被折叠 + while 1: + # 等个折叠窗口展开动画 + sleep(1) + self.screenshot() + if self.appear_then_click(self.I_WT_OPEN_FOLD_WINDOW): + continue + if self.appear(self.I_WT_FOLD_WINDOW): + break + # 保存次数,后续做了也会邮件返还福运御守 + self.ui_goto(page_touch_fish) + self.screenshot() + if self.appear(self.I_WT_SAVE_ALL): + _, cu_tickts, _ = self.O_WT_LUCKY_TICKETS.ocr(self.device.image) + cost_tickts = self.O_WT_SAVE_COST.ocr(self.device.image) + get_timer = Timer(7) + get_timer.start() + while 1: + sleep(0.5) + self.screenshot() + if self.appear(self.I_WT_LAST_SAVE): + self.click( + random_click(ltrb=(True, False, False, False)), interval=1.5 + ) + continue + if self.appear_then_click(self.I_WT_HAPPY_GET): + continue + if cu_tickts >= cost_tickts: + self.appear_then_click(self.I_WT_SAVE_ALL) + cu_tickts = 0 + continue + if self.appear_then_click(self.I_WT_TF_CONFIRM): + continue + if self.appear(self.I_WT_TF_SAVE_SUCCESS) and not self.appear( + self.I_WT_SAVE_ALL + ): + logger.info('Touch fish save success') + break + if cu_tickts < cost_tickts: + logger.warning('Touch fish tickts not enough, exit!') + break + if get_timer.reached(): + logger.warning('Touch fish timeout, exit!') + break + logger.hr('Touch fish finished') + self.ui_goto(page_main) + def _share_collect(self): """ 图鉴分享 @@ -92,7 +159,9 @@ def _share_collect(self): logger.info('Get reward') break - if self.appear_then_click(self.I_WT_QR_CODE, self.C_WT_WECHAT, interval=2.8): + if self.appear_then_click( + self.I_WT_QR_CODE, self.C_WT_WECHAT, interval=2.8 + ): continue if get_timer.reached(): logger.warning('Share timeout. The reward may have been obtained') @@ -112,10 +181,13 @@ def _share_area_boss(self): 地鬼分享 :return: """ + def back_boss(): while 1: self.screenshot() - if self.appear(self.I_WT_DAY_BATTLE) or self.appear(self.I_CHECK_EXPLORATION): + if self.appear(self.I_WT_DAY_BATTLE) or self.appear( + self.I_CHECK_EXPLORATION + ): break if self.appear_then_click(self.I_UI_BACK_RED, interval=1): continue @@ -124,6 +196,7 @@ def back_boss(): if self.appear_then_click(self.I_UI_BACK_YELLOW, interval=1): continue logger.info('Back to boss') + logger.hr('Share area boss') self.ui_get_current_page() self.ui_goto(page_area_boss) @@ -205,6 +278,7 @@ def _broken_amulet(self, dest_num: int): :param dest_num: :return: """ + def exit_amulet(): self.wait_until_appear(self.I_BM_CONFIRM, wait_time=3) while 1: @@ -233,7 +307,9 @@ def exit_amulet(): if real_num <= 0: exit_amulet() return - if self.appear_then_click(self.I_BM_ENTER, interval=1) or self.appear_then_click(self.I_BM_AGAIN, interval=1): + if self.appear_then_click( + self.I_BM_ENTER, interval=1 + ) or self.appear_then_click(self.I_BM_AGAIN, interval=1): sleep(0.4) # 等待动画开始 timeout_timer = Timer(5).start() # 随机点击直到再次召唤出现或者超时 @@ -244,7 +320,9 @@ def exit_amulet(): if self.appear(self.I_BM_AGAIN, interval=0.8): break else: - logger.warning(f'Wait for again timeout:Count[{count}], Remain[{real_num}]') + logger.warning( + f'Wait for again timeout:Count[{count}], Remain[{real_num}]' + ) exit_amulet() return else: @@ -255,19 +333,29 @@ def exit_amulet(): x_50, _, _, _ = self.O_BA_TIMES.ocr(self.device.image, '50次') self.I_BMT_CHECK.match(self.device.image) x_check, y_check, width_check, height_check = self.I_BMT_CHECK.roi_front - selected_10 = min(abs(x_10 - x_check), abs(x_50 - x_check)) == abs(x_10 - x_check) + selected_10 = min(abs(x_10 - x_check), abs(x_50 - x_check)) == abs( + x_10 - x_check + ) logger.info(f'Current selected {"10" if selected_10 else "50"} amulet') count += 10 if selected_10 else 50 logger.info(f'Broken amulet:Count[{count}], Remain[{real_num}]') # 一次50票不超过限制且当前选择的是10票则切换50票 if count + 50 < dest_num and selected_10: logger.hr('Switch to 50 amulet') - self.device.click(x=x_50 - width_check // 2, y=y_check + height_check // 2, control_name='Click_50') + self.device.click( + x=x_50 - width_check // 2, + y=y_check + height_check // 2, + control_name='Click_50', + ) self.device.click_record_clear() # 一次50票会超过限制且当前选择的是50票则切换10票 if count + 50 >= dest_num and not selected_10: logger.hr('Switch to 10 amulet') - self.device.click(x=x_10 - width_check // 2, y=y_check + height_check // 2, control_name='Click_10') + self.device.click( + x=x_10 - width_check // 2, + y=y_check + height_check // 2, + control_name='Click_10', + ) self.device.click_record_clear() # 正常结束且还有票, 则执行一次退出 exit_amulet() @@ -276,6 +364,7 @@ def exit_amulet(): if __name__ == '__main__': from module.config.config import Config from module.device.device import Device + c = Config('oas1') d = Device(c) t = ScriptTask(c, d) @@ -284,5 +373,3 @@ def exit_amulet(): # t._share_collect() t._share_area_boss() # t.click_share(t.I_WT_SE_WECHAT) - - diff --git a/tasks/WeeklyTrifles/touch_fish/image.json b/tasks/WeeklyTrifles/touch_fish/image.json new file mode 100644 index 0000000000..5c183ce99f --- /dev/null +++ b/tasks/WeeklyTrifles/touch_fish/image.json @@ -0,0 +1,92 @@ +[ + { + "itemName": "guild_goto_tf", + "imageName": "touch_fish_guild_goto_tf.png", + "roiFront": "30,433,88,30", + "roiBack": "7,153,227,329", + "method": "Template matching", + "threshold": 0.8, + "description": "前往摸鱼页面" + }, + { + "itemName": "wt_happy_get", + "imageName": "touch_fish_wt_happy_get.png", + "roiFront": "736,461,119,35", + "roiBack": "736,461,119,35", + "method": "Template matching", + "threshold": 0.8, + "description": "关闭首次进入弹窗" + }, + { + "itemName": "wt_save_all", + "imageName": "touch_fish_wt_save_all.png", + "roiFront": "1115,598,79,74", + "roiBack": "1115,598,79,74", + "method": "Template matching", + "threshold": 0.8, + "description": "全部保存本周次数" + }, + { + "itemName": "check_touch_fish", + "imageName": "touch_fish_check_touch_fish.png", + "roiFront": "1082,20,27,27", + "roiBack": "1082,20,27,27", + "method": "Template matching", + "threshold": 0.8, + "description": "摸鱼行动页面标志" + }, + { + "itemName": "wt_tf_goto_main", + "imageName": "touch_fish_wt_tf_goto_main.png", + "roiFront": "93,17,42,39", + "roiBack": "0,0,198,82", + "method": "Template matching", + "threshold": 0.8, + "description": "返回庭院" + }, + { + "itemName": "wt_open_fold_window", + "imageName": "touch_fish_wt_open_fold_window.png", + "roiFront": "28,155,21,21", + "roiBack": "28,155,21,21", + "method": "Template matching", + "threshold": 0.8, + "description": "打开折叠窗口" + }, + { + "itemName": "wt_fold_window", + "imageName": "touch_fish_wt_fold_window.png", + "roiFront": "261,155,21,21", + "roiBack": "261,155,21,21", + "method": "Template matching", + "threshold": 0.8, + "description": "已经打开的折叠窗口" + }, + { + "itemName": "wt_tf_confirm", + "imageName": "touch_fish_wt_tf_confirm.png", + "roiFront": "672,588,139,56", + "roiBack": "672,588,139,56", + "method": "Template matching", + "threshold": 0.8, + "description": "确认储存的按钮" + }, + { + "itemName": "wt_tf_save_success", + "imageName": "touch_fish_wt_tf_save_success.png", + "roiFront": "942,243,62,25", + "roiBack": "871,212,219,70", + "method": "Template matching", + "threshold": 0.8, + "description": "储存成功的标志" + }, + { + "itemName": "wt_last_save", + "imageName": "touch_fish_wt_last_save.png", + "roiFront": "590,78,99,24", + "roiBack": "540,58,213,59", + "method": "Template matching", + "threshold": 0.8, + "description": "上周存储界面" + } +] \ No newline at end of file diff --git a/tasks/WeeklyTrifles/touch_fish/ocr.json b/tasks/WeeklyTrifles/touch_fish/ocr.json new file mode 100644 index 0000000000..72aa650111 --- /dev/null +++ b/tasks/WeeklyTrifles/touch_fish/ocr.json @@ -0,0 +1,20 @@ +[ + { + "itemName": "wt_lucky_tickets", + "roiFront": "1145,16,95,32", + "roiBack": "1145,16,95,32", + "mode": "DigitCounter", + "method": "Default", + "keyword": "", + "description": "现有福运御守数量" + }, + { + "itemName": "wt_save_cost", + "roiFront": "1162,676,35,24", + "roiBack": "1156,670,52,40", + "mode": "Digit", + "method": "Default", + "keyword": "", + "description": "全部储存需要的御守数量" + } +] \ No newline at end of file diff --git a/tasks/WeeklyTrifles/touch_fish/touch_fish_check_touch_fish.png b/tasks/WeeklyTrifles/touch_fish/touch_fish_check_touch_fish.png new file mode 100644 index 0000000000000000000000000000000000000000..1cb56abb7f0de4ecccf3ecf838bdb3dac00fdbc7 GIT binary patch literal 2188 zcmV;72y^#|P)i@pn|ACI3+-U+~iKkOY)MJyxrOFgS_@nKKlKaSu!icc#iqt z(GPcdY+jHi(-%SAk$ON5Fz0u;Dr5i+gK(za121km4=HVr_okv8%>cT4b^k8XF{n9_#HM z&*bd)LzCY*0?@7YLv)l*BngV5008-g4WXdp%s;-8s#KVm=e4=tI=Ph%$Ydx+CP@y9 z#S*c)-@cOx68fF1l)5UI0bmp-gHgYA;V*yv;({Xx{joY%TcMQ8Gzc990C*=HB$2Lt z_1Q!$S@C=+3DK7PO9?zdrbqw)9f1J=LNR-3HsBt%!MB`rjIDmH0jAS4iPW9Owi-*<7@Sic_pE>9#whqd2!bet0Vtr=2vi!uvb7~? zKLIlkGLj+|afHY2J>GG&e;oSHj+L5n4PAkWYc)(Uhm54iG)YmJ(fd>GNpCirgVY%GXYfZ;e z6`!^772I2&_AXijx{4g0TwJ}WCJ~JzY>o6n56{GP4l}rP#(;ryC*X^lSDkY;w zJGnY3Jrg(kBGABV8$uSpdVOIEC*<`yMuEKRlWrKLrEn4eu=Hc^);;HI+)-arRWNkuqCm+s`y!Cz>Yt}R{dtK1ES;q0i*1j- zxjAQc$`pK(q(b3z^Zr$eLPgXYV)9tl9xwS24p_ATUe=y(*aX0BynQs_8D3Z-&{Z^My zDaE5PJQ6cBR5DmB%iv7s$JYdW*1qFyci;R0rom8h=G$y4<96AEI&9>|n9I5VBcQyg zDjtb=y#xb+^GpRYotTCI+we>xLU1rXjlayiV>)8Ql^u${N9Tv>`g=*n19RpH5-}F-R!;5z@wM>?$ zlqe*QF>7wQdZKsSXAcfKKRf0)Gco5H0Svs9`jx8|x?5{t(!4c*V`s-s$vya z#3keD_!923`EIn|1^}#lW*rfaL4gNnT;>H{DQ9ufKYVYrZcB~V=Fc-|lCgxcK$(mN z!pnmC+?Zfjz37^4Z;$Dw$-mB8*Og(lo_{h`~2Of?aR_L4eG>1K5aw(ol zrnn;Rg%g*;fymmX&5og7=<0@A1ctFzqeLzK=1-kD`PjbCUQfk|fYk*6uusln3f}af zXZ(q??O=6oN#VrfnWqoueV)Ls_jmJ|XfzzllH`x4E=PmWded4eLqh*-T3)8Z?njok z9pCu({pWLyiuH$I$SKH6Cy2R+L!LRessQW0{8VpJsPaT{f|hU8c>OrOl+I8Y6rwTN zD2K;5x#wif^EG)`RnqH)-ZdGw73;dKzQ&WQr>@PI=ch$7e#@@)d@*KuFs#971VK(% zf)%xeah%}E6+_qiCWofi9N0ATXnOgJCYf4x`PAi^fti|(m3lm%ugZlER~GFrEtmiq zM8&PyS9$i>zeqCk^1FMfG?@s;cx2c-AKe-Z`NHXJCKe+EeAeFg+U}q0SB!fA0FX&8Y1L~RN^{bd zAipIqZIC{^GH9Q78S09*9cfLtLNq$<;^)^QLA+RBR`eP^F^&}g&-1JfB2mnAwkqiamcyeakWqs%<(1@~ek_iL3K|%y@GM)`jdjAXEktuzCS{FC~ O0000QJ5JBgi%<1Et|H#uph?VQueoN3RT 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 M07*qoM6N<$g017FEdT%j literal 0 HcmV?d00001 diff --git a/tasks/WeeklyTrifles/touch_fish/touch_fish_wt_fold_window.png b/tasks/WeeklyTrifles/touch_fish/touch_fish_wt_fold_window.png new file mode 100644 index 0000000000000000000000000000000000000000..218784d395c0dcb53a94c8fe2281f31b089d3ff5 GIT binary patch literal 1111 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 literal 0 HcmV?d00001 diff --git a/tasks/WeeklyTrifles/touch_fish/touch_fish_wt_last_save.png b/tasks/WeeklyTrifles/touch_fish/touch_fish_wt_last_save.png new file mode 100644 index 0000000000000000000000000000000000000000..f818523f016d84c2374ebbf57031f791ade9fb03 GIT binary patch literal 3920 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 U;WXmT^#A|>07*qoM6N<$g2!G1-v9sr literal 0 HcmV?d00001 diff --git a/tasks/WeeklyTrifles/touch_fish/touch_fish_wt_save_all.png b/tasks/WeeklyTrifles/touch_fish/touch_fish_wt_save_all.png new file mode 100644 index 0000000000000000000000000000000000000000..bfff9d98ac868e801428fefa58573bc0a0498911 GIT binary patch literal 13220 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 literal 0 HcmV?d00001 diff --git a/tasks/WeeklyTrifles/touch_fish/touch_fish_wt_tf_confirm.png b/tasks/WeeklyTrifles/touch_fish/touch_fish_wt_tf_confirm.png new file mode 100644 index 0000000000000000000000000000000000000000..946411b883085891094021164738a0d5bfb68c8f GIT binary patch literal 5969 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 literal 0 HcmV?d00001 diff --git a/tasks/WeeklyTrifles/touch_fish/touch_fish_wt_tf_goto_main.png b/tasks/WeeklyTrifles/touch_fish/touch_fish_wt_tf_goto_main.png new file mode 100644 index 0000000000000000000000000000000000000000..4790956db43d968f920875d6fe849c81f8d9f777 GIT binary patch literal 3588 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 KMNUMnLSTZ@#po{p literal 0 HcmV?d00001 diff --git a/tasks/WeeklyTrifles/touch_fish/touch_fish_wt_tf_save_success.png b/tasks/WeeklyTrifles/touch_fish/touch_fish_wt_tf_save_success.png new file mode 100644 index 0000000000000000000000000000000000000000..70a829764fd453efc4e777b902af9b07f6224470 GIT binary patch literal 3321 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 DUdLi^ literal 0 HcmV?d00001 From ac4016f0a93198e44a010a61333378a9a7371f68 Mon Sep 17 00:00:00 2001 From: Hurry2 <1546296221@qq.com> Date: Fri, 18 Sep 2026 17:37:45 +0800 Subject: [PATCH 06/13] =?UTF-8?q?feat(WeeklyTrifles):=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E6=89=A7=E8=A1=8C=E9=A1=BA=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tasks/WeeklyTrifles/script_task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tasks/WeeklyTrifles/script_task.py b/tasks/WeeklyTrifles/script_task.py index c3e18f38cb..0b05653df5 100644 --- a/tasks/WeeklyTrifles/script_task.py +++ b/tasks/WeeklyTrifles/script_task.py @@ -26,14 +26,14 @@ class ScriptTask(GameUi, WeeklyTriflesAssets): def run(self): con = self.config.weekly_trifles.trifles - if con.save_touch_fish: - self._save_touch_fish() if con.share_collect: self._share_collect() if con.share_area_boss: self._share_area_boss() if con.share_secret: self._share_secret() + if con.save_touch_fish: + self._save_touch_fish() if con.broken_amulet: self._broken_amulet(con.broken_amulet) From 722ac818cb74dfd17f90b38a594d456afe20b8f1 Mon Sep 17 00:00:00 2001 From: Hurry2 <1546296221@qq.com> Date: Fri, 18 Sep 2026 17:55:46 +0800 Subject: [PATCH 07/13] =?UTF-8?q?feat(WeeklyTrifles):=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=91=B8=E9=B1=BC=E9=A1=B5=E9=9D=A2=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tasks/GameUi/page.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tasks/GameUi/page.py b/tasks/GameUi/page.py index da4b355e43..162d74ec46 100644 --- a/tasks/GameUi/page.py +++ b/tasks/GameUi/page.py @@ -209,6 +209,12 @@ def link(self, button, destination): page_dokan.additional = [GeneralBattleAssets.I_EXIT, DokanAssets.I_RYOU_DOKAN_EXIT_ENSURE, G.I_BACK_BLUE] page_dokan.link(button=G.I_BACK_Y, destination=page_main) +# 摸鱼行动页面 +from tasks.WeeklyTrifles.assets import WeeklyTriflesAssets +page_touch_fish = Page(WeeklyTriflesAssets.I_CHECK_TOUCH_FISH) +page_guild.link(button=WeeklyTriflesAssets.I_GUILD_GOTO_TF,destination=page_touch_fish) +page_touch_fish.link(button=WeeklyTriflesAssets.I_WT_TF_GOTO_MAIN,destination=page_main) + # ************************************* 战斗部分 *****************************************# # 战斗界面 From d2f98590f8c2ea0f52558eead69db662bc0d42fb Mon Sep 17 00:00:00 2001 From: neptine <2234044577@qq.com> Date: Sat, 19 Sep 2026 01:49:41 +0800 Subject: [PATCH 08/13] feat(GeneralBattle): Add prepare/preset/green/echo/randomclick hooks and config-driven loadout - add per-task/per-battle states and options for the new battle wait hooks - add loadout_default/loadout_show/state_show and loadout_from_config for config-driven setup - add randomclick_gate with start_delay/execution_limit/cooldown/trigger_probability gates - dispatch options per plan in runtime.update_options and extract setup_hook enable/disable - wire ActivityShikigami ScriptTask to Battle loadout flow - add tests for randomclick_gate and remove debug prints --- tasks/ActivityShikigami/script_task.py | 22 +- tasks/Component/GeneralBattle/battle.py | 175 +++- tasks/Component/GeneralBattle/battle_wait.py | 746 +++++++++++++++--- .../Component/GeneralBattle/general_battle.py | 2 - .../GeneralBattle/test_randomclick_gate.py | 196 +++++ 5 files changed, 1006 insertions(+), 135 deletions(-) create mode 100644 tests/tasks/Component/GeneralBattle/test_randomclick_gate.py diff --git a/tasks/ActivityShikigami/script_task.py b/tasks/ActivityShikigami/script_task.py index eb22b679ad..83aa6d2112 100644 --- a/tasks/ActivityShikigami/script_task.py +++ b/tasks/ActivityShikigami/script_task.py @@ -19,8 +19,8 @@ from module.logger import logger from tasks.base_task import BaseTask -from tasks.Component.GeneralBattle.battle_wait import battle_wait_strategy -from tasks.Component.GeneralBattle.config_general_battle import GeneralBattleConfig +from tasks.Component.GeneralBattle.battle_wait import battle_wait_strategy, battle_wait_options +from tasks.Component.GeneralBattle.battle import Battle from tasks.ActivityShikigami.assets import ActivityShikigamiAssets from tasks.ActivityShikigami.config import SwitchSoulConfig, GeneralBattleConfig, ActivityShikigami from tasks.Component.BaseActivity.base_activity import BaseActivity @@ -143,7 +143,7 @@ def switch_next(self): return True -class ScriptTask(StateMachine, GameUi, BaseActivity, SwitchSoul, ActivityShikigamiAssets): +class ScriptTask(StateMachine, GameUi, Battle, BaseActivity, SwitchSoul, ActivityShikigamiAssets): """ 更新前请先看 ./README.md """ @@ -295,11 +295,17 @@ def start_battle(self): logger.info(f'Try click fire, remain times[{max_times - click_times}]') continue # 运行战斗 - self.run_general_battle(config=self.get_general_battle_conf()) - - @battle_wait_strategy(success='activity') - def battle_wait(self, *args, **kwargs): - return self.battle_wait_with_strategy(*args, **kwargs) + # self.run_general_battle(config=self.get_general_battle_conf()) + strategies, options = self.loadout_from_config(self.get_general_battle_conf()) + strategies['success'] = 'activity' + self.loadout_show((strategies, options)) + self.state_show() + with battle_wait_strategy(**strategies), battle_wait_options(**options): + return self.battle_wait() + + # @battle_wait_strategy(success='activity') + # def battle_wait(self, *args, **kwargs): + # return self.battle_wait_with_strategy(*args, **kwargs) def switch_soul(self, enter_button: RuleImage, cur_img: RuleImage): conf = self.conf.switch_soul_config diff --git a/tasks/Component/GeneralBattle/battle.py b/tasks/Component/GeneralBattle/battle.py index c2760790cf..db8249d3fc 100644 --- a/tasks/Component/GeneralBattle/battle.py +++ b/tasks/Component/GeneralBattle/battle.py @@ -1,4 +1,175 @@ +import random +from module.logger import logger +from module.atom.image import RuleImage +from module.atom.ocr import RuleOcr -# 支持 外界的战斗开始阶段 | 内部的: 战斗准备阶段、切buff、切阵容、换式神。 -# 在 battle_wait 退出后多一层校验, \ No newline at end of file +from tasks.Component.GeneralBattle.battle_wait import ( + BattleWait, battle_wait_strategy, battle_wait_options, + OptionPrepareDefault, OptionPresetDefault, OptionGreenDefault, OptionRandomclickDefault, +) +from tasks.Component.GeneralBattle.config_general_battle import GeneralBattleConfig, GreenMarkType + + +class Battle(BattleWait): + + def start_fire(self, buttons: list[RuleOcr | RuleImage], checks: list[RuleImage]) -> bool: + """ + 感觉这种适配要很难 + Args: + buttons: 出现就点击 + checks: 必须在当前界面 + + Returns: + 在战斗中就返回 + """ + click_times, max_times = 0, random.randint(4, 8) + while 1: + self.screenshot() + if self.is_in_battle(False): + break + if click_times >= max_times: + logger.warning(f'Climb {self.climb_type} cannot enter, maybe already end, try next') + return False + if (self.appear_then_click(self.I_UI_CONFIRM_SAMLL, interval=1) or + self.appear_then_click(self.I_UI_CONFIRM, interval=1)): + continue + if any([self.appear(check, interval=1) for check in checks]): + for button in buttons: + if isinstance(button, RuleOcr) and self.ocr_appear_click(button, interval=2): + click_times += 1 + logger.info(f'Try click fire, remain times[{max_times - click_times}]') + continue + if isinstance(button, RuleImage) and self.appear_then_click(button, interval=2): + click_times += 1 + logger.info(f'Try click fire, remain times[{max_times - click_times}]') + continue + return True + + @battle_wait_strategy() + def battle_wait(self, *args, **kwargs): + return self.battle_wait_with_strategy(*args, **kwargs) + + + def battle_run(self): + with battle_wait_strategy(), battle_wait_options(): + return self.battle_wait() + + def battle_setup(self): + pass + + def loadout_from_config(self, conf: GeneralBattleConfig) -> tuple[dict, dict]: + """ + 与 loadout_default 相同格式,但由 GeneralBattleConfig 驱动:。 + """ + strategies, options = self.loadout_default() + options['prepare'] = OptionPrepareDefault(lock_team=conf.lock_team_enable) + if conf.preset_enable: + options['preset'] = OptionPresetDefault( + preset_enable=True, preset_group=conf.preset_group, preset_team=conf.preset_team, + ) + if conf.green_enable: + options['green'] = OptionGreenDefault(green_enable=True, green_mark=conf.green_mark) + if conf.random_click_swipt_enable: + strategies['randomclick'] = 'default' + options['randomclick'] = OptionRandomclickDefault() + return strategies, options + + +from tasks.Component.GeneralBattle.general_battle import GeneralBattle +from tasks.ActivityShikigami.assets import ActivityShikigamiAssets +class BattleTest(Battle, GeneralBattle, ActivityShikigamiAssets): + + def my_test_activity_prepare(self): + """ + 活动里面测试,需要先点击战斗准备阶段 + """ + strategies, options = self.loadout_default() + strategies['prepare'] = 'default' + strategies['success'] = 'activity' + options['prepare'] = OptionPrepareDefault(lock_team=False) + self.loadout_show((strategies, options)) + with battle_wait_strategy(**strategies), battle_wait_options(**options): + self.battle_wait() + self.state_show() + + def my_test_activity_preset(self, preset_group: int = 5, preset_team: int = 3): + """ + 活动里面测试,准备阶段开启切阵容(预设) + """ + strategies, options = self.loadout_default() + strategies['preset'] = 'default' + strategies['success'] = 'activity' + options['preset'] = OptionPresetDefault(preset_enable=True, preset_group=preset_group, preset_team=preset_team) + self.loadout_show((strategies, options)) + with battle_wait_strategy(**strategies), battle_wait_options(**options): + self.battle_wait() + self.state_show() + + def my_test_activity_green(self, green_mark: GreenMarkType = GreenMarkType.GREEN_LEFT1): + """ + 活动里面测试,绿标标记 + """ + strategies, options = self.loadout_default() + strategies['green'] = 'default' + strategies['success'] = 'activity' + options['green'] = OptionGreenDefault(green_enable=True, green_mark=green_mark) + self.loadout_show((strategies, options)) + with battle_wait_strategy(**strategies), battle_wait_options(**options): + self.battle_wait() + self.state_show() + + def my_test_activity_randomclick( + self, + start_delay: tuple[float, float] = (5.0, 10.0), + cooldown: tuple[float, float] = (15.0, 30.0), + trigger_probability: float = 0.6, + execution_limit: tuple[int, int] = (1, 2), + ): + """ + 活动里面测试,随机点击 + """ + strategies, options = self.loadout_default() + strategies['randomclick'] = 'default' + strategies['success'] = 'activity' + options['randomclick'] = OptionRandomclickDefault( + start_delay=start_delay, + cooldown=cooldown, + trigger_probability=trigger_probability, + execution_limit=execution_limit, + ) + self.loadout_show((strategies, options)) + with battle_wait_strategy(**strategies), battle_wait_options(**options): + self.battle_wait() + self.state_show() + + def my_test_activity_continuous(self): + """ + 活动里面测试,连续执行 + """ + strategies, options = self.loadout_default() + strategies['prepare'] = 'default' + strategies['success'] = 'activity' + options['prepare'] = OptionPrepareDefault(lock_team=False) + self.loadout_show((strategies, options)) + for i in range(5): + with battle_wait_strategy(**strategies), battle_wait_options(**options): + self.start_fire(buttons=[self.O_FIRE], checks=[self.I_CHECK_BATTLE_MAIN, self.I_CHECK_BATTLE_BOSS]) + self.battle_wait() + self.state_show() + + + +if __name__ == '__main__': + from module.config.config import Config + from module.device.device import Device + from tasks.Component.GeneralBattle.general_battle import GeneralBattle + c = Config('oas1') + d = Device(c) + t = BattleTest(c, d) + + # t.my_test_activity_prepare() + # t.my_test_activity_preset() + # t.my_test_activity_green() + # t.my_test_activity_randomclick() + t.my_test_activity_continuous() diff --git a/tasks/Component/GeneralBattle/battle_wait.py b/tasks/Component/GeneralBattle/battle_wait.py index 164a718745..19ca9f4b82 100644 --- a/tasks/Component/GeneralBattle/battle_wait.py +++ b/tasks/Component/GeneralBattle/battle_wait.py @@ -80,7 +80,7 @@ from functools import wraps, update_wrapper -from dataclasses import dataclass, field +from dataclasses import dataclass, field, fields from typing import TypeVar, ParamSpec, Callable from enum import Enum, auto from cached_property import cached_property @@ -88,9 +88,11 @@ from module.logger import logger from module.base.timer import Timer from module.atom.click import RuleClickExclude +from module.base.utils import get_color, color_similar from tasks.base_task import BaseTask from tasks.Component.GeneralBattle.assets import GeneralBattleAssets +from tasks.Component.GeneralBattle.config_general_battle import GreenMarkType P = ParamSpec('P') @@ -111,9 +113,9 @@ class BattleResult(Enum): # ─── 默认值常量 ─────────────────────────────────── # 战斗 hook 事件列表 & 默认调用顺序 _HOOKS_DEFAULT: tuple[str, ...] = ( - 'setup', 'completion', 'interrupt', 'success', 'failure', 'idle', + 'setup', 'completion', 'interrupt', 'prepare', 'preset', 'green', 'red', 'echo', 'success', 'failure', 'idle', ) -_SEQUENCE_DEFAULT: str = 'completion > interrupt > success > failure >' +_SEQUENCE_DEFAULT: str = 'completion > interrupt > prepare > preset > green > red > echo > success > failure >' # 跨任务/跨战斗状态的初始 @dataclass @@ -138,16 +140,7 @@ class OptionSetupDefault: @dataclass class OptionCompletionDefault: - reward_exclude_click_1 = [ - 'C_END_MESSAGE_RIGHT_TOP', 'C_END_BUFF_AREA_1', - 'C_END_BUFF_AREA_2', 'C_END_SOUL_RECORD', 'C_END_SOUL_DETAILS', - ] - reward_exclude_click_2 = [ - 'C_END_MESSAGE_RIGHT_TOP', 'C_END_BUFF_AREA_1', - 'C_END_BUFF_AREA_2', 'C_END_SOUL_RECORD', 'C_END_SOUL_DETAILS', - 'C_END_1_1', 'C_END_1_2', 'C_END_1_3', 'C_END_1_4', - 'C_END_1_5', 'C_END_1_6', - ] + pass @dataclass @@ -157,7 +150,18 @@ class OptionInterruptDefault: @dataclass class OptionSuccessDefault: - excludes: list | None = None + excludes_1: list[str] = field(default_factory=lambda: + [ + 'C_END_MESSAGE_RIGHT_TOP', 'C_END_BUFF_AREA_1', + 'C_END_BUFF_AREA_2', 'C_END_SOUL_RECORD', 'C_END_SOUL_DETAILS', + ]) + excludes_2: list[str] = field(default_factory=lambda: + [ + 'C_END_MESSAGE_RIGHT_TOP', 'C_END_BUFF_AREA_1', + 'C_END_BUFF_AREA_2', 'C_END_SOUL_RECORD', 'C_END_SOUL_DETAILS', + 'C_END_1_1', 'C_END_1_2', 'C_END_1_3', 'C_END_1_4', + 'C_END_1_5', 'C_END_1_6', + ]) @dataclass @@ -170,6 +174,46 @@ class OptionIdleDefault: pass +@dataclass +class OptionPrepareDefault: + lock_team: bool = False + + +@dataclass +class OptionPresetDefault: + preset_enable: bool = False + preset_group: int = 1 + preset_team: int = 1 + + +@dataclass +class OptionGreenDefault: + green_enable: bool = False + green_mark: GreenMarkType = GreenMarkType.GREEN_LEFT1 + + +@dataclass +class OptionRedDefault: + pass + + +@dataclass +class OptionEchoDefault: + pass + + +@dataclass +class OptionRandomclickDefault: + # 战斗开始后,首次允许执行前等待多久 + start_delay: tuple[float, float] = (5.0, 10.0) + # 两次执行之间的最小间隔 + cooldown: tuple[float, float] = (15.0, 30.0) + # 到达可执行时间后,本次实际执行的概率 + trigger_probability: float = 0.6 + # 本场战斗最多执行几次;元组表示开场时随机取一个上限 + execution_limit: tuple[int, int] = (1, 2) + + # event → options 数据类, update_options 靠它按 hook2event 自动实例化分发 _OPTION_CLASSES: dict[str, type] = { 'setup': OptionSetupDefault, @@ -178,6 +222,12 @@ class OptionIdleDefault: 'success': OptionSuccessDefault, 'failure': OptionFailureDefault, 'idle': OptionIdleDefault, + 'prepare': OptionPrepareDefault, + 'preset': OptionPresetDefault, + 'green': OptionGreenDefault, + 'red': OptionRedDefault, + 'echo': OptionEchoDefault, + 'randomclick': OptionRandomclickDefault, } @@ -228,6 +278,38 @@ class PerTaskIdle: pass +@dataclass +class PerTaskPrepare: + pass + + +@dataclass +class PerTaskPreset: + # 一次任务类型触发一次,想要再次触发把done设置False + done: bool = False + + +@dataclass +class PerTaskGreen: + pass + + +@dataclass +class PerTaskRed: + pass + + +@dataclass +class PerTaskEcho: + pass + + +@dataclass +class PerTaskRandomclick: + # 上一次点击的时间 + last_attempt_time: float = 0.0 + + @dataclass class PerBattleSetup: pass @@ -245,7 +327,23 @@ class PerBattleInterrupt: @dataclass class PerBattleSuccess: - pass + click_stage_1: RuleClickExclude | None = None + click_stage_2: RuleClickExclude | None = None + + @classmethod + def reward_exclude_click( + cls, + onwer, + areas: list[str] = None, + name: str = 'success_exclude_click' + ) -> RuleClickExclude: + inputs = [] + for area in areas: + click = getattr(onwer, area, None) + if click is None: + raise ValueError(f'Unknown success exclusion click: {area!r}') + inputs.append(click) + return RuleClickExclude(inputs, name=name) @dataclass @@ -258,6 +356,41 @@ class PerBattleIdle: pass +@dataclass +class PerBattlePrepare: + done: bool = False + timer: Timer = None + + +@dataclass +class PerBattlePreset: + pass + + +@dataclass +class PerBattleGreen: + # 本场战斗完成 + done: bool = False + + +@dataclass +class PerBattleRed: + pass + + +@dataclass +class PerBattleEcho: + pass + + +@dataclass +class PerBattleRandomclick: + execution_count: int = 0 + execution_limit_resolved: int = 0 + start_delay_resolved: float = 0.0 + first_allowed_time: float = 0.0 + + # event → pri 私有状态数据类 _PER_TASK_CLASSES: dict[str, type] = { 'setup': PerTaskSetup, @@ -266,6 +399,12 @@ class PerBattleIdle: 'success': PerTaskSuccess, 'failure': PerTaskFailure, 'idle': PerTaskIdle, + 'prepare': PerTaskPrepare, + 'preset': PerTaskPreset, + 'green': PerTaskGreen, + 'red': PerTaskRed, + 'echo': PerTaskEcho, + 'randomclick': PerTaskRandomclick, } _PER_BATTLE_CLASSES: dict[str, type] = { @@ -275,6 +414,12 @@ class PerBattleIdle: 'success': PerBattleSuccess, 'failure': PerBattleFailure, 'idle': PerBattleIdle, + 'prepare': PerBattlePrepare, + 'preset': PerBattlePreset, + 'green': PerBattleGreen, + 'red': PerBattleRed, + 'echo': PerBattleEcho, + 'randomclick': PerBattleRandomclick, } # ──────────────────────────────────────────── @@ -498,7 +643,7 @@ def inner(owner, *args: P.args, **kwargs: P.kwargs) -> T: # ) options = battle_wait_options.options or None runtime.reset_per_battle() - runtime.update_options(options) + runtime.update_options(options, plan=current_plan) return func(owner, battle_wait_plan=current_plan, options=options) \ if options else func(owner, battle_wait_plan=current_plan) @@ -591,8 +736,8 @@ def __init__(self, func: Callable): def __get__(self, obj, objtype=None): if obj is None: return self - runtime._ensure_pub() - runtime._ensure_pri(self.hook_name) + runtime._ensure_pub_default() + runtime._ensure_pri_default(self.hook_name) @wraps(self.func) def bound(*args, **kwargs): @@ -604,8 +749,8 @@ def __call__(self, owner, *args, **kwargs): if runtime.task_owner is None or owner is not runtime.task_owner: runtime.task_owner = owner runtime.reset_per_task() - runtime._ensure_pub() - runtime._ensure_pri(self.hook_name) + runtime._ensure_pub_default() + runtime._ensure_pri_default(self.hook_name) return self.func(owner, pub=runtime.pub_ctx, pri=runtime.pri_ctx[self.hook_name]) @@ -631,7 +776,7 @@ def __repr__(self): @classmethod def reset_per_task(cls): - cls._ensure_pub() + cls._ensure_pub_default() cls.pub_ctx.per_task = PerTaskState() for hook_name, ctx in cls.pri_ctx.items(): event = cls.hook2event(hook_name) @@ -640,7 +785,7 @@ def reset_per_task(cls): @classmethod def reset_per_battle(cls): - cls._ensure_pub() + cls._ensure_pub_default() cls.pub_ctx.per_battle = PerBattleState() for hook_name, ctx in cls.pri_ctx.items(): event = cls.hook2event(hook_name) @@ -648,12 +793,12 @@ def reset_per_battle(cls): ctx.per_battle = cls_type() @classmethod - def _ensure_pub(cls): + def _ensure_pub_default(cls): if cls.pub_ctx is None: cls.pub_ctx = PublicContext() @classmethod - def _ensure_pri(cls, name): + def _ensure_pri_default(cls, name): if name not in cls.pri_ctx: event = cls.hook2event(name) cls.pri_ctx[name] = PrivateContext( @@ -667,19 +812,38 @@ def hook2event(cls, func_name: str) -> str: return func_name[len('_bw_'):].rsplit('_', 1)[0] @classmethod - def update_options(cls, options: dict[str, object] | None): - """options 是 dict[str, OptionXxx] 实例(由 battle_wait_options 转好),直接按 event 分发""" - cls._ensure_pub() + def event2hook(cls, event_name: str, strategy_name: str) -> str: + return f'_bw_{event_name}_{strategy_name}' + + @classmethod + def update_options(cls, options: dict[str, object] | None, plan: BattleWaitPlan): + """ + 输入 options = { + 'setup': OptionSetupDefault(excludes=None), + 'completion': OptionCompletionDefault(), + 'prepare': OptionPrepareDefault(lock_team=True), + 'preset': OptionPresetDefault(preset_enable=True, preset_group=1, preset_team=2), + 'success': OptionSuccessDefault(), # 没传字段 → 全默认 + ... # 其余 event 都有 + } + + 设置值, 这里key没有直接使用 event 而是 _bw_event_strategy 是因为可能这场战斗和下一场战斗的options不同,需要区分开来 + cls.pub_ctx.options = options // 直接的一个引用 + runtime.pri_ctx['_bw_prepare_default'].options == OptionPrepareDefault(lock_team=True) + runtime.pri_ctx['_bw_preset_default'].options == OptionPresetDefault(preset_enable=True, preset_group=1, preset_team=2) + runtime.pri_ctx['_bw_success_default'].options == OptionSuccessDefault() + """ + cls._ensure_pub_default() if not isinstance(options, dict): - cls.pub_ctx.options = {} - for hook_name, ctx in cls.pri_ctx.items(): - ctx.options = cls._option_class(hook_name)() - return + raise cls.pub_ctx.options = options - for hook_name, ctx in cls.pri_ctx.items(): - event = cls.hook2event(hook_name) - opt = options.get(event) - ctx.options = opt if isinstance(opt, cls._option_class(hook_name)) else cls._option_class(hook_name)() + if plan is not None: + for hook in [plan.function_setup_name] + plan.sequence_function_names(): + cls._ensure_pri_default(hook) + opt = options.get(cls.hook2event(hook)) + ctx = cls.pri_ctx[hook] + ctx.options = opt if isinstance(opt, cls._option_class(hook)) \ + else cls._option_class(hook)() @classmethod def _option_class(cls, hook_name: str) -> type: @@ -697,11 +861,69 @@ def hook_enabled_update(cls, enable: tuple[str, ...], disable: tuple[str, ...]) """ hook_enabled 的更新只能在 BattleWait 上。不可以在 battle_wait_options 或者 battle_wait_strategy 上 """ - cls.pub_ctx.per_battle.hook_enabled.update(enable) - cls.pub_ctx.per_battle.hook_enabled.difference_update(disable) + if enable: + cls.pub_ctx.per_battle.hook_enabled.update(enable) + if disable: + cls.pub_ctx.per_battle.hook_enabled.difference_update(disable) return HookSignal.CONTINUE +def randomclick_gate(func: Callable) -> Callable: + """ + randomclick 专属拦截装饰器,零自身状态。全部状态读写透传进来的 pri: + - per_battle(PerBattleRandomclick): 开场 delay/上限/次数,每场重置 + - per_task (PerTaskRandomclick): cooldown 上次尝试时刻,跨战斗 + 四道门全过才放行 func 真正执行随机动作。 + """ + @wraps(func) + def wrapper(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: + options = pub.options.get('randomclick') + state_battle = pri.per_battle + state_task = pri.per_task + if not isinstance(options, OptionRandomclickDefault): + raise TypeError(f"randomclick options must be OptionRandomclickDefault, got {type(options)!r}") + if not isinstance(state_battle, PerBattleRandomclick): + raise TypeError(f"randomclick per_battle state must be PerBattleRandomclick, got {type(state_battle)!r}") + if not isinstance(state_task, PerTaskRandomclick): + raise TypeError(f"randomclick per_task state must be PerTaskRandomclick, got {type(state_task)!r}") + if not self.is_in_battle(is_screenshot=False): + return HookSignal.CONTINUE + + now = time.time() + + # 第一次调用:解析 start_delay 和 execution_limit(只解析一次) + if state_battle.first_allowed_time == 0.0: + state_battle.start_delay_resolved = random.uniform(*options.start_delay) + state_battle.execution_limit_resolved = random.randint(*options.execution_limit) + state_battle.first_allowed_time = now + state_battle.start_delay_resolved + return HookSignal.CONTINUE + + # Gate 1: start_delay 未到 + if now < state_battle.first_allowed_time: + return HookSignal.CONTINUE + + # Gate 2: execution_limit 已满 + if state_battle.execution_count >= state_battle.execution_limit_resolved: + return HookSignal.CONTINUE + + # Gate 3: cooldown 未到(记在 per_task 跨战斗,秒掉的小战斗也能续上间隔) + if now - state_task.last_attempt_time < random.uniform(*options.cooldown): + return HookSignal.CONTINUE + + # 记录本次尝试时间(无论概率是否通过,都算一次 cooldown 周期) + state_task.last_attempt_time = now + + # Gate 4: trigger_probability 概率判定 + if random.random() >= options.trigger_probability: + return HookSignal.CONTINUE + + # 通过全部四道门,放行真正执行随机动作 + state_battle.execution_count += 1 + return func(self, pub, pri) + + return wrapper + + class BattleWait(BaseTask, GeneralBattleAssets): def __new__(cls, *args, **kwargs): @@ -717,60 +939,102 @@ def __new__(cls, *args, **kwargs): setattr(klass, name, runtime(attr)) return super().__new__(cls) - # ------------------------------------------------------------------------------------------------------------------ - @cached_property - def exclude_button_stage_1(self): - return ['C_END_MESSAGE_RIGHT_TOP', 'C_END_BUFF_AREA_1', 'C_END_BUFF_AREA_2', - 'C_END_SOUL_RECORD', 'C_END_SOUL_DETAILS', - ] - - @cached_property - def exclude_button_stage_2(self): - return ['C_END_MESSAGE_RIGHT_TOP', 'C_END_BUFF_AREA_1', 'C_END_BUFF_AREA_2', 'C_END_SOUL_RECORD', 'C_END_SOUL_DETAILS', - 'C_END_1_1', 'C_END_1_2', 'C_END_1_3', 'C_END_1_4', 'C_END_1_5', 'C_END_1_6', - ] - - def reward_exclude_click(self, areas: list[str] = None, name: str = 'success_exclude_click') -> RuleClickExclude: - inputs = [] - for area in areas: - click = getattr(self, area, None) - if click is None: - raise ValueError(f'Unknown success exclusion click: {area!r}') - inputs.append(click) - return RuleClickExclude(inputs, name=name) + # ---------------------------------------------------------------------------------------------------------------- + # loadout:一行一个 event(event + strategy + options),全量快照,默认值来源 battle_wait.py,不裸写: + # strategies: dict[event -> strategy],有序,key 是 hook 名,支持 prepare='default' 这类逐 hook 设置 + # options: dict[event -> Option 实例],全部字段可见 + # 跑法: + # with battle_wait_strategy(**strategies), battle_wait_options(**options): + # self.battle_wait() + # ---------------------------------------------------------------------------------------------------------------- + @classmethod + def loadout_default(cls) -> tuple[dict, dict]: + """ + 默认策略 + 默认 options 的全量快照(不是 diff,是完整的)。 + 策略来源 BattleWaitPlan()(battle_wait.py),按 sequence 顺序展开成 dict; + options 来源 battle_wait_options.options(battle_wait.py)。 + """ + plan = BattleWaitPlan() + strategies = {e: getattr(plan, e) for e in plan.sequence.replace(' ', '').split('>') if e} + options = deepcopy(battle_wait_options.options) or {} + return strategies, options - def reward_exclude_click_1(self, areas: list[str] = None) -> RuleClickExclude: - return self.reward_exclude_click(areas, name='reward_exclude_click_1') + @classmethod + def loadout_show(cls, loadout: tuple[dict, dict] | None = None) -> None: + """ + 输出类似: + event strategy options + idle default OptionIdleDefault() + prepare default OptionPrepareDefault(lock_team=False) + preset default OptionPresetDefault(preset_enable=False, preset_group=1, preset_team=1) + green default OptionGreenDefault(green_enable=False, green_mark=) + """ + strategies, options = loadout if loadout is not None else cls.loadout_default() + order = [] + seen = set() + for event in options: + order.append(event) + seen.add(event) + for event in strategies: + if event not in seen: + order.append(event) + print('=' * 64) + print(f'{"event":<12}{"strategy":<12}options') + for event in order: + print(f'{event:<12}{strategies.get(event, "default"):<12}{options.get(event)!r}') + print('=' * 64) - def reward_exclude_click_2(self, areas: list[str] = None) -> RuleClickExclude: - return self.reward_exclude_click(areas, name='reward_exclude_click_2') + @classmethod + def state_show(cls) -> None: + """ + 输出类似: + event strategy enabled per_task | per_battle + completion default True {} | {} + idle default True {} | {} + prepare default False {} | {'done': True} + ---------------------------------------------------------------- + pub per_task count 等全局跨任务状态 + pub per_battle success / hook_enabled 等全局跨战斗状态 + """ + runtime._ensure_pub_default() + enabled = runtime.hook_enabled() + + def _nondefault(o): + if isinstance(o, dict): + return dict(o) + return {f.name: getattr(o, f.name) for f in fields(o) if getattr(o, f.name) != f.default} + + options = cls.loadout_default()[1] + order = list(options) + hooks = sorted( + runtime.pri_ctx, + key=lambda h: order.index(runtime.hook2event(h)) if runtime.hook2event(h) in order else len(order), + ) + print('=' * 64) + print(f'{"event":<12}{"strategy":<12}{"enabled":<10}per_task | per_battle') + for hook in hooks: + event = runtime.hook2event(hook) + strategy = hook[len(f'_bw_{event}_'):] + ctx = runtime.pri_ctx[hook] + print(f'{event:<12}{strategy:<12}{str(event in enabled):<10}' + f'{_nondefault(ctx.per_task)} | {_nondefault(ctx.per_battle)}') + print('-' * 64) + pub = runtime.pub_ctx + print(f'{"pub":<12}{"":<12}{"per_task":<10}{_nondefault(pub.per_task)}') + print(f'{"pub":<12}{"":<12}{"per_battle":<10}{_nondefault(pub.per_battle)}') + print('=' * 64) + time.sleep(0.6) + # ------------------------------------------------------------------------------------------------------------------ # build in # ------------------------------------------------------------------------------------------------------------------ def _bw_setup_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: - """ - options: dict = { - ‘excludes’ - } - """ self.C_REWARD_1.name = 'C_REWARD' self.C_REWARD_2.name = 'C_REWARD' self.C_REWARD_3.name = 'C_REWARD' self.device.stuck_record_add('BATTLE_STATUS_S') self.device.click_record_clear() logger.info('Start battle process') - - pri_options = pri.options if isinstance(pri.options, dict) else {} - success_options = pri_options.get('success') or {} - if not isinstance(success_options, dict): - raise TypeError("battle wait option 'success' must be a dict") - excludes = success_options.get('excludes') - exclude_stage_1 = excludes if excludes is not None else self.exclude_button_stage_1 - exclude_stage_2 = excludes if excludes is not None else self.exclude_button_stage_2 - self._reward_exclude_click_1 = self.reward_exclude_click_1(exclude_stage_1) - self._reward_exclude_click_2 = self.reward_exclude_click_2(exclude_stage_2) - # print(self._reward_exclude_click_1) - # print(self._reward_exclude_click_2) return HookSignal.DONE def _bw_completion_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: @@ -783,8 +1047,17 @@ def _bw_interrupt_default(self, pub: PublicContext, pri: PrivateContext) -> Hook return HookSignal.CONTINUE def _bw_success_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: + state = pri.per_battle + options = pri.options + if not isinstance(state, PerBattleSuccess) or not isinstance(options, OptionSuccessDefault): + raise + if state.click_stage_1 is None: + state.click_stage_1 = PerBattleSuccess.reward_exclude_click(self, pri.options.excludes_1, name='reward_exclude_click_1') + if state.click_stage_2 is None: + state.click_stage_2 = PerBattleSuccess.reward_exclude_click(self, pri.options.excludes_2, name='reward_exclude_click_2') + if self.appear_then_click(self.I_WIN, interval=0.8): - self.click(self._reward_exclude_click_1) + self.click(state.click_stage_1) return HookSignal.CONTINUE appear_ghost, appear_reward, appear_gold, appear_skin = ( self.appear(self.I_GREED_GHOST), @@ -813,15 +1086,17 @@ def _bw_success_default(self, pub: PublicContext, pri: PrivateContext) -> HookSi if any([_appear_ghost, _appear_reward, _appear_gold, _appear_skin]): if random.random() < 0.02: # 有一定的概率专门点击具体的奖励物品 - x, y = self._reward_exclude_click_2.coord_in_excluded(None) + x, y = state.click_stage_2.coord_in_excluded(None) self.device.click(x=x, y=y, control_name='reward_item') continue - self.click(self._reward_exclude_click_2, interval=1.5) + self.click(state.click_stage_2, interval=1.5) else: logger.info('Get all reward') - pub.success = True - pub.completion = True - return HookSignal.CONTINUE + pub.per_battle.success = BattleResult.SUCCESS + return runtime.hook_enabled_update( + enable=('completion',), + disable=tuple(['success', 'failure']), + ) if timer.reached_and_reset(): logger.warning('battle') break @@ -841,40 +1116,252 @@ def _bw_failure_default(self, pub: PublicContext, pri: PrivateContext) -> HookSi def _bw_idle_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: return HookSignal.CONTINUE - def _bw_randomclick_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: + def _bw_prepare_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: + pri_options = pri.options if isinstance(pri.options, OptionPrepareDefault) else None + state = pri.per_battle if isinstance(pri.per_battle, PerBattlePrepare) else None + if pri_options is None or state is None: + return HookSignal.CONTINUE + if state.done: + return HookSignal.CONTINUE + # 如果是锁队伍的,就直接跳过, 开启后面的 绿标,红标,应声虫 + # 如果确定进入正式战斗中了 也是同样的操作 + if pri_options.lock_team or self.is_in_real_battle(False): + logger.info('Prepare stage finish') + state.done = True + return runtime.hook_enabled_update( + enable=('green', 'red', 'echo'), + disable=('prepare', 'preset'), + ) + # 如果开启了切阵容,就等执行切阵容完成(切阵容操作会自动把准备按钮给按消失), 这里会等8秒钟,时间一到自动关闭准备阶段 + # 切阵容按 per_task 是一次性的:本任务已切过(done)就不再开定时器等,直接按准备开打 + preset_done = next(( + ctx.per_task.done + for hook, ctx in runtime.pri_ctx.items() + if runtime.hook2event(hook) == 'preset' and isinstance(ctx.per_task, PerTaskPreset) + ), False) + if pub.options.get('preset', OptionPresetDefault).preset_enable and not preset_done: + if state.timer is None: + state.timer = Timer(6).start() + # return HookSignal.CONTINUE + return runtime.hook_enabled_update( + enable=('preset',), + disable=(), + ) + # 时间没到就一直等吧 + if not state.timer.reached(): + return HookSignal.CONTINUE + # 计时8秒时间到了, 那就一直按 准备按钮 直到消失去 + # 不要把这行代码合并到下面去 + self.appear_then_click(self.I_PREPARE_HIGHLIGHT, interval=0.8) + return HookSignal.CONTINUE + + # 没有切阵容那就一直按 准备按钮 直到消失去] + + self.appear_then_click(self.I_PREPARE_HIGHLIGHT, interval=1) return HookSignal.CONTINUE - done = pri.per_battle.get('_bw_randomclick_state', False) - if done: + + def _bw_preset_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: + """ + 切阵容:切换预设队伍,每个任务只执行一次 + """ + pri_options = pri.options if isinstance(pri.options, OptionPresetDefault) else None + state = pri.per_task if isinstance(pri.per_task, PerTaskPreset) else None + if pri_options is None or state is None: return HookSignal.CONTINUE - if not self.is_in_battle(is_screenshot=False): + if not pri_options.preset_enable or state.done: return HookSignal.CONTINUE - if random.random() < 0.005 : # 低概率 - rand_type = random.randint(0, 2) - match rand_type: - case 0: - self.click(self.C_RANDOM_CLICK, interval=20) - case 1: - self.swipe(self.S_BATTLE_RANDOM_LEFT, interval=20) - case 2: - self.swipe(self.S_BATTLE_RANDOM_RIGHT, interval=20) - pri.per_battle['_bw_randomclick_state'] = True - # 重新设置为长战斗 - # self.device.stuck_record_add('BATTLE_STATUS_S') - else: - time.sleep(0.4) # 这样的好像不对 + + logger.info("Preset is enable") + # 点击预设按钮 + while 1: + self.screenshot() + if self.appear(self.I_PRESET_ENSURE): + break + # 首个队伍没有满足5个式神,未出现预设按钮的情况下跳出循环 + if self.appear(self.I_PRESENT_LESS_THAN_5): + break + if self.appear_then_click(self.I_PRESET, threshold=0.8, interval=1): + continue + if self.appear_then_click(self.I_PRESET_WIT_NUMBER, threshold=0.8, interval=1): + continue + if self.ocr_appear(self.O_PRESET): + self.click(self.O_PRESET, interval=1) + continue + if self.ocr_appear(self.O_PRESET_FULL): + self.click(self.O_PRESET_FULL, interval=1) + continue + logger.info("Click preset button") + + def get_unselect_color(tmp1, tmp2, tmp3, size): + # 获取未选择分组的颜色,3组之中必定存在两个颜色相似 + color_1 = get_color(self.device.image, + (tmp1.roi_back[0], tmp1.roi_back[1], + tmp1.roi_back[0] + size[0], tmp1.roi_back[1] + size[1])) + color_2 = get_color(self.device.image, + (tmp2.roi_back[0], tmp2.roi_back[1], + tmp2.roi_back[0] + size[0], tmp2.roi_back[1] + size[1])) + color_3 = get_color(self.device.image, + (tmp3.roi_back[0], tmp3.roi_back[1], + tmp3.roi_back[0] + size[0], tmp3.roi_back[1] + size[1])) + if color_similar(color_1, color_2): + return color_1 + if color_similar(color_2, color_3): + return color_2 + return color_3 + + # 选择预设组 + tmp = self.__getattribute__("C_PRESET_GROUP_" + str(pri_options.preset_group)) + if tmp is None: + tmp = self.C_PRESET_GROUP_1 + color_size = [self.C_PRESET_GROUP_1.roi_back[2], + self.C_PRESET_GROUP_1.roi_back[3]] + unselected_color = (224.9, 208.3, 187.4) + while True: + self.screenshot() + color_tmp = get_color(self.device.image, + (tmp.roi_back[0], tmp.roi_back[1], tmp.roi_back[0] + color_size[0], + tmp.roi_back[1] + color_size[1])) + if color_similar(color_tmp, unselected_color): + self.click(tmp, interval=0.2) + continue + break + logger.info("Select preset group") + + # 选择预设的队伍 + time.sleep(0.5) + tmp = self.__getattribute__("C_PRESET_TEAM_" + str(pri_options.preset_team)) + if tmp is None: + tmp = self.C_PRESET_TEAM_1 + color_size = [5, 5] + unselected_color = (216.8, 185.0, 146.8) + while True: + self.screenshot() + color_tmp = get_color(self.device.image, + (tmp.roi_back[0], tmp.roi_back[1], tmp.roi_back[0] + color_size[0], + tmp.roi_back[1] + color_size[1])) + if color_similar(color_tmp, unselected_color): + self.click(tmp, interval=0.2) + continue + break + self.click(tmp) + logger.info("Select preset team") + + # 点击预设确认 + self.wait_until_appear(self.I_PRESET_ENSURE, wait_time=1) + click_timer = Timer(4).start() + while 1: + self.screenshot() + if click_timer.reached(): + logger.warning("Switch preset failure") + if not self.appear(self.I_PRESET_ENSURE): + break + if self.appear_then_click(self.I_PRESET_ENSURE, threshold=0.8, interval=1): + continue + logger.info("Click preset ensure") + state.done = True + return runtime.hook_enabled_update( + enable=(), + disable=('preset',), + ) + + def _bw_green_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: + """ + 绿标:标记己方式神,每场战斗只执行一次 + """ + pri_options = pri.options if isinstance(pri.options, OptionGreenDefault) else None + state = pri.per_battle if isinstance(pri.per_battle, PerBattleGreen) else None + if pri_options is None or state is None: + return HookSignal.CONTINUE + if not pri_options.green_enable or state.done: + return runtime.hook_enabled_update( + enable=(), + disable=('green',), + ) + + logger.info("Green is enable") + x, y = None, None + match pri_options.green_mark: + case GreenMarkType.GREEN_LEFT1: + x, y = self.C_GREEN_LEFT_1.coord() + logger.info("Green left 1") + case GreenMarkType.GREEN_LEFT2: + x, y = self.C_GREEN_LEFT_2.coord() + logger.info("Green left 2") + case GreenMarkType.GREEN_LEFT3: + x, y = self.C_GREEN_LEFT_3.coord() + logger.info("Green left 3") + case GreenMarkType.GREEN_LEFT4: + x, y = self.C_GREEN_LEFT_4.coord() + logger.info("Green left 4") + case GreenMarkType.GREEN_LEFT5: + x, y = self.C_GREEN_LEFT_5.coord() + logger.info("Green left 5") + case GreenMarkType.GREEN_MAIN: + x, y = self.C_GREEN_MAIN.coord() + logger.info("Green main") + + # 等待那个准备的消失 + while 1: + self.screenshot() + if not self.appear(self.I_PREPARE_HIGHLIGHT): + break + + # 判断有无坐标的偏移 + self.appear_then_click(self.I_LOCAL) + time.sleep(0.3) + # 点击绿标 + self.device.click(x, y) + state.done = True + return runtime.hook_enabled_update( + enable=(), + disable=('green',), + ) + + def _bw_red_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: + """ + 红标:点击对手的式神,暂为空壳 + """ + state = pri.per_battle + + state.done = True + return runtime.hook_enabled_update( + enable=(), + disable=('red',), + ) + + def _bw_echo_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: + """ + 应声虫:战斗中进入聊天界面复制粘贴,暂为空壳 + """ + state = pri.per_battle + + state.done = True + return runtime.hook_enabled_update( + enable=(), + disable=('echo',), + ) + + @randomclick_gate + def _bw_randomclick_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: + match random.randint(0, 2): + case 0: + self.click(self.C_RANDOM_CLICK, interval=20) + case 1: + self.swipe(self.S_BATTLE_RANDOM_LEFT, interval=20) + case 2: + self.swipe(self.S_BATTLE_RANDOM_RIGHT, interval=20) return HookSignal.CONTINUE # custom # ------------------------------------------------------------------------------------------------------------------ def _bw_success_soul(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: - success_options = pub.options.get('success') if isinstance(pub.options, dict) else {} - if isinstance(success_options, OptionSuccessDefault): - excludes = success_options.excludes - elif isinstance(success_options, dict): - excludes = success_options.get('excludes') - else: + state = pri.per_battle + options = pri.options + if not isinstance(state, PerBattleSuccess) or not isinstance(options, OptionSuccessDefault): raise TypeError("battle wait option 'success' must be an OptionSuccessDefault") - action_click = self.reward_exclude_click(excludes) + if state.click_stage_1 is None: + state.click_stage_1 = PerBattleSuccess.reward_exclude_click(self, options.excludes_1, name='success_exclude_click') + action_click = state.click_stage_1 if self.appear_then_click(self.I_WIN, interval=0.8): return HookSignal.CONTINUE appear_ghost, appear_reward, appear_gold, appear_skin = ( @@ -901,9 +1388,11 @@ def _bw_success_soul(self, pub: PublicContext, pri: PrivateContext) -> HookSigna self.click(action_click, interval=1.5) else: logger.info('Get all reward') - pub.success = True - pub.completion = True - return HookSignal.CONTINUE + pub.per_battle.success = BattleResult.SUCCESS + return runtime.hook_enabled_update( + enable=('completion',), + disable=tuple(['success', 'failure']), + ) if timer.reached_and_reset(): logger.warning('battle ') break @@ -970,8 +1459,8 @@ def battle_wait_with_strategy(self, *args, **kwargs) -> bool: """ 理解 event + strategy 概念: 把战斗过程抽象为一系列触发事件以及对应的实现函数,也称hook。 event + strategy 拼成了一个hook, 一个 event 在一次战斗过程中只能挂载一个 strategy - 默认定义的 hook 有 BattleWaitPlan.HOOKS_DEFAULT = ('setup', 'completion', 'interrupt', 'success', 'failure', 'idle') - 这些 hook 跑在一个 while 里面,默认的调用顺序 BattleWaitPlan.SEQUENCE_DEFAULT = 'completion > interrupt > success > failure > idle' + 默认定义的 hook 有 BattleWaitPlan.HOOKS_DEFAULT = ('setup', 'completion', 'interrupt', 'success', 'failure', 'idle', 'prepare', 'preset', 'green', 'red', 'echo') 如果你填的没有,会使用默认的补全去的 + 这些 hook 跑在一个 while 里面,默认的调用顺序 BattleWaitPlan.SEQUENCE_DEFAULT = 'completion > interrupt > prepare > preset > green > red > echo > success > failure > idle' setup 没有跑在 while里面 而是在 while之前, 比如一个战斗过程可以表示为: setup_default() while 1: @@ -1024,22 +1513,20 @@ def battle_wait(self, *args, **kwargs): battle_wait_plan = kwargs.get('battle_wait_plan') if battle_wait_plan is None: battle_wait_plan = BattleWaitPlan() - print(battle_wait_plan) - # print(battle_wait_plan.sequence_function_names()) - options = runtime.pub_ctx.options - logger.info('options: {}'.format(options)) setup_func = getattr(self, battle_wait_plan.function_setup_name, self._bw_setup_default) setup_func() + runtime.hook_enabled_update( + enable=(), + disable=('setup',), + ) handlers = [getattr(self, func_name, None) for func_name in battle_wait_plan.sequence_function_names()] - _hook_enabled = tuple(runtime.hook2event(h.__name__) for h in handlers if h) - _hook_disabled = ('completion', ) - runtime.hook_enabled_update(enable=_hook_enabled, disable=_hook_disabled) - print(f'first runtime.hook_enabled(): {runtime.hook_enabled()}') + self.setup_hook(handlers=handlers) while True: self.screenshot() hook_enabled = runtime.hook_enabled() + # self.state_show() for handler in handlers: if runtime.hook2event(handler.__name__) not in hook_enabled: continue @@ -1049,6 +1536,19 @@ def battle_wait(self, *args, **kwargs): if result == HookSignal.CONTINUE: continue + @classmethod + def setup_hook(cls, handlers: list[Callable]): + """ + 默认的: 'setup', 'completion', 'interrupt', 'prepare', 'preset', 'green', 'red', 'echo', 'success', 'failure', 'idle' + 战斗开始下关闭的 completion, 'preset', 'green', 'red', 'echo' + 检测到进入准备阶段后开启 `preset` 来进入切换阵容,准备阶段结束后开启 'green', 'red', 'echo' + + """ + _hook_enabled = tuple(runtime.hook2event(h.__name__) for h in handlers if h) + _hook_disabled = ('completion', 'preset', 'green', 'red', 'echo') + runtime.hook_enabled_update(enable=_hook_enabled, disable=_hook_disabled) + + if __name__ == '__main__': diff --git a/tasks/Component/GeneralBattle/general_battle.py b/tasks/Component/GeneralBattle/general_battle.py index d533393356..71be0e0a41 100644 --- a/tasks/Component/GeneralBattle/general_battle.py +++ b/tasks/Component/GeneralBattle/general_battle.py @@ -666,9 +666,7 @@ def get_unselect_color(tmp1, tmp2, tmp3, size): self.C_PRESET_GROUP_1.roi_back[3]] unselected_color = get_unselect_color(self.C_PRESET_GROUP_1, self.C_PRESET_GROUP_2, self.C_PRESET_GROUP_3, size=color_size) - print("") color_size = [5, 5] unselected_color = get_unselect_color(self.C_PRESET_TEAM_1, self.C_PRESET_TEAM_2, self.C_PRESET_TEAM_3, size=color_size ) - print("") diff --git a/tests/tasks/Component/GeneralBattle/test_randomclick_gate.py b/tests/tasks/Component/GeneralBattle/test_randomclick_gate.py new file mode 100644 index 0000000000..33d21b712e --- /dev/null +++ b/tests/tasks/Component/GeneralBattle/test_randomclick_gate.py @@ -0,0 +1,196 @@ +import time + +import pytest + +from tasks.Component.GeneralBattle.battle_wait import ( + HookSignal, + OptionRandomclickDefault, + PerBattleRandomclick, + PerTaskRandomclick, + PrivateContext, + PublicContext, + randomclick_gate, +) + + +class FakeSelf: + in_battle = True + calls = 0 + + def is_in_battle(self, is_screenshot=False): + return self.in_battle + + def click(self, *args, **kwargs): + self.calls += 1 + + def swipe(self, *args, **kwargs): + self.calls += 1 + + +def make_pub(options=None): + options = options if options is not None else OptionRandomclickDefault() + return PublicContext(options={'randomclick': options}) + + +def make_pri(per_battle=None, per_task=None): + return PrivateContext( + per_battle=per_battle if per_battle is not None else PerBattleRandomclick(), + per_task=per_task if per_task is not None else PerTaskRandomclick(), + ) + + +def make_gate(): + fake = FakeSelf() + + @randomclick_gate + def _bw_randomclick_default(self, pub, pri): + self.calls += 1 + return HookSignal.CONTINUE + + return fake, _bw_randomclick_default + + +def test_type_mismatch_raises(): + fake, gate = make_gate() + + with pytest.raises(TypeError, match='OptionRandomclick'): + gate(fake, PublicContext(options={'randomclick': {'bad': 1}}), make_pri()) + + with pytest.raises(TypeError, match='PerBattleRandomclick'): + pub = make_pub() + gate(fake, pub, PrivateContext(per_battle={}, per_task=PerTaskRandomclick())) + + with pytest.raises(TypeError, match='PerTaskRandomclick'): + pub = make_pub() + gate(fake, pub, PrivateContext(per_battle=PerBattleRandomclick(), per_task={})) + + +def test_not_in_battle_skips(): + fake, gate = make_gate() + fake.in_battle = False + + pub = make_pub(OptionRandomclickDefault(start_delay=(0.0, 0.0), cooldown=(0.0, 0.0), trigger_probability=1.0)) + pri = make_pri() + pri.per_battle.first_allowed_time = time.time() - 10 # 已过 delay + pri.per_battle.execution_limit_resolved = 5 + + result = gate(fake, pub, pri) + + assert result == HookSignal.CONTINUE + assert fake.calls == 0 + + +def test_first_call_resolves_delay_and_limit(): + fake, gate = make_gate() + pub = make_pub() + pri = make_pri() + + result = gate(fake, pub, pri) + + assert result == HookSignal.CONTINUE + assert fake.calls == 0 + assert 5.0 <= pri.per_battle.start_delay_resolved <= 10.0 + assert 1 <= pri.per_battle.execution_limit_resolved <= 2 + assert pri.per_battle.first_allowed_time > 0.0 + + +def test_start_delay_blocks_until_first_allowed_time(): + fake, gate = make_gate() + pub = make_pub(OptionRandomclickDefault(start_delay=(100.0, 100.0))) + pri = make_pri() + pri.per_battle.start_delay_resolved = 100.0 + pri.per_battle.execution_limit_resolved = 5 + pri.per_battle.first_allowed_time = time.time() + 100.0 + + result = gate(fake, pub, pri) + + assert result == HookSignal.CONTINUE + assert fake.calls == 0 + + +def test_execution_limit_blocks_after_count_reached(): + fake, gate = make_gate() + pub = make_pub(OptionRandomclickDefault(start_delay=(0.0, 0.0), cooldown=(0.0, 0.0), trigger_probability=1.0)) + pri = make_pri() + pri.per_battle.start_delay_resolved = 0.0 + pri.per_battle.execution_limit_resolved = 1 + pri.per_battle.first_allowed_time = time.time() - 10 + pri.per_battle.execution_count = 1 + + result = gate(fake, pub, pri) + + assert result == HookSignal.CONTINUE + assert fake.calls == 0 + + +def test_cooldown_blocks_within_cooling_window(): + fake, gate = make_gate() + pub = make_pub(OptionRandomclickDefault(start_delay=(0.0, 0.0), cooldown=(9999.0, 9999.0), trigger_probability=1.0)) + pri = make_pri() + pri.per_battle.start_delay_resolved = 0.0 + pri.per_battle.execution_limit_resolved = 5 + pri.per_battle.first_allowed_time = time.time() - 10 + pri.per_task.last_attempt_time = time.time() + + result = gate(fake, pub, pri) + + assert result == HookSignal.CONTINUE + assert fake.calls == 0 + + +def test_cooldown_records_attempt_even_when_probability_blocked(): + fake, gate = make_gate() + pub = make_pub(OptionRandomclickDefault(start_delay=(0.0, 0.0), cooldown=(0.0, 0.0), trigger_probability=0.0)) + pri = make_pri() + pri.per_battle.start_delay_resolved = 0.0 + pri.per_battle.execution_limit_resolved = 5 + pri.per_battle.first_allowed_time = time.time() - 10 + pri.per_task.last_attempt_time = time.time() - 9999 + + result = gate(fake, pub, pri) + + assert result == HookSignal.CONTINUE + assert fake.calls == 0 + # 概率未通过也算一次尝试,cooldown 周期被刷新 + assert pri.per_task.last_attempt_time > time.time() - 1 + + +def test_cooldown_survives_reset_per_battle(): + fake, gate = make_gate() + pub = make_pub(OptionRandomclickDefault(start_delay=(0.0, 0.0), cooldown=(9999.0, 9999.0), trigger_probability=1.0)) + pri = make_pri() + pri.per_battle.start_delay_resolved = 0.0 + pri.per_battle.execution_limit_resolved = 5 + pri.per_battle.first_allowed_time = time.time() - 10 + + # 上一场战斗刚尝试过 → last_attempt_time 在 per_task 保留 + pri.per_task.last_attempt_time = time.time() + + # 模拟新一场战斗: per_battle 更新, per_task 不变 + pri.per_battle = PerBattleRandomclick() + pri.per_battle.start_delay_resolved = 0.0 + pri.per_battle.execution_limit_resolved = 5 + pri.per_battle.first_allowed_time = time.time() - 10 + + result = gate(fake, pub, pri) + + assert result == HookSignal.CONTINUE + assert fake.calls == 0 + + +def test_all_gates_pass_executes_once_and_increments(): + fake, gate = make_gate() + pub = make_pub(OptionRandomclickDefault(start_delay=(0.0, 0.0), cooldown=(0.0, 0.0), trigger_probability=1.0)) + pri = make_pri() + pri.per_battle.start_delay_resolved = 0.0 + pri.per_battle.execution_limit_resolved = 3 + pri.per_battle.first_allowed_time = time.time() - 10 + pri.per_task.last_attempt_time = time.time() - 9999 + + result = gate(fake, pub, pri) + + assert result == HookSignal.CONTINUE + assert fake.calls == 1 + assert pri.per_battle.execution_count == 1 + # cooldown 周期刷新 + assert pri.per_task.last_attempt_time > time.time() - 1 \ No newline at end of file From 0e7112381eeb54a2c277742419032a624a653270 Mon Sep 17 00:00:00 2001 From: neptine <2234044577@qq.com> Date: Sat, 19 Sep 2026 13:01:13 +0800 Subject: [PATCH 09/13] feat(GeneralBattle): Add completion fallback click and randomclick expectation model --- tasks/Component/GeneralBattle/battle_wait.py | 93 ++++++++++++++++++-- 1 file changed, 85 insertions(+), 8 deletions(-) diff --git a/tasks/Component/GeneralBattle/battle_wait.py b/tasks/Component/GeneralBattle/battle_wait.py index 19ca9f4b82..d8bd20257a 100644 --- a/tasks/Component/GeneralBattle/battle_wait.py +++ b/tasks/Component/GeneralBattle/battle_wait.py @@ -76,6 +76,7 @@ import random import time import copy +import math from copy import deepcopy @@ -88,6 +89,7 @@ from module.logger import logger from module.base.timer import Timer from module.atom.click import RuleClickExclude +from module.atom.image import RuleImage from module.base.utils import get_color, color_similar from tasks.base_task import BaseTask @@ -140,7 +142,10 @@ class OptionSetupDefault: @dataclass class OptionCompletionDefault: - pass + # 这里是为了, 确认结束战斗后退回到 “fire” 的界面 + check_imgs: list[RuleImage] = None + # 兜底:check_imgs 都检测不到时,间隔点击的排除位 + excludes: list[str] = field(default_factory=list) @dataclass @@ -204,15 +209,56 @@ class OptionEchoDefault: @dataclass class OptionRandomclickDefault: - # 战斗开始后,首次允许执行前等待多久 + # 战斗开始后,首次允许执行前等待多久 每次战斗重抽存状态到 per_battle.start_delay_resolved start_delay: tuple[float, float] = (5.0, 10.0) - # 两次执行之间的最小间隔 - cooldown: tuple[float, float] = (15.0, 30.0) + # 两次执行之间的最小间隔, 每次执行的时候随机选一个,不存状态, 这个时间包括战斗结束后到下一轮的时间哈 + cooldown: tuple[float, float] = (30.0, 60.0) # 到达可执行时间后,本次实际执行的概率 trigger_probability: float = 0.6 - # 本场战斗最多执行几次;元组表示开场时随机取一个上限 + # 本场战斗最多执行几次;元组表示开场时随机取一个上限, 每次战斗重抽一次,存状态到per_battle.execution_limit_resolved execution_limit: tuple[int, int] = (1, 2) + def expected_random_clicks( + self, + battle_seconds: float = 30.0, + gap_seconds: float = 10.0, + battles: int = 100 + ) -> float: + """估算多场战斗累计的期望随机动作执行次数(稳态解析模型)。 + + 实际触发 14/115 ≈ 12.2% swipe 8 + random_click 6,平均约每 8.2 场实操一次 + 模型预测 单场 0.125 → 115 场 ≈ 14.4 次 (12.5%) expected_random_clicks(8.75, 6.24, 115) + 这行是统计数据: 实际战斗时间,约 7.0 ~ 7.7 s:约 30 场,约 8.9 ~ 10.2 s:约 85 场。gap时间平均gap 约 6.24 s。 整体周期均值8.75 + 6.24 ≈ 15.0 s + + Args: + battle_seconds: 单场战斗的持续时间(秒)。 + gap_seconds: 战斗结束到下一轮战斗的间隔(秒),冷却在该时段内持续流逝。 + battles: 统计的战斗场数。 + + Returns: + 累计 battles 场的期望随机动作执行次数。 + """ + cooldown_low, cooldown_high = self.cooldown + cooldown_window_mean = cooldown_low + (math.sqrt(math.pi) / 2) * math.sqrt(cooldown_high - cooldown_low) + start_delay_low, start_delay_high = self.start_delay + start_delay_mean = (start_delay_low + start_delay_high) / 2 + attempts_per_battle = (battle_seconds + gap_seconds - start_delay_mean) / cooldown_window_mean + attempts_can_execute = self.trigger_probability * attempts_per_battle + attempts_can_execute_exp = math.exp(-attempts_can_execute) + # 这里建模成泊松分布 + limit_low, limit_high = self.execution_limit + per_battle = 0.0 + for limit in range(limit_low, limit_high + 1): + probability_below = attempts_can_execute_exp # P(成功次数 < k) + expected_with_limit = 0.0 + # E[min(H, limit)] = Σ_{k=1}^{limit} P(成功次数 ≥ k) + for k in range(1, limit + 1): + expected_with_limit += 1 - probability_below + probability_below += attempts_can_execute_exp * attempts_can_execute ** k / math.factorial(k) + per_battle += expected_with_limit + per_battle /= limit_high - limit_low + 1 + return per_battle * battles + # event → options 数据类, update_options 靠它按 hook2event 自动实例化分发 _OPTION_CLASSES: dict[str, type] = { @@ -317,7 +363,9 @@ class PerBattleSetup: @dataclass class PerBattleCompletion: - pass + click_stage_2: RuleClickExclude | None = None + # 兜底点击节拍器:进入 completion 起算,满 8s 才首次点击,之后每 8s 一次 + fallback_timer: Timer | None = None @dataclass @@ -385,9 +433,13 @@ class PerBattleEcho: @dataclass class PerBattleRandomclick: + # 本场战斗已执行的随机动作次数 execution_count: int = 0 + # 本场战斗的执行次数上限,首次拦截时从 execution_limit 随机解析 execution_limit_resolved: int = 0 + # 本场战斗的首次执行延迟,首次拦截时从 start_delay 随机解析 start_delay_resolved: float = 0.0 + # 首次允许执行的时间戳(0.0 表示尚未解析,首次拦截时写入) first_allowed_time: float = 0.0 @@ -1039,8 +1091,33 @@ def _bw_setup_default(self, pub: PublicContext, pri: PrivateContext) -> HookSign def _bw_completion_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: logger.info('Battle completion process') - pub.per_task.count += 1 - return HookSignal.DONE + state = pri.per_battle + options = pri.options + if not isinstance(state, PerBattleCompletion) or not isinstance(options, OptionCompletionDefault): + raise + if options.check_imgs is None: + pub.per_task.count += 1 + return HookSignal.DONE + # 如果需要进一步确认 + if not isinstance(options.check_imgs, list): + raise + appears = [self.appear(check) for check in options.check_imgs] + if any(appears): + pub.per_task.count += 1 + return HookSignal.DONE + # check_imgs 全没出现 → 兜底点击回退界面:进入 completion 满 8s 才首次点击,之后每 8s 一次 + if state.fallback_timer is None: + state.fallback_timer = Timer(8).start() + if not isinstance(state.fallback_timer, Timer): + raise + if not state.fallback_timer.reached(): + return HookSignal.CONTINUE + state.fallback_timer.reset() + if state.click_stage_2 is None: + state.click_stage_2 = PerBattleSuccess.reward_exclude_click(self, options.excludes, name='completion_exclude_click') + x, y = state.click_stage_2.coord() + self.device.click(x=x, y=y, control_name='completion_exclude_click') + return HookSignal.CONTINUE def _bw_interrupt_default(self, pub: PublicContext, pri: PrivateContext) -> HookSignal: # 比如 御魂溢出 From d54042b53837eaa12e21c9987884ae77b57c1c86 Mon Sep 17 00:00:00 2001 From: neptine <2234044577@qq.com> Date: Sat, 19 Sep 2026 14:40:08 +0800 Subject: [PATCH 10/13] feat(GeneralBattle): Add battle count and state reset helpers, restore climb counting add Battle.battle_count_reached to compare per-task count against task limit add Battle.battle_state_reset to clear per_task/per_battle state on task switch restore ActivityShikigami count_map increment on battle success and reset battle state when climbing type switches migrate battle_wait strategy usage docs into battle_wait_strategy docstring and prune outdated examples --- tasks/ActivityShikigami/script_task.py | 12 +++-- tasks/Component/GeneralBattle/battle.py | 21 ++++++++ tasks/Component/GeneralBattle/battle_wait.py | 51 ++++++++------------ 3 files changed, 48 insertions(+), 36 deletions(-) diff --git a/tasks/ActivityShikigami/script_task.py b/tasks/ActivityShikigami/script_task.py index 83aa6d2112..3515cced86 100644 --- a/tasks/ActivityShikigami/script_task.py +++ b/tasks/ActivityShikigami/script_task.py @@ -137,8 +137,8 @@ def switch_next(self): if self.run_idx >= len(self.conf.general_climb.run_sequence_v): logger.info('All climbing activities have been completed') return False - # 切换爬塔类型了, 恢复所有状态 - self.current_count = 0 + # 切换爬塔类型, count_map 按类型独立统计无需重置;但战斗框架状态需要清掉上一类型的残留 + self.battle_state_reset() logger.hr(f'Climb switch to {self.climb_type}', 2) return True @@ -295,13 +295,17 @@ def start_battle(self): logger.info(f'Try click fire, remain times[{max_times - click_times}]') continue # 运行战斗 - # self.run_general_battle(config=self.get_general_battle_conf()) strategies, options = self.loadout_from_config(self.get_general_battle_conf()) strategies['success'] = 'activity' self.loadout_show((strategies, options)) self.state_show() with battle_wait_strategy(**strategies), battle_wait_options(**options): - return self.battle_wait() + win = self.battle_wait() + # 对齐历史语义: 战斗成功结束后记为一次 + if win: + self.count_map[self.climb_type] += 1 + logger.info(f'Count {self.climb_type}: {self.count_map[self.climb_type]}') + return win # @battle_wait_strategy(success='activity') # def battle_wait(self, *args, **kwargs): diff --git a/tasks/Component/GeneralBattle/battle.py b/tasks/Component/GeneralBattle/battle.py index db8249d3fc..2b1d4aa672 100644 --- a/tasks/Component/GeneralBattle/battle.py +++ b/tasks/Component/GeneralBattle/battle.py @@ -7,6 +7,7 @@ from tasks.Component.GeneralBattle.battle_wait import ( BattleWait, battle_wait_strategy, battle_wait_options, OptionPrepareDefault, OptionPresetDefault, OptionGreenDefault, OptionRandomclickDefault, + runtime, ) from tasks.Component.GeneralBattle.config_general_battle import GeneralBattleConfig, GreenMarkType @@ -55,6 +56,26 @@ def battle_run(self): with battle_wait_strategy(), battle_wait_options(): return self.battle_wait() + @classmethod + def battle_count_reached(cls, limit: int) -> bool: + """ + 判断当前任务累计战斗次数是否已达到上限。 + 对比的是新流程的任务级计数 per_task.count(每完成一场结算 +1)。 + + Args: + limit: 上限对比值,如 FallenSun 的 limit_count。 + + Returns: + 达到或超过上限返回 True,否则 False。 + """ + count = runtime.pub_ctx.per_task.count if runtime.pub_ctx else 0 + return count >= limit + + @classmethod + def battle_state_reset(cls): + runtime.reset_per_task() + runtime.reset_per_battle() + def battle_setup(self): pass diff --git a/tasks/Component/GeneralBattle/battle_wait.py b/tasks/Component/GeneralBattle/battle_wait.py index d8bd20257a..7513bae95a 100644 --- a/tasks/Component/GeneralBattle/battle_wait.py +++ b/tasks/Component/GeneralBattle/battle_wait.py @@ -634,6 +634,21 @@ def override(self, *arg, **kwargs): return self class battle_wait_strategy: + """ + 用法(入参为 'event_strategy' 字符串或 event=strategy 的 kwargs): + 1) 装饰器 = 永久覆盖(任务级): + @battle_wait_strategy(success='activity') + def battle_wait(self, *args, **kwargs): + return self.battle_wait_with_strategy(*args, **kwargs) + 2) with = 临时覆盖(本次调用, 退出还原): + with battle_wait_strategy(success='activity'): + task.battle_wait() + 3) 调用时动态传参(临时更新, 不覆盖, 兼容旧接口): + task.battle_wait(random_click_swipt_enable=True) + + 新增事件需实现对应 _bw__ 的 hook;自定义顺序用 sequence 参数, + 未指定时新增事件插到 failure 与 idle 之间。 + """ battle_wait_plan: BattleWaitPlan = None def __init__(self, *arg, **kwargs): @@ -1097,6 +1112,7 @@ def _bw_completion_default(self, pub: PublicContext, pri: PrivateContext) -> Hoo raise if options.check_imgs is None: pub.per_task.count += 1 + # self.current_count += 1 # 兼容旧的计数 return HookSignal.DONE # 如果需要进一步确认 if not isinstance(options.check_imgs, list): @@ -1104,6 +1120,7 @@ def _bw_completion_default(self, pub: PublicContext, pri: PrivateContext) -> Hoo appears = [self.appear(check) for check in options.check_imgs] if any(appears): pub.per_task.count += 1 + # self.current_count += 1 # 兼容旧的计数 return HookSignal.DONE # check_imgs 全没出现 → 兜底点击回退界面:进入 completion 满 8s 才首次点击,之后每 8s 一次 if state.fallback_timer is None: @@ -1554,38 +1571,8 @@ def battle_wait_with_strategy(self, *args, **kwargs) -> bool: | public | cross | per_task | per_battle | | private | cross | per_task | per_battle | - ---------------------------------------------------------------------------------------------------------------- - 三种自定义策略方法: - 1. 使用装饰器battle_wait_strategy, 将会覆盖掉类变量 - @battle_wait_strategy( 'reserve_default', 'idle_default', failure='default') - def battle_wait(self, *args, **kwargs): - return self.battle_wait_with_strategy(*args, **kwargs) - 2. 使用 with 上下文 (!在1基础上), 将会临时覆盖掉原先的类变量,退出后恢复 - with battle_wait_strategy('reserve_default'): - test_battle_wait.battle_wait() - 3. 调用时动态传参 (!在1基础上), 不覆盖,就临时更新策略 - obj.battle_wait(random_click_swipt_enable=1) # 详细参数看 battle_wait_strategy.__call__() - - 自定义hook就是字符串拼起来: battle_wait_strategy的入参可以有 ‘event_strategy’ 或者 'event=strategy' - 可以添加任意 event 以及其对应的 strategy。比如 ‘yyy_default’ 'abcd_edf' - 但是必须要实现对应的hook 上面的比如 _bw_yyy_default() 以及 _bw_abcd_edf() - hook 可以自定义顺序,比如 battle_wait_strategy(sequence='completion > interrupt > success > failure > idle') - 如果没有指定sequence, 新增的event会按照传参时候从左到右排序,左边高优先级,新增的会插入到 failure 和 idle 之间 - - ---------------------------------------------------------------------------------------------------------------- - 如果希望每一个hook带上参数: - 1. 在 battle_wait_strategy 定义了一组默认的 options - 2. 可以在装饰器定义 @battle_wait_strategy(options = options),这里将会覆盖掉原先的 battle_wait_strategy.options - 3. 上下文带上 with battle_wait_strategy(...).with_options(options),同样也是临时覆盖掉 battle_wait_strategy.options - 4. - options: dict[str: dict] = { - "setup": {...} - ... - } - ---------------------------------------------------------------------------------------------------------------- - 跨战斗,考虑把状态挂到方法上,而不是挂到类对象上。】 - 我突然感觉 一个类里面装了 策略和参数,这样不好,考虑拆分成两个装饰器 - + 三种自定义策略方法见 battle_wait_strategy 的 docstring。 + 自定义options方法见 battle_wait_options 的 docstring """ battle_wait_plan = kwargs.get('battle_wait_plan') if battle_wait_plan is None: From 36e1a822a50336035226701adf4a0bb820180f24 Mon Sep 17 00:00:00 2001 From: neptine <2234044577@qq.com> Date: Sat, 19 Sep 2026 17:34:05 +0800 Subject: [PATCH 11/13] =?UTF-8?q?feat(Costume):=20Support=20time-variant?= =?UTF-8?q?=20courtyard=20skin=20=E7=8E=89=E5=B2=9A=E7=8B=90=E5=BA=AD=20#1?= =?UTF-8?q?824?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/i18n/zh-CN.json | 1 + module/atom/gif.py | 11 +- tasks/Component/Costume/assets.py | 44 +++++- tasks/Component/Costume/config.py | 1 + tasks/Component/Costume/costume_base.py | 20 ++- tasks/Component/Costume/main17/image.json | 137 ++++++++++++++++++ .../Costume/main17/main17_check_main_17_a.png | Bin 0 -> 9442 bytes .../Costume/main17/main17_check_main_17_b.png | Bin 0 -> 9124 bytes .../Costume/main17/main17_check_main_17_c.png | Bin 0 -> 9504 bytes .../main17_main_goto_exploration_17_a.png | Bin 0 -> 4473 bytes .../main17_main_goto_exploration_17_b.png | Bin 0 -> 4176 bytes .../main17_main_goto_exploration_17_c.png | Bin 0 -> 4768 bytes .../main17/main17_main_goto_summon_17_a.png | Bin 0 -> 4008 bytes .../main17/main17_main_goto_summon_17_b.png | Bin 0 -> 3496 bytes .../main17/main17_main_goto_summon_17_c.png | Bin 0 -> 3704 bytes .../main17/main17_main_goto_town_17_a.png | Bin 0 -> 3980 bytes .../main17/main17_main_goto_town_17_b.png | Bin 0 -> 2137 bytes .../main17/main17_main_goto_town_17_c.png | Bin 0 -> 3181 bytes .../Costume/main17/main17_pet_house_17_a.png | Bin 0 -> 3015 bytes .../Costume/main17/main17_pet_house_17_b.png | Bin 0 -> 3089 bytes .../Costume/main17/main17_pet_house_17_c.png | Bin 0 -> 4571 bytes tasks/base_task.py | 6 +- 22 files changed, 207 insertions(+), 13 deletions(-) create mode 100644 tasks/Component/Costume/main17/image.json create mode 100644 tasks/Component/Costume/main17/main17_check_main_17_a.png create mode 100644 tasks/Component/Costume/main17/main17_check_main_17_b.png create mode 100644 tasks/Component/Costume/main17/main17_check_main_17_c.png create mode 100644 tasks/Component/Costume/main17/main17_main_goto_exploration_17_a.png create mode 100644 tasks/Component/Costume/main17/main17_main_goto_exploration_17_b.png create mode 100644 tasks/Component/Costume/main17/main17_main_goto_exploration_17_c.png create mode 100644 tasks/Component/Costume/main17/main17_main_goto_summon_17_a.png create mode 100644 tasks/Component/Costume/main17/main17_main_goto_summon_17_b.png create mode 100644 tasks/Component/Costume/main17/main17_main_goto_summon_17_c.png create mode 100644 tasks/Component/Costume/main17/main17_main_goto_town_17_a.png create mode 100644 tasks/Component/Costume/main17/main17_main_goto_town_17_b.png create mode 100644 tasks/Component/Costume/main17/main17_main_goto_town_17_c.png create mode 100644 tasks/Component/Costume/main17/main17_pet_house_17_a.png create mode 100644 tasks/Component/Costume/main17/main17_pet_house_17_b.png create mode 100644 tasks/Component/Costume/main17/main17_pet_house_17_c.png diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index 64614e290c..560803595f 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -43,6 +43,7 @@ "exit_four_help": "支持打九退四,保证稳定57级,要求设置好战斗主题,即 脚本->全局配置->游戏装饰自定义->战斗主题", "costume_main_15": "茨心乐园", "costume_main_16": "狐栖归处", + "costume_main_17": "玉岚狐庭", "check_true_orochi_enable": "是否在结束后检测真蛇", "check_true_orochi_enable_help": "开启后会在任务结束后检测是否出现真蛇, 若出现则自动拉起真蛇", "enable_book_auto": "自动选择", diff --git a/module/atom/gif.py b/module/atom/gif.py index 946972ac79..1d6a45fab1 100644 --- a/module/atom/gif.py +++ b/module/atom/gif.py @@ -12,11 +12,13 @@ class RuleGif: @property def name(self) -> str: - return self.appear_target.name + return self.targets[0].name def __init__(self, targets: list[RuleImage]): self.targets = targets self.roi_front: list = [0, 0, 0, 0] + self.roi_back: list = targets[0].roi_back + self._match_init = False self.appear_target = targets[0] def pre_process(self, image): @@ -44,18 +46,21 @@ def search(self, image, roi: list = None, threshold: float = None) -> tuple: return True, target return False, None - def search_with_multi_scale(self, image, roi=None, threshold=None, scale_range=(0.8, 1.1, 0.05)): + def search_with_multi_scale(self, image, roi=None, threshold=None, scales=None, scale_range=(0.8, 1.1, 0.05)): image = self.pre_process(image) threshold = self.targets[0].threshold if threshold is None else threshold roi = self.targets[0].roi_back if roi is None else roi for target in self.targets: target.roi_back = roi - if target.match_multi_scale(image, threshold=threshold, scale_range=scale_range): + if target.match_multi_scale(image, threshold=threshold, scales=scales, scale_range=scale_range): self.roi_front = target.roi_front self.appear_target = target return True, target return False, None + def match_multi_scale(self, image, threshold: float = None, scales: list = None, scale_range: tuple = None) -> bool: + return self.search_with_multi_scale(image, threshold=threshold, scales=scales, scale_range=scale_range)[0] + def match(self, image, threshold: float = None) -> bool: return self.search(image, threshold=threshold)[0] diff --git a/tasks/Component/Costume/assets.py b/tasks/Component/Costume/assets.py index dfd97438fb..411f3287c1 100644 --- a/tasks/Component/Costume/assets.py +++ b/tasks/Component/Costume/assets.py @@ -102,18 +102,51 @@ class CostumeAssets: # Image Rule Assets - # 狐栖归处庭院判定 + # 狐栖归处庭院判定 I_CHECK_MAIN_16 = RuleImage(roi_front=(260,168,80,54), roi_back=(180,100,240,180), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main16/main16_check_main_16.png") - # 探索 + # 探索 I_MAIN_GOTO_EXPLORATION_16 = RuleImage(roi_front=(729,231,28,52), roi_back=(680,180,130,150), threshold=0.55, method="Template matching", file="./tasks/Component/Costume/main16/main16_main_goto_exploration_16.png") - # 召唤屋 + # 召唤屋 I_MAIN_GOTO_SUMMON_16 = RuleImage(roi_front=(981,218,28,24), roi_back=(930,170,130,150), threshold=0.55, method="Template matching", file="./tasks/Component/Costume/main16/main16_main_goto_summon_16.png") - # 町中 + # 町中 I_MAIN_GOTO_TOWN_16 = RuleImage(roi_front=(1014,346,14,34), roi_back=(960,300,100,120), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main16/main16_main_goto_town_16.png") - # 宠物屋 + # 宠物屋 I_PET_HOUSE_16 = RuleImage(roi_front=(1256,402,20,58), roi_back=(1210,340,70,140), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main16/main16_pet_house_16.png") + # Image Rule Assets + # 庭院判定 a + I_CHECK_MAIN_17_A = RuleImage(roi_front=(260,168,80,54), roi_back=(180,100,240,180), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main17/main17_check_main_17_a.png") + # 庭院判定 b + I_CHECK_MAIN_17_B = RuleImage(roi_front=(260,168,80,54), roi_back=(180,100,240,180), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main17/main17_check_main_17_b.png") + # 庭院判定 c + I_CHECK_MAIN_17_C = RuleImage(roi_front=(260,168,80,54), roi_back=(180,100,240,180), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main17/main17_check_main_17_c.png") + # 探索 a + I_MAIN_GOTO_EXPLORATION_17_A = RuleImage(roi_front=(714,224,43,40), roi_back=(680,180,130,150), threshold=0.55, method="Template matching", file="./tasks/Component/Costume/main17/main17_main_goto_exploration_17_a.png") + # 探索 b + I_MAIN_GOTO_EXPLORATION_17_B = RuleImage(roi_front=(719,216,41,42), roi_back=(667,180,143,106), threshold=0.55, method="Template matching", file="./tasks/Component/Costume/main17/main17_main_goto_exploration_17_b.png") + # 探索 c + I_MAIN_GOTO_EXPLORATION_17_C = RuleImage(roi_front=(717,222,43,42), roi_back=(680,180,125,110), threshold=0.55, method="Template matching", file="./tasks/Component/Costume/main17/main17_main_goto_exploration_17_c.png") + # 召唤屋 a + I_MAIN_GOTO_SUMMON_17_A = RuleImage(roi_front=(981,218,40,41), roi_back=(930,170,130,150), threshold=0.55, method="Template matching", file="./tasks/Component/Costume/main17/main17_main_goto_summon_17_a.png") + # 召唤屋 b + I_MAIN_GOTO_SUMMON_17_B = RuleImage(roi_front=(981,218,39,38), roi_back=(930,170,130,150), threshold=0.55, method="Template matching", file="./tasks/Component/Costume/main17/main17_main_goto_summon_17_b.png") + # 召唤屋 c + I_MAIN_GOTO_SUMMON_17_C = RuleImage(roi_front=(981,218,39,37), roi_back=(930,170,130,150), threshold=0.55, method="Template matching", file="./tasks/Component/Costume/main17/main17_main_goto_summon_17_c.png") + # 町中 a + I_MAIN_GOTO_TOWN_17_A = RuleImage(roi_front=(1004,336,35,49), roi_back=(960,300,100,120), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main17/main17_main_goto_town_17_a.png") + # 町中 b + I_MAIN_GOTO_TOWN_17_B = RuleImage(roi_front=(1007,346,26,31), roi_back=(960,300,100,120), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main17/main17_main_goto_town_17_b.png") + # 町中 c + I_MAIN_GOTO_TOWN_17_C = RuleImage(roi_front=(1008,343,29,43), roi_back=(960,300,100,120), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main17/main17_main_goto_town_17_c.png") + # 宠物屋 a + I_PET_HOUSE_17_A = RuleImage(roi_front=(1226,423,34,37), roi_back=(1191,340,89,152), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main17/main17_pet_house_17_a.png") + # 宠物屋 b + I_PET_HOUSE_17_B = RuleImage(roi_front=(1221,422,33,39), roi_back=(1191,366,89,116), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main17/main17_pet_house_17_b.png") + # 宠物屋 c + I_PET_HOUSE_17_C = RuleImage(roi_front=(1226,417,42,45), roi_back=(1210,340,70,140), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main17/main17_pet_house_17_c.png") + + # Image Rule Assets # description I_CHECK_MAIN_2 = RuleImage(roi_front=(1090,146,48,55), roi_back=(880,108,271,100), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main2/main2_check_main_2.png") @@ -217,3 +250,4 @@ class CostumeAssets: # description I_PET_HOUSE_9 = RuleImage(roi_front=(888,327,40,35), roi_back=(730,316,440,57), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main9/main9_pet_house_9.png") + diff --git a/tasks/Component/Costume/config.py b/tasks/Component/Costume/config.py index 61df75783d..547fc6529b 100644 --- a/tasks/Component/Costume/config.py +++ b/tasks/Component/Costume/config.py @@ -23,6 +23,7 @@ class MainType(str, Enum): COSTUME_MAIN_14 = 'costume_main_14' # 雪月华庭 COSTUME_MAIN_15 = 'costume_main_15' # 茨心乐园 COSTUME_MAIN_16 = 'costume_main_16' # 狐栖归处 + COSTUME_MAIN_17 = 'costume_main_17' # 玉岚狐庭 # 结界皮肤 class RealmType(str, Enum): diff --git a/tasks/Component/Costume/costume_base.py b/tasks/Component/Costume/costume_base.py index d20d3ea567..316e2c0e06 100644 --- a/tasks/Component/Costume/costume_base.py +++ b/tasks/Component/Costume/costume_base.py @@ -3,6 +3,7 @@ # github https://github.com/runhey from module.atom.image import RuleImage +from module.atom.gif import RuleGif from module.logger import logger from tasks.Component.Costume.config import (MainType, CostumeConfig, RealmType, @@ -23,6 +24,14 @@ 'I_PET_HOUSE': f'I_PET_HOUSE_{i}' } for i in range(1, 17) } +# 玉岚狐庭(issue #1824) +main_costume_model[getattr(MainType, "COSTUME_MAIN_17")] = { + 'I_CHECK_MAIN': ['I_CHECK_MAIN_17_A', 'I_CHECK_MAIN_17_B', 'I_CHECK_MAIN_17_C'], + 'I_MAIN_GOTO_EXPLORATION': ['I_MAIN_GOTO_EXPLORATION_17_A', 'I_MAIN_GOTO_EXPLORATION_17_B', 'I_MAIN_GOTO_EXPLORATION_17_C'], + 'I_MAIN_GOTO_SUMMON': ['I_MAIN_GOTO_SUMMON_17_A', 'I_MAIN_GOTO_SUMMON_17_B', 'I_MAIN_GOTO_SUMMON_17_C'], + 'I_MAIN_GOTO_TOWN': ['I_MAIN_GOTO_TOWN_17_A', 'I_MAIN_GOTO_TOWN_17_B', 'I_MAIN_GOTO_TOWN_17_C'], + 'I_PET_HOUSE': ['I_PET_HOUSE_17_A', 'I_PET_HOUSE_17_B', 'I_PET_HOUSE_17_C'], +} # 鲤鱼旗皮肤 @@ -95,14 +104,21 @@ def replace_img(self, asset_before_object.threshold = asset_after.threshold asset_before_object.file = asset_after.file + def set_asset(self, asset_before: str, rule: RuleImage | RuleGif) -> None: + setattr(self, asset_before, rule) + def check_costume_main(self, main_type: MainType): if main_type == MainType.COSTUME_MAIN: return logger.info(f'Switch main costume to {main_type}') costume_assets = CostumeAssets() for key, value in main_costume_model[main_type].items(): - assert_value: RuleImage = getattr(costume_assets, value) - self.replace_img(key, assert_value) + if isinstance(value, list): + rules: list[RuleImage] = [getattr(costume_assets, item) for item in value] + self.set_asset(key, RuleGif(rules)) + else: + assert_value: RuleImage = getattr(costume_assets, value) + self.replace_img(key, assert_value) def check_costume_carpbanner(self, carpbanner_type: CarpBannerType): if carpbanner_type == CarpBannerType.COSTUME_CARPBANNER_DEFAULT: diff --git a/tasks/Component/Costume/main17/image.json b/tasks/Component/Costume/main17/image.json new file mode 100644 index 0000000000..d56f642f0a --- /dev/null +++ b/tasks/Component/Costume/main17/image.json @@ -0,0 +1,137 @@ +[ + { + "itemName": "check_main_17_a", + "imageName": "main17_check_main_17_a.png", + "roiFront": "260,168,80,54", + "roiBack": "180,100,240,180", + "method": "Template matching", + "threshold": 0.8, + "description": "庭院判定 a" + }, + { + "itemName": "check_main_17_b", + "imageName": "main17_check_main_17_b.png", + "roiFront": "260,168,80,54", + "roiBack": "180,100,240,180", + "method": "Template matching", + "threshold": 0.8, + "description": "庭院判定 b" + }, + { + "itemName": "check_main_17_c", + "imageName": "main17_check_main_17_c.png", + "roiFront": "260,168,80,54", + "roiBack": "180,100,240,180", + "method": "Template matching", + "threshold": 0.8, + "description": "庭院判定 c" + }, + { + "itemName": "main_goto_exploration_17_a", + "imageName": "main17_main_goto_exploration_17_a.png", + "roiFront": "714,224,43,40", + "roiBack": "680,180,130,150", + "method": "Template matching", + "threshold": 0.55, + "description": "探索 a" + }, + { + "itemName": "main_goto_exploration_17_b", + "imageName": "main17_main_goto_exploration_17_b.png", + "roiFront": "719,216,41,42", + "roiBack": "667,180,143,106", + "method": "Template matching", + "threshold": 0.55, + "description": "探索 b" + }, + { + "itemName": "main_goto_exploration_17_c", + "imageName": "main17_main_goto_exploration_17_c.png", + "roiFront": "717,222,43,42", + "roiBack": "680,180,125,110", + "method": "Template matching", + "threshold": 0.55, + "description": "探索 c" + }, + { + "itemName": "main_goto_summon_17_a", + "imageName": "main17_main_goto_summon_17_a.png", + "roiFront": "981,218,40,41", + "roiBack": "930,170,130,150", + "method": "Template matching", + "threshold": 0.55, + "description": "召唤屋 a" + }, + { + "itemName": "main_goto_summon_17_b", + "imageName": "main17_main_goto_summon_17_b.png", + "roiFront": "981,218,39,38", + "roiBack": "930,170,130,150", + "method": "Template matching", + "threshold": 0.55, + "description": "召唤屋 b" + }, + { + "itemName": "main_goto_summon_17_c", + "imageName": "main17_main_goto_summon_17_c.png", + "roiFront": "981,218,39,37", + "roiBack": "930,170,130,150", + "method": "Template matching", + "threshold": 0.55, + "description": "召唤屋 c" + }, + { + "itemName": "main_goto_town_17_a", + "imageName": "main17_main_goto_town_17_a.png", + "roiFront": "1004,336,35,49", + "roiBack": "960,300,100,120", + "method": "Template matching", + "threshold": 0.8, + "description": "町中 a" + }, + { + "itemName": "main_goto_town_17_b", + "imageName": "main17_main_goto_town_17_b.png", + "roiFront": "1007,346,26,31", + "roiBack": "960,300,100,120", + "method": "Template matching", + "threshold": 0.8, + "description": "町中 b" + }, + { + "itemName": "main_goto_town_17_c", + "imageName": "main17_main_goto_town_17_c.png", + "roiFront": "1008,343,29,43", + "roiBack": "960,300,100,120", + "method": "Template matching", + "threshold": 0.8, + "description": "町中 c" + }, + { + "itemName": "pet_house_17_a", + "imageName": "main17_pet_house_17_a.png", + "roiFront": "1226,423,34,37", + "roiBack": "1191,340,89,152", + "method": "Template matching", + "threshold": 0.8, + "description": "宠物屋 a" + }, + { + "itemName": "pet_house_17_b", + "imageName": "main17_pet_house_17_b.png", + "roiFront": "1221,422,33,39", + "roiBack": "1191,366,89,116", + "method": "Template matching", + "threshold": 0.8, + "description": "宠物屋 b" + }, + { + "itemName": "pet_house_17_c", + "imageName": "main17_pet_house_17_c.png", + "roiFront": "1226,417,42,45", + "roiBack": "1210,340,70,140", + "method": "Template matching", + "threshold": 0.8, + "description": "宠物屋 c" + } +] \ No newline at end of file diff --git a/tasks/Component/Costume/main17/main17_check_main_17_a.png b/tasks/Component/Costume/main17/main17_check_main_17_a.png new file mode 100644 index 0000000000000000000000000000000000000000..30df69ab8c66b431360344a8bb50b0c0732bac59 GIT binary patch literal 9442 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 P00000NkvXXu0mjfe`E`Q literal 0 HcmV?d00001 diff --git a/tasks/Component/Costume/main17/main17_main_goto_town_17_c.png b/tasks/Component/Costume/main17/main17_main_goto_town_17_c.png new file mode 100644 index 0000000000000000000000000000000000000000..c9c452e1e0152276a3d705bd3215e193a6081835 GIT binary patch literal 3181 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 literal 0 HcmV?d00001 diff --git a/tasks/Component/Costume/main17/main17_pet_house_17_a.png b/tasks/Component/Costume/main17/main17_pet_house_17_a.png new file mode 100644 index 0000000000000000000000000000000000000000..c7f963d4b674f3038dd8a67bcc416f1bc7dbe67b GIT binary patch literal 3015 zcmV;&3pn(NP)`{bYpST*Po9RI|gh9eK>h<-LKXtsaSqr-5w=P`Vs#Mu=*fGcMzWuIyj#oRKpWB~n zG*^FcalWZoUun4MR5p{@tkj&CQI&{b{lZtjnaOw$JaGJ@i*vhoc^`Z*ujoV(M0jXV zt`jgrEP~^hN~K&Z4lT|v9Xn*c_n(*B&4z}0`#pD@IC)~D)oN^({_p{rs~!*$GDZXc z!cP|5c&4w*akTBhz!G{+t=yH4u0BzuudKQWU|7Y1nnY_N!xm^{*GRId^zypdBFSNFjxpkJplJ z;?#W;@6F8JsMW>`U^8;h?div#czSg79J>+8=*wbIbqDFP^wOyC6;`2xMmF%JA+&s1Ok%L<*Y_0bwWvFtg(rW^{a5 zj;S@#X{;YTa$xfEwBJET5IOOIoL8$gJAS9pXp0CUSby@De_b5P%wAbZ#Lebb-A&ky z#$TeMf)ViCCuO#cF`mtcq}lDt zR->sAh}nAL^lKEXmN#ns*?dh=EiF0BWTD$?SQ-F`$G97cDz-af;+$~%BGR2Z_q5wh zC2VZAHtYUlzwxD~UU;jokPiZJ9IjW&5qP^J&6aQ3U}o#g8<$#@$wpQB3mHwO_YIGz z2p}v?>C{NEu|Dn4hKkOw2CG#sPQlt{;JHS{kH<|&Y{!zRWG-&Z-16eSJ%z2U*2rLb zX1N)vG+SY_(X@%Jzc_t%I916Do`m1d(ARbK%Ds*Q|m zPdkoFS|`HP)uj#GU#a>;xIFvu9pgLOvzJFFZhL!H0K^l92sYSy;@OvTw3}j=`2#c$$1zP%~-Wo0xB|E{yiq8ZJ>t9}cJ>zgO!P84~{lf>C;2Oh>bc~JT z$JtA#&}!5if^CC|JKah=mh(HU5D)=7Zc~h?@rEFHsb!Q|QNRI1D0gx0aLc9#QeT(iMp%GB>Jz z++W;cdxisPK_ zcIzBTjF9r!(b{@LiYe5COd!$?p_Y+|6{fxMRwp&V*I$YiMvWs&!FYt5hyhs7{qUWv zV-n1-z4hX~Cm*sIj?GL+Fmg$@W9hJ4iCJKRYZ&mSm;CX4tSSxP^95u~#|OwvU3hZv z_FqvT5cEh7J|h5ut(V_@FU9N-zdLzh{MbaCI38mH97Z|;1_NS4`jQ+3l1Q4%*J@+C zJcr>25(H4kbOJDY&pr7^_kHCX(gOyd3Qk{O(67W^G`p1^2@)c3RQp*h~Oqs5V3Xs zqmNuNamGiNChxd&9K;TT08&XMxLsOEW^;+SAp&!eh^MZ;^trJILE`(hKmkI8j1r!E z>Tgc{(r<}ERTNOLiU2GsL~Olz>5?JiKqjZAkKQ@{$s4ciKlKn4fb^%#f4_e2@Wk;% z+%SO%Dn4^vwW#<^pvFs9iGhD7D^hZjbVOhkYJ zqM`~_04xfHSU>)FRs$fy)rI--BL|5vfBwbW9(sg`UB|xk>UZxxb(ioE6a@cA-+ppz zVszL3`&C2~fr3%%(1@s9nY=i9cw8fd3IU)HMNgn8t(UC%mFta0OG7LzE{z{OXjxc! z_Z1Cf|EWjfY+rim+xLFu*KpGZFT7M38{2!^A?=1?Fdt7C4W)~b`j!NOrMbC%w~mTJ zLWBwhDx0NuKD-!6>-f>nTgx}rZfw*MO3U{Hzj^r3==ABotuK8%@-q+La^KHSzV=ks zEtap%tzDVE^VffO&uxeB|8=`zeM0tO1{d^STsx#d?Y6(YVeHSfA+@92O=OoUKTsn)Dt{+(~O z+imG~H5AV^BSXdQKvF4_OJ{<>2Y6^O4?qS$RABMbCF{?hI-|Hr5os7>yDtG|j;AuM z%6b_3&8C0tdO6?6Yh~YwbBs+a#*s=l6cyn(r<0y#A&SIvO@B6H;;f8b0m0TYK zxKRTUdMYh!HbMZZQfVR#MO9Qq1tNYBNTdiOUlnX~t`Frc{0~tbXA2INL+k(m002ov JPDHLkV1gTjy=edd literal 0 HcmV?d00001 diff --git a/tasks/Component/Costume/main17/main17_pet_house_17_b.png b/tasks/Component/Costume/main17/main17_pet_house_17_b.png new file mode 100644 index 0000000000000000000000000000000000000000..985425573cb3bbfd6df3e17847af17b05599eb16 GIT binary patch literal 3089 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 literal 0 HcmV?d00001 diff --git a/tasks/Component/Costume/main17/main17_pet_house_17_c.png b/tasks/Component/Costume/main17/main17_pet_house_17_c.png new file mode 100644 index 0000000000000000000000000000000000000000..2e9eecfb942b89b1616dddc962630bf90f59668e GIT binary patch literal 4571 zcmV<15hU)3P)steIM{||N1xGJ=49@^Vr?l=j^@qUW!h-lXOBahXV`b;0KPWqRI(Mj6A;K638$$uc2z2rZ1V}pFd!^mI-Mf9w?#xclZqM}0{HN#F z|IX@Bi9a9XxBlfHCK)@?Fbpva(*V2?yW1XQJ-Zi$?|<|Iuip64a3~*qul>TM0U37G zZx{TrrKQ_5vy-Yn=&P<6X3l0c5YKg$8WcHsNYGk0KK-dLS0?hcT5DpWw6oQ6a<-GTiO{MCQ?ujEMvG1KI^BLxkqOsvO$){H zcocRW$3y~2l13v)Ge`|qeZ?7$hhkd{u#<2>%LzN7Uns=`VFUZ^aAKl7 zK2};=s~H9l23^as`?2JUZezDSF*UwZt1~-^z_Qq8voz-W1)mJNzFq4yTh$}8*Wb9h zw0y((>}S3<9E!NwgUN|0q{%QAG2oAtYFiD1kR)PdTuRxpnN3@gf#S^>*d{Ssrs{UX1eV37X(kB@1uqKKrI#gZ0vpQNTOCnK%a@lV-gPG~d=2no+I{ipFjtc`O*e^PJK{QtJ zcN^i@xNoG}-}&QbuQM6$sK2t_c;SavFW*`jE6$Gkxzk6d8l8cccNy)l*L6L2W~%z; zjk~QNERK$zpRW{4Zfj>RSD4xBNoBgczFF(_)o962Qiz7_IYT8@)*i&d#D>AeqPx2r z&Q>d18$o5#Z)^v~mw)%a6Paw`r!Ot8H(R|}hN6>15|Nzi4pW()EbX?#V@KxOQ8$g_ z%{x158{4kyR|@3^?>RIz;qM2XLx=8JzFpTgh_KVz&AU0@_wQ`p6_M%ji9)%Iq4ac; zq-KVss|K!P_j+B&agERZ`ZG%Fzx&a{|NGgONQ(=%GgJn&lOuM0&#{LhX$mH4?uRpz zQ@7V|pL^iQr5E1p_hOOc!FvuLKXNFj1^w>u^gT+a4-1)q>@>Q!*B6h^PFUqiucrz* zSCT{yBp`^BbxlThFfji0$3CTEoj@to%VwP2wM~D#G&(tM0*+$?)b1$Tc9KMxX*iS= z*xcSad;c*z?kz8@4`Gdsjg?(HR~XSloV`Dsu2!0@{V#r@R?Pb*F`0Bgh8hL{(uRQ) zWD=N(T;p@kJUdxge(g24xx0PO-??vPX^AZ^7YiBNS-rcOwX*S0ibXe*@lPL@7cVu) z)Fb|AYUp-5oE`HwYkN_z|MagNy7;{~qtuD{Vb+`K6R!OzoE^_PrnWt=n?R{pYfZ$K z%WTBj-BZb zhK7+D4Ejo`T4Vjl)NvsUwWPh?;@#SjiLtOFkDr{Wcyuce5?B_sgU+d+dtl+)ufk-_ z*)boR>p`zPX?*%)|0)<5AN zbH?xeyWc}>cKbU7BGYVa)gqj03U?5+iN)}1(grq--WEV-;>{GZQ% zHLQ0HW!=ie zm_RqyH}ZwK$aY5!CSwn&uxa%Nl3{zK=xHsD#dbUOY>>hcee%>HXAxJ^0+ge?nJ@$}S$pcJ)Xo+-}0&%jA3`8^EeC<^#YHegxEX|!A z+nTV$m%ZvQuhniQNm|ThO=dDER=w1u;htpJDor2C9X-C^37o8F{NQWf-P^9Ks8ty& z!7FQkh_%Qt7U=}2*Zq_CdY;b~LnBrL0U)qEn*eBOx4Q9gDYjkVQacTe`1Gkomq!y78JEH~#m#!G+s{r!5q9|a?p=WUy3{46OkzswY~l!}4PhfLE5fH!lZ|f1rBi;p0RM z2*5rN8)_OvajjHQBsp+yoA0JdZA-MW&St&ZfqH}3|GHFe)Z*xK+8otR<*u|p&P1BhDT)gQi& zREqQGuH0H0pR8nau5t6_SNg4nYKB`I_0ty~Cqk-mKw{x`P(4=O?NuZW0Zh-?dQkhp z%fb5}Er%_)6S2Xta;Dx~N8T^x{Z`D^f4V+@s-CqX;R_(}+we9OMa!2Lh`{BeKe@Ry zTb;^SjW1y7X4rY&4_LL1kBq>jt!$G*aKA1UdCO~7` zCt@q-$@+p-@av0fBd5+0L*p%41OO4U#WR>-=bmYWiXQ82W z0u4>z{Xn&XTy-7*>7+RAhuhI^^Si~u&rUxuSHHFvgIU;7b;Zt$&;8kZp8fNi5`NGA=-uGmxO=ELuF}i)HAMH0IIraEMTH_re!uyS-zxu)PU;agQ zToC(XWi7I`@wM5xk7yv=4zw21?8Nn$obNsNXFv0g{~0l$0RlAMLVyN{p+&36A`wZ} zYPfV|@#u*Y8Mcj`J1f!3YHvRb6iz&P_S#>6_ueO;(jr>KX|4bH@BXh}_{=Arysxzo zvD&HUk9K=|Qy?a=hLqbE_i#oNzw*t$I`jAcArS}+0yH$?Em{DE7Kc>E0f-k~4=>$Z zId<|m6B~`Swf)7VIO+yUPCfR};>GVuBB$Q>lukgkr7!%!7yiMgf79`N5DqL~{8qpB z(=#9XZD>FPB)-2MuK6H-(1$hN zqP4i{`GGnQ;#;r2e)zEq#0)?K(0G@YU^BRNobu zNiW(}$^0V^0$9BG*AmOACqIO(H?Cb-c=YLC$J<`{`k#&-J9GTr(;(H|em>(@m_3qe zXL&)hvwUq~?vV>1yh}s1>#x1^QruadyZ@IQYrnbJ-B{VpWk#H$6K8Bytc;CL8~yFq zw=dTSY1{KdN$$C4{?#x2kG-{(!$0$Z`IE7ObtT&OUMh05RUBap!J*u37=$(@Go%ebsHX{MWv{raGdE&io@!8jVYTs(QW_ ziS%G}+$Z4j^WW(<>txdYAd}|-OwzGf#qlS9m9V-&KextJ58Wc)?9K(3+PoAsZe|G=s6_t0!A@JNw-+zJo0a6WU zq6`cKJHes%efZIjd>99`;D81oS_>ozZ!HROP+PB@zkmaTw`lx7cW$qpf2^W^w1h{h zXw`4MF}>9jW=CQ%YOlS*ITyrNUU>2Fu_K5jY;LVxeIbs8O(K)Spgno=-0AoKJ$B4J zjcm@#S+-vpB?2_0wH7X~0W2ykRHqHgwuh=>}esc|sqeqV}FRna(%#)=$?kigy z{qV;HWx1B+Z*Hy{U-~a!+}^6ivHsY{-xs$-EALCMyH{IlZq?W__iCHo=+u$>9>xK} zfuuLc&-GuDg`5sI6L%bL`n_Ap@a&SzCN(C%E_FGO<8~(sN&1EH7TKmuD(ki}4@+$$yO1 za5#*$NRsHvR5>NG3-;#fcEQa%j_o+iCQE_?WPm6wm%U&kR3cCP?3qK4JQUtp)qwPr z2B3urL}FR6aM5jJCx{1eWwID2qEz23v{bh;=6V2p-=`E>Bq7+AqXj{8KSzaZ9>4Ny zPYhdi_K>mp&Q>r{aQ9omb~9r3+?-PwpSt|w!m0P1F@EPa|C`p3REz{N7_wzpZdryh zf4IrUR5cE)xLdAoAN5%a(8Fn+SpBlQ`>z6CO7CP9)MCIGDy zq|ielws9ci#oa)sNE6Arwo^Ib70Sgz*le02j`7t$S=4x&#yjF(1Yp<;K!~G&h(!w% zYYoeCyi!@i1|Z}xxmp91^!Dq-W-kh$p(O+uqBP#6L_E)nV=3g^=6qJ4yoTE?++IfTt_yA=S?60ze!o3SKEkDtDA^ZIP{^iU+5 z_dsb!4Wed{%eh**s@EhSTPW1;u9aryCaXtw8g)_pe*r13;jG1)U}yjU002ovPDHLk FV1go#7zY3V literal 0 HcmV?d00001 diff --git a/tasks/base_task.py b/tasks/base_task.py index 60447c8a42..4eb387219f 100644 --- a/tasks/base_task.py +++ b/tasks/base_task.py @@ -283,7 +283,7 @@ def appear_then_click_multi_scale(self, return appear def wait_until_appear(self, - target: RuleImage | RuleOcr, + target: RuleImage | RuleGif | RuleOcr, skip_first_screenshot=False, wait_time: int = None) -> bool: """ @@ -305,13 +305,13 @@ def wait_until_appear(self, if wait_timer and wait_timer.reached(): logger.warning(f"Wait until appear {target.name} timeout") return False - if isinstance(target, RuleImage) and self.appear(target): + if isinstance(target, (RuleImage, RuleGif)) and self.appear(target): return True if isinstance(target, RuleOcr) and self.ocr_appear(target): return True def wait_until_appear_then_click(self, - target: RuleImage, + target: RuleImage | RuleGif, action: Union[RuleClick, RuleLongClick] = None, wait_time: int = None) -> bool: """ From bb808e1bd6b34fab497d9e05f109da0cc0cbd6de Mon Sep 17 00:00:00 2001 From: neptine <2234044577@qq.com> Date: Mon, 21 Sep 2026 01:42:26 +0800 Subject: [PATCH 12/13] feat(Costume): Attach GIF multi-frame behavior in-place so page routing works #1824 - add RuleGif.attach_to to swap GIF match methods onto a RuleImage instance - switch COSTUME_MAIN_17 replacement from set_asset to in-place attach_to - retune pet_house 17_a/17_b frame ROI and images --- module/atom/gif.py | 26 ++++++++++++++++++ tasks/Component/Costume/assets.py | 6 ++-- tasks/Component/Costume/costume_base.py | 15 ++++++---- tasks/Component/Costume/costume_test.py | 2 +- tasks/Component/Costume/main17/image.json | 12 ++++---- .../Costume/main17/main17_pet_house_17_a.png | Bin 3015 -> 3021 bytes .../Costume/main17/main17_pet_house_17_b.png | Bin 3089 -> 2050 bytes .../Costume/main17/main17_pet_house_17_c.png | Bin 4571 -> 2662 bytes 8 files changed, 46 insertions(+), 15 deletions(-) diff --git a/module/atom/gif.py b/module/atom/gif.py index 1d6a45fab1..0ecc56607e 100644 --- a/module/atom/gif.py +++ b/module/atom/gif.py @@ -1,6 +1,8 @@ # This Python file uses the following encoding: utf-8 # @author runhey # github https://github.com/runhey +import types + import numpy as np from module.atom.image import RuleImage @@ -10,6 +12,30 @@ class RuleGif: # 大部分实现同RuleImage 的接口 + @staticmethod + def attach_to(image_obj: RuleImage, targets: list[RuleImage]) -> None: + """ + 把GIF的多帧匹配行为原地挂载到一个RuleImage实例上 + + 与replace_img的原地换属性同思路: 不换对象、不换引用, + 持有该对象引用的Page/寻路会立即生效 + :param image_obj: 被挂载的RuleImage实例 + :param targets: 多帧RuleImage, 顺序即匹配优先级 + """ + # GIF状态 + image_obj.targets = targets + image_obj.roi_front = [0, 0, 0, 0] + image_obj.roi_back = targets[0].roi_back + image_obj.appear_target = targets[0] + image_obj.name = targets[0].name + image_obj._match_init = False + # 方法级替换: 把RuleGif的所有公开方法绑定到该实例上 + for name, attr in RuleGif.__dict__.items(): + if name.startswith('_'): + continue + if isinstance(attr, types.FunctionType): + setattr(image_obj, name, types.MethodType(attr, image_obj)) + @property def name(self) -> str: return self.targets[0].name diff --git a/tasks/Component/Costume/assets.py b/tasks/Component/Costume/assets.py index 411f3287c1..ff5f6acf91 100644 --- a/tasks/Component/Costume/assets.py +++ b/tasks/Component/Costume/assets.py @@ -140,11 +140,11 @@ class CostumeAssets: # 町中 c I_MAIN_GOTO_TOWN_17_C = RuleImage(roi_front=(1008,343,29,43), roi_back=(960,300,100,120), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main17/main17_main_goto_town_17_c.png") # 宠物屋 a - I_PET_HOUSE_17_A = RuleImage(roi_front=(1226,423,34,37), roi_back=(1191,340,89,152), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main17/main17_pet_house_17_a.png") + I_PET_HOUSE_17_A = RuleImage(roi_front=(1249,404,23,56), roi_back=(1231,340,49,154), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main17/main17_pet_house_17_a.png") # 宠物屋 b - I_PET_HOUSE_17_B = RuleImage(roi_front=(1221,422,33,39), roi_back=(1191,366,89,116), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main17/main17_pet_house_17_b.png") + I_PET_HOUSE_17_B = RuleImage(roi_front=(1251,417,19,45), roi_back=(1231,366,49,124), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main17/main17_pet_house_17_b.png") # 宠物屋 c - I_PET_HOUSE_17_C = RuleImage(roi_front=(1226,417,42,45), roi_back=(1210,340,70,140), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main17/main17_pet_house_17_c.png") + I_PET_HOUSE_17_C = RuleImage(roi_front=(1248,406,19,59), roi_back=(1228,340,52,176), threshold=0.8, method="Template matching", file="./tasks/Component/Costume/main17/main17_pet_house_17_c.png") # Image Rule Assets diff --git a/tasks/Component/Costume/costume_base.py b/tasks/Component/Costume/costume_base.py index 316e2c0e06..67f7759e7e 100644 --- a/tasks/Component/Costume/costume_base.py +++ b/tasks/Component/Costume/costume_base.py @@ -32,6 +32,14 @@ 'I_MAIN_GOTO_TOWN': ['I_MAIN_GOTO_TOWN_17_A', 'I_MAIN_GOTO_TOWN_17_B', 'I_MAIN_GOTO_TOWN_17_C'], 'I_PET_HOUSE': ['I_PET_HOUSE_17_A', 'I_PET_HOUSE_17_B', 'I_PET_HOUSE_17_C'], } +main_costume_model[getattr(MainType, "COSTUME_MAIN_13")] = { + 'I_CHECK_MAIN': ['I_CHECK_MAIN_13',], + 'I_MAIN_GOTO_EXPLORATION': ['I_MAIN_GOTO_EXPLORATION_13', ], + 'I_MAIN_GOTO_SUMMON': ['I_MAIN_GOTO_SUMMON_13',], + 'I_MAIN_GOTO_TOWN': ['I_MAIN_GOTO_TOWN_13',], + 'I_PET_HOUSE': ['I_PET_HOUSE_13',], +} + # 鲤鱼旗皮肤 @@ -104,9 +112,6 @@ def replace_img(self, asset_before_object.threshold = asset_after.threshold asset_before_object.file = asset_after.file - def set_asset(self, asset_before: str, rule: RuleImage | RuleGif) -> None: - setattr(self, asset_before, rule) - def check_costume_main(self, main_type: MainType): if main_type == MainType.COSTUME_MAIN: return @@ -115,7 +120,7 @@ def check_costume_main(self, main_type: MainType): for key, value in main_costume_model[main_type].items(): if isinstance(value, list): rules: list[RuleImage] = [getattr(costume_assets, item) for item in value] - self.set_asset(key, RuleGif(rules)) + RuleGif.attach_to(getattr(self, key), rules) else: assert_value: RuleImage = getattr(costume_assets, value) self.replace_img(key, assert_value) @@ -167,4 +172,4 @@ def check_costume_shikigami(self, shikigami_type: ShikigamiType): if __name__ == '__main__': c = CostumeBase() - c.check_costume_main(MainType.COSTUME_MAIN_2) + c.check_costume_main(MainType.COSTUME_MAIN_13) diff --git a/tasks/Component/Costume/costume_test.py b/tasks/Component/Costume/costume_test.py index f63d30aac3..5667e62e46 100644 --- a/tasks/Component/Costume/costume_test.py +++ b/tasks/Component/Costume/costume_test.py @@ -20,7 +20,7 @@ def run(self): self.ui_click(self.I_TOWN_GOTO_MAIN, self.I_CHECK_MAIN) # 探索测试 self.ui_click(self.I_MAIN_GOTO_EXPLORATION, self.I_CHECK_EXPLORATION) - self.ui_click(self.I_UI_BACK_BLUE, self.I_CHECK_MAIN) + self.ui_click(self.I_UI_BACK_YELLOW, self.I_CHECK_MAIN) # 召唤测试 self.ui_click(self.I_MAIN_GOTO_SUMMON, self.I_CHECK_SUMMON) self.ui_click(self.I_SUMMON_GOTO_MAIN, self.I_CHECK_MAIN) diff --git a/tasks/Component/Costume/main17/image.json b/tasks/Component/Costume/main17/image.json index d56f642f0a..820bb5fed8 100644 --- a/tasks/Component/Costume/main17/image.json +++ b/tasks/Component/Costume/main17/image.json @@ -110,8 +110,8 @@ { "itemName": "pet_house_17_a", "imageName": "main17_pet_house_17_a.png", - "roiFront": "1226,423,34,37", - "roiBack": "1191,340,89,152", + "roiFront": "1249,404,23,56", + "roiBack": "1231,340,49,154", "method": "Template matching", "threshold": 0.8, "description": "宠物屋 a" @@ -119,8 +119,8 @@ { "itemName": "pet_house_17_b", "imageName": "main17_pet_house_17_b.png", - "roiFront": "1221,422,33,39", - "roiBack": "1191,366,89,116", + "roiFront": "1251,417,19,45", + "roiBack": "1231,366,49,124", "method": "Template matching", "threshold": 0.8, "description": "宠物屋 b" @@ -128,8 +128,8 @@ { "itemName": "pet_house_17_c", "imageName": "main17_pet_house_17_c.png", - "roiFront": "1226,417,42,45", - "roiBack": "1210,340,70,140", + "roiFront": "1248,406,19,59", + "roiBack": "1228,340,52,176", "method": "Template matching", "threshold": 0.8, "description": "宠物屋 c" diff --git a/tasks/Component/Costume/main17/main17_pet_house_17_a.png b/tasks/Component/Costume/main17/main17_pet_house_17_a.png index c7f963d4b674f3038dd8a67bcc416f1bc7dbe67b..9f50362a2e277fd988ed08a52d06d63e539ffd9a 100644 GIT binary patch literal 3021 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 P00000NkvXXu0mjf1%KvZ literal 3015 zcmV;&3pn(NP)`{bYpST*Po9RI|gh9eK>h<-LKXtsaSqr-5w=P`Vs#Mu=*fGcMzWuIyj#oRKpWB~n zG*^FcalWZoUun4MR5p{@tkj&CQI&{b{lZtjnaOw$JaGJ@i*vhoc^`Z*ujoV(M0jXV zt`jgrEP~^hN~K&Z4lT|v9Xn*c_n(*B&4z}0`#pD@IC)~D)oN^({_p{rs~!*$GDZXc z!cP|5c&4w*akTBhz!G{+t=yH4u0BzuudKQWU|7Y1nnY_N!xm^{*GRId^zypdBFSNFjxpkJplJ z;?#W;@6F8JsMW>`U^8;h?div#czSg79J>+8=*wbIbqDFP^wOyC6;`2xMmF%JA+&s1Ok%L<*Y_0bwWvFtg(rW^{a5 zj;S@#X{;YTa$xfEwBJET5IOOIoL8$gJAS9pXp0CUSby@De_b5P%wAbZ#Lebb-A&ky z#$TeMf)ViCCuO#cF`mtcq}lDt zR->sAh}nAL^lKEXmN#ns*?dh=EiF0BWTD$?SQ-F`$G97cDz-af;+$~%BGR2Z_q5wh zC2VZAHtYUlzwxD~UU;jokPiZJ9IjW&5qP^J&6aQ3U}o#g8<$#@$wpQB3mHwO_YIGz z2p}v?>C{NEu|Dn4hKkOw2CG#sPQlt{;JHS{kH<|&Y{!zRWG-&Z-16eSJ%z2U*2rLb zX1N)vG+SY_(X@%Jzc_t%I916Do`m1d(ARbK%Ds*Q|m zPdkoFS|`HP)uj#GU#a>;xIFvu9pgLOvzJFFZhL!H0K^l92sYSy;@OvTw3}j=`2#c$$1zP%~-Wo0xB|E{yiq8ZJ>t9}cJ>zgO!P84~{lf>C;2Oh>bc~JT z$JtA#&}!5if^CC|JKah=mh(HU5D)=7Zc~h?@rEFHsb!Q|QNRI1D0gx0aLc9#QeT(iMp%GB>Jz z++W;cdxisPK_ zcIzBTjF9r!(b{@LiYe5COd!$?p_Y+|6{fxMRwp&V*I$YiMvWs&!FYt5hyhs7{qUWv zV-n1-z4hX~Cm*sIj?GL+Fmg$@W9hJ4iCJKRYZ&mSm;CX4tSSxP^95u~#|OwvU3hZv z_FqvT5cEh7J|h5ut(V_@FU9N-zdLzh{MbaCI38mH97Z|;1_NS4`jQ+3l1Q4%*J@+C zJcr>25(H4kbOJDY&pr7^_kHCX(gOyd3Qk{O(67W^G`p1^2@)c3RQp*h~Oqs5V3Xs zqmNuNamGiNChxd&9K;TT08&XMxLsOEW^;+SAp&!eh^MZ;^trJILE`(hKmkI8j1r!E z>Tgc{(r<}ERTNOLiU2GsL~Olz>5?JiKqjZAkKQ@{$s4ciKlKn4fb^%#f4_e2@Wk;% z+%SO%Dn4^vwW#<^pvFs9iGhD7D^hZjbVOhkYJ zqM`~_04xfHSU>)FRs$fy)rI--BL|5vfBwbW9(sg`UB|xk>UZxxb(ioE6a@cA-+ppz zVszL3`&C2~fr3%%(1@s9nY=i9cw8fd3IU)HMNgn8t(UC%mFta0OG7LzE{z{OXjxc! z_Z1Cf|EWjfY+rim+xLFu*KpGZFT7M38{2!^A?=1?Fdt7C4W)~b`j!NOrMbC%w~mTJ zLWBwhDx0NuKD-!6>-f>nTgx}rZfw*MO3U{Hzj^r3==ABotuK8%@-q+La^KHSzV=ks zEtap%tzDVE^VffO&uxeB|8=`zeM0tO1{d^STsx#d?Y6(YVeHSfA+@92O=OoUKTsn)Dt{+(~O z+imG~H5AV^BSXdQKvF4_OJ{<>2Y6^O4?qS$RABMbCF{?hI-|Hr5os7>yDtG|j;AuM z%6b_3&8C0tdO6?6Yh~YwbBs+a#*s=l6cyn(r<0y#A&SIvO@B6H;;f8b0m0TYK zxKRTUdMYh!HbMZZQfVR#MO9Qq1tNYBNTdiOUlnX~t`Frc{0~tbXA2INL+k(m002ov JPDHLkV1gTjy=edd diff --git a/tasks/Component/Costume/main17/main17_pet_house_17_b.png b/tasks/Component/Costume/main17/main17_pet_house_17_b.png index 985425573cb3bbfd6df3e17847af17b05599eb16..ef47f7d8b4cfe3bd64421d060a8b06b37b0993a8 100644 GIT binary patch literal 2050 zcmV+d2>thoP)L+Qe)IWP>2Z?MU4uQRuO5N0%D;TX4>hLxty6f*M0V7t@ZzwsNaX*|H;p)rb&~y ztg1AM$5q`*g!d_M9 z69RM^7qlidpt$bi>Ox?JcqPnOe6*=dK5JaG6A{vj!`25RfMh)z2ZLzS9Y#iIN ztkNW@>L#|faZU+ADeQa{nX0T~9aUAG#1>CK^&K;PY`a@jWl2On1Y@)|QRAGj5Rr{+ z)A$&)E4?<#H9pd&scU@tN8c8_Lp5le$Lk-gA3L`6&kO%PytKqkO(x>NqI6(DBHuWr z6*FsX@bT|{t(B%9Ke62Dk6rD~y!`ivKXdBYXJ7jK7am5`HqvPt0}`WQija_yf@%4t z#%G@VCLr2KADW+@jMDe6-8}d9R@#!OTxp{khb(Xhk!Xd8K!iw0NciGUzjb+a|M;=R zQ!6Wr4?M^Q-#vTr*6r1+H*Q2$*(e?i$IPsao;ff%91gQQZ)Mr~jWztiW8a7XKKH3p zE2mEA#1dI%sA~88Q>XWv27&v7(bDq4)oUNj%+1#xP{&2TF97(=55NA<(!|Fe`6@)@ zt3umMKzx9~&eq?~JipyU`~4!Yr?7#_GE4`Id^Mh0SjMeCf9b#iSN1_~mV$F}I#oC0 zFy4XU%9P%F>&o;Ex9nWKv3fx%9c8)KM@%v`Id>H0@|QvNm}&$900AH%_%EixdiIyw z_k%H=%{5n#quG_IyFY12g9vuder0m15gPy+06|1R1_lVirF!FBvzdLGNp<%&F-lZ^ zS8MJ>WUSH}U;o4Dw0d_cAH;ebTZP0f_+fv%vkPmT9PA-hQB_w}&2g63s-4cAICSW4 zK*S5@UMPmWG|R8t9JNz58aBs|PC4(!#ZUm6x)R}?%?i|XbwJtqoPBCAFgkSrYsK3f)j@ud!&`7fdy|~{m1V1^_hdmZmeFOnQoyu zkftpOJm_vLpPbLzce$}Ue||0110>;Ox-?6XwIMLxx_-X1_5RGkdlh=Yh(#oLv;WG> z;T1i*BA`KVZ+E!;-ft;eaoIv;4jZkR1wUN7u)BTjz`=Wj1pq|E_1{0D{^X-LkP;9C zLEks3zH#dl)!mN_c{+~c5CV4AFLm}-r>2)!7!g6h7aJ3!GkL27WQZ6@ur7)rukCy` zKKaFNlNw{01MY9V)$6S$ttAjZBq9VPVPAJHNz;#v3m^=+iVvp~vwM~F!kp4Zm~nsm z?eTasiVgt)A|VkV5)uFi2mp$(!4Mk>>^ei&_4A;JnX$L?PSt#9%wYgRREPk800;;o zf{4Jf+u7LOTEFM$QK$2LcK{H42-x4h=$d_{7Kumz;SNE-JHWf9s)}Cc8iv8`U17k3 zhnFQkjs(F6?C)I+GSIXLNQi`p0N`D{yLBF=%JX>}w;DI>?)E#~W;k{xnX>JJHlN2~ z_e~c@QFN~&1&Hu}2p)t0iL2{PRSbvY$%$+<8f|QLd#>G@xeo{3H(5$+??Y6G2q5r3 z2nYZHeGi0y8{uNF%6grwO#B@kN0}2KOT8z?Dx(B3~kgxB1BX` z07irn6$lDPA}y4XAr~Isy3yP0;fU(s0~Ukpp8M6PCL|;v@p4WzE<$j_?=HJRMsTM6A^yn8wLq)N4;n zApimxt=es?P>hK{LY5^_KHY7yAt-$5cfaBgg7+bK5eeRhkB9&$B4d;wL^=W@_C9M* zxx#)c5A(Go(0s^H*42;K({o}KqTgwQ&Ad~$9cyp)3hKKI?cvZ}X(bXi2e*d$bC goY<70G}gNMKk0d&whEhz2><{907*qoM6N<$g2p4ue*gdg literal 3089 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 diff --git a/tasks/Component/Costume/main17/main17_pet_house_17_c.png b/tasks/Component/Costume/main17/main17_pet_house_17_c.png index 2e9eecfb942b89b1616dddc962630bf90f59668e..03829079faa4ab41927ecb1bd6bce952c9670b6b 100644 GIT binary patch literal 2662 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 FV1go#7zY3V From 37a884efe291fb93a8086207ca0760b568464dbd Mon Sep 17 00:00:00 2001 From: neptine <2234044577@qq.com> Date: Mon, 21 Sep 2026 19:39:24 +0800 Subject: [PATCH 13/13] fix(Costume): Skip missing assets when attaching GIF frames --- tasks/Component/Costume/costume_base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tasks/Component/Costume/costume_base.py b/tasks/Component/Costume/costume_base.py index 67f7759e7e..9b49abe209 100644 --- a/tasks/Component/Costume/costume_base.py +++ b/tasks/Component/Costume/costume_base.py @@ -119,6 +119,8 @@ def check_costume_main(self, main_type: MainType): costume_assets = CostumeAssets() for key, value in main_costume_model[main_type].items(): if isinstance(value, list): + if not hasattr(self, key): + continue rules: list[RuleImage] = [getattr(costume_assets, item) for item in value] RuleGif.attach_to(getattr(self, key), rules) else: