run_juece13_wugong_autofight.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. #!/usr/bin/env python3
  2. """run_juece13_wugong_autofight.py — 蜈蚣洞穴自动战斗实测(寻路+杀怪合体).
  3. 组合架构(2026-09-30 Crystal 原版对照提炼):
  4. AutoFightBehavior = MirWalkExecutor 的 A* 移动 + kill.py 的战斗状态机
  5. - 无怪: BFS 远点漫游(权威墙寻路, 混合步长)
  6. - 有怪(8 格视野): 逼近(ATTACK_RADIUS=3) → CS3014 攻击循环
  7. - 低血(<30%): flee(背离威胁源, kill.py 同款)
  8. - 死亡: 检测后 DONE 退出(不再污染网格, run3 事故修复)
  9. - Crystal 语义: 反击优先于漫游(遇袭立即打, 不跑路找死)
  10. 用法:
  11. uv run python scripts/run_juece13_wugong_autofight.py -u longwu988 \
  12. -p Longwu1988 --zone 14570166 --name 新角色 --minutes 20
  13. """
  14. from __future__ import annotations
  15. import argparse
  16. import logging
  17. import sys
  18. import time
  19. sys.path.insert(0, "scripts")
  20. from run_juece13_mine_farthest import ( # noqa: E402
  21. DIR_VECS, MirWalkExecutor, setup_logging, banner,
  22. )
  23. from run_juece13_wugong_farthest import ( # noqa: E402
  24. CAVE_MAP, CaveEnterBehavior, _cave_walls, find_farthest,
  25. )
  26. import requests
  27. from pbot.bot.behavior import Behavior, TickResult
  28. from pbot.bot.pathfind import LearnedGrid, astar
  29. from pbot.bot.runner import pump, run
  30. from pbot.config import Account
  31. from pbot.errors import PbotError
  32. from pbot.game.session import enter_world
  33. from pbot.login.box_client import enter_game_client, login
  34. from pbot.transport.gateway import GatewayConnection, login_gate_with_retry
  35. from pbot.types import ZoneId
  36. from pbot.world.mirror import WorldMirror
  37. _LOG = logging.getLogger("juece13_af")
  38. VISION = 8 # (历史) SC14 雷达采信半径; 选怪已改全视野(见 _pick_monster)
  39. # run6 蜈蚣洞穴实证: 5 杀全部 dist=1 结算; 矿区 run1 实证 dist=3 连打 360
  40. # 刀零结算(近战有效距离=1, 3 格外的 3014 服务器静默不结算)
  41. ATTACK_RADIUS = 1
  42. # 客户端 CalcSkillCDMediator: attackglobalCD=900ms 全局闸 — 0.65 偏快被容忍,
  43. # 沿用实测值
  44. ATTACK_INTERVAL = 0.65
  45. # 短ID分类终审(cap_client_mine2): 5-6位数=怪(111340 实证), 7位数起=
  46. # 宠物/玩家量级(2293955 误攻实证); SC10 人形由 world.humanoids 拦截
  47. PAPET_MIN_ID = 1_000_000
  48. # 技能轮换表(cap_client_mine2 真客户端 40 刀攻击流逆向): 普攻/刺杀/
  49. # 攻杀/半月/烈火 全部 dist=1 有效结算
  50. SKILL_ROTATION = (0, 12, 7, 25, 26)
  51. # 客户端 AutoProxy:SetAFKTargetDeath — 击杀后原地等 0.2s(等掉落/尸体),
  52. # 窗口内不选新怪不漫游
  53. KILL_WAIT_S = 0.2
  54. # 零结算放弃: 连续 N 刀目标怪血完全无变化(SC31 无该怪掉血) = 打不中/
  55. # 幽灵怪, 拉黑换目标 — 怪掉血则计数归零, 厚血怪一直打到死
  56. GIVEUP_HITS = 10
  57. # 怪窝撤离开关(仅高危图开): 蜈蚣洞穴 dmg60-83 需要撤离; 矿区 dmg3-14
  58. # 关闭 — 成群怪是常态, 开着=绕着怪走永远不杀
  59. DANGER_DENSITY = False
  60. # run2 实证: 蜈蚣洞穴怪单刀 dmg 59, 双怪夹击 6s 掉 344 血 — 30% 阈值触发
  61. # 时仅剩 2s 命, 来不及逃; 55% + 避强怪(单刀>25)才活得下来
  62. FLEE_RATIO = 0.55
  63. BIG_HIT = 25
  64. STEP_INTERVAL = 0.6
  65. import re
  66. def _strike_damage(detail: str) -> int:
  67. """从 SC31 struck 事件 detail 提取 dmg=N; 无则 0."""
  68. m = re.search(r"dmg=(\d+)", detail)
  69. return int(m.group(1)) if m else 0
  70. def _strike_attacker(detail: str) -> int:
  71. """SC31 detail 的 by= 才是攻击者(record 的 actor 是被打者)."""
  72. m = re.search(r"by=(\d+)", detail)
  73. return int(m.group(1)) if m else 0
  74. class AutoFightBehavior(Behavior):
  75. """自动战斗: 漫游(A*) + 选怪 + 逼近 + 攻击 + 低血逃跑(地图无关).
  76. 参数化(矿区/洞穴通用): map_name=守卫图名, walls=权威墙集,
  77. farthest_fn=BFS 最远点函数(返回 (dest, dist, enclave)).
  78. """
  79. name = "autofight"
  80. def __init__(self, sid: int, kill_target: int = 0,
  81. map_name: str = "", walls: set[tuple[int, int]] | None = None,
  82. farthest_fn=None, kill_goal: int = 0) -> None:
  83. self._sid = sid
  84. self._map_name = map_name
  85. self._farthest = farthest_fn
  86. self._kill_goal = kill_goal
  87. self._grid = LearnedGrid()
  88. self._grid.mark_from_map(walls if walls is not None else _cave_walls())
  89. self._roam_dest: tuple[int, int] | None = None
  90. self._target_key: int | None = None
  91. self._last_attack_t = 0.0
  92. self._last_step_t = 0.0
  93. self._pending = False
  94. self._pending_t = 0.0
  95. self._last_target_cell: tuple[int, int] | None = None
  96. self._hit_damage: dict[int, int] = {} # 怪key → 我吃过它的最大单刀
  97. self._big_monsters: set[int] = set() # 单刀>BIG_HIT 的强怪黑名单
  98. self._hit_counter: dict[int, int] = {} # 目标key → 连续零结算刀数
  99. self._kill_wait_until = 0.0 # 击杀后等待窗口(AutoProxy 0.2s)
  100. self._last_scan_log_t = 0.0 # 扫描观测日志节流
  101. self._map_bounds: tuple[int, int, int, int] | None = None
  102. self._roam_blacklist: dict[tuple[int, int], float] = {} # 不可达点→解禁时刻
  103. self._fleeing_last = False # 上一 tick 低血态(沿检测用, run14)
  104. self._dead_wait = False # 死亡待机: 屏蔽一切动作等回城(run14)
  105. self._hit_seq = 0 # 技能轮换指针
  106. self._done = False
  107. self._now_override: float | None = None # 测试时钟注入
  108. self.kills = 0
  109. self.legs = 0
  110. self.rejected = 0
  111. def _now(self) -> float:
  112. """统一时钟(测试可注入)."""
  113. return self._now_override if self._now_override is not None \
  114. else time.time()
  115. # ---- 怪物选择: 客户端 AutoFindMonsterCommand 定案 ----
  116. # 全视野(=服务器 SC10/30 推送范围)取切比雪夫最近; 过滤链:
  117. # 死亡/黑名单/宠物量级ID(IsHaveMaster 等价)/与我同格(dist=0 打不中
  118. # 且客户端 FindNearestMonster 明确跳过)
  119. def _pick_monster(self, world: WorldMirror):
  120. me = world.me
  121. best = None
  122. best_d = 1 << 30
  123. for m in world.monsters.values():
  124. if m.is_dead or int(m.key) in self._big_monsters:
  125. continue
  126. if int(m.key) >= PAPET_MIN_ID:
  127. continue
  128. if (m.x, m.y) == (me.x, me.y):
  129. continue
  130. if (m.x, m.y) == (0, 0):
  131. continue # attacker 揭示怪: 坐标未补全前不可作为目标
  132. d = max(abs(m.x - me.x), abs(m.y - me.y))
  133. if d < best_d:
  134. best, best_d = m, d
  135. return best
  136. def _tick_fight_move(self, conn: GatewayConnection,
  137. world: WorldMirror, now: float) -> bool:
  138. """朝目标逼近一步; True=战斗已处理(攻击循环或移动中).
  139. 客户端 RequestLaunchCommand 语义: 不在出刀位只发移动不发攻击;
  140. A* 终点=怪相邻格(绝不踩进怪格 — dist=0 客户端本地就拒绝);
  141. dist≤2 时强制 1 格走(最后一步永远 walk, 防跑过头).
  142. """
  143. m = world.monsters.get(self._target_key) if self._target_key else None
  144. if m is None:
  145. self._target_key = None
  146. return False
  147. if m.is_dead:
  148. self.kills += 1
  149. _ = self._hit_counter.pop(self._target_key, None)
  150. _LOG.info("[击杀✓] #%d %s 累计 %d/%s", m.key, m.name,
  151. self.kills, self._kill_goal or "∞")
  152. self._kill_wait_until = now + KILL_WAIT_S
  153. self._target_key = None
  154. if self._kill_goal and self.kills >= self._kill_goal:
  155. _LOG.info("[目标达成] %d 杀 — 收工", self.kills)
  156. self._done = True
  157. return False
  158. me = world.me
  159. dist = max(abs(m.x - me.x), abs(m.y - me.y))
  160. if dist <= ATTACK_RADIUS:
  161. return True
  162. if self._pending and now - self._pending_t < 1.0:
  163. return True
  164. if now - self._last_step_t < STEP_INTERVAL:
  165. return True
  166. if dist <= 2:
  167. # 末段直走 1 格(客户端最后一步永远 walk): 距怪≤2 时
  168. # 怪相邻格距我≤1, A* 视为"已到达"返回空 — 直接朝怪走一步
  169. # 即落出刀位, 绝不会踩进怪格(dist≥2 时朝怪 1 步至多到 dist=1)
  170. ddx = max(-1, min(1, m.x - me.x))
  171. ddy = max(-1, min(1, m.y - me.y))
  172. d = next((i for i, (ox, oy) in enumerate(DIR_VECS)
  173. if ox == ddx and oy == ddy), 4)
  174. fx, fy = me.x + ddx, me.y + ddy
  175. dist2 = 1
  176. else:
  177. adj = self._adjacent_cell(me, m)
  178. path = astar((me.x, me.y), adj, self._grid, max_expand=100000)
  179. if not path or len(path) < 2:
  180. return True
  181. nxt = path[1]
  182. ddx = nxt[0] - me.x
  183. ddy = nxt[1] - me.y
  184. dist2 = 2 if max(abs(ddx), abs(ddy)) == 2 else 1
  185. ddx = max(-1, min(1, ddx))
  186. ddy = max(-1, min(1, ddy))
  187. d = next((i for i, (ox, oy) in enumerate(DIR_VECS)
  188. if ox == ddx and oy == ddy), 4)
  189. fx, fy = me.x + ddx, me.y + ddy
  190. if dist - 2 <= 2 and dist2 == 2:
  191. # 预计 2 格跑落点后距怪≤2: 降为 1 格走防冲过出刀位
  192. dist2 = 1
  193. fx, fy = me.x + ddx, me.y + ddy
  194. from pbot.game.messages import protocol_map_name
  195. mn = protocol_map_name(me.map_id, me.map_name)
  196. _ = conn.send_frame(3011 if dist2 == 1 else 3013, 0, d,
  197. (me.y << 16) | me.x, self._sid, mn.encode(),
  198. pre=(fy << 16) | fx)
  199. self._pending = True
  200. self._pending_t = now
  201. self._last_step_t = now
  202. self.legs += 1
  203. return True
  204. def _adjacent_cell(self, me, m) -> tuple[int, int]:
  205. best = (m.x, m.y)
  206. best_cost = 1 << 30
  207. for dx in (-1, 0, 1):
  208. for dy in (-1, 0, 1):
  209. if dx == 0 and dy == 0:
  210. continue
  211. c = (m.x + dx, m.y + dy)
  212. if c in self._grid.blocked:
  213. continue
  214. cost = max(abs(c[0] - me.x), abs(c[1] - me.y))
  215. if cost < best_cost:
  216. best, best_cost = c, cost
  217. return best
  218. def _send_attack(self, conn: GatewayConnection, world: WorldMirror) -> None:
  219. import json
  220. m = world.monsters.get(self._target_key)
  221. if m is None:
  222. return
  223. me = world.me
  224. dx = max(-1, min(1, m.x - me.x))
  225. dy = max(-1, min(1, m.y - me.y))
  226. d = next((i for i, (ox, oy) in enumerate(DIR_VECS)
  227. if ox == dx and oy == dy), 4)
  228. # 技能轮换(cap_client_mine2 真客户端攻击流): 普攻0 → 刺杀12 →
  229. # 攻杀7 → 半月25 → 烈火26 循环 — 同 dist=1 全部有效结算
  230. self._hit_seq += 1
  231. skill = SKILL_ROTATION[self._hit_seq % len(SKILL_ROTATION)]
  232. body = json.dumps({"skillID": skill, "dir": d, "srcX": me.x,
  233. "srcY": me.y, "destX": m.x, "destY": m.y,
  234. "targetID": str(m.key)},
  235. separators=(",", ":")).encode()
  236. _ = conn.send_frame(3014, 0, 0, 0, 0, body)
  237. _LOG.info("[攻击] #%d @(%d,%d) dist=%d skill=%d hp=%d/%d",
  238. m.key, m.x, m.y,
  239. max(abs(m.x-me.x), abs(m.y-me.y)), skill, me.hp, me.max_hp)
  240. def tick(self, conn: GatewayConnection, world: WorldMirror) -> TickResult:
  241. me = world.me
  242. if not me.known():
  243. return TickResult.RUNNING
  244. if self._map_name and me.map_name != self._map_name:
  245. _LOG.warning("[✗] 换图 %s(死亡回城?) — 退出: 击杀=%d 腿=%d",
  246. me.map_name, self.kills, self.legs)
  247. return TickResult.DONE
  248. if self._done:
  249. return TickResult.DONE
  250. # run14: hp=0 后仍出刀 4 次 — 死亡待机态屏蔽选怪/攻击/漫游,
  251. # 只等换图(死亡回城)触发上面的 DONE 退出
  252. if self._dead_wait:
  253. return TickResult.RUNNING
  254. now = self._now()
  255. # 事件: SC1 确认 / SC28 弹回标墙 / SC31 受击(强怪判定) / 死亡
  256. for e in world.events:
  257. if e.kind == "self_move_confirmed":
  258. self._pending = False
  259. elif e.kind == "me_die":
  260. _LOG.warning("[死亡] 击杀=%d 腿=%d — 停止一切, 等回城",
  261. self.kills, self.legs)
  262. self._target_key = None
  263. self._dead_wait = True
  264. self._roam_dest = None
  265. elif e.kind == "mon_die" and self._target_key == e.actor:
  266. self.kills += 1
  267. _ = self._hit_counter.pop(self._target_key, None)
  268. _LOG.info("[击杀✓] %s 累计 %d/%s", e.detail,
  269. self.kills, self._kill_goal or "∞")
  270. self._kill_wait_until = now + KILL_WAIT_S
  271. self._target_key = None
  272. if self._kill_goal and self.kills >= self._kill_goal:
  273. _LOG.info("[目标达成] %d 杀 — 收工", self.kills)
  274. self._done = True
  275. elif e.kind == "pull_back":
  276. self.rejected += 1
  277. self._pending = False
  278. if self._last_target_cell is not None:
  279. self._grid.mark_blocked(self._last_target_cell)
  280. elif e.kind == "struck" and e.actor:
  281. # SC31 结算反馈(actor=被打者):
  282. # actor=我 → by=攻击怪(强怪判定)
  283. # actor=目标怪 → 怪掉血了(hp 前进), 厚血怪继续打
  284. if str(e.actor) == world.me.uid:
  285. dmg = _strike_damage(e.detail)
  286. attacker = _strike_attacker(e.detail)
  287. if dmg > 0 and attacker:
  288. if dmg > self._hit_damage.get(attacker, 0):
  289. self._hit_damage[attacker] = dmg
  290. if dmg > BIG_HIT and attacker not in self._big_monsters:
  291. self._big_monsters.add(attacker)
  292. _LOG.warning("[强怪] #%d 单刀 %d > %d — 拉黑绕开",
  293. attacker, dmg, BIG_HIT)
  294. if self._target_key == attacker:
  295. self._target_key = None
  296. elif e.actor == self._target_key:
  297. # 目标怪掉血 = 攻击有效, 重置零结算计数(厚血怪一直打)
  298. self._hit_counter[e.actor] = 0
  299. world.events.clear()
  300. # 击杀等待窗口(AutoProxy SetAFKTargetDeath 0.2s): 等 loot/尸体,
  301. # 窗口内不选新怪不漫游, 窗口过后下一 tick 立即重扫
  302. if now < self._kill_wait_until:
  303. return TickResult.RUNNING
  304. # ★ 保命优先(Crystal AutoPot 在 Search 之前): 低血立即弃目标
  305. # 逃跑 — 放在战斗分支后会被 return 挡住永远走不到(run5 实证:
  306. # hp 39→15→8→0 打了 6 秒不撤, 死战)
  307. # run14 实证三重死循环: 逃跑点(向量*30)拉黑后同向量重算同点 →
  308. # 60ms 一轮"选怪→弃目标→换点→不可达", 24s 原地挨打致死。
  309. # 修复: fleeing 先算(选怪守卫用) + 逃跑点拉黑即换新点(垂直扰动)
  310. # + 脱离低血才恢复选怪。
  311. fleeing = me.max_hp > 0 and 0 < me.hp / me.max_hp < FLEE_RATIO
  312. if fleeing:
  313. if self._target_key is not None:
  314. _LOG.warning("[低血] %d/%d — 弃目标 #%d 逃跑",
  315. me.hp, me.max_hp, self._target_key)
  316. self._target_key = None
  317. if self._roam_dest is None or \
  318. self._roam_dest in self._roam_blacklist:
  319. nearest = self._pick_monster(world)
  320. if nearest is not None:
  321. # 4 方向确定性轮试(反怪向→垂直→垂直→顺怪向),
  322. # 跳过已拉黑点 — run14: 随机扰动有 50% 撞回同点
  323. fx = -1 if nearest.x < me.x else 1
  324. fy = -1 if nearest.y < me.y else 1
  325. for dx, dy in ((fx, fy), (fy, -fx), (-fy, fx), (-fx, -fy)):
  326. cand = (max(0, me.x + dx*30), max(0, me.y + dy*30))
  327. if cand not in self._roam_blacklist:
  328. self._roam_dest = cand
  329. _LOG.warning("[低血] 远离怪, 逃跑目标 %s", cand)
  330. break
  331. elif self._fleeing_last and self._roam_dest is not None:
  332. _LOG.info("[低血✓] 回血 %d/%d — 恢复战斗", me.hp, me.max_hp)
  333. self._roam_dest = None
  334. self._fleeing_last = fleeing
  335. # ★ 客户端 checkAutoFindTarget: 无目标才扫(有目标绝不换),
  336. # 全视野最近者胜 — 视野=服务器推送范围(SC11 雷达/SC10/SC14)
  337. # 逃跑中不选怪(run14: 选怪→弃目标 60ms 死循环)
  338. if self._target_key is None and not fleeing:
  339. m = self._pick_monster(world)
  340. if m is not None:
  341. self._target_key = int(m.key)
  342. self._pending = False # 立即打断漫游移动节拍
  343. self._roam_dest = None
  344. tag = "雷达" if m.name == "radar" else m.name
  345. _LOG.info("[选怪] #%d %s @(%d,%d) dist=%d 视野怪=%d — 转战斗",
  346. m.key, tag, m.x, m.y,
  347. max(abs(m.x - me.x), abs(m.y - me.y)),
  348. len(world.monsters))
  349. self._last_scan_log_t = now
  350. elif now - getattr(self, "_last_scan_log_t", 0.0) > 15.0:
  351. _LOG.info("[扫描] 无可打目标 视野怪=%d 黑名单=%d 位置=(%d,%d) "
  352. "漫游点=%s",
  353. len(world.monsters), len(self._big_monsters),
  354. me.x, me.y, self._roam_dest)
  355. self._last_scan_log_t = now
  356. # 1. 战斗(RequestLaunchCommand: 先走位后出刀, 永不并发)
  357. if self._target_key is not None:
  358. if self._tick_fight_move(conn, world, now):
  359. m = world.monsters.get(self._target_key)
  360. if m is not None and \
  361. max(abs(m.x-me.x), abs(m.y-me.y)) <= ATTACK_RADIUS \
  362. and now - self._last_attack_t >= ATTACK_INTERVAL:
  363. self._send_attack(conn, world)
  364. self._last_attack_t = now
  365. n = self._hit_counter.get(self._target_key, 0) + 1
  366. self._hit_counter[self._target_key] = n
  367. if n == 4:
  368. # 4 刀不中: 目标可能已移动 — 重锁让逼近链重新对齐
  369. _LOG.info("[重锁] #%d 连 %d 刀不中 — 重新对齐",
  370. self._target_key, n)
  371. self._target_key = None
  372. self._roam_dest = None
  373. elif n >= GIVEUP_HITS:
  374. _LOG.warning("[放弃目标] #%d %d 刀零结算 — 拉黑换怪",
  375. self._target_key, n)
  376. self._big_monsters.add(self._target_key)
  377. self._target_key = None
  378. self._roam_dest = None
  379. return TickResult.RUNNING
  380. self._target_key = None
  381. # 2. 怪窝撤离(仅高危图; 矿区 False)
  382. if DANGER_DENSITY:
  383. near = sum(1 for x in world.monsters.values()
  384. if not x.is_dead
  385. and max(abs(x.x-me.x), abs(x.y-me.y)) <= 4)
  386. if near >= 3 and self._target_key is None:
  387. if self._roam_dest is None or \
  388. max(abs(self._roam_dest[0]-me.x),
  389. abs(self._roam_dest[1]-me.y)) <= 3:
  390. self._roam_dest = self._farthest((me.x, me.y))[0]
  391. _LOG.warning("[怪窝] 4格内 %d 只 — 撤离至 %s", near,
  392. self._roam_dest)
  393. # 3. 漫游(客户端 checkAimlessMove): 随机中距点(±50, |dx|+|dy|>20),
  394. # 到达或无路换点 — 替代旧 BFS 最远点(钻隧道错过怪)
  395. if self._roam_dest is None or \
  396. max(abs(self._roam_dest[0]-me.x),
  397. abs(self._roam_dest[1]-me.y)) <= 3:
  398. self._roam_dest = self._new_roam_dest(me, now)
  399. _LOG.info("[漫游] 新随机点 %s", self._roam_dest)
  400. if self._pending and now - self._pending_t < 1.0:
  401. return TickResult.RUNNING
  402. if now - self._last_step_t < STEP_INTERVAL:
  403. return TickResult.RUNNING
  404. path = astar((me.x, me.y), self._roam_dest, self._grid, max_expand=100000)
  405. if not path or len(path) < 2:
  406. # run12 实证: 8s 换 4 个反向漫游点 — 不可达点立即置空导致振荡;
  407. # 失败点拉黑 30s, 换点时避开
  408. self._roam_blacklist[self._roam_dest] = now + 30.0
  409. _LOG.info("[漫游] %s 不可达 — 拉黑换点", self._roam_dest)
  410. self._roam_dest = None
  411. return TickResult.RUNNING
  412. nxt = path[1]
  413. dx = max(-1, min(1, nxt[0]-me.x))
  414. dy = max(-1, min(1, nxt[1]-me.y))
  415. d = next((i for i, (ox, oy) in enumerate(DIR_VECS) if ox == dx and oy == dy), 4)
  416. dist2 = 2 if max(abs(nxt[0]-me.x), abs(nxt[1]-me.y)) == 2 else 1
  417. from pbot.game.messages import protocol_map_name
  418. mn = protocol_map_name(me.map_id, me.map_name)
  419. fx, fy = me.x + dx*dist2, me.y + dy*dist2
  420. self._last_target_cell = (fx, fy)
  421. _ = conn.send_frame(3013 if dist2 == 2 else 3011, 0, d,
  422. (me.y << 16) | me.x, self._sid, mn.encode(),
  423. pre=(fy << 16) | fx)
  424. self._pending = True
  425. self._pending_t = now
  426. self._last_step_t = now
  427. self.legs += 1
  428. return TickResult.RUNNING
  429. def _new_roam_dest(self, me, now: float) -> tuple[int, int]:
  430. """随机中距漫游点(AssistProxy checkAimlessMove: ±50, 距离>20).
  431. farthest_fn 仅供高危图怪窝撤离调用, 漫游永远随机点 —
  432. BFS 最远点=钻隧道错过怪(run8 实证: 固定两点往返 96s 空窗).
  433. 漫游点钳制在地图包围盒内(墙集推得) — run9 实证负坐标点
  434. A* 必失败, 0.6s 连刷 5 个漫游点空转.
  435. """
  436. import random
  437. if self._map_bounds is None:
  438. xs = [c[0] for c in self._grid.blocked]
  439. ys = [c[1] for c in self._grid.blocked]
  440. # 真实地图墙集覆盖全图, 稀疏墙集(测试)兜底 ±100
  441. x1 = max(xs) if xs and max(xs) > 50 else min(me.x + 100, 199)
  442. y1 = max(ys) if ys and max(ys) > 50 else min(me.y + 100, 199)
  443. self._map_bounds = (0, 0, x1, y1)
  444. x0, y0, x1, y1 = self._map_bounds
  445. for t, until in list(self._roam_blacklist.items()):
  446. if now > until:
  447. del self._roam_blacklist[t]
  448. for _ in range(20):
  449. x = max(x0, min(x1, me.x + random.randint(-50, 50)))
  450. y = max(y0, min(y1, me.y + random.randint(-50, 50)))
  451. if (x, y) in self._grid.blocked:
  452. continue
  453. if abs(x - me.x) + abs(y - me.y) <= 20:
  454. continue
  455. if (x, y) in self._roam_blacklist:
  456. continue
  457. return (x, y)
  458. return (me.x, me.y)
  459. def parse_args() -> argparse.Namespace:
  460. """命令行."""
  461. ap = argparse.ArgumentParser()
  462. _ = ap.add_argument("-u", "--user", required=True)
  463. _ = ap.add_argument("-p", "--password", required=True)
  464. _ = ap.add_argument("--zone", type=int, required=True)
  465. _ = ap.add_argument("--name", required=True)
  466. _ = ap.add_argument("--minutes", type=float, default=20.0)
  467. return ap.parse_args()
  468. def main() -> int:
  469. """登录 → 蜈蚣洞穴 → 自动战斗 N 分钟 → 退出."""
  470. a = parse_args()
  471. setup_logging()
  472. banner("[0] 自动战斗实测: 区=%d 角色=%s 时长=%.0fmin",
  473. a.zone, a.name, a.minutes)
  474. http = requests.Session()
  475. http.trust_env = False
  476. http.verify = False
  477. jwt, uid = login(Account(username=a.user, password=a.password))
  478. _ = enter_game_client(http, jwt, ZoneId(a.zone))
  479. key32, sid, real_name, ticket_key, game_port = login_gate_with_retry(
  480. ZoneId(a.zone), jwt, uid, a.name, (8021, 8061, 8001, 8041))
  481. stop: list[bool] = [False]
  482. conn, world, updater = enter_world(ZoneId(a.zone), real_name, ticket_key,
  483. sid, game_port, uid, stop=stop)
  484. _LOG.info("[✓] 出生 map=%s @(%d,%d)", world.me.map_name, world.me.x,
  485. world.me.y)
  486. try:
  487. from pbot.game.heal import enable_auto_heal
  488. state = enable_auto_heal(conn, sid, want_on=True, world=world)
  489. _LOG.info("[喝药] %s", state)
  490. if world.me.map_name != CAVE_MAP:
  491. banner("[1] 传送蜈蚣洞穴")
  492. bh = CaveEnterBehavior(sid=sid, kill_target=0)
  493. r = run(conn, world, updater, bh, duration_s=300.0)
  494. if world.me.map_name != CAVE_MAP:
  495. _LOG.error("[✗] 传送失败 result=%s", r.value)
  496. return 1
  497. time.sleep(3.0)
  498. _ = pump(conn, world, updater, 2.0)
  499. banner("[2] 自动战斗 %.0f 分钟", a.minutes)
  500. af = AutoFightBehavior(sid=sid)
  501. r = run(conn, world, updater, af, duration_s=a.minutes * 60)
  502. _LOG.info("[结束] result=%s 击杀=%d 腿=%d 弹回=%d 位置=(%d,%d) hp=%d/%d",
  503. r.value, af.kills, af.legs, af.rejected,
  504. world.me.x, world.me.y, world.me.hp, world.me.max_hp)
  505. return 0
  506. finally:
  507. stop[0] = True
  508. conn.close()
  509. _LOG.info("连接已关闭")
  510. if __name__ == "__main__":
  511. try:
  512. sys.exit(main())
  513. except PbotError as e:
  514. _LOG.error("失败: %s", e.detail)
  515. sys.exit(1)