@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])