🔥 新作首發 🎮 天堂私服 YOLOv8 物件偵測實戰 — 從資料蒐集、模型訓練到即時偵測 立即閱讀 →
熱門系列
Like Share Discussion Bookmark Smile

J.J. Huang   2026-04-20   Python OpenCV 08.專案實作篇   瀏覽次數:次   DMCA.com Protection Status

Python | OpenCV 專案:天堂私服遊戲輔助(二)自動補藥

⚠️ 免責聲明

本文章內容僅供學術研究與電腦視覺技術學習之用途,所有程式碼與技術說明均以教育目的為出發點。

  • 本文作者不提供任何形式的輔助程式販售、散佈或商業服務
  • 本文所有範例程式僅限在自行架設的私有伺服器環境中測試,不得用於任何正式營運的線上遊戲伺服器。
  • 使用遊戲輔助程式可能違反個別遊戲的使用者條款,並導致帳號封鎖、法律責任等後果,讀者須自行承擔一切相關風險與責任
  • 本文技術內容若被用於任何違法或損害他人利益之行為,作者概不負責
  • 私有伺服器的架設與使用涉及遊戲著作權相關法律問題,讀者應自行評估所在地區的法規,並確認於合法範圍內使用。

📚 前言

在上一篇 (一)主程式 GUI 框架與 HP/MP 血量監控 中,我們建立了 tkinter 骨架,可以即時顯示 HP/MP 比例。

本篇加入 自動補藥 功能:當 HP 或 MP 低於設定閾值時,自動模擬按下補藥熱鍵,並新增設定面板讓使用者在 UI 上調整參數。

系列總覽:

篇次 主題
主程式 GUI 框架與 HP/MP 血量監控
本篇(二) 自動補藥
YOLOv8 怪物偵測整合
目標優先序選擇
自動撿取道具

🎯 本篇目標

  • 建立 auto_potion.py,封裝補藥邏輯與冷卻時間
  • PostMessageWM_KEYDOWN / WM_KEYUP 直接送到目標視窗的 hwnd,背景也能按鍵(不搶焦點)
  • 在 UI 新增:啟用開關、HP/MP 閾值滑桿、F5~F12 熱鍵下拉選單
  • Worker Thread 1 整合補藥邏輯,main.py 整體更新

🗂️ 本篇新增檔案

1
2
3
4
5
6
lineage_assistant/
├── main.py ← 更新(加入補藥設定面板 + 傳入 AutoPotion)
├── window_capture.py ← 不變
├── hp_monitor.py ← 不變
├── auto_potion.py ← 新增(本篇重點)
└── config.json

本篇沿用第一篇已安裝的 pywin32,不需要額外套件。

💻 自動補藥模組:auto_potion.py

設計重點:

  • check(hp, mp) 由 Worker Thread 1 呼叫,不能操作 tkinter 元件
  • 使用 PostMessageWM_KEYDOWN / WM_KEYUP 送到目標 hwnd,視窗可在背景;不像 pyautogui.press() 只會送到有焦點的視窗
  • 使用 log_fn callback 把訊息傳回主執行緒的 log_q
  • cooldown 防止連按,避免瞬間消耗大量藥水
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# auto_potion.py
import time
from typing import Callable, Optional

import win32api
import win32con


VK_MAP = {
"f1": win32con.VK_F1, "f2": win32con.VK_F2,
"f3": win32con.VK_F3, "f4": win32con.VK_F4,
"f5": win32con.VK_F5, "f6": win32con.VK_F6,
"f7": win32con.VK_F7, "f8": win32con.VK_F8,
"f9": win32con.VK_F9, "f10": win32con.VK_F10,
"f11": win32con.VK_F11, "f12": win32con.VK_F12,
}


def post_key(hwnd: int, key: str):
"""把 WM_KEYDOWN/UP PostMessage 到目標 hwnd,不需視窗在前景"""
vk = VK_MAP.get(key.lower())
if vk is None or not hwnd:
return
scan = win32api.MapVirtualKey(vk, 0)
lparam_down = (scan << 16) | 1
lparam_up = (scan << 16) | (1 | (1 << 30) | (1 << 31))
win32api.PostMessage(hwnd, win32con.WM_KEYDOWN, vk, lparam_down)
win32api.PostMessage(hwnd, win32con.WM_KEYUP, vk, lparam_up)


