Add Ghostbox hardware and logistics automation

This commit is contained in:
王鹏
2026-05-07 15:19:59 +08:00
parent bf26de3244
commit a48ed597b4
14 changed files with 1287 additions and 323 deletions

View File

@@ -20,14 +20,20 @@ class LogisticsManager:
self.route_file = route_file or VENDOR_FILE
self.bag_full_hearthstone = False # 包满时用炉石回城而非走路修理
self.hearthstone_key = "b" # 炉石按键
self.hearthstone_cast_sec = 10.0 # 炉石施法等待秒数
self.hearthstone_cast_sec = 12.0 # 炉石施法等待秒数
self.enable_bag_full_mail = False
self.logistics_full_auto = False
self.bag_slot_threshold = 2
self.durability_threshold = 0.2
self.mailbox_route_file = os.path.join("recorder", "mailbox.json")
self.mailbox_interact_key = "8"
self.mail_recipient_key = ""
self.mail_send_key = "f8"
self.mailbox_open_wait_sec = 2.0
self.mail_send_wait_sec = 60.0
self.repair_target_key = "8"
self.repair_interact_key = "4"
self.repair_interact_wait_sec = 2.0
def _resolve_path(self, path):
if not path:
@@ -47,14 +53,60 @@ class LogisticsManager:
return False
time.sleep(min(0.1, end_at - time.time()))
return True
def _read_position(self, get_state):
if not callable(get_state):
return None
st = get_state()
if not st:
return None
try:
x = st.get("x")
y = st.get("y")
if x is None or y is None:
return None
return float(x), float(y)
except Exception:
return None
def _state_bag_full(self, state):
try:
return int(state.get("free_slots", 0) or 0) < int(self.bag_slot_threshold)
except Exception:
return False
def _state_durability_low(self, state):
try:
threshold = float(self.durability_threshold)
if threshold > 1.0:
threshold = threshold / 100.0
durability = state.get("durability", 1.0)
if durability is None:
return False
return float(durability) < threshold
except Exception:
return False
def get_pending_tasks(self, state):
tasks = []
bag_full = self._state_bag_full(state)
durability_low = self._state_durability_low(state)
if bag_full and self.enable_bag_full_mail:
tasks.append("mail")
if durability_low:
tasks.append("repair")
if not tasks and bag_full and self.bag_full_hearthstone:
tasks.append("hearthstone_stop")
return tasks
def check_logistics(self, state):
"""
state['free_slots']: 剩余空格数量
state['durability']: 0.0 ~ 1.0 的耐久度
"""
# 触发阈值:空格少于 2 个,或耐久度低于 20%
if state['free_slots'] < 2 or state['durability'] < 0.2:
# 触发阈值:空格少于配置值,或耐久度低于配置值。
if self.get_pending_tasks(state):
if not self.is_returning:
print(f">>> [后勤警告] 背包/耐久不足!触发回城程序。")
self.is_returning = True
@@ -67,11 +119,7 @@ class LogisticsManager:
success = False
for i in range(max_retries):
start_pos = None
if get_state:
st = get_state()
if st:
start_pos = (st.get('x'), st.get('y'))
start_pos = self._read_position(get_state)
print(f">>> [后勤] 第 {i+1} 次尝试使用炉石(按键: {self.hearthstone_key}...")
# 先按一下 S 确保停止移动,防止移动中按炉石失败
@@ -80,25 +128,34 @@ class LogisticsManager:
hw_ctrl.press(self.hearthstone_key)
# 等待施法过程
print(f">>> [后勤] 正在等待施法 {self.hearthstone_cast_sec}s...")
time.sleep(self.hearthstone_cast_sec + 2.0) # 多等 2 秒保险
print(f">>> [后勤] 正在等待炉石 {self.hearthstone_cast_sec}s...")
time.sleep(float(self.hearthstone_cast_sec))
if get_state and start_pos and start_pos[0] is not None:
st_now = get_state()
if st_now:
end_pos = (st_now.get('x'), st_now.get('y'))
if get_state and start_pos is not None:
for _ in range(5):
end_pos = self._read_position(get_state)
if end_pos is None:
time.sleep(0.5)
continue
dist = math.dist(start_pos, end_pos)
# 如果坐标发生了明显跳变(大于 2.0),证明回城成功
if dist > 2.0:
print(f">>> [后勤] 炉石回城成功!位置跳变距离: {dist:.2f}")
success = True
break
else:
print(f">>> [后勤] 炉石似乎失败(位置未变化),准备重试...")
else:
time.sleep(0.5)
if success:
break
print(f">>> [后勤] 炉石似乎失败(位置未变化),准备重试...")
elif get_state:
st_now = get_state()
if st_now is None:
# 获取不到状态可能已经卡死或窗口关闭,默认成功以退出循环
success = True
break
else:
success = True
break
else:
# 如果没法校验坐标,就只执行一次
success = True
@@ -130,13 +187,13 @@ class LogisticsManager:
self.is_returning = False
def _do_vendor_interact(self):
"""执行与修理商/背包的交互按键8、4"""
hw_ctrl.press("8")
"""执行与修理商/背包的交互按键。"""
hw_ctrl.press(self.repair_target_key)
time.sleep(0.5)
hw_ctrl.press("4")
time.sleep(2)
hw_ctrl.press(self.repair_interact_key)
time.sleep(float(self.repair_interact_wait_sec))
def run_bag_full_mail_flow(self, get_state, patrol, stop_check=None):
def run_bag_full_mail_flow(self, get_state, patrol, stop_check=None, use_hearthstone=True):
"""
包满邮寄第一版流程:
炉石 -> 跑到邮箱路线终点 -> 交互打开邮箱 -> 按 MailboxCourier 宏键 -> 等待后停止。
@@ -152,11 +209,12 @@ class LogisticsManager:
self.is_returning = False
return False
ok = self.use_hearthstone_and_stop(get_state=get_state)
if not ok:
print(">>> [后勤-邮箱] 炉石失败,未继续跑邮箱。")
self.is_returning = False
return False
if use_hearthstone:
ok = self.use_hearthstone_and_stop(get_state=get_state)
if not ok:
print(">>> [后勤-邮箱] 炉石失败,未继续跑邮箱。")
self.is_returning = False
return False
try:
with open(route_file, "r", encoding="utf-8") as f:
@@ -176,7 +234,13 @@ class LogisticsManager:
if old_enable_mount is not None:
patrol.enable_mount = False
try:
ok = patrol.navigate_path(get_state, path, forward=True, arrival_threshold=VENDOR_ARRIVAL_THRESHOLD)
ok = patrol.navigate_path(
get_state,
path,
forward=True,
arrival_threshold=VENDOR_ARRIVAL_THRESHOLD,
snap_to_nearest=True,
)
finally:
if old_enable_mount is not None:
patrol.enable_mount = old_enable_mount
@@ -213,6 +277,119 @@ class LogisticsManager:
self.is_returning = False
return True
def run_repair_flow(self, get_state, patrol, stop_check=None, use_hearthstone=True):
"""
耐久低修理流程:
炉石 -> 从修理路线最近点接入 -> 到达 NPC -> 按目标键 -> 按交互/修理键。
"""
route_file = self._resolve_path(self.route_file)
if not route_file or not os.path.exists(route_file):
print(f">>> [后勤-修理] 修理路线不存在,已停止: {route_file}")
self.is_returning = False
return False
if callable(stop_check) and stop_check():
self.is_returning = False
return False
if use_hearthstone:
ok = self.use_hearthstone_and_stop(get_state=get_state)
if not ok:
print(">>> [后勤-修理] 炉石失败,未继续跑修理路线。")
self.is_returning = False
return False
try:
with open(route_file, "r", encoding="utf-8") as f:
path = json.load(f)
except Exception as exc:
print(f">>> [后勤-修理] 修理路线读取失败: {exc}")
self.is_returning = False
return False
if not path:
print(f">>> [后勤-修理] 修理路线为空,已停止: {route_file}")
self.is_returning = False
return False
print(f">>> [后勤-修理] 开始跑修理路线: {route_file}")
ok = patrol.navigate_path(
get_state,
path,
forward=True,
arrival_threshold=VENDOR_ARRIVAL_THRESHOLD,
snap_to_nearest=True,
)
if not ok:
print(">>> [后勤-修理] 修理路线未完成,已停止。")
self.is_returning = False
return False
if callable(stop_check) and stop_check():
self.is_returning = False
return False
patrol.stop_all()
print(f">>> [后勤-修理] 到达 NPC按目标键: {self.repair_target_key}")
hw_ctrl.press(self.repair_target_key)
if not self._sleep_with_stop(0.5, stop_check=stop_check):
self.is_returning = False
return False
print(f">>> [后勤-修理] 按修理交互键: {self.repair_interact_key}")
hw_ctrl.press(self.repair_interact_key)
if not self._sleep_with_stop(self.repair_interact_wait_sec, stop_check=stop_check):
self.is_returning = False
return False
print(">>> [后勤-修理] 修理流程结束。")
self.is_returning = False
return True
def run_pending_tasks(self, tasks, get_state, patrol, stop_check=None):
completed = []
already_homed = False
for task in tasks:
if callable(stop_check) and stop_check():
self.is_returning = False
return False, completed
if task == "mail":
ok = self.run_bag_full_mail_flow(
get_state,
patrol,
stop_check=stop_check,
use_hearthstone=not already_homed,
)
if not ok:
return False, completed
already_homed = True
completed.append(task)
continue
if task == "repair":
ok = self.run_repair_flow(
get_state,
patrol,
stop_check=stop_check,
use_hearthstone=not already_homed,
)
if not ok:
return False, completed
already_homed = True
completed.append(task)
continue
if task == "hearthstone_stop":
ok = self.use_hearthstone_and_stop(get_state=get_state)
if not ok:
return False, completed
already_homed = True
completed.append(task)
self.is_returning = False
return True, completed
def run_route1_round(self, get_state, patrol, route_file=None):
"""
读取 route1.json 路径先正向走完执行交互8、4再反向走完然后结束。