Files
mtgodot-poc/project/test_combat_death_pickup_entry_parity.gd
T
shenandshen c93894313a fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧
- inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域
- 包含其他累积的功能开发和测试文件
2026-09-21 16:38:59 -07:00

210 lines
8.0 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_main_player_death_gate()
_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()
func _test_main_player_death_gate() -> void:
print("\n--- Testing Main Character Death Movement Gate ---")
var pc := PlayerCtl.new()
var dummy := Node3D.new()
dummy.position = Vector3.ZERO
pc.player = dummy
pc._is_going = true
pc._key_up = true
pc.set_dead(true)
_check(pc.dead, "set_dead(true) marks the controller dead")
_check(not pc.is_going(), "death clears point-to-move state")
_check(pc._wasd() == Vector2.ZERO, "death clears WASD state")
_check(not pc._goto(Vector3(5, 0, 5)), "dead controller rejects new goto")
pc.set_dead(false)
_check(not pc.dead, "positive-life transition clears controller dead")
_check(pc._goto(Vector3(5, 0, 5)), "revived controller accepts goto")
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")
# 有效 MSA 的技能必须由动作结束事件解锁,不能被名义 clip 时长提前解除。
np.start_skill_cast(0.05, false, false, true)
_check(np.is_lock() == true, "motion-driven skill is locked during the MSA motion")
np._local_time += 1.0
_check(np.is_lock() == true, "motion-driven skill ignores timer fallback before motion_bound(wait)")
np._on_motion_bound("wait")
_check(np.is_lock() == false, "motion-driven skill unlocks on motion_bound(wait)")
_check(np._skill_motion_driven == false, "motion-driven flag clears after motion completion")
# 模拟冲锋技能(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()