class AutoPotion:
def __init__(self,
hwnd: int,
hp_threshold: float = 0.6,
mp_threshold: float = 0.4,
hp_key: str = "f5",
mp_key: str = "f6",
cooldown: float = 1.5,
enabled: bool = True,
log_fn: Optional[Callable] = None):
self.hwnd = hwnd
self.hp_threshold = hp_threshold
self.mp_threshold = mp_threshold
self.hp_key = hp_key
self.mp_key = mp_key
self.cooldown = cooldown
self.enabled = enabled # 可由主執行緒即時切換
self._log = log_fn or (lambda msg: None)
self._last_hp_t = 0.0
self._last_mp_t = 0.0

def check(self, hp: float, mp: float):
"""根據當前 HP/MP 決定是否補藥"""
if not self.enabled:
return
now = time.time()
if hp < self.hp_threshold and (now - self._last_hp_t) >= self.cooldown:
post_key(self.hwnd, self.hp_key)
self._last_hp_t = now
self._log(
f"HP 補藥({hp*100:.0f}% < {self.hp_threshold*100:.0f}%)"
f",按 [{self.hp_key.upper()}]"
)
if mp < self.mp_threshold and (now - self._last_mp_t) >= self.cooldown:
post_key(self.hwnd, self.mp_key)
self._last_mp_t = now
self._log(
f"MP 補藥({mp*100:.0f}% < {self.mp_threshold*100:.0f}%)"
f",按 [{self.mp_key.upper()}]"
)

💻 主程式:main.py(第二篇版本)

相較第一篇,主要變更:

  1. 匯入 AutoPotionmonitor_loop 加入 potion 參數,每次偵測後呼叫 potion.check()
  2. UI 新增「自動補藥設定」面板(開關、HP/MP 滑桿、F5~F12 熱鍵下拉選單)
  3. _start() 讀取面板設定後建立 AutoPotion,連同 monitor 一起傳進執行緒
  4. checkbox 用 command callback、滑桿與下拉選單由 _poll 每 200ms 推進 AutoPotion,面板調整皆即時生效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
# main.py(第二篇版本)
import json
import queue
import threading
import time
import tkinter as tk
from tkinter import ttk, scrolledtext
from typing import Optional

import cv2
import win32api

from window_capture import capture_window, window_from_point
from hp_monitor import HPMonitor, CONFIG_FILE
from auto_potion import AutoPotion

VK_LBUTTON = 0x01
VK_ESCAPE = 0x1B

POTION_KEYS = ["F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"]


# ── Shared State ───────────────────────────────────────────────
class SharedState:
"""執行緒安全的共享資料容器"""
def __init__(self):
self._lock = threading.Lock()
self.hp = 1.0
self.mp = 1.0
self.hwnd = None
self.running = False

def update(self, **kwargs):
with self._lock:
for k, v in kwargs.items():
setattr(self, k, v)

def get(self, *keys):
with self._lock:
if len(keys) == 1:
return getattr(self, keys[0])
return tuple(getattr(self, k) for k in keys)


# ── Worker Thread 1:監控 + 補藥 ───────────────────────────────
def monitor_loop(state: SharedState, monitor: HPMonitor, log_q: queue.Queue,
potion: Optional[AutoPotion] = None):
"""以 try/except 包住整個迴圈,單次擷取或 PostMessage 失敗不會讓執行緒靜默死掉"""
hwnd = state.get("hwnd")
log_q.put(f"監控執行緒啟動,hwnd={hwnd}")

try:
while state.get("running"):
try:
frame = capture_window(hwnd)
if frame is None:
time.sleep(0.2)
continue
hp, mp = monitor.read(frame)
state.update(hp=hp, mp=mp)
if potion is not None:
potion.check(hp, mp)
time.sleep(0.2)
except Exception as e:
# 視窗瞬間被關閉 / 切桌面 / GDI 失敗 時吞例外,下一輪再試
log_q.put(f"⚠️ 監控迴圈錯誤:{type(e).__name__}: {e}")
time.sleep(0.5)
finally:
state.update(running=False)
log_q.put("監控執行緒結束")


