153 lines
6.6 KiB
GDScript
153 lines
6.6 KiB
GDScript
@tool
|
||
extends McpTestSuite
|
||
## 回归测试:抱起物品系统的契约。
|
||
##
|
||
## 谁能被抱由**物品自己**决定(挂 carryable.gd + 进 carryable 组),玩家只管找/抱/扔;
|
||
## 抱持偏移和投掷速度也写在物品上。这里盯住这几条契约,改物品或改玩家时先让测试红一次。
|
||
|
||
const PLAYER_SCENE_PATH := "res://character/player/player.tscn"
|
||
const ITEM_SCENE_PATH := "res://world/items/carryable_box.tscn"
|
||
const CARRY_ACTION := "carry"
|
||
const THROW_ACTION := "throw_carry"
|
||
|
||
|
||
func suite_name() -> String:
|
||
return "carry"
|
||
|
||
|
||
## 编辑器进程里的 InputMap 只有内建动作,项目动作要手动 load 一次才在
|
||
func _ensure_input_map() -> void:
|
||
InputMap.load_from_project_settings()
|
||
|
||
|
||
## 把节点塞进场景树,让 _ready() 真的跑起来;track() 负责回收
|
||
func _into_tree(node: Node) -> Node:
|
||
var editor := Engine.get_singleton("EditorInterface")
|
||
var root: Node = null
|
||
if editor != null:
|
||
root = editor.get_edited_scene_root()
|
||
if root == null:
|
||
root = Engine.get_main_loop().root
|
||
root.add_child(node)
|
||
track(node)
|
||
return node
|
||
|
||
|
||
func test_carry_actions_exist() -> void:
|
||
_ensure_input_map()
|
||
# 动作没了按键就完全没反应,而且不报错 —— 这种静默失效必须由测试兜住
|
||
assert_true(InputMap.has_action(CARRY_ACTION), "缺少 %s 动作,左键抱不起来" % CARRY_ACTION)
|
||
assert_true(InputMap.has_action(THROW_ACTION), "缺少 %s 动作,长按 Q 扔不出去" % THROW_ACTION)
|
||
|
||
|
||
func test_player_scene_has_carry_nodes() -> void:
|
||
var packed := load(PLAYER_SCENE_PATH) as PackedScene
|
||
assert_true(packed != null, "%s 未能加载" % PLAYER_SCENE_PATH)
|
||
if packed == null:
|
||
return
|
||
var player := packed.instantiate()
|
||
track(player)
|
||
var socket := player.get_node_or_null("CameraRig/CarrySocket")
|
||
assert_true(socket is Marker3D, "CameraRig 下缺少 CarrySocket(Marker3D)—— 物品没有抱持点")
|
||
var arms := player.get_node_or_null("Body/CarryArms")
|
||
assert_true(arms != null, "Body 下缺少 CarryArms 节点")
|
||
if arms != null:
|
||
assert_true(arms.get_script() != null and str(arms.get_script().resource_path).ends_with("carry_arms.gd"),
|
||
"Body/CarryArms 没挂 carry_arms.gd,胳膊不会抱东西")
|
||
|
||
|
||
func test_example_item_is_carryable() -> void:
|
||
var packed := load(ITEM_SCENE_PATH) as PackedScene
|
||
assert_true(packed != null, "%s 未能加载 —— 可抱物品的示例没了" % ITEM_SCENE_PATH)
|
||
if packed == null:
|
||
return
|
||
var item := packed.instantiate()
|
||
assert_true(item is Carryable, "示例物品根节点不是 Carryable(%s)" % item.get_class())
|
||
if not item is Carryable:
|
||
track(item)
|
||
return
|
||
var carryable := item as Carryable
|
||
_into_tree(carryable)
|
||
assert_true(carryable.is_in_group(Carryable.GROUP),
|
||
"可抱物品没进 %s 组,玩家就找不到它" % Carryable.GROUP)
|
||
assert_true(carryable.get_node_or_null("Collision") is CollisionShape3D,
|
||
"示例物品没有碰撞体,扔出去会穿地")
|
||
|
||
|
||
func test_carry_freezes_body_and_restores_on_release() -> void:
|
||
var item := _into_tree((load(ITEM_SCENE_PATH) as PackedScene).instantiate()) as Carryable
|
||
assert_true(item != null, "示例物品实例化失败")
|
||
if item == null:
|
||
return
|
||
var layer := item.collision_layer
|
||
assert_true(item.can_be_carried(), "新物品应该是可以抱的")
|
||
var holder := Node3D.new()
|
||
track(holder)
|
||
item.begin_carry(holder)
|
||
assert_true(not item.can_be_carried(), "抱起来之后不该还能被别人抱走")
|
||
assert_true(item.freeze, "抱起来要冻结物理,否则会和抱着它的人打架")
|
||
assert_eq(item.collision_layer, 0, "抱起来要清掉碰撞层,否则物品会把玩家顶飞")
|
||
item.end_carry(Vector3(1.0, 2.0, 3.0))
|
||
assert_true(item.can_be_carried(), "放下之后应该又能被抱")
|
||
assert_true(not item.freeze, "放下要恢复物理")
|
||
assert_eq(item.collision_layer, layer, "放下要恢复原来的碰撞层")
|
||
assert_true(item.linear_velocity.is_equal_approx(Vector3(1.0, 2.0, 3.0)),
|
||
"放下时要继承扔出去的速度,实际 %s" % item.linear_velocity)
|
||
|
||
|
||
func test_hold_transform_snaps_to_socket() -> void:
|
||
var item := _into_tree((load(ITEM_SCENE_PATH) as PackedScene).instantiate()) as Carryable
|
||
if item == null:
|
||
return
|
||
var socket := Marker3D.new()
|
||
track(socket)
|
||
socket.position = Vector3(1.0, 2.0, -3.0)
|
||
socket.rotation = Vector3(0.0, PI * 0.5, 0.0)
|
||
var root: Node = Engine.get_main_loop().root
|
||
root.add_child(socket)
|
||
item.apply_hold_transform(socket)
|
||
var expected := socket.global_position + socket.global_basis * item.hold_offset
|
||
assert_true(item.global_position.distance_to(expected) < 0.001,
|
||
"物品没摆到抱持点上:实际 %s,期望 %s" % [item.global_position, expected])
|
||
|
||
|
||
func test_throw_speed_scales_with_charge() -> void:
|
||
var item := _into_tree((load(ITEM_SCENE_PATH) as PackedScene).instantiate()) as Carryable
|
||
if item == null:
|
||
return
|
||
assert_true(absf(item.throw_speed(0.0) - item.throw_speed_min) < 0.001,
|
||
"蓄力为 0 时应该是最低出手速度")
|
||
assert_true(absf(item.throw_speed(1.0) - item.throw_speed_max) < 0.001,
|
||
"蓄满时应该是最高出手速度")
|
||
assert_true(item.throw_speed(0.5) > item.throw_speed_min and item.throw_speed(0.5) < item.throw_speed_max,
|
||
"出手速度应该随蓄力单调变化")
|
||
assert_true(item.throw_speed(9.0) <= item.throw_speed_max, "蓄力超出范围要被夹住")
|
||
|
||
|
||
## 抱起姿势必须真的把胳膊伸到身体**前方**(模型正面是本地 +Z,尾巴在 -Z 那一侧)。
|
||
## 参数写反或者骨骼名写错时,这里会红 —— 光看代码看不出胳膊朝哪边伸。
|
||
func test_carry_pose_reaches_forward() -> void:
|
||
var packed := load(PLAYER_SCENE_PATH) as PackedScene
|
||
if packed == null:
|
||
return
|
||
var player := _into_tree(packed.instantiate())
|
||
var arms := player.get_node_or_null("Body/CarryArms") as CarryArms
|
||
var skeleton := player.get_node_or_null("Body/mouse/MouseRig/Skeleton3D") as Skeleton3D
|
||
assert_true(arms != null and skeleton != null, "玩家场景里缺 CarryArms 或骨架")
|
||
if arms == null or skeleton == null:
|
||
return
|
||
var hand := skeleton.find_bone("arm_l_03")
|
||
assert_true(hand >= 0, "骨架里没有 arm_l_03,胳膊姿势会不完整")
|
||
if hand < 0:
|
||
return
|
||
var before := skeleton.get_bone_global_pose(hand).origin
|
||
arms.set_carrying(true, 0.0)
|
||
# 直接推进两次让它混合到位(测试里没有引擎帧)
|
||
arms._process(1.0)
|
||
arms._process(1.0)
|
||
var after := skeleton.get_bone_global_pose(hand).origin
|
||
assert_true(after.z > before.z + 0.5,
|
||
"抱起来时手要伸到身体前方:抱之前 z=%.2f,抱之后 z=%.2f" % [before.z, after.z])
|
||
assert_true(after.y > before.y,
|
||
"抱起来时手要抬到身前的高度:抱之前 y=%.2f,抱之后 y=%.2f" % [before.y, after.y])
|