Просмотр исходного кода

feat(t02): 比奇无怪15s自动点【点我刷怪】NPC — CS1010开对话+SC643点确认(kill.py同源协议), 已有怪提示无副作用

wuzilong 1 месяц назад
Родитель
Сommit
dbcd518f8b
1 измененных файлов с 107 добавлено и 6 удалено
  1. 107 6
      pipeline_bot/pipeline/tasks/t02_biqi_kill20.py

+ 107 - 6
pipeline_bot/pipeline/tasks/t02_biqi_kill20.py

@@ -3,23 +3,102 @@
 传送链: KillBehavior renwu_mov,1(任务指引, 等待图/异图→比奇);
 战斗: AutoFightBehavior(客户端 RequestLaunchCommand 复刻 — 贴身出刀
 ATTACK_RADIUS=1, 4刀不中重锁, 10刀拉黑, 低血逃跑)。
-比奇是开放主城(非副本), 死亡回城后仍在比奇 → 轮次循环续杀。
+无怪刷怪: 视野 15s 无可打怪 → 走向【点我刷怪】NPC → CS1010 开对话 →
+SC643 → CS1011 点确认按钮(协议与 kill.py 刷怪链同源, 2026-09-27 实跑
+定案; 服务器若回"已有怪物"提示则无副作用, 回视野有怪即继续杀)。
 """
 from __future__ import annotations
 
+import json
+import re
 import time
 
 from pipeline.ctx import PipelineContext, TaskResult
 
+from pbot.world.actor import Npc
+
 TASK_TITLE = "T2 比奇大城杀怪20"
 
 KILL_TARGET = 20
 BIQI_MAP = "比奇大城"
 _MAX_ROUNDS = 6
 
+_LINK_RE: re.Pattern[str] = re.compile(r"link=@([^\|<>]+)")
+_NO_MON_SPAWN_AFTER_S = 15.0
+_SPAWN_GAP_S = 8.0
+_SPAWN_TALK_DIST = 3
+
+
+def _find_spawn_npc(world) -> "Npc | None":
+    """视野内名字含'刷怪'的 NPC(KillBehavior._find_spawn_npc 同源)。"""
+    for npc in world.npcs.values():
+        if "刷怪" in npc.name:
+            return npc
+    return None
+
+
+def _try_spawn(ctx: PipelineContext, af, last: dict) -> bool:
+    """无怪时点【点我刷怪】: True=已处理(本轮不必再漫游)。
+
+    链路: 走到 NPC 旁 → CS1010 开对话 → 下一 tick 收到 SC643 后点确认
+    按钮。'已经有怪'提示 = 对话无副作用直接返回, 战斗循环自然接管。
+    """
+    from pbot.bot.wander import DIRS as WDIRS
+    from pbot.game.messages import protocol_map_name
+
+    world = ctx.world
+    me = world.me
+    now = time.time()
+    dlg = world.npc_dialog
+    # 阶段2: 对话已开 → 点确认按钮(刷怪本尊链接)
+    if dlg is not None and not last.get("dlg_clicked") \
+            and now - last.get("dlg_open_t", 0.0) < 10.0:
+        smsg = str(dlg.get("sMsg", ""))
+        act = next((a for a in _LINK_RE.findall(smsg)
+                    if a not in ("exit",)), None)
+        if act is not None:
+            body = json.dumps({"UserID": dlg.get("UserID", "999999999"),
+                               "Act": f"@{act}",
+                               "index": dlg.get("index", 35)},
+                              separators=(",", ":")).encode()
+            _ = ctx.conn.send_frame(1011, 0, 0, 0, ctx.sid, body)
+            ctx.log.info("[T2·刷怪] 对话点按 @%s", act)
+        last["dlg_clicked"] = True
+        world.npc_dialog = None
+        last["spawn_cd"] = now + _SPAWN_GAP_S
+        return True
+    # 阶段1: 找 NPC → 走近 → 开对话
+    snpc = _find_spawn_npc(world)
+    if snpc is None:
+        return False
+    dist = max(abs(snpc.x - me.x), abs(snpc.y - me.y))
+    if dist > _SPAWN_TALK_DIST:
+        if now - last.get("step_t", 0.0) < 0.7:
+            return True
+        d = af._dir8(me.x, me.y, snpc.x, snpc.y)  # noqa: SLF001
+        dx, dy = WDIRS[d]
+        stride = 2 if dist > 4 else 1
+        mn = protocol_map_name(me.map_id, me.map_name)
+        _ = ctx.conn.send_frame(3013 if stride == 2 else 3011, 0, d,
+                                (me.y << 16) | me.x, ctx.sid, mn.encode(),
+                                pre=((me.y + dy * stride) << 16)
+                                | (me.x + dx * stride))
+        last["step_t"] = now
+        return True
+    if now >= last.get("spawn_cd", 0.0):
+        body = json.dumps({"UserID": snpc.aid, "index": snpc.index},
+                          separators=(",", ":")).encode()
+        _ = ctx.conn.send_frame(1010, 0, 0, 0, ctx.sid, body)
+        last["dlg_clicked"] = False
+        last["dlg_open_t"] = now
+        ctx.log.info("[T2·刷怪] 点NPC «%s» @(%d,%d)", snpc.name,
+                     snpc.x, snpc.y)
+        last["spawn_cd"] = now + _SPAWN_GAP_S
+    return True
+
 
 def run_task(ctx: PipelineContext) -> TaskResult:
-    """进比奇(renwu 链) → AutoFight 轮次杀满 20 只增量。"""
+    """进比奇(renwu 链) → AutoFight 轮次杀满 20 只; 无怪点刷怪 NPC。"""
     import sys
     from pathlib import Path
 
@@ -30,7 +109,7 @@ def run_task(ctx: PipelineContext) -> TaskResult:
 
     from pbot.bot.kill import KillBehavior
     from pbot.bot.pathfind import load_walls_from_map
-    from pbot.bot.runner import pump, run
+    from pbot.bot.runner import pump
     from pipeline.tasks.heal_guard import ensure_heal
 
     root = Path(__file__).resolve().parents[2]
@@ -67,11 +146,33 @@ def run_task(ctx: PipelineContext) -> TaskResult:
         need = KILL_TARGET - total
         af = AutoFightBehavior(sid=ctx.sid, map_name=BIQI_MAP, walls=walls,
                                farthest_fn=None, kill_goal=need)
-        r = run(ctx.conn, ctx.world, ctx.updater, af,
-                duration_s=max(600.0, need * 60.0))
+        # 无怪刷怪监督循环: 替代 run() 的裸驱动, 在 AutoFight 决策外围
+        # 注入"视野无怪 15s → 点刷怪 NPC"逻辑(有怪时 AutoFight 自己接管)
+        af.on_enter(ctx.conn, ctx.world)
+        last: dict[str, float | bool] = {"no_mon_t": 0.0, "spawn_cd": 0.0}
+        deadline = time.time() + max(600.0, need * 60.0)
+        while time.time() < deadline:
+            _ = pump(ctx.conn, ctx.world, ctx.updater, 0.05)
+            _ = af.tick(ctx.conn, ctx.world)
+            monsters = [m for m in ctx.world.monsters.values()
+                        if not m.is_dead
+                        and int(m.key) < 1_000_000]
+            if monsters or af._target_key is not None:  # noqa: SLF001
+                last["no_mon_t"] = 0.0
+                ctx.world.npc_dialog = None
+                continue
+            now = time.time()
+            if not last["no_mon_t"]:
+                last["no_mon_t"] = now
+                last["dlg_clicked"] = True  # 怪清零后允许重新开对话
+            elif now - float(last["no_mon_t"]) > _NO_MON_SPAWN_AFTER_S:
+                _ = _try_spawn(ctx, af, last)
+            if af._done:  # noqa: SLF001
+                break
         total += af.kills
+        r = "done" if af._done else "timeout"  # noqa: SLF001
         ctx.log.info("[T2] 第%d轮 result=%s 轮杀=%d 累计=%d/%d", rnd,
-                     r.value, af.kills, total, KILL_TARGET)
+                     r, af.kills, total, KILL_TARGET)
         if total >= KILL_TARGET:
             return TaskResult.done(f"比奇击杀 {total}/{KILL_TARGET}")
     return TaskResult.fail(f"{total}/{KILL_TARGET} 轮次耗尽")