# ── tkinter 主視窗 ─────────────────────────────────────────────
class LineageAssistant:
BG = "#1a1a2e"
FG = "#e0e0e0"
RED = "#e94560"
BLUE = "#4fc3f7"
GREEN = "#00ff99"
AMBER = "#f5a623"
AMBER_HOVER = "#ffc857"
MUTED = "#555577"

def __init__(self):
self.state = SharedState()
self.monitor = HPMonitor()
self.log_q = queue.Queue()
self.t_monitor = None
self.potion = None # _start 時建立、_on_potion_toggle 即時調整
self._build_ui()
self._poll()
self.root.mainloop()

def _make_icon_btn(self, parent, text, cmd, font):
btn = tk.Button(
parent, text=text,
bg=self.BG, fg=self.AMBER,
activebackground=self.BG, activeforeground=self.AMBER_HOVER,
disabledforeground=self.MUTED,
font=font, relief="flat", bd=0, cursor="hand2",
command=cmd)

def on_enter(_):
if btn["state"] != "disabled":
btn.config(fg=self.AMBER_HOVER)

def on_leave(_):
if btn["state"] != "disabled":
btn.config(fg=self.AMBER)

btn.bind("<Enter>", on_enter)
btn.bind("<Leave>", on_leave)
return btn

def _build_ui(self):
self.root = tk.Tk()
self.root.title("天堂私服輔助 v0.2")
self.root.configure(bg=self.BG)
self.root.resizable(False, False)

# ── 標題 ──
tk.Label(self.root, text="⚔ 天堂私服輔助",
bg=self.BG, fg=self.FG,
font=("Consolas", 16, "bold")).pack(pady=10)

# ── 工具列 ──
toolbar = tk.Frame(self.root, bg=self.BG)
toolbar.pack(padx=20, pady=(0, 4), fill="x")

self.btn_pick = self._make_icon_btn(
toolbar, "🎯 選取視窗", self._pick_window,
font=("Consolas", 11, "bold"))
self.btn_pick.pack(side="left")

self.btn_calib = self._make_icon_btn(
toolbar, "⚙ 校準血條", self._calibrate,
font=("Consolas", 11, "bold"))
self.btn_calib.pack(side="right")

# ── 目標視窗資訊 ──
self.target_var = tk.StringVar(value="目標視窗:未選擇")
tk.Label(self.root, textvariable=self.target_var,
bg=self.BG, fg=self.FG,
font=("Consolas", 10), anchor="w"
).pack(padx=20, pady=(0, 6), fill="x")

# ── HP / MP 血條顯示 ──
bar_frame = tk.Frame(self.root, bg=self.BG)
bar_frame.pack(padx=20, fill="x")

style = ttk.Style()
style.theme_use("default")
style.configure("Red.Horizontal.TProgressbar",
troughcolor="#330000", background="#e94560")
style.configure("Blue.Horizontal.TProgressbar",
troughcolor="#001133", background="#4fc3f7")

tk.Label(bar_frame, text="HP", bg=self.BG, fg=self.RED,
font=("Consolas", 11, "bold"), width=4
).grid(row=0, column=0, sticky="w")
self.hp_var = tk.DoubleVar(value=1.0)
ttk.Progressbar(bar_frame, variable=self.hp_var, maximum=1.0,
length=300, style="Red.Horizontal.TProgressbar"
).grid(row=0, column=1, padx=5, pady=4)
self.hp_lbl = tk.Label(bar_frame, text="100%",
bg=self.BG, fg=self.FG,
font=("Consolas", 10), width=6)
self.hp_lbl.grid(row=0, column=2)

tk.Label(bar_frame, text="MP", bg=self.BG, fg=self.BLUE,
font=("Consolas", 11, "bold"), width=4
).grid(row=1, column=0, sticky="w")
self.mp_var = tk.DoubleVar(value=1.0)
ttk.Progressbar(bar_frame, variable=self.mp_var, maximum=1.0,
length=300, style="Blue.Horizontal.TProgressbar"
).grid(row=1, column=1, padx=5, pady=4)
self.mp_lbl = tk.Label(bar_frame, text="100%",
bg=self.BG, fg=self.FG,
font=("Consolas", 10), width=6)
self.mp_lbl.grid(row=1, column=2)

