Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion autowsgr/combat/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,8 @@ def click_speed_up(device: AndroidController, *, battle_mode: bool = False) -> N
coords = Coords.SPEED_UP_BATTLE if battle_mode else Coords.SPEED_UP_NORMAL

# 由于加速过快可能导致卡顿, 增加极短延迟以确保点击生效
device.click(*coords, delay=False)
time.sleep(0.05)
device.click(*coords, delay=False)


def click_skip_missile_animation(device: AndroidController) -> None:
Expand Down
14 changes: 14 additions & 0 deletions autowsgr/combat/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,21 @@ def is_selected_node(self, node: str) -> bool:

@classmethod
def from_yaml(cls, path: str | Path) -> CombatPlan:
from autowsgr.infra.config_compat import (
LegacyConfigError,
detect_legacy_plan,
)

data = load_yaml(path)
legacy = detect_legacy_plan(data)
if legacy:
raise LegacyConfigError(
f'计划文件 {path} 含 classic 老版本格式, 拒绝加载:\n - '
+ '\n - '.join(legacy)
+ '\n请先运行迁移工具迁移计划目录 (原文件不动):\n'
' python tools/migrate_config.py --planroot <计划目录>'
' (默认输出到 migrated_config/)',
)
return cls.from_dict(data, name=Path(path).stem)

@classmethod
Expand Down
88 changes: 88 additions & 0 deletions autowsgr/context/bathroom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""浴室修理槽位状态 — 跟踪各修理槽的释放时间。

移植自 classic ``port/common.py::BathRoom`` 的状态机, 但**不移植其逐槽像素
扫描** (classic 在 ``task_runner.py`` 用硬编码坐标 + 「快速修理」OCR 文字
逐个点开槽位读取剩余时间)。dev 浴室页面用像素签名模型, 坐标与判定方式
不同, 逐槽扫描无法直接复用且无法离线验证。

替代方案 (等价空位调度)
-----------------------
``available_time`` 仅跟踪**本脚本派出的修理**: :meth:`BathRoom.occupy` 在
成功派修后记录该槽释放时间。配合 ``BathPage`` 选择修理 overlay 的点击反馈
(``_try_wait_overlay_close``: overlay 关闭 = 修成功 / 未关 = 浴场满) 实现:

- 有空闲槽才派修 (:meth:`is_available`);
- 跟踪已派修理的进度 (释放时间戳自然到期 = 槽回收);
- 浴场满则 :meth:`mark_unknown` 退避, 下次触发重试。

启动时状态未知 (``None``), 首次触发尝试派修即可恢复。纯内存不持久化
(与 classic 一致)。
"""

from __future__ import annotations

import time
from dataclasses import dataclass


@dataclass
class BathRoom:
"""浴室修理槽位状态机。

Attributes
----------
available_time:
每个修理槽的绝对释放时间戳 (``time.time()`` 基); ``None`` 表示状态
未知 (需尝试派修以重建)。元素值 ``<= now`` 表示该槽空闲。
slot_count:
修理槽总数 (来自 ``config.bathroom_count``), 用于初始化槽位数。
"""

available_time: list[float] | None = None
slot_count: int = 2

def _ensure_initialized(self) -> None:
"""首次使用时按 slot_count 初始化为全空闲。"""
if self.available_time is None:
self.available_time = [0.0] * max(1, self.slot_count)

def is_available(self) -> bool:
"""是否有空闲槽位。状态未知时返回 True (允许尝试派修)。"""
if self.available_time is None:
return True
return self.get_waiting_time() == 0.0

def get_waiting_time(self) -> float:
"""最近一个槽释放还需等待的秒数; ``0.0`` 表示已有空位。

状态未知 (``None``) 返回 ``0.0``。
"""
if self.available_time is None:
return 0.0
now = time.time()
waiting = float('inf')
for release in self.available_time:
if now >= release:
return 0.0
waiting = min(waiting, release - now)
return waiting

def occupy(self, repair_seconds: int) -> None:
"""占用一个空闲槽: 记录其释放时间 = ``now + repair_seconds``。

若状态未初始化会先初始化; 若无空闲槽则静默忽略 (调用方应先判
:meth:`is_available`)。
"""
self._ensure_initialized()
assert self.available_time is not None
if repair_seconds <= 0:
return
now = time.time()
for i, release in enumerate(self.available_time):
if now >= release:
self.available_time[i] = now + repair_seconds
return

def mark_unknown(self) -> None:
"""标记状态未知, 强制下次触发重新尝试派修。"""
self.available_time = None
43 changes: 43 additions & 0 deletions autowsgr/context/game_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
from __future__ import annotations

import threading
import time
from dataclasses import dataclass, field
from typing import TYPE_CHECKING

from autowsgr.infra.logger import get_logger
from autowsgr.types import ShipDamageState

from .bathroom import BathRoom
from .build import BuildQueue
from .expedition import ExpeditionQueue
from .fleet import Fleet
Expand Down Expand Up @@ -79,6 +81,8 @@ class GameContext:
"""远征队列。"""
build_queue: BuildQueue = field(default_factory=BuildQueue)
"""建造队列。"""
bathroom: BathRoom = field(default_factory=BathRoom)
"""浴室修理槽位状态 (空位调度用)。"""
ship_registry: dict[str, Ship] = field(default_factory=dict)
"""舰船注册表, 以名称为键。"""
current_page: PageName | None = None
Expand Down Expand Up @@ -212,3 +216,42 @@ def sync_after_combat(
fleet_id,
[s.damage_state.name for s in fleet.ships],
)

def sync_daily_drop_counts(self) -> None:
"""导航到出征面板, 识别今日已获取舰船/战利品数并同步每日计数器。

``dropped_ship_count`` / ``dropped_loot_count`` 初始为 0, 仅靠战斗后
同步不可靠 (战斗未必掉落舰船/战利品), 故调度层在 ``run_daily`` 启动时
调本方法校准真实值, 避免依赖计数器的停止条件 (``stop_max_ship`` /
``stop_max_loot``) 首次误判而触发一场多余的常规战。读取后返回主页面。

Raises
------
RuntimeError
OCR 引擎不可用, 无法识别掉落数量。调用方应据此**禁用**依赖计数器
的触发器 (常规战) 并提示用户, 而非降级 —— 降级会退化为"靠首场
战斗自行校准", 但战斗未必掉落, 计数器可能一直为 0 → 持续误触发。
"""
from autowsgr.ops import goto_page
from autowsgr.types import PageName
from autowsgr.ui import MapPage, MapPanel

goto_page(self, PageName.MAP)
map_page = MapPage(self)
map_page.ensure_panel(MapPanel.SORTIE)
time.sleep(0.25)
# OCR 引擎不可用时 get_loot_and_ship_count 抛 RuntimeError — 不吞, 上抛
counts = map_page.get_loot_and_ship_count()
if counts.ship is not None:
self.dropped_ship_count = counts.ship
if counts.loot is not None:
self.dropped_loot_count = counts.loot
_log.info(
'[Context] 每日掉落计数同步: 舰船 {}, 战利品 {}',
counts.ship,
counts.loot,
)
try:
goto_page(self, PageName.MAIN)
except Exception:
_log.warning('[Context] 每日掉落计数同步后返回主页面失败')
Loading
Loading