- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
210 lines
8.0 KiB
GDScript
210 lines
8.0 KiB
GDScript
# test_combat_death_pickup_entry_parity.gd
|
||
# 验证:
|
||
# 1. 选角进游戏后,无玩家输入时角色绝对静止,不发生自动位移
|
||
# 2. 攻击动作结束后解除 is_lock,combo_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()
|