# ── 自動補藥設定面板 ──
pot_frame = tk.LabelFrame(self.root, text=" 自動補藥設定 ",
bg=self.BG, fg="#aaaaaa",
font=("Consolas", 9))
pot_frame.pack(padx=20, pady=6, fill="x")

self.potion_enabled = tk.BooleanVar(value=True)
tk.Checkbutton(pot_frame, text="啟用自動補藥",
variable=self.potion_enabled,
command=self._on_potion_toggle,
bg=self.BG, fg=self.FG,
selectcolor=self.BG,
font=("Consolas", 9)
).grid(row=0, column=0, columnspan=5,
sticky="w", padx=6, pady=2)

tk.Label(pot_frame, text="HP 觸發 %", bg=self.BG, fg=self.FG,
font=("Consolas", 9)).grid(row=1, column=0, sticky="w", padx=6)
self.hp_thresh = tk.IntVar(value=60)
ttk.Scale(pot_frame, from_=10, to=90, variable=self.hp_thresh,
orient="horizontal", length=130
).grid(row=1, column=1, padx=4)
self.hp_thresh_lbl = tk.Label(pot_frame, text="60%",
bg=self.BG, fg=self.RED,
font=("Consolas", 9), width=5)
self.hp_thresh_lbl.grid(row=1, column=2)
tk.Label(pot_frame, text="鍵", bg=self.BG, fg=self.FG,
font=("Consolas", 9)).grid(row=1, column=3, padx=2)
self.hp_key_var = tk.StringVar(value="F5")
ttk.Combobox(pot_frame, textvariable=self.hp_key_var,
values=POTION_KEYS, state="readonly",
width=4, font=("Consolas", 10)
).grid(row=1, column=4, padx=4)

tk.Label(pot_frame, text="MP 觸發 %", bg=self.BG, fg=self.FG,
font=("Consolas", 9)).grid(row=2, column=0, sticky="w", padx=6)
self.mp_thresh = tk.IntVar(value=40)
ttk.Scale(pot_frame, from_=10, to=90, variable=self.mp_thresh,
orient="horizontal", length=130
).grid(row=2, column=1, padx=4)
self.mp_thresh_lbl = tk.Label(pot_frame, text="40%",
bg=self.BG, fg=self.BLUE,
font=("Consolas", 9), width=5)
self.mp_thresh_lbl.grid(row=2, column=2)
tk.Label(pot_frame, text="鍵", bg=self.BG, fg=self.FG,
font=("Consolas", 9)).grid(row=2, column=3, padx=2)
self.mp_key_var = tk.StringVar(value="F6")
ttk.Combobox(pot_frame, textvariable=self.mp_key_var,
values=POTION_KEYS, state="readonly",
width=4, font=("Consolas", 10)
).grid(row=2, column=4, padx=4)

# ── 控制按鈕:單顆 ▶ / ■ toggle ──
btn_frame = tk.Frame(self.root, bg=self.BG)
btn_frame.pack(pady=10)

self.btn_toggle = self._make_icon_btn(
btn_frame, "▶ 啟動", self._toggle,
font=("Consolas", 13, "bold"))
self.btn_toggle.pack()

# ── 狀態列 ──
self.status_var = tk.StringVar(value="就緒")
tk.Label(self.root, textvariable=self.status_var,
bg=self.BG, fg="#888888",
font=("Consolas", 9)).pack()

# ── 日誌視窗 ──
self.log_box = scrolledtext.ScrolledText(
self.root, width=52, height=10, state="disabled",
bg="#0d0d1a", fg=self.GREEN,
font=("Consolas", 9), insertbackground="white")
self.log_box.pack(padx=10, pady=(5, 10))

def _log(self, msg: str):
self.log_q.put(msg)

def _flush_log(self):
while not self.log_q.empty():
msg = self.log_q.get_nowait()
ts = time.strftime("%H:%M:%S")
self.log_box.config(state="normal")
self.log_box.insert("end", f"{ts} {msg}\n")
self.log_box.see("end")
self.log_box.config(state="disabled")

