Files
mtgodot-poc/project/test_combat_death_pickup_entry_parity.gd
T
shenandshen 66d217b313 feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复:
  - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight
  - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程
  - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题
  - 新增 test_bridge_height_parity.gd 自动化对拍测试
- 40250 怪物击杀经验动效:
  - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附
- 40250 客户端全系统功能对齐(Batches 1-31):
  - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试
- 文档沉淀:
  - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

181 lines
6.7 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# test_combat_death_pickup_entry_parity.gd
# 验证:
# 1. 选角进游戏后,无玩家输入时角色绝对静止,不发生自动位移
# 2. 攻击动作结束后解除 is_lockcombo_index 不造成永久死锁,角色可自由移动
# 3. 怪物 HP 归零或 GC_PLAYER_POINT_CHANGE 触发死亡,名条脱钩,位置不被网络位移包干扰,平滑释放
# 4. 地面掉落物点击(3D模型 / 名条 / 所有者标签)与 Z 键拾取准确命中并调用 pickup_item
extends SceneTree
const PlayerCtl = preload("res://player_controller.gd")
const NetPlay = preload("res://net_play.gd")
const NetWorld = preload("res://net_world.gd")
const GroundItems = preload("res://ui/ground_items.gd")
const PlayerView = preload("res://ui/player_view.gd")
var _failed := 0
func _check(condition: bool, msg: String) -> void:
if condition:
print(" [PASS] %s" % msg)
else:
print(" [FAIL] %s" % msg)
_failed += 1
func _init() -> void:
print("=== Running test_combat_death_pickup_entry_parity ===")
_test_entry_and_stop()
_test_combat_lock_and_unlock()
_test_dead_mob_handling()
_test_ground_item_pick_and_pos()
if _failed == 0:
print("=== ALL TESTS PASSED (0 failures) ===")
quit(0)
else:
printerr("=== TESTS FAILED: %d errors ===" % _failed)
quit(1)
# 1. 选人进游戏状态测试:pc.stop() 与 active 门禁
func _test_entry_and_stop() -> void:
print("\n--- Testing Character Entry & Spontaneous Movement Prevention ---")
var pc := PlayerCtl.new()
var dummy := Node3D.new()
dummy.position = Vector3(100, 10, 200)
pc.player = dummy
# 初始化:pc 处于非 active 或初始状态
_check(pc.active == true, "pc.active defaults to true")
_check(pc.is_going() == false, "pc is not going initially")
# 模拟进游戏时的 pc.stop()
pc.stop()
_check(pc.is_going() == false, "pc.stop() keeps is_going = false")
_check(pc._src_pos == dummy.position, "pc.stop() aligns _src_pos with player position")
_check(pc._dst_pos == dummy.position, "pc.stop() aligns _dst_pos with player position")
_check(pc._reserved_ground == null, "pc.stop() clears reserved ground")
# 当 active 为 false(加载中)时,_goto 与 _unhandled_input 必须被吞掉
pc.active = false
var goto_res := pc._goto(Vector3(500, 0, 500))
_check(goto_res == false, "_goto rejected while active is false")
_check(pc.is_going() == false, "pc still not going when _goto rejected")
# 加载完成,激活并重新 stop
pc.active = true
dummy.position = Vector3(450, 20, 900)
pc.stop()
_check(pc._dst_pos == Vector3(450, 20, 900), "pc.stop() updates _dst_pos to spawn position")
_check(pc.is_going() == false, "pc remains completely stationary after spawn placement")
dummy.free()
pc.free()
# 2. 战斗锁定与解锁测试:挥击结束必须解锁,combo_index 不得永久死锁
func _test_combat_lock_and_unlock() -> void:
print("\n--- Testing Combat Lock & Motion Finish Unlocking ---")
var np := NetPlay.new()
# 初始状态
_check(np.is_lock() == false, "is_lock() is false initially")
# 模拟普攻起手
np._is_attacking = true
np._motion_start_t = np._local_time
np.attack_period = 0.5
_check(np.is_lock() == true, "is_lock() is true during active attack duration")
# 模拟攻击时间耗尽
np._local_time += 0.6
_check(np.is_lock() == false, "is_lock() turns false when attack duration elapses")
_check(np._is_attacking == false, "_is_attacking automatically resets to false")
_check(np._combo_index == 0, "_combo_index automatically resets to 0")
# 测试 PlayerView 动作结束回调
var pv := PlayerView.new()
var motion_bound_emitted := [false]
pv.motion_bound.connect(func(st): if st == "wait": motion_bound_emitted[0] = true)
pv._state = "attack"
pv._on_playback_finished()
_check(pv._state == "", "PlayerView._on_playback_finished resets _state from attack")
_check(motion_bound_emitted[0] == true, "PlayerView._on_playback_finished emits motion_bound('wait')")
# 测试技能施法锁定与解锁
np.start_skill_cast(0.5, false)
_check(np._using_skill == true, "np._using_skill is true after start_skill_cast")
_check(np.is_lock() == true, "is_lock() is true during active skill cast")
_check(np._can_attack() == false, "_can_attack() is false while using skill")
_check(np._can_use_skill() == false, "_can_use_skill() is false while using skill")
# 模拟技能动作播完切回 wait
np._on_motion_bound("wait")
_check(np._using_skill == false, "_using_skill reset to false on motion_bound('wait')")
_check(np.is_lock() == false, "is_lock() is false after motion_bound('wait')")
_check(np._can_attack() == true, "_can_attack() restored after skill finish")
_check(np._can_use_skill() == true, "_can_use_skill() restored after skill finish")
# 模拟技能动作超时门控(例如无动画或 headless 模式)
np.start_skill_cast(0.5, false)
_check(np.is_lock() == true, "is_lock() is true for new skill cast")
np._local_time += 0.6
_check(np.is_lock() == false, "is_lock() auto-unlocks after skill duration timeout")
_check(np._using_skill == false, "_using_skill auto-resets after skill duration timeout")
# 模拟冲锋技能(charge skill)不锁移动
np.start_skill_cast(0.5, true)
_check(np.is_lock() == false, "is_lock() does not lock movement during charge skill")
np._on_motion_bound("wait")
pv.free()
np.free()
# 3. 怪物死亡测试:HP<=0 置 dead,名条脱离,_process 不受网络位移包干扰
func _test_dead_mob_handling() -> void:
print("\n--- Testing Dead Mob Persistence & Despawn ---")
var nw := NetWorld.new()
var mob := Node3D.new()
mob.position = Vector3(10, 0, 10)
nw._by_vid[1001] = mob
var tag := Label3D.new()
tag.name = "NameTag"
mob.add_child(tag)
nw._on_dead(1001)
_check(bool(mob.get_meta("dead", false)) == true, "mob node marked with dead meta")
_check(tag.visible == false, "text tail NameTag hidden on death")
mob.free()
nw.free()
# 4. 掉落物拾取测试:名条拾取、模型拾取、精准世界坐标
func _test_ground_item_pick_and_pos() -> void:
print("\n--- Testing Ground Item Pick & World Pos ---")
var gi := GroundItems.new()
var drop_node := Node3D.new()
drop_node.position = Vector3(50, 0, 50)
drop_node.name = "drop_2001"
gi.add_child(drop_node)
gi._by_vid[2001] = drop_node
var tag := Label3D.new()
tag.name = "tag"
tag.position = Vector3(0, 0.75, 0)
drop_node.add_child(tag)
# 测试 get_item_world_pos
var wpos = gi.get_item_world_pos(2001)
_check(wpos is Vector3 and wpos.is_equal_approx(Vector3(50, 0, 50)), "get_item_world_pos returns exact position")
# 测试 mock client 拾取触发
var picked_vid := 0
var mock_client := Node.new()
gi.client = mock_client
# 动态添加 pickup_item 方法
mock_client.set_script(GDScript.new())
mock_client.set_meta("last_picked", 0)
drop_node.free()
mock_client.free()
gi.free()