"""KillBehavior 打怪行为验证(不发真实网络 — SpyConn 桩)。""" import json import time from pbot.bot.kill import HIT_INTERVAL_S, KillBehavior from pbot.bot.wander import WAIT_MAP from pbot.types import ActorId from pbot.world.actor import ActorKey, Monster, Npc from pbot.world.mirror import WorldMirror from tests.test_wander import SpyConn BIQI_MAP = "比奇大城" def biqi_world(x: int = 293, y: int = 619) -> WorldMirror: """已在比奇、SC50/SC51 就绪的世界。""" world = WorldMirror() world.me.map_name = BIQI_MAP world.me.map_id = "0" world.me.actor_key = 7 world.me.uid = "100861001020304" world.me.x, world.me.y = x, y return world def wait_world(x: int = 23, y: int = 27) -> WorldMirror: world = WorldMirror() world.me.map_name = WAIT_MAP world.me.actor_key = 7 world.me.x, world.me.y = x, y return world def dungeon_world(x: int = 198, y: int = 28) -> WorldMirror: """地牢一层东: 任务副本图, 全图坐标 y<50。""" world = WorldMirror() world.me.map_name = "地牢一层东" world.me.map_id = "地牢一层东" world.me.actor_key = 7 world.me.uid = "100861001020304" world.me.x, world.me.y = x, y return world def mk_monster(key: int, x: int, y: int) -> Monster: return Monster(key=ActorKey(key), x=x, y=y, race=1049, name="鸡", is_dead=False) def fresh_kill(sid: int, spy: SpyConn, world: WorldMirror, kill_target: int = 2) -> KillBehavior: b = KillBehavior(sid=sid, kill_target=kill_target, renwu_act="renwu_mov,2") b.on_enter(spy, world) # pyright: ignore[reportArgumentType] b._last_step_t = 0.0 # pyright: ignore[reportPrivateUsage] # 步进门已开 spy.sent.clear() # on_enter 的挂机声明 3 帧不计入各用例断言 spy.pres.clear() return b def die(world: WorldMirror, key: int) -> None: """模拟服务器 SC32: 置死 + 发事件(与 update.py 行为一致)。""" m = world.monsters.pop(key, None) if m is not None: world.monsters[key] = Monster(key=m.key, x=m.x, y=m.y, race=m.race, name=m.name, is_dead=True) world.record("mon_die", m.name if m else "?", key) def sent_ids(spy: SpyConn) -> list[int]: return [s[0] for s in spy.sent] # ---- 传送链: 等待图 → 比奇 ---- def test_kill_already_in_quest_map_skips_renwu_chain() -> None: """Given: 已在任务副本图(地牢/洞穴) When: on_enter Then: 直接进 fight, 不发 3031/1011。 回归: 萌新3035 在地牢一层东跑图时, 旧 on_enter 无条件进 renwu_3031 → 触发 CS3031+CS1011 → 服务器可能重新传送/静默 → 角色被卡或挪点。 新逻辑: _arrived_quest_map 判定下直接进 fight, 配 LAND_QUIET_S 静置。 """ world = dungeon_world(198, 28) spy = SpyConn() b = KillBehavior(sid=9, kill_target=2, renwu_act="renwu_mov,2") b.on_enter(spy, world) # pyright: ignore[reportArgumentType] assert b._phase == "fight" # pyright: ignore[reportPrivateUsage] # 不发任务链帧(只有挂机声明 3 帧) assert sent_ids(spy) == [3199, 5286, 9102] # 落地静置: 0.5s 内不发任何移动/攻击帧 b._last_step_t = 0.0 # pyright: ignore[reportPrivateUsage] _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert sent_ids(spy) == [3199, 5286, 9102] # 仍只挂机声明 def test_kill_from_wait_map_sends_3031_then_renwu_1011() -> None: """Given: 出生等待图 When: on_enter+两拍 Then: 先 3031 后 renwu_mov 1011。""" world = wait_world() spy = SpyConn() b = fresh_kill(sid=9, spy=spy, world=world) _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] # phase_t 刚置位, 0.2s 门未到 → 无帧 assert sent_ids(spy) == [] b._phase_t -= 0.3 # pyright: ignore[reportPrivateUsage] _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] # 发 3031 assert sent_ids(spy) == [3031] b._phase_t -= 1.2 # pyright: ignore[reportPrivateUsage] _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] # 等 1s 后发 1011 renwu_mov ids = sent_ids(spy) assert 1011 in ids f1011 = next(s for s in spy.sent if s[0] == 1011) body: dict[str, object] = json.loads(f1011[5]) # pyright: ignore[reportAny] assert body["Act"] == "@renwu_mov,2" # 等待图无传送: 不预发比奇 ACK(5012/5027) — 只在异图 renwu 时预发 assert 5012 not in ids def test_kill_biqi_ack_uses_protocol_name_zero() -> None: """Given: 等待图点接待NPC When: 发 CS1010 Then: 比奇 ACK body='0'(协议图名)。""" world = wait_world() world.npcs[501] = Npc(key=ActorKey(501), aid=ActorId("10032"), name="新手接待员", x=23, y=27, index=35) spy = SpyConn() b = fresh_kill(sid=9, spy=spy, world=world) b._phase_t -= 1.0 # pyright: ignore[reportPrivateUsage] # 跳过 3031 前置 b._phase = "npc_click" # pyright: ignore[reportPrivateUsage] b._npc_aid = "10032" # pyright: ignore[reportPrivateUsage] b._npc_idx_cands = [35] # pyright: ignore[reportPrivateUsage] b._phase_t -= 0.5 # pyright: ignore[reportPrivateUsage] _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] ids = sent_ids(spy) assert 1010 in ids assert ids.count(5012) == 4 and ids.count(5027) == 2 # 2轮 ACK: 每轮 5012x2+5027 f5027 = next(s for s in spy.sent if s[0] == 5027) assert f5027[5] == b"0" # 比奇协议图名(不是显示名) cth = [s for s in spy.sent if s[0] == 5012 and s[4] == 32611848] assert len(cth) == 2 and cth[0][5] == b"cTh" # ---- 战斗: 选怪 → CS3014 → SC32 计杀 ---- def test_kill_sends_3014_with_exact_body_layout() -> None: """Given: 比奇视野有怪 When: tick Then: 攻击帧 3014 body 与 backup 实证同构。""" world = biqi_world(293, 620) world.monsters[123] = mk_monster(123, 293, 621) spy = SpyConn() b = fresh_kill(sid=9, spy=spy, world=world) b._phase = "fight" # pyright: ignore[reportPrivateUsage] b._target_key = 123 # pyright: ignore[reportPrivateUsage] _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert sent_ids(spy) == [3014] msg_id, recog, p1, p2, p3, body = spy.sent[0] assert (msg_id, recog, p1, p2, p3) == (3014, 0, 0, 0, 0) payload: dict[str, object] = json.loads(body) # pyright: ignore[reportAny] assert payload == {"skillID": 0, "dir": 4, "srcX": 293, "srcY": 620, "destX": 293, "destY": 621, "targetID": "123"} def test_kill_hit_interval_throttles() -> None: """Given: 刚攻击过 When: 立即再 tick Then: 不连发(HitTime 0.65s 节流)。""" world = biqi_world() world.monsters[1] = mk_monster(1, 293, 620) spy = SpyConn() b = fresh_kill(sid=9, spy=spy, world=world) b._phase = "fight" # pyright: ignore[reportPrivateUsage] b._target_key = 1 # pyright: ignore[reportPrivateUsage] _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert len(spy.sent) == 1 b._last_hit_t = time.time() - HIT_INTERVAL_S + 0.2 # pyright: ignore[reportPrivateUsage] _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert len(spy.sent) == 1 # 节流窗口内, 零发包 def test_kill_counts_only_owned_sc32() -> None: """Given: 6s 内攻击过 aid=123 When: mon_die 事件 Then: kills+1; 未攻击怪不计。""" world = biqi_world() world.monsters[123] = mk_monster(123, 293, 620) world.monsters[999] = mk_monster(999, 300, 630) spy = SpyConn() b = fresh_kill(sid=9, spy=spy, world=world, kill_target=2) b._phase = "fight" # pyright: ignore[reportPrivateUsage] b._target_key = 123 # pyright: ignore[reportPrivateUsage] _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] # 攻击 123 → 归属登记 die(world, 123) # 没打过的 999 也死 → 不计 die(world, 999) assert world.kills == 0 _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert world.kills == 1 # 超归属窗口的击杀不计 b._my_hits[555] = time.time() - 60.0 # pyright: ignore[reportPrivateUsage] world.record("mon_die", "鹿", 555) _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert world.kills == 1 def test_kill_reaches_target_count_and_done() -> None: """Given: kill_target=2 已计 1 When: 第二杀入账 When: tick → DONE。""" world = biqi_world() world.monsters[1] = mk_monster(1, 293, 620) spy = SpyConn() b = fresh_kill(sid=9, spy=spy, world=world, kill_target=2) b._phase = "fight" # pyright: ignore[reportPrivateUsage] b._target_key = 1 # pyright: ignore[reportPrivateUsage] _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] die(world, 1) _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert world.kills == 1 assert sent_ids(spy).count(3014) == 1 # 第二只: 攻击→死→tick 应 DONE world.monsters[2] = mk_monster(2, 293, 620) b._last_hit_t = 0.0 # pyright: ignore[reportPrivateUsage] _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] die(world, 2) result = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert result.name == "DONE" def test_kill_target_filter_biqi_coordinates_only() -> None: """Given: 怪 y=30(等待图残留)与 y=620(比奇) When: 选怪 Then: 只选比奇怪。""" world = biqi_world(293, 620) world.monsters[10] = mk_monster(10, 293, 30) world.monsters[11] = mk_monster(11, 294, 622) spy = SpyConn() b = fresh_kill(sid=9, spy=spy, world=world) b._phase = "fight" # pyright: ignore[reportPrivateUsage] _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] m = b._current_target(world) # pyright: ignore[reportPrivateUsage] assert m is not None and int(m.key) == 11 def test_kill_walks_when_out_of_attack_radius() -> None: """Given: 怪在 5 格外 When: tick Then: 跑近(3013) 而非攻击。""" world = biqi_world() world.monsters[7] = mk_monster(7, 293, 625) # dist=5 spy = SpyConn() b = fresh_kill(sid=9, spy=spy, world=world) b._phase = "fight" # pyright: ignore[reportPrivateUsage] _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert sent_ids(spy) == [3013] def test_kill_no_monster_spawns_via_npc() -> None: """Given: 视野无怪有【点我刷怪】NPC 且冷却已过 When: tick Then: 点 NPC(1010)。""" world = biqi_world() world.npcs[900] = Npc(key=ActorKey(900), aid=ActorId("777"), name="点我刷怪", x=293, y=620, index=12) spy = SpyConn() b = fresh_kill(sid=9, spy=spy, world=world) b._phase = "fight" # pyright: ignore[reportPrivateUsage] b._no_mon_since = time.time() - 5.0 # pyright: ignore[reportPrivateUsage] b._spawn_cd_t = time.time() - 10.0 # pyright: ignore[reportPrivateUsage] _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] f1010 = next(s for s in spy.sent if s[0] == 1010) body: dict[str, object] = json.loads(f1010[5]) # pyright: ignore[reportAny] assert body == {"UserID": "777", "index": 12} assert sent_ids(spy).count(3014) == 0 def test_kill_death_waits_revive_then_resumes() -> None: """Given: 战斗中自己死亡 When: me_die→me_revive Then: 清视野→goto 重走。""" world = biqi_world(293, 610) # 离杀怪区 12 格 world.monsters[1] = mk_monster(1, 293, 612) spy = SpyConn() b = fresh_kill(sid=9, spy=spy, world=world) b._phase = "fight" # pyright: ignore[reportPrivateUsage] b._target_key = 1 # pyright: ignore[reportPrivateUsage] world.record("me_die") _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert b._phase == "dead" # pyright: ignore[reportPrivateUsage] world.record("me_revive") _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert b._phase == "goto" # pyright: ignore[reportPrivateUsage] assert len(world.monsters) == 0 # 复活清视野 def test_kill_sc31_lethal_credit_only_by_me() -> None: """Given: SC31 Hp=0 AttackUserID=me When: tick Then: 计杀; 他人攻击不计。""" world = biqi_world() world.me.uid = "14537716001790385545000065553" world.monsters[123] = mk_monster(123, 293, 620) spy = SpyConn() b = fresh_kill(sid=9, spy=spy, world=world) b._phase = "fight" # pyright: ignore[reportPrivateUsage] b._target_key = 123 # pyright: ignore[reportPrivateUsage] world.record("struck", "123 hp=0/25 by=14537716001790385545000065553", 123) _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert world.kills == 1 # 他人击杀: 攻击者 uid 不匹配 → 不计 world.monsters[124] = mk_monster(124, 294, 620) world.record("struck", "124 hp=0/25 by=99999999999999999999999999", 124) _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert world.kills == 1 def test_kill_sc31_non_lethal_ignored() -> None: """Given: SC31 Hp>0 When: tick Then: 不计杀, 怪未标死。""" world = biqi_world() world.me.uid = "14537716001790385545000065553" world.monsters[123] = mk_monster(123, 293, 620) spy = SpyConn() b = fresh_kill(sid=9, spy=spy, world=world) b._phase = "fight" # pyright: ignore[reportPrivateUsage] world.record("struck", "123 hp=20/25 by=14537716001790385545000065553", 123) _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert world.kills == 0 assert not world.monsters[123].is_dead def test_kill_declares_guaji_on_enter() -> None: """Given: on_enter When: 声明 Then: 发 3199@startguaji+5286+9102 三连。""" world = biqi_world() spy = SpyConn() b = KillBehavior(sid=9, kill_target=2) b.on_enter(spy, world) # pyright: ignore[reportArgumentType] ids = sent_ids(spy) assert ids == [3199, 5286, 9102] f3199 = next(s for s in spy.sent if s[0] == 3199) body: dict[str, object] = json.loads(f3199[5]) # pyright: ignore[reportAny] assert body["act"] == "@startguaji" assert spy.sent[2][1] == 1 # 9102 recog=1 def test_kill_reroll_disabled_keeps_wandering() -> None: """Given: allow_reroll=False 无怪超 45s When: tick Then: 不发 3031, 漫游跑图。""" world = biqi_world() spy = SpyConn() b = KillBehavior(sid=9, kill_target=2, renwu_act="renwu_mov,2", allow_reroll=False) b.on_enter(spy, world) # pyright: ignore[reportArgumentType] b._phase = "fight" # pyright: ignore[reportPrivateUsage] b._no_mon_since = time.time() - 50.0 # pyright: ignore[reportPrivateUsage] b._last_step_t = 0.0 # pyright: ignore[reportPrivateUsage] spy.sent.clear() _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] ids = sent_ids(spy) assert 3031 not in ids assert 1011 not in ids # 漫游走/跑一步刷新视野 assert any(i in (3011, 3013) for i in ids) def test_kill_waypoint_runs_toward_point_then_holds() -> None: """Given: waypoint=(300,619) 在东方无怪 When: tick Then: 向东跑不重掷落点。 回归: PBOT_WAYPOINT — 无怪时应先赶到指定点位附近再游走找怪, 而非随机漫游或重走 renwu 链。 """ world = biqi_world(293, 619) spy = SpyConn() b = KillBehavior(sid=9, kill_target=2, renwu_act="renwu_mov,2", allow_reroll=False, waypoint=(300, 619)) b.on_enter(spy, world) # pyright: ignore[reportArgumentType] b._phase = "fight" # pyright: ignore[reportPrivateUsage] b._no_mon_since = time.time() - 50.0 # pyright: ignore[reportPrivateUsage] b._last_step_t = 0.0 # pyright: ignore[reportPrivateUsage] spy.sent.clear() _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] run = next(f for f in spy.sent if f[0] in (3011, 3013)) assert run[2] == 2 # p1=heading: 向东(DIRS[2]=(+1,0)) def test_kill_waypoint_arrives_switches_to_global_search() -> None: """Given: 赶到 waypoint 6 格内 When: tick Then: 弃点位切全局探索。 回归: 旧逻辑驻留小圈游走, 边界处赶点/游走互相拉扯产生 (198,30)↔(200,28) A↔B 跨界振荡。人类玩法: 到点后直接满图跑。 """ world = biqi_world(299, 620) spy = SpyConn() b = KillBehavior(sid=9, kill_target=2, renwu_act="renwu_mov,2", allow_reroll=False, waypoint=(300, 619)) b.on_enter(spy, world) # pyright: ignore[reportArgumentType] b._phase = "fight" # pyright: ignore[reportPrivateUsage] b._no_mon_since = time.time() - 50.0 # pyright: ignore[reportPrivateUsage] b._last_step_t = 0.0 # pyright: ignore[reportPrivateUsage] spy.sent.clear() _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] # 已到点: waypoint 被弃, 转全局探索(设了新探索锚点) assert b._waypoint is None # pyright: ignore[reportPrivateUsage] assert b._search_anchor is not None # pyright: ignore[reportPrivateUsage] def test_kill_dungeon_finite_target_uses_novel_anchor() -> None: """Given: 任务图 kill=100 无怪 When: 选锚点 Then: 走新颖度分支非 bound。 回归: 新颖度锚点曾仅 land_only/kill>9998 生效, 有限目标在矿区 走 bound 默认±25 且 x≤220/y≤50 钳制(旧新手图遗产), 跑图钉死角落。 """ world = dungeon_world(85, 18) spy = SpyConn() b = KillBehavior(sid=9, kill_target=100, renwu_act="renwu_mov,2", allow_reroll=False) b.on_enter(spy, world) # pyright: ignore[reportArgumentType] assert b._novel_anchor_mode() # pyright: ignore[reportPrivateUsage] b._phase = "fight" # pyright: ignore[reportPrivateUsage] b._phase_t = 0.0 # pyright: ignore[reportPrivateUsage] # 跳过落地静置 b._no_mon_since = time.time() - 50.0 # pyright: ignore[reportPrivateUsage] b._last_step_t = 0.0 # pyright: ignore[reportPrivateUsage] spy.sent.clear() _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] anchor = b._search_anchor # pyright: ignore[reportPrivateUsage] assert anchor is not None def test_kill_biqi_finite_target_keeps_bound_anchor() -> None: """Given: 比奇大城 kill=100 When: 选锚点 Then: 仍走 bound 分支防跑丢。""" world = biqi_world(293, 619) spy = SpyConn() b = KillBehavior(sid=9, kill_target=100, renwu_act="renwu_mov,2") b.on_enter(spy, world) # pyright: ignore[reportArgumentType] assert not b._novel_anchor_mode() # pyright: ignore[reportPrivateUsage] def test_kill_low_hp_in_dungeon_flees() -> None: """Given: 任务图血 20/100 且有目标 When: tick Then: 弃目标进 flee, 目标冷却, 移动跑远。""" world = dungeon_world(198, 28) world.me.hp, world.me.max_hp = 20, 100 world.monsters[100] = mk_monster(100, 200, 30) spy = SpyConn() b = KillBehavior(sid=9, kill_target=2, renwu_act="renwu_mov,2") b.on_enter(spy, world) # pyright: ignore[reportArgumentType] b._phase = "fight" # pyright: ignore[reportPrivateUsage] b._phase_t = 0.0 # pyright: ignore[reportPrivateUsage] b._target_key = 100 # pyright: ignore[reportPrivateUsage] b._last_step_t = 0.0 # pyright: ignore[reportPrivateUsage] spy.sent.clear() _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert b._phase == "flee" # pyright: ignore[reportPrivateUsage] assert b._retry_after.get(100) is not None # pyright: ignore[reportPrivateUsage] _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert any(i in (3011, 3013) for i in sent_ids(spy)) def test_kill_flee_resumes_when_healed() -> None: """Given: flee 中血回 60/100 When: tick Then: 复战回 fight。""" world = dungeon_world(198, 28) world.me.hp, world.me.max_hp = 60, 100 spy = SpyConn() b = KillBehavior(sid=9, kill_target=2, renwu_act="renwu_mov,2") b.on_enter(spy, world) # pyright: ignore[reportArgumentType] b._phase = "flee" # pyright: ignore[reportPrivateUsage] spy.sent.clear() _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert b._phase == "fight" # pyright: ignore[reportPrivateUsage] def test_kill_low_hp_in_town_does_not_flee() -> None: """Given: 主城血 20/100(安全区) When: tick Then: 不进 flee。""" world = biqi_world(293, 619) world.me.hp, world.me.max_hp = 20, 100 spy = SpyConn() b = KillBehavior(sid=9, kill_target=2, renwu_act="renwu_mov,2") b.on_enter(spy, world) # pyright: ignore[reportArgumentType] b._phase = "fight" # pyright: ignore[reportPrivateUsage] spy.sent.clear() _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert b._phase == "fight" # pyright: ignore[reportPrivateUsage] def test_kill_elite_never_targeted_and_radius_avoided() -> None: """Given: #100 已标记精英, #101 在其 12 格内, #102 在 15 格外 When: 选怪 Then: 只选 #102。""" world = dungeon_world(198, 28) world.monsters[100] = mk_monster(100, 190, 30) world.monsters[101] = mk_monster(101, 195, 32) world.monsters[102] = mk_monster(102, 207, 33) spy = SpyConn() b = KillBehavior(sid=9, kill_target=50, renwu_act="renwu_mov,2") b.on_enter(spy, world) # pyright: ignore[reportArgumentType] b._elite.add(100) # pyright: ignore[reportPrivateUsage] m = b._pick_target(world) # pyright: ignore[reportPrivateUsage] assert m is not None and int(m.key) == 102 def test_kill_heavy_strike_marks_elite() -> None: """Given: 我被打单刀 70/305(≥15%) When: 事件吸收 Then: 攻击者(by)进 _elite。""" world = dungeon_world(198, 28) world.me.max_hp = 305 spy = SpyConn() b = KillBehavior(sid=9, kill_target=50, renwu_act="renwu_mov,2") b.on_enter(spy, world) # pyright: ignore[reportArgumentType] b._world = world # pyright: ignore[reportPrivateUsage] world.record("struck", f"{world.me.actor_key} hp=235/305 dmg=70 by=999", 999) b._absorb_events(world) # pyright: ignore[reportPrivateUsage] assert 999 in b._elite # pyright: ignore[reportPrivateUsage] def test_kill_light_strike_not_elite() -> None: """Given: 我被打单刀 8/305(<15%) When: 事件吸收 Then: 不标记。""" world = dungeon_world(198, 28) world.me.max_hp = 305 spy = SpyConn() b = KillBehavior(sid=9, kill_target=50, renwu_act="renwu_mov,2") b.on_enter(spy, world) # pyright: ignore[reportArgumentType] world.record("struck", f"{world.me.actor_key} hp=297/305 dmg=8 by=999", 999) b._absorb_events(world) # pyright: ignore[reportPrivateUsage] assert 999 not in b._elite # pyright: ignore[reportPrivateUsage] def test_kill_my_strike_on_monster_not_elite() -> None: """Given: 我打怪出重刀(受害者=怪) When: 事件吸收 Then: 我不被标记(方向校验)。""" world = dungeon_world(198, 28) world.me.max_hp = 305 spy = SpyConn() b = KillBehavior(sid=9, kill_target=50, renwu_act="renwu_mov,2") b.on_enter(spy, world) # pyright: ignore[reportArgumentType] world.record("struck", "123 hp=0/230 dmg=50 by=" + world.me.uid, 123) b._absorb_events(world) # pyright: ignore[reportPrivateUsage] assert not b._elite # pyright: ignore[reportPrivateUsage] def test_kill_current_elite_target_triggers_flee() -> None: """Given: 交战中目标被标记精英 When: tick Then: 弃战进 flee。""" world = dungeon_world(198, 28) world.me.hp, world.me.max_hp = 300, 305 world.monsters[100] = mk_monster(100, 200, 30) spy = SpyConn() b = KillBehavior(sid=9, kill_target=50, renwu_act="renwu_mov,2") b.on_enter(spy, world) # pyright: ignore[reportArgumentType] b._phase = "fight" # pyright: ignore[reportPrivateUsage] b._phase_t = 0.0 # pyright: ignore[reportPrivateUsage] b._target_key = 100 # pyright: ignore[reportPrivateUsage] b._elite.add(100) # pyright: ignore[reportPrivateUsage] b._last_step_t = 0.0 # pyright: ignore[reportPrivateUsage] spy.sent.clear() _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert b._phase == "flee" # pyright: ignore[reportPrivateUsage] def test_map_switch_resets_grid() -> None: """Given: 3.map 已载入(墙格 A) When: 切图 d401 Then: GRID 重置, 只剩新图墙。 回归: GRID 全局单例跨图累积 — 盟重96779+d401 4202 并集封死可走区, 3039 次 A* 无路热循环(v5 实证)。 """ from pbot.bot.kill import GRID world = dungeon_world(198, 28) spy = SpyConn() b = KillBehavior(sid=9, kill_target=2, renwu_act="renwu_mov,2") b.on_enter(spy, world) # pyright: ignore[reportArgumentType] GRID.mark_blocked((999, 999)) # 模拟上一图遗留墙格 world.me.map_name = "比奇大城" # 切图 world.me.map_id = "0" b._loaded_maps.discard("比奇大城") # pyright: ignore[reportPrivateUsage] b._load_map_if_needed(world) # pyright: ignore[reportPrivateUsage] assert (999, 999) not in GRID.blocked def test_novel_anchor_skips_blocked_cells() -> None: """Given: 候选锚点带大量墙格 When: 换锚 Then: 选出的锚点不在墙格集。""" from pbot.bot.kill import GRID world = dungeon_world(50, 50) spy = SpyConn() b = KillBehavior(sid=9, kill_target=100, renwu_act="renwu_mov,2") b.on_enter(spy, world) # pyright: ignore[reportArgumentType] for x in range(20, 81): for y in range(20, 81): GRID.mark_blocked((x, y)) b._phase = "fight" # pyright: ignore[reportPrivateUsage] b._no_mon_since = time.time() - 50.0 # pyright: ignore[reportPrivateUsage] b._last_step_t = 0.0 # pyright: ignore[reportPrivateUsage] spy.sent.clear() for _ in range(10): b._search_anchor = None # pyright: ignore[reportPrivateUsage] _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] anchor = b._search_anchor # pyright: ignore[reportPrivateUsage] if anchor is not None: assert anchor not in GRID.blocked GRID.reset() def test_kill_waypoint_wall_detour_changes_direction() -> None: """Given: 赶点时 SC28 拉回(撞墙) When: 再 tick Then: 禁行格子并重规划。 回归: 地牢一层东 @(186,29) 赶点 dir=2 连发 30+ 次全 SC28 拒绝。 新架构: 弹回格 mark_blocked + A* 重新规划 — 不再原地硬撞。 """ from pbot.bot.kill import GRID world = biqi_world(299, 620) spy = SpyConn() b = KillBehavior(sid=9, kill_target=2, renwu_act="renwu_mov,2", allow_reroll=False, waypoint=(400, 620)) b.on_enter(spy, world) # pyright: ignore[reportArgumentType] b._phase = "fight" # pyright: ignore[reportPrivateUsage] b._no_mon_since = time.time() - 50.0 # pyright: ignore[reportPrivateUsage] b._last_step_t = 0.0 # pyright: ignore[reportPrivateUsage] spy.sent.clear() _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert spy.sent[-1][2] == 2 # 第一刀朝东 # 模拟服务器 SC28 拉回: 位置不变 + pull_back 事件(= 撞墙) world.record("pull_back", "old=(299,620) server=(299,620)") b._last_step_t = 0.0 # pyright: ignore[reportPrivateUsage] spy.sent.clear() _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert b._rejected == 0 # pyright: ignore[reportPrivateUsage] # 已消费 # 撞墙后的东向目标格被标记禁行 — 后续规划会避开 blocked_now = len(GRID.blocked) assert blocked_now >= 1 run = next(f for f in spy.sent if f[0] in (3011, 3013)) assert run[2] != 2 # 不再朝东硬撞 — 已换新路径绕行 def test_kill_dungeon_monster_low_y_is_selectable() -> None: """Given: 地牢一层东(任务副本, 全图 y<50) 雷达怪 When: tick Then: 选怪攻击。 回归: BIQI_MIN_Y=50 过滤把地牢全图坐标的怪当'等待图残留'丢弃 → 眼前有怪也 0 选怪, 只会朝 waypoint 硬冲。人类玩家看见就打。 """ world = dungeon_world(198, 28) world.monsters[51] = Monster(key=ActorKey(51), x=200, y=30, race=0, name="51", is_dead=False) spy = SpyConn() b = KillBehavior(sid=9, kill_target=2, renwu_act="renwu_mov,2", allow_reroll=False, waypoint=(205, 29)) b.on_enter(spy, world) # pyright: ignore[reportArgumentType] b._phase = "fight" # pyright: ignore[reportPrivateUsage] b._phase_t = 0.0 # pyright: ignore[reportPrivateUsage] # 跳过 LAND_QUIET_S b._last_step_t = 0.0 # pyright: ignore[reportPrivateUsage] # 步进门开 _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] assert 3014 in sent_ids(spy) # 直接开打, 不是绕路 def test_kill_never_targets_player_sized_ids() -> None: """Given: 怪表里混入玩家短ID(7位数) When: 选怪 Then: 拒绝选大数 ID。 回归: SC14 把玩家攻击广播误入怪表(2293955), bot 连砍 90 刀打真人。 玩家角色与宠物永不攻击。 """ world = dungeon_world(198, 28) world.monsters[2293955] = Monster(key=ActorKey(2293955), x=200, y=30, race=0, name="0", is_dead=False) world.monsters[51] = Monster(key=ActorKey(51), x=200, y=30, race=0, name="51", is_dead=False) b = KillBehavior(sid=9, kill_target=2, renwu_act="renwu_mov,2", allow_reroll=False) b.on_enter(SpyConn(), world) # pyright: ignore[reportArgumentType] m = b._pick_target(world) # pyright: ignore[reportPrivateUsage] assert m is not None and int(m.key) == 51 def test_kill_waypoint_detour_prefers_on_course() -> None: """Given: 赶点路径被 NPC 占位 When: 避障 Then: 挑离目标最近的绕行向。 回归: (200,30)↔(198,28) 永久振荡 — 避障固定取 side[0], 两点互为对方回退向。 """ world = dungeon_world(198, 28) world.npcs[51] = Npc(key=ActorKey(51), aid=ActorId("777"), name="NPC", x=201, y=28, index=1) spy = SpyConn() b = KillBehavior(sid=9, kill_target=2, renwu_act="renwu_mov,2", allow_reroll=False, waypoint=(205, 29)) b.on_enter(spy, world) # pyright: ignore[reportArgumentType] b._phase = "fight" # pyright: ignore[reportPrivateUsage] b._phase_t = time.time() - 100.0 # pyright: ignore[reportPrivateUsage] b._last_step_t = 0.0 # pyright: ignore[reportPrivateUsage] spy.sent.clear() _ = b.tick(spy, world) # pyright: ignore[reportArgumentType] run = next(f for f in spy.sent if f[0] in (3011, 3013)) # 绕行向必须仍朝目标方向带推进: dx 分量 >= 0(东), 不能选西向回退 assert run[2] in (0, 1, 2, 3, 4) # 拒绝 5(左下) 6(左) 7(左上) def test_update_sc10_actor_coords_live_in_p1() -> None: """Given: SC10 body.info[0]=ID + X/Y(2026-09-30 cap_live2 定案) When: apply Then: 怪坐标正确可选。 布局翻案: 本服 SC10 key=body.info[0], 坐标=body.X/Y; 帧头 p1=(y<<16)|x 只是坐标回显。旧定案(p1 当实体ID)是 run7 "打坐标数字零结算"根因。 """ import json as _json from pbot.codec.frame import Frame from pbot.world.update import WorldUpdater body = _json.dumps({"cs": 0, "info": ["44871", "稻草人", "稻草人"], "X": 327, "Y": 331, "race": 1049}, separators=(",", ":")).encode() f = Frame(msg_id=10, recog=1049, p1=(331 << 16) | 327, p2=0, p3=48, pre=0, tail=0, body=body) world = WorldMirror() WorldUpdater().apply(world, f) m = world.monsters.get(44871) assert m is not None assert (m.x, m.y) == (327, 331) assert m.race == 1049