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 00000000..2d372139 Binary files /dev/null and b/autowsgr/data/images/combat/garrison_540p.png differ 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 快速穿行 / 慢速采集双模式。"""