| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557 |
- #!/usr/bin/env python3
- """run_juece13_wugong_autofight.py — 蜈蚣洞穴自动战斗实测(寻路+杀怪合体).
- 组合架构(2026-09-30 Crystal 原版对照提炼):
- AutoFightBehavior = MirWalkExecutor 的 A* 移动 + kill.py 的战斗状态机
- - 无怪: BFS 远点漫游(权威墙寻路, 混合步长)
- - 有怪(8 格视野): 逼近(ATTACK_RADIUS=3) → CS3014 攻击循环
- - 低血(<30%): flee(背离威胁源, kill.py 同款)
- - 死亡: 检测后 DONE 退出(不再污染网格, run3 事故修复)
- - Crystal 语义: 反击优先于漫游(遇袭立即打, 不跑路找死)
- 用法:
- uv run python scripts/run_juece13_wugong_autofight.py -u longwu988 \
- -p Longwu1988 --zone 14570166 --name 新角色 --minutes 20
- """
- from __future__ import annotations
- import argparse
- import logging
- import sys
- import time
- sys.path.insert(0, "scripts")
- from run_juece13_mine_farthest import ( # noqa: E402
- DIR_VECS, MirWalkExecutor, setup_logging, banner,
- )
- from run_juece13_wugong_farthest import ( # noqa: E402
- CAVE_MAP, CaveEnterBehavior, _cave_walls, find_farthest,
- )
- import requests
- from pbot.bot.behavior import Behavior, TickResult
- from pbot.bot.pathfind import LearnedGrid, astar
- from pbot.bot.runner import pump, run
- from pbot.config import Account
- from pbot.errors import PbotError
- from pbot.game.session import enter_world
- from pbot.login.box_client import enter_game_client, login
- from pbot.transport.gateway import GatewayConnection, login_gate_with_retry
- from pbot.types import ZoneId
- from pbot.world.mirror import WorldMirror
- _LOG = logging.getLogger("juece13_af")
- VISION = 8 # (历史) SC14 雷达采信半径; 选怪已改全视野(见 _pick_monster)
- # run6 蜈蚣洞穴实证: 5 杀全部 dist=1 结算; 矿区 run1 实证 dist=3 连打 360
- # 刀零结算(近战有效距离=1, 3 格外的 3014 服务器静默不结算)
- ATTACK_RADIUS = 1
- # 客户端 CalcSkillCDMediator: attackglobalCD=900ms 全局闸 — 0.65 偏快被容忍,
- # 沿用实测值
- ATTACK_INTERVAL = 0.65
- # 短ID分类终审(cap_client_mine2): 5-6位数=怪(111340 实证), 7位数起=
- # 宠物/玩家量级(2293955 误攻实证); SC10 人形由 world.humanoids 拦截
- PAPET_MIN_ID = 1_000_000
- # 技能轮换表(cap_client_mine2 真客户端 40 刀攻击流逆向): 普攻/刺杀/
- # 攻杀/半月/烈火 全部 dist=1 有效结算
- SKILL_ROTATION = (0, 12, 7, 25, 26)
- # 客户端 AutoProxy:SetAFKTargetDeath — 击杀后原地等 0.2s(等掉落/尸体),
- # 窗口内不选新怪不漫游
- KILL_WAIT_S = 0.2
- # 零结算放弃: 连续 N 刀目标怪血完全无变化(SC31 无该怪掉血) = 打不中/
- # 幽灵怪, 拉黑换目标 — 怪掉血则计数归零, 厚血怪一直打到死
- GIVEUP_HITS = 10
- # 怪窝撤离开关(仅高危图开): 蜈蚣洞穴 dmg60-83 需要撤离; 矿区 dmg3-14
- # 关闭 — 成群怪是常态, 开着=绕着怪走永远不杀
- DANGER_DENSITY = False
- # run2 实证: 蜈蚣洞穴怪单刀 dmg 59, 双怪夹击 6s 掉 344 血 — 30% 阈值触发
- # 时仅剩 2s 命, 来不及逃; 55% + 避强怪(单刀>25)才活得下来
- FLEE_RATIO = 0.55
- BIG_HIT = 25
- STEP_INTERVAL = 0.6
- import re
- def _strike_damage(detail: str) -> int:
- """从 SC31 struck 事件 detail 提取 dmg=N; 无则 0."""
- m = re.search(r"dmg=(\d+)", detail)
- return int(m.group(1)) if m else 0
- def _strike_attacker(detail: str) -> int:
- """SC31 detail 的 by= 才是攻击者(record 的 actor 是被打者)."""
- m = re.search(r"by=(\d+)", detail)
- return int(m.group(1)) if m else 0
- class AutoFightBehavior(Behavior):
- """自动战斗: 漫游(A*) + 选怪 + 逼近 + 攻击 + 低血逃跑(地图无关).
- 参数化(矿区/洞穴通用): map_name=守卫图名, walls=权威墙集,
- farthest_fn=BFS 最远点函数(返回 (dest, dist, enclave)).
- """
- name = "autofight"
- def __init__(self, sid: int, kill_target: int = 0,
- map_name: str = "", walls: set[tuple[int, int]] | None = None,
- farthest_fn=None, kill_goal: int = 0) -> None:
- self._sid = sid
- self._map_name = map_name
- self._farthest = farthest_fn
- self._kill_goal = kill_goal
- self._grid = LearnedGrid()
- self._grid.mark_from_map(walls if walls is not None else _cave_walls())
- self._roam_dest: tuple[int, int] | None = None
- self._target_key: int | None = None
- self._last_attack_t = 0.0
- self._last_step_t = 0.0
- self._pending = False
- self._pending_t = 0.0
- self._last_target_cell: tuple[int, int] | None = None
- self._hit_damage: dict[int, int] = {} # 怪key → 我吃过它的最大单刀
- self._big_monsters: set[int] = set() # 单刀>BIG_HIT 的强怪黑名单
- self._hit_counter: dict[int, int] = {} # 目标key → 连续零结算刀数
- self._kill_wait_until = 0.0 # 击杀后等待窗口(AutoProxy 0.2s)
- self._last_scan_log_t = 0.0 # 扫描观测日志节流
- self._map_bounds: tuple[int, int, int, int] | None = None
- self._roam_blacklist: dict[tuple[int, int], float] = {} # 不可达点→解禁时刻
- self._fleeing_last = False # 上一 tick 低血态(沿检测用, run14)
- self._dead_wait = False # 死亡待机: 屏蔽一切动作等回城(run14)
- self._hit_seq = 0 # 技能轮换指针
- self._done = False
- self._now_override: float | None = None # 测试时钟注入
- self.kills = 0
- self.legs = 0
- self.rejected = 0
- def _now(self) -> float:
- """统一时钟(测试可注入)."""
- return self._now_override if self._now_override is not None \
- else time.time()
- # ---- 怪物选择: 客户端 AutoFindMonsterCommand 定案 ----
- # 全视野(=服务器 SC10/30 推送范围)取切比雪夫最近; 过滤链:
- # 死亡/黑名单/宠物量级ID(IsHaveMaster 等价)/与我同格(dist=0 打不中
- # 且客户端 FindNearestMonster 明确跳过)
- def _pick_monster(self, world: WorldMirror):
- me = world.me
- best = None
- best_d = 1 << 30
- for m in world.monsters.values():
- if m.is_dead or int(m.key) in self._big_monsters:
- continue
- if int(m.key) >= PAPET_MIN_ID:
- continue
- if (m.x, m.y) == (me.x, me.y):
- continue
- if (m.x, m.y) == (0, 0):
- continue # attacker 揭示怪: 坐标未补全前不可作为目标
- d = max(abs(m.x - me.x), abs(m.y - me.y))
- if d < best_d:
- best, best_d = m, d
- return best
- def _tick_fight_move(self, conn: GatewayConnection,
- world: WorldMirror, now: float) -> bool:
- """朝目标逼近一步; True=战斗已处理(攻击循环或移动中).
- 客户端 RequestLaunchCommand 语义: 不在出刀位只发移动不发攻击;
- A* 终点=怪相邻格(绝不踩进怪格 — dist=0 客户端本地就拒绝);
- dist≤2 时强制 1 格走(最后一步永远 walk, 防跑过头).
- """
- m = world.monsters.get(self._target_key) if self._target_key else None
- if m is None:
- self._target_key = None
- return False
- if m.is_dead:
- self.kills += 1
- _ = self._hit_counter.pop(self._target_key, None)
- _LOG.info("[击杀✓] #%d %s 累计 %d/%s", m.key, m.name,
- self.kills, self._kill_goal or "∞")
- self._kill_wait_until = now + KILL_WAIT_S
- self._target_key = None
- if self._kill_goal and self.kills >= self._kill_goal:
- _LOG.info("[目标达成] %d 杀 — 收工", self.kills)
- self._done = True
- return False
- me = world.me
- dist = max(abs(m.x - me.x), abs(m.y - me.y))
- if dist <= ATTACK_RADIUS:
- return True
- if self._pending and now - self._pending_t < 1.0:
- return True
- if now - self._last_step_t < STEP_INTERVAL:
- return True
- if dist <= 2:
- # 末段直走 1 格(客户端最后一步永远 walk): 距怪≤2 时
- # 怪相邻格距我≤1, A* 视为"已到达"返回空 — 直接朝怪走一步
- # 即落出刀位, 绝不会踩进怪格(dist≥2 时朝怪 1 步至多到 dist=1)
- ddx = max(-1, min(1, m.x - me.x))
- ddy = max(-1, min(1, m.y - me.y))
- d = next((i for i, (ox, oy) in enumerate(DIR_VECS)
- if ox == ddx and oy == ddy), 4)
- fx, fy = me.x + ddx, me.y + ddy
- dist2 = 1
- else:
- adj = self._adjacent_cell(me, m)
- path = astar((me.x, me.y), adj, self._grid, max_expand=100000)
- if not path or len(path) < 2:
- return True
- nxt = path[1]
- ddx = nxt[0] - me.x
- ddy = nxt[1] - me.y
- dist2 = 2 if max(abs(ddx), abs(ddy)) == 2 else 1
- ddx = max(-1, min(1, ddx))
- ddy = max(-1, min(1, ddy))
- d = next((i for i, (ox, oy) in enumerate(DIR_VECS)
- if ox == ddx and oy == ddy), 4)
- fx, fy = me.x + ddx, me.y + ddy
- if dist - 2 <= 2 and dist2 == 2:
- # 预计 2 格跑落点后距怪≤2: 降为 1 格走防冲过出刀位
- dist2 = 1
- fx, fy = me.x + ddx, me.y + ddy
- from pbot.game.messages import protocol_map_name
- mn = protocol_map_name(me.map_id, me.map_name)
- _ = conn.send_frame(3011 if dist2 == 1 else 3013, 0, d,
- (me.y << 16) | me.x, self._sid, mn.encode(),
- pre=(fy << 16) | fx)
- self._pending = True
- self._pending_t = now
- self._last_step_t = now
- self.legs += 1
- return True
- def _adjacent_cell(self, me, m) -> tuple[int, int]:
- best = (m.x, m.y)
- best_cost = 1 << 30
- for dx in (-1, 0, 1):
- for dy in (-1, 0, 1):
- if dx == 0 and dy == 0:
- continue
- c = (m.x + dx, m.y + dy)
- if c in self._grid.blocked:
- continue
- cost = max(abs(c[0] - me.x), abs(c[1] - me.y))
- if cost < best_cost:
- best, best_cost = c, cost
- return best
- def _send_attack(self, conn: GatewayConnection, world: WorldMirror) -> None:
- import json
- m = world.monsters.get(self._target_key)
- if m is None:
- return
- me = world.me
- dx = max(-1, min(1, m.x - me.x))
- dy = max(-1, min(1, m.y - me.y))
- d = next((i for i, (ox, oy) in enumerate(DIR_VECS)
- if ox == dx and oy == dy), 4)
- # 技能轮换(cap_client_mine2 真客户端攻击流): 普攻0 → 刺杀12 →
- # 攻杀7 → 半月25 → 烈火26 循环 — 同 dist=1 全部有效结算
- self._hit_seq += 1
- skill = SKILL_ROTATION[self._hit_seq % len(SKILL_ROTATION)]
- body = json.dumps({"skillID": skill, "dir": d, "srcX": me.x,
- "srcY": me.y, "destX": m.x, "destY": m.y,
- "targetID": str(m.key)},
- separators=(",", ":")).encode()
- _ = conn.send_frame(3014, 0, 0, 0, 0, body)
- _LOG.info("[攻击] #%d @(%d,%d) dist=%d skill=%d hp=%d/%d",
- m.key, m.x, m.y,
- max(abs(m.x-me.x), abs(m.y-me.y)), skill, me.hp, me.max_hp)
- def tick(self, conn: GatewayConnection, world: WorldMirror) -> TickResult:
- me = world.me
- if not me.known():
- return TickResult.RUNNING
- if self._map_name and me.map_name != self._map_name:
- _LOG.warning("[✗] 换图 %s(死亡回城?) — 退出: 击杀=%d 腿=%d",
- me.map_name, self.kills, self.legs)
- return TickResult.DONE
- if self._done:
- return TickResult.DONE
- # run14: hp=0 后仍出刀 4 次 — 死亡待机态屏蔽选怪/攻击/漫游,
- # 只等换图(死亡回城)触发上面的 DONE 退出
- if self._dead_wait:
- return TickResult.RUNNING
- now = self._now()
- # 事件: SC1 确认 / SC28 弹回标墙 / SC31 受击(强怪判定) / 死亡
- for e in world.events:
- if e.kind == "self_move_confirmed":
- self._pending = False
- elif e.kind == "me_die":
- _LOG.warning("[死亡] 击杀=%d 腿=%d — 停止一切, 等回城",
- self.kills, self.legs)
- self._target_key = None
- self._dead_wait = True
- self._roam_dest = None
- elif e.kind == "mon_die" and self._target_key == e.actor:
- self.kills += 1
- _ = self._hit_counter.pop(self._target_key, None)
- _LOG.info("[击杀✓] %s 累计 %d/%s", e.detail,
- self.kills, self._kill_goal or "∞")
- self._kill_wait_until = now + KILL_WAIT_S
- self._target_key = None
- if self._kill_goal and self.kills >= self._kill_goal:
- _LOG.info("[目标达成] %d 杀 — 收工", self.kills)
- self._done = True
- elif e.kind == "pull_back":
- self.rejected += 1
- self._pending = False
- if self._last_target_cell is not None:
- self._grid.mark_blocked(self._last_target_cell)
- elif e.kind == "struck" and e.actor:
- # SC31 结算反馈(actor=被打者):
- # actor=我 → by=攻击怪(强怪判定)
- # actor=目标怪 → 怪掉血了(hp 前进), 厚血怪继续打
- if str(e.actor) == world.me.uid:
- dmg = _strike_damage(e.detail)
- attacker = _strike_attacker(e.detail)
- if dmg > 0 and attacker:
- if dmg > self._hit_damage.get(attacker, 0):
- self._hit_damage[attacker] = dmg
- if dmg > BIG_HIT and attacker not in self._big_monsters:
- self._big_monsters.add(attacker)
- _LOG.warning("[强怪] #%d 单刀 %d > %d — 拉黑绕开",
- attacker, dmg, BIG_HIT)
- if self._target_key == attacker:
- self._target_key = None
- elif e.actor == self._target_key:
- # 目标怪掉血 = 攻击有效, 重置零结算计数(厚血怪一直打)
- self._hit_counter[e.actor] = 0
- world.events.clear()
- # 击杀等待窗口(AutoProxy SetAFKTargetDeath 0.2s): 等 loot/尸体,
- # 窗口内不选新怪不漫游, 窗口过后下一 tick 立即重扫
- if now < self._kill_wait_until:
- return TickResult.RUNNING
- # ★ 保命优先(Crystal AutoPot 在 Search 之前): 低血立即弃目标
- # 逃跑 — 放在战斗分支后会被 return 挡住永远走不到(run5 实证:
- # hp 39→15→8→0 打了 6 秒不撤, 死战)
- # run14 实证三重死循环: 逃跑点(向量*30)拉黑后同向量重算同点 →
- # 60ms 一轮"选怪→弃目标→换点→不可达", 24s 原地挨打致死。
- # 修复: fleeing 先算(选怪守卫用) + 逃跑点拉黑即换新点(垂直扰动)
- # + 脱离低血才恢复选怪。
- fleeing = me.max_hp > 0 and 0 < me.hp / me.max_hp < FLEE_RATIO
- if fleeing:
- if self._target_key is not None:
- _LOG.warning("[低血] %d/%d — 弃目标 #%d 逃跑",
- me.hp, me.max_hp, self._target_key)
- self._target_key = None
- if self._roam_dest is None or \
- self._roam_dest in self._roam_blacklist:
- nearest = self._pick_monster(world)
- if nearest is not None:
- # 4 方向确定性轮试(反怪向→垂直→垂直→顺怪向),
- # 跳过已拉黑点 — run14: 随机扰动有 50% 撞回同点
- fx = -1 if nearest.x < me.x else 1
- fy = -1 if nearest.y < me.y else 1
- for dx, dy in ((fx, fy), (fy, -fx), (-fy, fx), (-fx, -fy)):
- cand = (max(0, me.x + dx*30), max(0, me.y + dy*30))
- if cand not in self._roam_blacklist:
- self._roam_dest = cand
- _LOG.warning("[低血] 远离怪, 逃跑目标 %s", cand)
- break
- elif self._fleeing_last and self._roam_dest is not None:
- _LOG.info("[低血✓] 回血 %d/%d — 恢复战斗", me.hp, me.max_hp)
- self._roam_dest = None
- self._fleeing_last = fleeing
- # ★ 客户端 checkAutoFindTarget: 无目标才扫(有目标绝不换),
- # 全视野最近者胜 — 视野=服务器推送范围(SC11 雷达/SC10/SC14)
- # 逃跑中不选怪(run14: 选怪→弃目标 60ms 死循环)
- if self._target_key is None and not fleeing:
- m = self._pick_monster(world)
- if m is not None:
- self._target_key = int(m.key)
- self._pending = False # 立即打断漫游移动节拍
- self._roam_dest = None
- tag = "雷达" if m.name == "radar" else m.name
- _LOG.info("[选怪] #%d %s @(%d,%d) dist=%d 视野怪=%d — 转战斗",
- m.key, tag, m.x, m.y,
- max(abs(m.x - me.x), abs(m.y - me.y)),
- len(world.monsters))
- self._last_scan_log_t = now
- elif now - getattr(self, "_last_scan_log_t", 0.0) > 15.0:
- _LOG.info("[扫描] 无可打目标 视野怪=%d 黑名单=%d 位置=(%d,%d) "
- "漫游点=%s",
- len(world.monsters), len(self._big_monsters),
- me.x, me.y, self._roam_dest)
- self._last_scan_log_t = now
- # 1. 战斗(RequestLaunchCommand: 先走位后出刀, 永不并发)
- if self._target_key is not None:
- if self._tick_fight_move(conn, world, now):
- m = world.monsters.get(self._target_key)
- if m is not None and \
- max(abs(m.x-me.x), abs(m.y-me.y)) <= ATTACK_RADIUS \
- and now - self._last_attack_t >= ATTACK_INTERVAL:
- self._send_attack(conn, world)
- self._last_attack_t = now
- n = self._hit_counter.get(self._target_key, 0) + 1
- self._hit_counter[self._target_key] = n
- if n == 4:
- # 4 刀不中: 目标可能已移动 — 重锁让逼近链重新对齐
- _LOG.info("[重锁] #%d 连 %d 刀不中 — 重新对齐",
- self._target_key, n)
- self._target_key = None
- self._roam_dest = None
- elif n >= GIVEUP_HITS:
- _LOG.warning("[放弃目标] #%d %d 刀零结算 — 拉黑换怪",
- self._target_key, n)
- self._big_monsters.add(self._target_key)
- self._target_key = None
- self._roam_dest = None
- return TickResult.RUNNING
- self._target_key = None
- # 2. 怪窝撤离(仅高危图; 矿区 False)
- if DANGER_DENSITY:
- near = sum(1 for x in world.monsters.values()
- if not x.is_dead
- and max(abs(x.x-me.x), abs(x.y-me.y)) <= 4)
- if near >= 3 and self._target_key is None:
- if self._roam_dest is None or \
- max(abs(self._roam_dest[0]-me.x),
- abs(self._roam_dest[1]-me.y)) <= 3:
- self._roam_dest = self._farthest((me.x, me.y))[0]
- _LOG.warning("[怪窝] 4格内 %d 只 — 撤离至 %s", near,
- self._roam_dest)
- # 3. 漫游(客户端 checkAimlessMove): 随机中距点(±50, |dx|+|dy|>20),
- # 到达或无路换点 — 替代旧 BFS 最远点(钻隧道错过怪)
- if self._roam_dest is None or \
- max(abs(self._roam_dest[0]-me.x),
- abs(self._roam_dest[1]-me.y)) <= 3:
- self._roam_dest = self._new_roam_dest(me, now)
- _LOG.info("[漫游] 新随机点 %s", self._roam_dest)
- if self._pending and now - self._pending_t < 1.0:
- return TickResult.RUNNING
- if now - self._last_step_t < STEP_INTERVAL:
- return TickResult.RUNNING
- path = astar((me.x, me.y), self._roam_dest, self._grid, max_expand=100000)
- if not path or len(path) < 2:
- # run12 实证: 8s 换 4 个反向漫游点 — 不可达点立即置空导致振荡;
- # 失败点拉黑 30s, 换点时避开
- self._roam_blacklist[self._roam_dest] = now + 30.0
- _LOG.info("[漫游] %s 不可达 — 拉黑换点", self._roam_dest)
- self._roam_dest = None
- return TickResult.RUNNING
- nxt = path[1]
- dx = max(-1, min(1, nxt[0]-me.x))
- dy = max(-1, min(1, nxt[1]-me.y))
- d = next((i for i, (ox, oy) in enumerate(DIR_VECS) if ox == dx and oy == dy), 4)
- dist2 = 2 if max(abs(nxt[0]-me.x), abs(nxt[1]-me.y)) == 2 else 1
- from pbot.game.messages import protocol_map_name
- mn = protocol_map_name(me.map_id, me.map_name)
- fx, fy = me.x + dx*dist2, me.y + dy*dist2
- self._last_target_cell = (fx, fy)
- _ = conn.send_frame(3013 if dist2 == 2 else 3011, 0, d,
- (me.y << 16) | me.x, self._sid, mn.encode(),
- pre=(fy << 16) | fx)
- self._pending = True
- self._pending_t = now
- self._last_step_t = now
- self.legs += 1
- return TickResult.RUNNING
- def _new_roam_dest(self, me, now: float) -> tuple[int, int]:
- """随机中距漫游点(AssistProxy checkAimlessMove: ±50, 距离>20).
- farthest_fn 仅供高危图怪窝撤离调用, 漫游永远随机点 —
- BFS 最远点=钻隧道错过怪(run8 实证: 固定两点往返 96s 空窗).
- 漫游点钳制在地图包围盒内(墙集推得) — run9 实证负坐标点
- A* 必失败, 0.6s 连刷 5 个漫游点空转.
- """
- import random
- if self._map_bounds is None:
- xs = [c[0] for c in self._grid.blocked]
- ys = [c[1] for c in self._grid.blocked]
- # 真实地图墙集覆盖全图, 稀疏墙集(测试)兜底 ±100
- x1 = max(xs) if xs and max(xs) > 50 else min(me.x + 100, 199)
- y1 = max(ys) if ys and max(ys) > 50 else min(me.y + 100, 199)
- self._map_bounds = (0, 0, x1, y1)
- x0, y0, x1, y1 = self._map_bounds
- for t, until in list(self._roam_blacklist.items()):
- if now > until:
- del self._roam_blacklist[t]
- for _ in range(20):
- x = max(x0, min(x1, me.x + random.randint(-50, 50)))
- y = max(y0, min(y1, me.y + random.randint(-50, 50)))
- if (x, y) in self._grid.blocked:
- continue
- if abs(x - me.x) + abs(y - me.y) <= 20:
- continue
- if (x, y) in self._roam_blacklist:
- continue
- return (x, y)
- return (me.x, me.y)
- def parse_args() -> argparse.Namespace:
- """命令行."""
- ap = argparse.ArgumentParser()
- _ = ap.add_argument("-u", "--user", required=True)
- _ = ap.add_argument("-p", "--password", required=True)
- _ = ap.add_argument("--zone", type=int, required=True)
- _ = ap.add_argument("--name", required=True)
- _ = ap.add_argument("--minutes", type=float, default=20.0)
- return ap.parse_args()
- def main() -> int:
- """登录 → 蜈蚣洞穴 → 自动战斗 N 分钟 → 退出."""
- a = parse_args()
- setup_logging()
- banner("[0] 自动战斗实测: 区=%d 角色=%s 时长=%.0fmin",
- a.zone, a.name, a.minutes)
- http = requests.Session()
- http.trust_env = False
- http.verify = False
- jwt, uid = login(Account(username=a.user, password=a.password))
- _ = enter_game_client(http, jwt, ZoneId(a.zone))
- key32, sid, real_name, ticket_key, game_port = login_gate_with_retry(
- ZoneId(a.zone), jwt, uid, a.name, (8021, 8061, 8001, 8041))
- stop: list[bool] = [False]
- conn, world, updater = enter_world(ZoneId(a.zone), real_name, ticket_key,
- sid, game_port, uid, stop=stop)
- _LOG.info("[✓] 出生 map=%s @(%d,%d)", world.me.map_name, world.me.x,
- world.me.y)
- try:
- from pbot.game.heal import enable_auto_heal
- state = enable_auto_heal(conn, sid, want_on=True, world=world)
- _LOG.info("[喝药] %s", state)
- if world.me.map_name != CAVE_MAP:
- banner("[1] 传送蜈蚣洞穴")
- bh = CaveEnterBehavior(sid=sid, kill_target=0)
- r = run(conn, world, updater, bh, duration_s=300.0)
- if world.me.map_name != CAVE_MAP:
- _LOG.error("[✗] 传送失败 result=%s", r.value)
- return 1
- time.sleep(3.0)
- _ = pump(conn, world, updater, 2.0)
- banner("[2] 自动战斗 %.0f 分钟", a.minutes)
- af = AutoFightBehavior(sid=sid)
- r = run(conn, world, updater, af, duration_s=a.minutes * 60)
- _LOG.info("[结束] result=%s 击杀=%d 腿=%d 弹回=%d 位置=(%d,%d) hp=%d/%d",
- r.value, af.kills, af.legs, af.rejected,
- world.me.x, world.me.y, world.me.hp, world.me.max_hp)
- return 0
- finally:
- stop[0] = True
- conn.close()
- _LOG.info("连接已关闭")
- if __name__ == "__main__":
- try:
- sys.exit(main())
- except PbotError as e:
- _LOG.error("失败: %s", e.detail)
- sys.exit(1)
|