| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693 |
- """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
|