# ── 🎯 滑鼠點擊選窗 ───────────────────────────────────────
def _pick_window(self):
if self.state.get("running"):
self._log("請先停止監控再重選目標視窗")
return
self._log("🎯 請點擊目標遊戲視窗(ESC 可取消)")
for btn in (self.btn_pick, self.btn_calib, self.btn_toggle):
btn.config(state="disabled")
self.root.config(cursor="crosshair")
self._poll_pick()

def _poll_pick(self):
if win32api.GetAsyncKeyState(VK_ESCAPE) & 0x8000:
self._reset_pick()
self._log("已取消選擇視窗")
return
if win32api.GetAsyncKeyState(VK_LBUTTON) & 0x8000:
x, y = win32api.GetCursorPos()
hwnd, title, pid = window_from_point(x, y)
self._reset_pick()
if hwnd:
self.state.update(hwnd=hwnd)
label = title or "<無標題>"
short = label if len(label) <= 30 else label[:28] + "…"
self.target_var.set(f"目標:{short} (pid={pid})")
self._log(f"已鎖定:{label} hwnd={hwnd} pid={pid}")
else:
self._log("未取得有效視窗,請再試一次")
return
self.root.after(30, self._poll_pick)

def _reset_pick(self):
self.root.config(cursor="")
for btn in (self.btn_pick, self.btn_calib, self.btn_toggle):
btn.config(state="normal")

# ── ⚙ ROI 校準 ───────────────────────────────────────
def _calibrate(self):
if self.state.get("running"):
self._log("請先停止監控再校準")
return
hwnd = self.state.get("hwnd")
if not hwnd:
self._log("請先按「🎯 選取視窗」選取目標視窗")
return

frame = capture_window(hwnd)
if frame is None:
self._log("擷取失敗,視窗可能被最小化")
return

self._log("框選 HP 血條(紅色),Enter 確認")
hp = cv2.selectROI("校準 HP 血條(Enter 確認 / C 重選)",
frame, fromCenter=False, showCrosshair=True)
cv2.destroyAllWindows()

self._log("框選 MP 魔力條(藍色),Enter 確認")
mp = cv2.selectROI("校準 MP 魔力條(Enter 確認 / C 重選)",
frame, fromCenter=False, showCrosshair=True)
cv2.destroyAllWindows()

if hp[2] == 0 or mp[2] == 0:
self._log("校準已取消(未框選有效範圍)")
return

self.monitor.hp_roi = tuple(hp)
self.monitor.mp_roi = tuple(mp)
self._save_config()
self._log(f"✅ 校準完成:HP={hp}, MP={mp}")

def _save_config(self):
cfg = {
"hp_roi": list(self.monitor.hp_roi),
"mp_roi": list(self.monitor.mp_roi),
}
with open(CONFIG_FILE, "w") as f:
json.dump(cfg, f, indent=2)
self._log(f"已寫入 {CONFIG_FILE}")

# ── ▶ / ■ 切換 ──
def _toggle(self):
if self.state.get("running"):
self._stop()
else:
self._start()

def _on_potion_toggle(self):
"""checkbox 即時生效:直接切換 AutoPotion.enabled,不需停止/重啟"""
if self.potion is not None:
self.potion.enabled = self.potion_enabled.get()
self._log(f"自動補藥:{'啟用' if self.potion_enabled.get() else '停用'}")

def _start(self):
if not self.state.get("hwnd"):
self._log("請先按「🎯 選取視窗」選取目標視窗")
return

# 讀取面板設定,建立 AutoPotion(一律建立,enabled 控制是否真的按鍵)
self.potion = AutoPotion(
hwnd = self.state.get("hwnd"),
hp_threshold = self.hp_thresh.get() / 100,
mp_threshold = self.mp_thresh.get() / 100,
hp_key = self.hp_key_var.get().lower(),
mp_key = self.mp_key_var.get().lower(),
cooldown = 1.5,
enabled = self.potion_enabled.get(),
log_fn = self._log,
)
self._log(
f"補藥設定:HP<{self.hp_thresh.get()}% 按{self.hp_key_var.get()},"
f"MP<{self.mp_thresh.get()}% 按{self.mp_key_var.get()}"
f"({'啟用' if self.potion_enabled.get() else '停用'})"
)

