完善客户端功能并同步差距文档
This commit is contained in:
+255
-6
@@ -18,18 +18,28 @@ class FakeClient extends Node:
|
||||
signal points_changed(points: Dictionary)
|
||||
signal target_info(vid: int, hp_percent: int)
|
||||
signal observer_mode_changed(enabled: bool)
|
||||
signal fishing_event(subheader: int, info: int, dir: int)
|
||||
signal chat(type: int, vid: int, text: String)
|
||||
var ents := {}
|
||||
var main := 0
|
||||
var in_game := true
|
||||
var calls := {"move": [], "attack": [], "set_target": [], "click_npc": []}
|
||||
var ground_items := []
|
||||
var skills := []
|
||||
var calls := {"move": [], "attack": [], "set_target": [], "click_npc": [],
|
||||
"pickup": [], "fly": [], "fishing": []}
|
||||
func is_in_game() -> bool: return in_game
|
||||
func get_main_vid() -> int: return main
|
||||
func get_entity(vid: int) -> Dictionary: return ents.get(vid, {})
|
||||
func get_entities() -> Array: return ents.values()
|
||||
func move(f, a, rot, x, y) -> bool: calls.move.append([f, a, rot, x, y]); return true
|
||||
func attack(motion, vid) -> bool: calls.attack.append([motion, vid]); return true
|
||||
func set_target(vid) -> bool: calls.set_target.append(vid); return true
|
||||
func click_npc(vid) -> bool: calls.click_npc.append(vid); return true
|
||||
func pickup_item(vid) -> bool: calls.pickup.append(vid); return true
|
||||
func add_fly_targeting(vid, x, y) -> bool: calls.fly.append([vid, x, y]); return true
|
||||
func fishing(rot_deg) -> bool: calls.fishing.append(rot_deg); return true
|
||||
func get_ground_items() -> Array: return ground_items
|
||||
func get_skills() -> Array: return skills
|
||||
|
||||
class FakePC extends Node:
|
||||
signal target_selected(node: Node3D)
|
||||
@@ -37,7 +47,10 @@ class FakePC extends Node:
|
||||
signal anim_state(state: String)
|
||||
var player: Node3D
|
||||
var server_speed := 1.0
|
||||
var walk_calls := []
|
||||
var frozen := false
|
||||
func set_server_speed(moving_speed: int) -> void: server_speed = float(moving_speed) / 100.0
|
||||
func walk_to(p: Vector3) -> void: walk_calls.append(p)
|
||||
|
||||
class FakeHud extends Node:
|
||||
var vitals := []
|
||||
@@ -87,7 +100,41 @@ func _run() -> void:
|
||||
fc.entity_main_set.emit(1000)
|
||||
fc.entity_info.emit(1000, fc.ents[1000])
|
||||
_ck(is_equal_approx(pc.server_speed, 1.5), "GC_CHANGE_SPEED -> local prediction speed scale")
|
||||
_ck(is_equal_approx(np.attack_period, 0.4), "entity attack_speed -> attack cadence")
|
||||
# §3.3:attack_speed 只喂攻速系数(缩放播放速率),不再在收包时把 period 直接写死
|
||||
_ck(is_equal_approx(np._atk_speed_factor, 1.5), "entity attack_speed -> attack-speed factor")
|
||||
_ck(is_equal_approx(np._current_attack_period(), np.DEFAULT_ATTACK_PERIOD / 1.5),
|
||||
"no motion data -> cadence = DEFAULT_ATTACK_PERIOD / factor")
|
||||
|
||||
# §5.3:SHORSE::CanAttack / CPythonPlayer::__CanAttack 的坐骑等级门。
|
||||
# 普通坐骑 level 1 不能攻击;新坐骑还需要骑乘术 level 11 或 master。
|
||||
fc.skills = [{"id": 109, "level": 1, "master": 0}]
|
||||
fc.ents[1000]["mount_vnum"] = 20101
|
||||
_ck(not np._can_attack_horse_level(), "horse level 1 -> CanAttackHorseLevel false")
|
||||
fc.ents[1000]["mount_vnum"] = 20205
|
||||
_ck(np._can_attack_horse_level(), "horse level 2 -> CanAttackHorseLevel true")
|
||||
_ck(not np._can_attack(), "new horse skill below level 11 -> attack blocked")
|
||||
fc.skills[0]["level"] = 11
|
||||
_ck(np._can_attack(), "new horse skill level 11 -> attack allowed")
|
||||
fc.ents[1000]["mount_vnum"] = 0
|
||||
fc.skills = []
|
||||
|
||||
# §5.3:PythonPlayerInputKeyboard::SetAttackKeyState 在 FISHING mode
|
||||
# 改走 NEW_Fishing,不设置普通攻击键;GC_FISHING 到达后方向键/地面点击
|
||||
# 走 NEW_CancelFishing,且 500ms 内不重复发取消包。
|
||||
np.combo_motion_mode = np.MOTION_MODE_FISHING
|
||||
pc.player.rotation.y = 0.0
|
||||
fc.calls.fishing.clear()
|
||||
np.set_attack_key(true)
|
||||
_ck(fc.calls.fishing.size() == 1 and is_equal_approx(float(fc.calls.fishing[0]), 270.0),
|
||||
"fishing attack key -> CG_FISHING with MapCoord heading")
|
||||
_ck(not np._attack_key_down, "fishing attack key does not enable normal attack")
|
||||
fc.fishing_event.emit(0, 1000, 0)
|
||||
_ck(np.is_fishing(), "GC_FISHING START -> IsFishing state")
|
||||
_ck(np.cancel_fishing(), "NEW_CancelFishing sends first cancel")
|
||||
_ck(fc.calls.fishing.size() == 2 and is_equal_approx(float(fc.calls.fishing[1]), 0.0),
|
||||
"cancel fishing -> CG_FISHING(0)")
|
||||
_ck(not np.cancel_fishing(), "NEW_CancelFishing throttles within 500ms")
|
||||
np.combo_motion_mode = np.MOTION_MODE_GENERAL
|
||||
|
||||
# 1) 点地移动 -> move(FUNC_MOVE) 带 server cm + heading
|
||||
pc.player.position = Vector3(3, 0, -5) # to_server_cm -> (300, -500)
|
||||
@@ -116,7 +163,7 @@ func _run() -> void:
|
||||
mob.set_meta("vid", 2000)
|
||||
root.add_child(mob)
|
||||
mob.position = Vector3(4, 0, -5) # 距玩家(3.3,0,-5) 0.7m -> 攻击距离内
|
||||
fc.ents[2000] = {"vid": 2000, "name": "Wolf", "pos": mob.position, "hp": 50, "max_hp": 50, "dead": false}
|
||||
fc.ents[2000] = {"vid": 2000, "ch_type": 2, "name": "Wolf", "pos": mob.position, "hp": 50, "max_hp": 50, "dead": false}
|
||||
nw._by_vid[2000] = mob # 让 net_world.node_for(2000) 命中
|
||||
pc.target_selected.emit(mob)
|
||||
_ck(fc.calls.set_target.size() == 1 and fc.calls.set_target[0] == 2000, "set_target(2000)")
|
||||
@@ -130,14 +177,191 @@ func _run() -> void:
|
||||
pc.target_selected.emit(npc)
|
||||
_ck(fc.calls.click_npc == [3000], "NPC selection -> click_npc(3000)")
|
||||
|
||||
# 5) 攻击距离内 -> 按 attack_period 自动 attack
|
||||
# 5) 只有按住 SPACE(参考端 m_isAtkKey)才在攻击距离内 attack
|
||||
np.set_attack_key(true)
|
||||
for i in 4: await process_frame
|
||||
_ck(fc.calls.attack.size() >= 1, "auto-attack fired, got %d" % fc.calls.attack.size())
|
||||
_ck(fc.calls.attack.size() >= 1, "space-held attack fired, got %d" % fc.calls.attack.size())
|
||||
if fc.calls.attack.size() >= 1:
|
||||
_ck(fc.calls.attack[0][1] == 2000, "attack victim == 2000")
|
||||
var n1: int = fc.calls.attack.size()
|
||||
await process_frame
|
||||
_ck(fc.calls.attack.size() == n1, "attack throttled within period")
|
||||
np.set_attack_key(false)
|
||||
|
||||
# 5b) §3.4 预约动作状态机(__ReserveProcess_ClickActor 的 10 步顺序)
|
||||
# 步骤 2:目标在可点击距离外 -> 继续靠近(walk_to),不发攻击
|
||||
pc.walk_calls.clear()
|
||||
fc.calls.attack.clear()
|
||||
mob.position = Vector3(40, 0, -5) # 距玩家 ~37m,远超 1.5m 默认可点击距离
|
||||
np.set_attack_key(true)
|
||||
for i in 3: await process_frame
|
||||
_ck(pc.walk_calls.size() >= 1, "out-of-range reserved target -> walk_to (approach)")
|
||||
_ck(fc.calls.attack.is_empty(), "out-of-range -> no attack packet")
|
||||
_ck(np._reserved_mode == NetPlay.ReservedMode.CLICK_ACTOR, "reservation kept while approaching")
|
||||
|
||||
# 步骤 10:进入距离 -> 攻击且预约清空
|
||||
mob.position = Vector3(4, 0, -5)
|
||||
np._attack_cd = 0.0
|
||||
fc.calls.attack.clear()
|
||||
await process_frame
|
||||
_ck(fc.calls.attack.size() >= 1, "in-range reserved target -> attack fired")
|
||||
_ck(np._reserved_mode == NetPlay.ReservedMode.NONE, "reservation cleared after swing")
|
||||
|
||||
# 步骤 5:自己在安全区 -> OnCannotAttack('IN_SAFE') + 清预约,不攻击
|
||||
var cannot: Array = []
|
||||
np.cannot_act.connect(func(code): cannot.append(code))
|
||||
fc.ents[1000]["in_safe"] = true
|
||||
np._attack_cd = 0.0
|
||||
fc.calls.attack.clear()
|
||||
for i in 2: await process_frame
|
||||
_ck(cannot.has("IN_SAFE"), "main in safe zone -> cannot_act('IN_SAFE')")
|
||||
_ck(fc.calls.attack.is_empty(), "safe zone -> no attack packet")
|
||||
fc.ents[1000]["in_safe"] = false
|
||||
|
||||
# __Update_AutoAttack:目标死亡 -> 清 auto-attack + 目标
|
||||
fc.ents[2000]["dead"] = true
|
||||
np._attack_cd = 0.0
|
||||
await process_frame
|
||||
_ck(np._auto_attack_vid == 0, "dead reserved target -> auto-attack cleared")
|
||||
fc.ents[2000]["dead"] = false
|
||||
np.set_attack_key(false)
|
||||
np._clear_reserved()
|
||||
|
||||
# 5c) §3.4 增量 91 —— CLICK_ITEM / CLICK_POSITION / USE_SKILL 预约分派
|
||||
# + __ChangeTargetToPickedInstance + 扇形/圆形补 fly-targeting
|
||||
np._target_vid = 0
|
||||
np._auto_attack_vid = 0
|
||||
np.set_attack_key(false)
|
||||
np._clear_reserved()
|
||||
pc.player.position = Vector3.ZERO
|
||||
pc.player.rotation.y = 0.0
|
||||
pc.walk_calls.clear()
|
||||
fc.calls.move.clear()
|
||||
|
||||
# CLICK_ITEM:远处掉落物 -> walk_to;到手 -> pickup + FUNC_WAIT + 清预约
|
||||
fc.ground_items = [{"vid": 777, "vnum": 1, "owner": "", "pos": Vector3(5, 0, 5)}] # to_world -> (5,0,-5)
|
||||
np.reserve_click_item(777)
|
||||
await process_frame
|
||||
_ck(pc.walk_calls.size() >= 1, "CLICK_ITEM far -> walk_to")
|
||||
_ck(np._reserved_mode == NetPlay.ReservedMode.CLICK_ITEM, "CLICK_ITEM reservation kept while approaching")
|
||||
pc.player.position = Vector3(5, 0, -5) # 站到物品上
|
||||
var mv_n: int = fc.calls.move.size()
|
||||
await process_frame
|
||||
_ck(fc.calls.pickup == [777], "CLICK_ITEM in-range -> pickup_item(777)")
|
||||
_ck(fc.calls.move.size() >= mv_n + 1 and fc.calls.move[mv_n][0] == np.FUNC_WAIT,
|
||||
"CLICK_ITEM in-range -> FUNC_WAIT state packet")
|
||||
_ck(np._reserved_mode == NetPlay.ReservedMode.NONE, "CLICK_ITEM cleared after pickup")
|
||||
|
||||
# CLICK_ITEM:物品已消失 -> 清预约,不走
|
||||
pc.walk_calls.clear()
|
||||
fc.ground_items = []
|
||||
np.reserve_click_item(888)
|
||||
await process_frame
|
||||
_ck(pc.walk_calls.is_empty() and np._reserved_mode == NetPlay.ReservedMode.NONE,
|
||||
"CLICK_ITEM missing item -> reservation cleared")
|
||||
|
||||
# CLICK_POSITION:延时未到不动;延时耗尽 -> walk_to + 清预约
|
||||
pc.walk_calls.clear()
|
||||
np.reserve_click_ground(Vector3(9, 0, -2))
|
||||
_ck(np._reserved_delay > 0.0, "reserve_click_ground sets a reserved delay")
|
||||
np._reserved_delay = 999.0
|
||||
await process_frame
|
||||
_ck(pc.walk_calls.is_empty(), "CLICK_POSITION waits out the reserved delay")
|
||||
np._reserved_delay = 0.0
|
||||
await process_frame
|
||||
_ck(pc.walk_calls.size() == 1 and pc.walk_calls[0] == Vector3(9, 0, -2),
|
||||
"CLICK_POSITION delay elapsed -> walk_to(target)")
|
||||
_ck(np._reserved_mode == NetPlay.ReservedMode.NONE, "CLICK_POSITION cleared after move")
|
||||
|
||||
# USE_SKILL:range>100 存 -10;射程外 walk_to;射程内 -> hook + set_target + 清预约
|
||||
var smob := Node3D.new()
|
||||
smob.set_meta("vid", 4100)
|
||||
root.add_child(smob)
|
||||
nw._by_vid[4100] = smob
|
||||
smob.position = Vector3(30, 0, 0)
|
||||
fc.ents[4100] = {"vid": 4100, "ch_type": 2, "pos": smob.position, "hp": 80, "max_hp": 80, "dead": false, "race": 101}
|
||||
var hook_calls: Array = []
|
||||
np.use_skill_hook = func(slot: int) -> bool: hook_calls.append(slot); return true
|
||||
pc.player.position = Vector3.ZERO
|
||||
np.reserve_use_skill(4100, 5, 800.0)
|
||||
_ck(is_equal_approx(np._skill_range_reserved, 790.0), "reserve_use_skill trims range >100 by 10")
|
||||
pc.walk_calls.clear()
|
||||
fc.calls.set_target.clear()
|
||||
await process_frame
|
||||
_ck(pc.walk_calls.size() >= 1, "USE_SKILL out of range -> walk_to")
|
||||
_ck(hook_calls.is_empty(), "USE_SKILL out of range -> no cast")
|
||||
smob.position = Vector3(3, 0, 0) # 3m < 7.9m 射程
|
||||
await process_frame
|
||||
_ck(hook_calls == [5], "USE_SKILL in range -> use_skill_hook(slot 5)")
|
||||
_ck(fc.calls.set_target.has(4100), "USE_SKILL in range -> set_target(4100)")
|
||||
_ck(np._reserved_mode == NetPlay.ReservedMode.NONE, "USE_SKILL cleared after cast")
|
||||
np.reserve_use_skill(999999, 5, 300.0)
|
||||
await process_frame
|
||||
_ck(np._reserved_mode == NetPlay.ReservedMode.NONE, "USE_SKILL missing target -> cleared")
|
||||
|
||||
# __ChangeTargetToPickedInstance:NPC(非战斗目标)-> 清 _target_vid + set_target(0)
|
||||
np._target_vid = 2000
|
||||
fc.calls.set_target.clear()
|
||||
fc.ents[5000] = {"vid": 5000, "ch_type": 1, "name": "NPC"}
|
||||
var ok_npc: bool = np._change_target_to_picked_instance(5000)
|
||||
_ck(not ok_npc and np._target_vid == 0 and fc.calls.set_target == [0],
|
||||
"_change_target_to_picked_instance(NPC) -> target cleared")
|
||||
fc.calls.set_target.clear()
|
||||
var ok_mob: bool = np._change_target_to_picked_instance(4100)
|
||||
_ck(ok_mob and np._target_vid == 4100 and fc.calls.set_target == [4100],
|
||||
"_change_target_to_picked_instance(monster) -> target set")
|
||||
|
||||
# send_fly_targeting —— 把早前实体挪远(不销毁,后续用例还引用),铺一组已知几何
|
||||
np._target_vid = 0
|
||||
np._clear_reserved()
|
||||
pc.player.position = Vector3.ZERO
|
||||
for v in [2000, 4100]:
|
||||
if nw._by_vid.has(v):
|
||||
nw._by_vid[v].position = Vector3(9999, 0, 9999)
|
||||
if fc.ents.has(v):
|
||||
fc.ents[v]["pos"] = Vector3(9999, 0, 9999)
|
||||
var mk := func(v: int, p: Vector3) -> void:
|
||||
var n := Node3D.new()
|
||||
n.set_meta("vid", v)
|
||||
n.position = p
|
||||
root.add_child(n)
|
||||
nw._by_vid[v] = n
|
||||
fc.ents[v] = {"vid": v, "ch_type": 2, "pos": p, "hp": 50, "max_hp": 50, "dead": false, "race": 101}
|
||||
mk.call(4200, Vector3(0, 0, -10)) # 主目标,正前方 (-Z)
|
||||
mk.call(4201, Vector3(2, 0, -10)) # 侧翼,扇内
|
||||
mk.call(4202, Vector3(-2, 0, -10)) # 侧翼,扇内
|
||||
mk.call(4203, Vector3(0, 0, 12)) # 背后,扇外
|
||||
|
||||
fc.calls.fly.clear()
|
||||
var extras: int = np.send_fly_targeting(4200, 2500.0, 3, NetPlay.FlyShape.FAN)
|
||||
_ck(extras == 2, "FAN send_fly_targeting: 2 extras (max 3 - primary), got %d" % extras)
|
||||
var fly_vids := []
|
||||
for f in fc.calls.fly:
|
||||
fly_vids.append(f[0])
|
||||
_ck(fly_vids.size() == 2 and not fly_vids.has(4200), "FAN: 2 extra packets, primary not re-sent")
|
||||
_ck(not fly_vids.has(4203), "FAN: target-behind instance excluded from fan")
|
||||
_ck(4201 in fly_vids and 4202 in fly_vids, "FAN: both flankers picked")
|
||||
|
||||
fc.calls.fly.clear()
|
||||
var extras_c: int = np.send_fly_targeting(4200, 2500.0, 4, NetPlay.FlyShape.CIRCLE)
|
||||
_ck(extras_c == 3, "CIRCLE: 3 extras (max 4 - primary), got %d" % extras_c)
|
||||
var cvids := []
|
||||
for f in fc.calls.fly:
|
||||
cvids.append(f[0])
|
||||
_ck(4203 in cvids, "CIRCLE: behind-target instance included (no fan filter)")
|
||||
|
||||
fc.calls.fly.clear()
|
||||
var extras_r: int = np.send_fly_targeting(4200, 2500.0, 6, NetPlay.FlyShape.FAN)
|
||||
_ck(extras_r == 5, "FAN shortfall: 5 extras for max 6, got %d" % extras_r)
|
||||
var zero_fills := 0
|
||||
for f in fc.calls.fly:
|
||||
if f[0] == 0:
|
||||
zero_fills += 1
|
||||
_ck(zero_fills >= 1, "FAN shortfall: random vid=0 positions fill the remainder")
|
||||
_ck(np.send_fly_targeting(4200, 2500.0, 1, NetPlay.FlyShape.FAN) == 0, "max_count<=1 -> no extras")
|
||||
_ck(np.send_fly_targeting(4200, 2500.0, 3, NetPlay.FlyShape.SINGLE) == 0, "SINGLE shape -> no extras")
|
||||
np._clear_reserved()
|
||||
np._target_vid = 0
|
||||
|
||||
# 6) points_changed -> HUD
|
||||
fc.points_changed.emit({"hp": 120, "max_hp": 200, "sp": 30, "max_sp": 80,
|
||||
@@ -165,9 +389,34 @@ func _run() -> void:
|
||||
# 10) 观战模式锁住本地移动/选目标,并清掉当前目标
|
||||
fc.ents[2000]["dead"] = false
|
||||
fc.observer_mode_changed.emit(true)
|
||||
var move_n: int = fc.calls.move.size()
|
||||
var target_n: int = fc.calls.set_target.size()
|
||||
pc.player.position = Vector3(8, 0, -5)
|
||||
pc.moved.emit(pc.player.position)
|
||||
pc.target_selected.emit(mob)
|
||||
_ck(fc.calls.move.size() == 2 and fc.calls.set_target.size() == 1,
|
||||
_ck(fc.calls.move.size() == move_n and fc.calls.set_target.size() == target_n,
|
||||
"observer mode blocks local movement and targeting")
|
||||
fc.observer_mode_changed.emit(false)
|
||||
|
||||
# 11) §3.2 上行 6 回调:OnMove 立即发 + 重置节流窗、OnMoving 300ms 节流、
|
||||
# OnStop 收尾、OnAttack 额外发 FUNC_COMBO、on_use_skill 发 FUNC_SKILL|motion。
|
||||
fc.observer_mode_changed.emit(false)
|
||||
np._observer_mode = false
|
||||
np._was_moving = false
|
||||
np._last_moving_sent_t = 0.0
|
||||
fc.calls.move.clear()
|
||||
pc.player.position = Vector3(1, 0, -1)
|
||||
pc.moved.emit(pc.player.position) # OnMove
|
||||
_ck(fc.calls.move.size() == 1 and fc.calls.move[0][0] == np.FUNC_MOVE, "OnMove -> immediate FUNC_MOVE")
|
||||
pc.player.position = Vector3(1.5, 0, -1)
|
||||
pc.moved.emit(pc.player.position) # OnMoving, throttled (<300ms)
|
||||
_ck(fc.calls.move.size() == 1, "OnMoving within 300ms throttled")
|
||||
np._last_moving_sent_t -= 1.0 # 让 300ms 窗口过期
|
||||
pc.moved.emit(pc.player.position) # OnMoving, now allowed
|
||||
_ck(fc.calls.move.size() == 2 and fc.calls.move[1][0] == np.FUNC_MOVE, "OnMoving after 300ms -> FUNC_MOVE")
|
||||
pc.anim_state.emit("wait") # OnStop
|
||||
_ck(fc.calls.move.size() == 3 and fc.calls.move[2][0] == np.FUNC_WAIT, "OnStop -> FUNC_WAIT")
|
||||
fc.calls.move.clear()
|
||||
np.on_use_skill(5, 2) # OnUseSkill
|
||||
_ck(fc.calls.move.size() == 1 and fc.calls.move[0][0] == (np.FUNC_SKILL | 5) and fc.calls.move[0][1] == 2,
|
||||
"on_use_skill -> FUNC_SKILL|motion + arg")
|
||||
|
||||
Reference in New Issue
Block a user