From 70958124318189bf8dfb727d6aa911710c49c5da Mon Sep 17 00:00:00 2001 From: KUAI Date: Mon, 5 Oct 2026 18:43:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=A9=BB=E9=98=B2?= =?UTF-8?q?=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- autowsgr/combat/actions.py | 9 ++++ autowsgr/combat/handlers.py | 27 ++++++++++-- autowsgr/combat/recognizer.py | 5 +++ autowsgr/combat/state.py | 10 ++++- autowsgr/data/images/combat/garrison_540p.png | Bin 0 -> 7098 bytes autowsgr/image_resources/combat.py | 2 + autowsgr/image_resources/keys.py | 2 + testing/combat/test_combat.py | 9 ++++ testing/combat/test_result_dismiss_unit.py | 41 ++++++++++++++++++ 9 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 autowsgr/data/images/combat/garrison_540p.png diff --git a/autowsgr/combat/actions.py b/autowsgr/combat/actions.py index 01309e90..63a344b5 100644 --- a/autowsgr/combat/actions.py +++ b/autowsgr/combat/actions.py @@ -72,6 +72,10 @@ class Coords: CLICK_RESULT = (0.953, 0.954) """点击战果页面继续。""" + # ── 驻防 ── + GARRISON_CANCEL = (0.624, 0.565) + """驻防取消按钮 (与强化/解装确认弹窗取消钮 ≈(0.62, 0.57) 同布局)。""" + # ── 加速点击 ── SPEED_UP_NORMAL = (0.260, 0.963) """常规战移动加速点击。""" @@ -177,6 +181,11 @@ def click_result(device: AndroidController) -> None: device.click(*Coords.CLICK_RESULT) +def click_garrison_cancel(device: AndroidController) -> None: + """点击驻防取消按钮 (回港)。""" + device.click(*Coords.GARRISON_CANCEL) + + def click_speed_up(device: AndroidController, *, battle_mode: bool = False) -> None: """点击加速(移动中或战役中)。 diff --git a/autowsgr/combat/handlers.py b/autowsgr/combat/handlers.py index c4471a9b..22673ffc 100644 --- a/autowsgr/combat/handlers.py +++ b/autowsgr/combat/handlers.py @@ -16,6 +16,7 @@ click_enter_fight, click_fight_condition, click_formation, + click_garrison_cancel, click_image, click_night_battle, click_proceed, @@ -63,6 +64,7 @@ CombatPhase.RESULT: '_handle_result', CombatPhase.EXP_SETTLEMENT: '_handle_exp_settlement', CombatPhase.GET_SHIP: '_handle_get_ship', + CombatPhase.GARRISON: '_handle_garrison', CombatPhase.PROCEED: '_handle_proceed', CombatPhase.FLAGSHIP_SEVERE_DAMAGE: '_handle_flagship_severe_damage', CombatPhase.DOCK_FULL: '_handle_dock_full', @@ -513,10 +515,14 @@ def _result_successors(self, phase: CombatPhase) -> list[CombatPhase]: 到达点逐页推进); 快速: 经验页是 pass_through 过渡页, 不在到达集。 RESULT 之后: PROCEED / 终态页 / GET_SHIP / 旗舰大破 (+经验页, 慢速); - EXP_SETTLEMENT 之后: 同上但去掉 EXP_SETTLEMENT 自身; - GET_SHIP 之后: 同上但去掉 GET_SHIP 自身。 + 驻防 (MAP 模式, 非必现) / EXP_SETTLEMENT 之后: 同上但去掉 + EXP_SETTLEMENT 自身; GET_SHIP 之后: 同上但去掉 GET_SHIP 自身。 """ - successors = [CombatPhase.PROCEED, CombatPhase.FLAGSHIP_SEVERE_DAMAGE] + successors = [ + CombatPhase.PROCEED, + CombatPhase.FLAGSHIP_SEVERE_DAMAGE, + CombatPhase.GARRISON, # 驻防: 仅 MAP 转移图含此状态 + ] end_phase = self._plan.end_phase if end_phase is not None: successors.append(end_phase) @@ -549,6 +555,21 @@ def _handle_get_ship(self) -> ConditionFlag: self._click_result_until_closed(CombatPhase.GET_SHIP) return ConditionFlag.FIGHT_CONTINUE + def _handle_garrison(self) -> ConditionFlag: + """处理驻防提示 — 点击取消即强制回港,结束战斗。""" + _log.info('[Combat] 检测到驻防,点击取消 (回港)') + click_garrison_cancel(self._device) + self._last_action = 'garrison_cancel' + + self._history.add( + CombatEvent( + event_type=EventType.AUTO_RETURN, + node=self._node, + action='驻防回港', + ) + ) + return ConditionFlag.FIGHT_END + def _handle_proceed(self) -> ConditionFlag: """处理继续前进 / 回港决策。 diff --git a/autowsgr/combat/recognizer.py b/autowsgr/combat/recognizer.py index e86e7c74..e19beb8a 100644 --- a/autowsgr/combat/recognizer.py +++ b/autowsgr/combat/recognizer.py @@ -160,6 +160,11 @@ def _get_event_map_title_templates() -> list[ImageTemplate]: default_timeout=5.0, after_match_delay=1.0, ), + CombatPhase.GARRISON: PhaseSignature( + template_key=TemplateKey.GARRISON, + default_timeout=3.0, # 非必现: 等待窗口短 + confidence=0.85, + ), CombatPhase.FLAGSHIP_SEVERE_DAMAGE: PhaseSignature( template_key=TemplateKey.FLAGSHIP_DAMAGE, default_timeout=7.5, diff --git a/autowsgr/combat/state.py b/autowsgr/combat/state.py index 2bf06abb..25da4078 100644 --- a/autowsgr/combat/state.py +++ b/autowsgr/combat/state.py @@ -63,6 +63,10 @@ class CombatPhase(Enum): GET_SHIP = auto() """获取舰船掉落。""" + # ── 驻防 ── + GARRISON = auto() + """驻防提示(结算结束出门前,掉落/无掉落均可能出现)。""" + # ── 旗舰大破 ── FLAGSHIP_SEVERE_DAMAGE = auto() """旗舰大破强制回港。""" @@ -155,7 +159,8 @@ def _build_map_transitions( ] # RESULT 之后 - after_result = [CombatPhase.PROCEED] + # 驻防 (非必现) 与继续前进是互斥分支: 出现驻防即强制回港, 不再有PROCEED、 + after_result = [CombatPhase.GARRISON, CombatPhase.PROCEED] if ep is not None: after_result.append(ep) after_result += [CombatPhase.GET_SHIP, CombatPhase.FLAGSHIP_SEVERE_DAMAGE] @@ -225,6 +230,9 @@ def _build_map_transitions( # GET_SHIP 后继 = 经验结算后继 去掉 GET_SHIP 自身 t[CombatPhase.GET_SHIP] = [p for p in after_result if p != CombatPhase.GET_SHIP] + # 取消驻防 = 强制回港 (与 PROCEED 点"回港"同语义), 后续只可能是结束页。 + t[CombatPhase.GARRISON] = [ep] if ep is not None else [] + if ep is not None: t[CombatPhase.FLAGSHIP_SEVERE_DAMAGE] = [ep] diff --git a/autowsgr/data/images/combat/garrison_540p.png b/autowsgr/data/images/combat/garrison_540p.png new file mode 100644 index 0000000000000000000000000000000000000000..2d372139195f03952e077ff7055e3decc0704e47 GIT binary patch literal 7098 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 literal 0 HcmV?d00001 diff --git a/autowsgr/image_resources/combat.py b/autowsgr/image_resources/combat.py index 5ef96617..c269c1da 100644 --- a/autowsgr/image_resources/combat.py +++ b/autowsgr/image_resources/combat.py @@ -52,6 +52,7 @@ class CombatTemplates: | RESULT_PAGE | combat/result_page_540p.png | | GET_SHIP | combat/get_ship_540p.png | | GET_ITEM | combat/get_item_540p.png | + | GARRISON | combat/garrison_540p.png | | END_MAP_PAGE | combat/end_map_page_540p.png | | END_BATTLE_PAGE | combat/end_battle_page_540p.png | | END_EXERCISE_PAGE | combat/end_exercise_page_540p.png | @@ -72,6 +73,7 @@ class CombatTemplates: RESULT_PAGE = LazyTemplate('combat/result_page_540p.png', 'result_page') GET_SHIP = LazyTemplate('combat/get_ship_540p.png', 'get_ship') GET_ITEM = LazyTemplate('combat/get_item_540p.png', 'get_item') + GARRISON = LazyTemplate('combat/garrison_540p.png', 'garrison') # ── 战斗终止态 ── END_MAP_PAGE = LazyTemplate('combat/end_map_page_540p.png', 'end_map_page') diff --git a/autowsgr/image_resources/keys.py b/autowsgr/image_resources/keys.py index 8935609a..84d3fe45 100644 --- a/autowsgr/image_resources/keys.py +++ b/autowsgr/image_resources/keys.py @@ -47,6 +47,7 @@ class TemplateKey(Enum): GET_SHIP = 'get_ship' GET_ITEM = 'get_item' GET_SHIP_OR_ITEM = 'get_ship_or_item' + GARRISON = 'garrison' # ── 战斗终止态 ── END_MAP_PAGE = 'end_map_page' @@ -99,6 +100,7 @@ def _build_map() -> dict[TemplateKey, list[ImageTemplate]]: TemplateKey.GET_SHIP: [T.GET_SHIP], TemplateKey.GET_ITEM: [T.GET_ITEM], TemplateKey.GET_SHIP_OR_ITEM: [T.GET_SHIP, T.GET_ITEM], + TemplateKey.GARRISON: [T.GARRISON], # 战斗终止态 TemplateKey.END_MAP_PAGE: [T.END_MAP_PAGE], TemplateKey.END_BATTLE_PAGE: [T.END_BATTLE_PAGE], diff --git a/testing/combat/test_combat.py b/testing/combat/test_combat.py index 46e423c2..71a19e7d 100644 --- a/testing/combat/test_combat.py +++ b/testing/combat/test_combat.py @@ -76,6 +76,15 @@ def test_normal_formation_no_branch(self): result = resolve_successors(normal, CombatPhase.FORMATION, '') assert CombatPhase.FIGHT_PERIOD in result + def test_map_after_result_includes_garrison(self): + """MAP 模式: 驻防 (非必现) 挂在结算共同后继 after_result 上, + 掉落/无掉落路径都可能弹出; 取消驻防 = 强制回港, 只到结束页。""" + normal = MODE_TRANSITIONS[CombatMode.NORMAL] + for phase in (CombatPhase.RESULT, CombatPhase.EXP_SETTLEMENT, CombatPhase.GET_SHIP): + result = resolve_successors(normal, phase, '') + assert CombatPhase.GARRISON in result, f'{phase.name} 后继应含 GARRISON' + assert resolve_successors(normal, CombatPhase.GARRISON, '') == [CombatPhase.MAP_PAGE] + def test_battle_transitions(self): battle = MODE_TRANSITIONS[CombatMode.BATTLE] # SINGLE 模式无 PROCEED,直接从 START_FIGHT 开始 diff --git a/testing/combat/test_result_dismiss_unit.py b/testing/combat/test_result_dismiss_unit.py index 1f17a78c..c2d6f2ca 100644 --- a/testing/combat/test_result_dismiss_unit.py +++ b/testing/combat/test_result_dismiss_unit.py @@ -21,6 +21,7 @@ from autowsgr.combat import handlers as handlers_mod from autowsgr.combat.handlers import PhaseHandlersMixin from autowsgr.combat.state import CombatPhase +from autowsgr.types import ConditionFlag class _Host(PhaseHandlersMixin): @@ -120,6 +121,7 @@ def test_event_result_includes_end_phase(self): CombatPhase.EVENT_MAP_PAGE, CombatPhase.GET_SHIP, CombatPhase.EXP_SETTLEMENT, + CombatPhase.GARRISON, } def test_fast_result_excludes_exp(self): @@ -130,6 +132,7 @@ def test_fast_result_excludes_exp(self): CombatPhase.FLAGSHIP_SEVERE_DAMAGE, CombatPhase.MAP_PAGE, CombatPhase.GET_SHIP, + CombatPhase.GARRISON, } def test_campaign_result_no_end_phase(self): @@ -140,6 +143,7 @@ def test_campaign_result_no_end_phase(self): CombatPhase.FLAGSHIP_SEVERE_DAMAGE, CombatPhase.GET_SHIP, CombatPhase.EXP_SETTLEMENT, + CombatPhase.GARRISON, } def test_exp_settlement_excludes_self(self): @@ -158,6 +162,43 @@ def test_get_ship_excludes_self_and_exp(self): assert CombatPhase.EXP_SETTLEMENT not in successors assert CombatPhase.MAP_PAGE in successors + def test_map_result_includes_garrison(self): + """落点集合含驻防 (非必现, 供复检识别弹出后交主循环)。 + + 驻防仅存在于 MAP 转移图, SINGLE 无法进入该状态; 落点集合无需按 + 模式区分, 统一含 GARRISON。 + """ + host, _, _ = _make_host([], end_phase=CombatPhase.MAP_PAGE, collect_result_info=True) + assert CombatPhase.GARRISON in host._result_successors(CombatPhase.RESULT) + + +class TestHandleGarrison: + """_handle_garrison — 点击取消即强制回港。""" + + def test_returns_fight_end(self): + """取消驻防 = 回港: 返回 FIGHT_END, 引擎立即结束战斗 (与 PROCEED 回港同语义)。""" + host, _device, _ = _make_host([], end_phase=CombatPhase.MAP_PAGE) + flag = host._handle_garrison() + assert flag == ConditionFlag.FIGHT_END + + def test_clicks_garrison_cancel_coordinate(self): + """点击驻防取消坐标 Coords.GARRISON_CANCEL (与 actions.click_garrison_cancel 一致)。""" + from autowsgr.combat.actions import Coords + + host, device, _ = _make_host([], end_phase=CombatPhase.MAP_PAGE) + host._handle_garrison() + device.click.assert_called_once_with(*Coords.GARRISON_CANCEL) + + def test_records_auto_return_event(self): + """记录 AUTO_RETURN 事件 (action='驻防回港')。""" + host, _, _ = _make_host([], end_phase=CombatPhase.MAP_PAGE) + host._handle_garrison() + host._history.add.assert_called_once() + event = host._history.add.call_args.args[0] + assert event.event_type.name == 'AUTO_RETURN' + assert event.action == '驻防回港' + assert event.node == 'A' + class TestHandleResultModes: """_handle_result 快速穿行 / 慢速采集双模式。"""