self.state.update(running=True)
self.t_monitor = threading.Thread(
target=monitor_loop,
args=(self.state, self.monitor, self.log_q, self.potion),
daemon=True)
self.t_monitor.start()
self.btn_toggle.config(text="■ 停止")
self.status_var.set("運行中...")

def _stop(self):
self.state.update(running=False)
self.btn_toggle.config(text="▶ 啟動")
self.status_var.set("已停止")
self._log("使用者手動停止")

def _poll(self):
"""每 200ms 將 SharedState 同步到 UI;順便把面板設定即時推到 AutoPotion"""
hp, mp = self.state.get("hp", "mp")
self.hp_var.set(hp)
self.mp_var.set(mp)
self.hp_lbl.config(text=f"{hp * 100:.0f}%")
self.mp_lbl.config(text=f"{mp * 100:.0f}%")
self.hp_thresh_lbl.config(text=f"{self.hp_thresh.get()}%")
self.mp_thresh_lbl.config(text=f"{self.mp_thresh.get()}%")

# 偵測 worker 是否意外結束(monitor_loop try/finally 會把 running 設回 False)
if (self.t_monitor is not None
and not self.t_monitor.is_alive()
and self.btn_toggle.cget("text").startswith("■")):
self.btn_toggle.config(text="▶ 啟動")
self.status_var.set("監控已停止(請看日誌)")

# 讓滑桿與下拉選單執行中即時生效(CPython 屬性寫入在 GIL 下為原子操作)
if self.potion is not None:
self.potion.hp_threshold = self.hp_thresh.get() / 100
self.potion.mp_threshold = self.mp_thresh.get() / 100
self.potion.hp_key = self.hp_key_var.get().lower()
self.potion.mp_key = self.mp_key_var.get().lower()

self._flush_log()
self.root.after(200, self._poll)


if __name__ == "__main__":
LineageAssistant()


圖:整合自動補藥模組的完整主程式,UI 新增閾值滑桿與 F5~F12 熱鍵下拉選單,啟動時連同目標 hwnd 一起傳入 Worker Thread

⚠️ 注意事項

  • 按鍵直接 PostMessage 到目標 hwnd:不搶焦點,你可以在別的視窗工作;按鍵只會進指定遊戲視窗。
  • DirectInput 遊戲可能不吃 PostMessage:本篇鎖定以 WM_KEYDOWN / WM_KEYUP 接收熱鍵的傳統 Win32 遊戲視窗(如天堂 L1J 系列私服);若遊戲採 DirectInput/Raw Input,要改用 SendInput 並把視窗帶到前景才能生效。
  • 冷卻時間 cooldown=1.5:防止連按,依藥水生效速度調整。
  • 面板設定皆為即時生效:checkbox、HP/MP 閾值滑桿、F5~F12 下拉選單都由 _poll 每 200ms 推進 AutoPotion,調了就馬上套用。
  • hwnd 例外,是啟動時一次讀取:切換目標視窗要先停止、重新 🎯 選取、再啟動。
  • 執行緒不會再靜默死掉monitor_loop 外層 try/except/finally 把 pywin32 例外吞進 log;_poll 偵測到執行緒掛掉會自動把 ▶ / ■ 按鈕切回「啟動」並提示看日誌。

🎯 結語

本篇的自動補藥功能是遊戲輔助的基礎,整個邏輯都在 Worker Thread 1 裡執行,與 UI 完全解耦。

下一篇將加入 (三)YOLOv8 怪物偵測整合,新增 Worker Thread 2 專門負責 YOLO 推論,並在 UI 中嵌入即時偵測預覽視窗。

📖 如在學習過程中遇到疑問,或是想了解更多相關主題,建議回顧一下 Python | OpenCV 系列導讀,掌握完整的章節目錄,方便快速找到你需要的內容。

註:以上參考了
pywin32 GitHub
PostMessage - Win32 API
WM_KEYDOWN - Win32 API
tkinter ttk.Scale