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