From da4352f0760c19b508dbcd0cb7a694981511b49a Mon Sep 17 00:00:00 2001 From: henry3218 Date: Mon, 27 Jul 2026 20:23:58 +0800 Subject: [PATCH] fix: prioritize build targets over power plans --- tasks/daily/buildtarget.py | 44 +++++++---- tasks/power/power.py | 17 +++-- .../test_tasks/test_build_target_priority.py | 76 +++++++++++++++++++ tests/test_tasks/test_power_plan.py | 50 +++++++++++- 4 files changed, 163 insertions(+), 24 deletions(-) create mode 100644 tests/test_tasks/test_build_target_priority.py diff --git a/tasks/daily/buildtarget.py b/tasks/daily/buildtarget.py index 6e46d9886..90f7f00eb 100644 --- a/tasks/daily/buildtarget.py +++ b/tasks/daily/buildtarget.py @@ -105,8 +105,7 @@ def collect(self) -> list[tuple[str, str]]: if validated: log.debug(f"副本名称 {validated} 检验通过,加入目标列表") results.append(validated) - if "饰品提取" in validated[0]: - break + else: log.warning(f"目标副本识别错误,{instance} 不在任何已知副本列表中") @@ -474,26 +473,39 @@ def get_target_instance() -> tuple[str, str] | None: if not BuildTarget._initialized: BuildTarget.init_build_targets() - require_ornament = False - only_erosion_and_ornament = False - if BuildTarget._target_instances: - only_erosion_and_ornament = all(("侵蚀隧洞" in instance_type) or ("饰品提取" in instance_type) for instance_type in BuildTarget._target_instances) - if only_erosion_and_ornament: - require_ornament = datetime.date.today().weekday() >= (7 - cfg.build_target_ornament_weekly_count) + target_instances = [ + (instance_type, instance_names[0]) + for instance_type, instance_names in BuildTarget._target_instances.items() + if instance_names and "历战余响" not in instance_type + ] + if not target_instances: + return None + + def is_erosion_or_ornament(instance_type: str) -> bool: + return "侵蚀隧洞" in instance_type or "饰品提取" in instance_type + only_erosion_and_ornament = all(is_erosion_or_ornament(instance_type) for instance_type, _ in target_instances) if only_erosion_and_ornament and cfg.build_target_use_user_instance_when_only_erosion_and_ornament: log.info("培养目标仅识别到侵蚀隧洞/饰品提取,按配置回退至自定义副本") return None - for instance_type, instance_names in BuildTarget._target_instances.items(): - if "历战余响" in instance_type: - continue - if not require_ornament: - return (instance_type, instance_names[0]) - if "饰品提取" in instance_type: - return (instance_type, instance_names[0]) + # 游戏界面可能将饰品提取显示在材料副本之前,因此不能依赖识别顺序。 + # 只要仍有角色或光锥素材需要刷取,就优先执行相应的材料副本。 + for instance_type, instance_name in target_instances: + if not is_erosion_or_ornament(instance_type): + return (instance_type, instance_name) - return None + require_ornament = datetime.date.today().weekday() >= (7 - cfg.build_target_ornament_weekly_count) + if require_ornament: + for instance_type, instance_name in target_instances: + if "饰品提取" in instance_type: + return (instance_type, instance_name) + + for instance_type, instance_name in target_instances: + if "侵蚀隧洞" in instance_type: + return (instance_type, instance_name) + + return target_instances[0] @staticmethod def get_target_echo_instance() -> tuple[str, str] | None: diff --git a/tasks/power/power.py b/tasks/power/power.py index 997a0cbe2..b109ec24b 100644 --- a/tasks/power/power.py +++ b/tasks/power/power.py @@ -12,21 +12,26 @@ class Power: def run(): Power.preprocess() - # 优先执行体力计划 - Power.execute_power_plan() - log.hr("开始清体力", 0) instance_type = cfg.instance_type instance_name = cfg.instance_names[instance_type] + target = None try: - if cfg.build_target_enable and (target := BuildTarget.get_target_instance()): - instance_type, instance_name = target - log.info(f"使用培养目标副本: {instance_type} - {instance_name}") + if cfg.build_target_enable: + target = BuildTarget.get_target_instance() except Exception as e: log.error(f"获取培养目标副本失败: {e}") + if target: + instance_type, instance_name = target + log.info(f"使用培养目标副本: {instance_type} - {instance_name}") + else: + # 未启用培养目标、识别失败,或培养目标按配置回退至自定义副本时, + # 才优先执行用户配置的体力计划。 + Power.execute_power_plan() + if not Instance.validate_instance(instance_type, instance_name): log.hr("完成", 2) return False diff --git a/tests/test_tasks/test_build_target_priority.py b/tests/test_tasks/test_build_target_priority.py new file mode 100644 index 000000000..249500504 --- /dev/null +++ b/tests/test_tasks/test_build_target_priority.py @@ -0,0 +1,76 @@ +from types import SimpleNamespace +from unittest.mock import patch + +from tasks.daily import buildtarget +from tasks.daily.buildtarget import BuildTarget, DefaultHandler + + +def test_default_handler_continues_collecting_after_ornament(): + handler = DefaultHandler.__new__(DefaultHandler) + instances = [ + ("饰品提取", "永恒笑剧"), + ("拟造花萼(金)", "回忆之蕾"), + ("凝滞虚影", "嗔怒之形"), + ] + + with ( + patch.object(handler, "_iter_scroll_windows", return_value=instances), + patch.object(handler, "_capture_items_in_window"), + patch.object(handler, "_is_valid_instance", side_effect=lambda instance: instance), + ): + assert handler.collect() == instances + + +def test_material_instance_is_prioritized_even_when_ornament_is_first(): + targets = { + "饰品提取": ["永恒笑剧"], + "拟造花萼(金)": ["回忆之蕾"], + "凝滞虚影": ["嗔怒之形"], + } + cfg = SimpleNamespace( + build_target_use_user_instance_when_only_erosion_and_ornament=True, + build_target_ornament_weekly_count=7, + ) + + with ( + patch.object(buildtarget, "cfg", cfg), + patch.object(BuildTarget, "_initialized", True), + patch.object(BuildTarget, "_target_instances", targets), + ): + assert BuildTarget.get_target_instance() == ("拟造花萼(金)", "回忆之蕾") + + +def test_only_erosion_and_ornament_returns_to_custom_instance_when_enabled(): + targets = { + "饰品提取": ["永恒笑剧"], + "侵蚀隧洞": ["睿治之径"], + } + cfg = SimpleNamespace( + build_target_use_user_instance_when_only_erosion_and_ornament=True, + build_target_ornament_weekly_count=7, + ) + + with ( + patch.object(buildtarget, "cfg", cfg), + patch.object(BuildTarget, "_initialized", True), + patch.object(BuildTarget, "_target_instances", targets), + ): + assert BuildTarget.get_target_instance() is None + + +def test_cavern_is_used_before_ornament_when_weekly_count_is_zero(): + targets = { + "饰品提取": ["永恒笑剧"], + "侵蚀隧洞": ["睿治之径"], + } + cfg = SimpleNamespace( + build_target_use_user_instance_when_only_erosion_and_ornament=False, + build_target_ornament_weekly_count=0, + ) + + with ( + patch.object(buildtarget, "cfg", cfg), + patch.object(BuildTarget, "_initialized", True), + patch.object(BuildTarget, "_target_instances", targets), + ): + assert BuildTarget.get_target_instance() == ("侵蚀隧洞", "睿治之径") diff --git a/tests/test_tasks/test_power_plan.py b/tests/test_tasks/test_power_plan.py index ea64f2317..bd6f5ec0b 100644 --- a/tests/test_tasks/test_power_plan.py +++ b/tests/test_tasks/test_power_plan.py @@ -7,11 +7,14 @@ class FakeConfig: - def __init__(self, power_plan, keep_plan): + def __init__(self, power_plan, keep_plan, build_target_enable=False): self.values = { "power_plan": power_plan, "power_plan_keep": keep_plan, } + self.build_target_enable = build_target_enable + self.instance_type = "侵蚀隧洞" + self.instance_names = {"侵蚀隧洞": "睿治之径"} self.writes = [] def get_value(self, key, default=None): @@ -50,7 +53,9 @@ def validate_instance(instance_type, instance_name): return True class FakeBuildTarget: - pass + @staticmethod + def get_target_instance(): + return None stub_modules = { "module.screen": _stub_module("module.screen", screen=object()), @@ -90,5 +95,46 @@ def test_completed_plan_is_unchanged_when_keep_is_enabled(self): self.assertEqual(cfg.get_value("power_plan"), plan) +class TestPowerRunPriority(unittest.TestCase): + def test_build_target_runs_before_power_plan(self): + cfg = FakeConfig( + [["侵蚀隧洞", "睿治之径", 2]], + keep_plan=True, + build_target_enable=True, + ) + module = _load_power_module(cfg) + target = ("拟造花萼(赤)", "毁灭之蕾•拟造花萼(赤)") + + with ( + patch.object(module.Power, "preprocess"), + patch.object(module.Power, "execute_power_plan") as execute_power_plan, + patch.object(module.Power, "process") as process, + patch.object(module.BuildTarget, "get_target_instance", return_value=target), + ): + module.Power.run() + + execute_power_plan.assert_not_called() + process.assert_called_once_with(*target) + + def test_power_plan_runs_when_build_target_returns_to_custom_instance(self): + cfg = FakeConfig( + [["侵蚀隧洞", "睿治之径", 2]], + keep_plan=True, + build_target_enable=True, + ) + module = _load_power_module(cfg) + + with ( + patch.object(module.Power, "preprocess"), + patch.object(module.Power, "execute_power_plan") as execute_power_plan, + patch.object(module.Power, "process") as process, + patch.object(module.BuildTarget, "get_target_instance", return_value=None), + ): + module.Power.run() + + execute_power_plan.assert_called_once_with() + process.assert_called_once_with("侵蚀隧洞", "睿治之径") + + if __name__ == "__main__": unittest.main()