抱起物品

This commit is contained in:
2026-10-03 18:04:07 +08:00
parent bc90e6d7b2
commit 6f3c89a4d2
17 changed files with 635 additions and 44 deletions
+152
View File
@@ -0,0 +1,152 @@
@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])
+1
View File
@@ -0,0 +1 @@
uid://bcvmvwwjphdf5
+5 -2
View File
@@ -138,8 +138,11 @@ func test_face_shader_has_expected_uniforms() -> void:
"shader 必须用 body_tex 采样全身底色,否则脸和身体不是同一块皮肤")
assert_true(code.contains("varying vec3 object_position") and code.contains("object_position.xy"),
"贴花必须用模型本地坐标定位,否则又会被 UV 孤岛边界卡住")
assert_true(code.contains("-normalize(object_normal).z"),
"贴花必须按「法线朝前」筛一遍,而且正面是模型本地 -Z,写反了表情会跑到背上")
assert_true(code.contains("normalize(object_normal).z"),
"贴花必须按「法线朝前」筛一遍,否则背面的点也会画上表情")
# 模型正面是本地 +Z(尾巴在 -Z 那一侧),写成 -normalize 表情就跑到背上去了
assert_true(not code.contains("-normalize(object_normal).z"),
"正面遮罩写反了:模型正面是本地 +Z,-normalize 会让表情出现在背上")
assert_true(code.contains("ROUGHNESS = surface_roughness"), "shader 必须复用 mat_body 的 roughness")
assert_true(code.contains("METALLIC = surface_metallic"), "shader 必须复用 mat_body 的 metallic")
# alpha 只代表墨线覆盖率,不能写进空间 ALPHA,否则身体面片会被抠出洞
-22
View File
@@ -1,22 +0,0 @@
@tool
extends McpTestSuite
func suite_name() -> String:
return "zzprobe"
func test_mesh_part_bounds() -> void:
var player := (load("res://character/player/player.tscn") as PackedScene).instantiate()
track(player)
var skeleton := player.get_node("Body/mouse/MouseRig/Skeleton3D") as Node3D
var rows: Array[String] = []
for child in skeleton.get_children():
var mi := child as MeshInstance3D
if mi == null:
continue
var box := mi.mesh.get_aabb()
rows.append("%-12s z %.2f ~ %.2f y %.2f ~ %.2f x %.2f ~ %.2f" % [
child.name, box.position.z, box.end.z, box.position.y, box.end.y,
box.position.x, box.end.x,
])
rows.sort()
assert_true(false, "\n".join(rows))
-1
View File
@@ -1 +0,0 @@
uid://dsrx5idy11dv4