@@ -189,6 +189,10 @@ public static object HandleCommand(JObject @params)
189189 return RemoveComponentFromTarget ( @params , targetToken , searchMethod ) ;
190190 case "set_component_property" :
191191 return SetComponentPropertyOnTarget ( @params , targetToken , searchMethod ) ;
192+ case "duplicate" :
193+ return DuplicateGameObject ( @params , targetToken , searchMethod ) ;
194+ case "move_relative" :
195+ return MoveRelativeToObject ( @params , targetToken , searchMethod ) ;
192196
193197 default :
194198 return new ErrorResponse ( $ "Unknown action: '{ action } '.") ;
@@ -898,6 +902,219 @@ string searchMethod
898902
899903 }
900904
905+ /// <summary>
906+ /// Duplicates a GameObject with all its properties, components, and children.
907+ /// </summary>
908+ private static object DuplicateGameObject ( JObject @params , JToken targetToken , string searchMethod )
909+ {
910+ GameObject sourceGo = FindObjectInternal ( targetToken , searchMethod ) ;
911+ if ( sourceGo == null )
912+ {
913+ return new ErrorResponse (
914+ $ "Target GameObject ('{ targetToken } ') not found using method '{ searchMethod ?? "default" } '."
915+ ) ;
916+ }
917+
918+ // Optional parameters
919+ string newName = @params [ "new_name" ] ? . ToString ( ) ;
920+ Vector3 ? position = ParseVector3 ( @params [ "position" ] as JArray ) ;
921+ Vector3 ? offset = ParseVector3 ( @params [ "offset" ] as JArray ) ;
922+ JToken parentToken = @params [ "parent" ] ;
923+
924+ // Duplicate the object
925+ GameObject duplicatedGo = UnityEngine . Object . Instantiate ( sourceGo ) ;
926+ Undo . RegisterCreatedObjectUndo ( duplicatedGo , $ "Duplicate { sourceGo . name } ") ;
927+
928+ // Set name (default: SourceName_Copy or SourceName (1))
929+ if ( ! string . IsNullOrEmpty ( newName ) )
930+ {
931+ duplicatedGo . name = newName ;
932+ }
933+ else
934+ {
935+ // Remove "(Clone)" suffix added by Instantiate and add "_Copy"
936+ duplicatedGo . name = sourceGo . name . Replace ( "(Clone)" , "" ) . Trim ( ) + "_Copy" ;
937+ }
938+
939+ // Handle positioning
940+ if ( position . HasValue )
941+ {
942+ // Absolute position specified
943+ duplicatedGo . transform . position = position . Value ;
944+ }
945+ else if ( offset . HasValue )
946+ {
947+ // Offset from original
948+ duplicatedGo . transform . position = sourceGo . transform . position + offset . Value ;
949+ }
950+ // else: keeps the same position as the original (default Instantiate behavior)
951+
952+ // Handle parent
953+ if ( parentToken != null )
954+ {
955+ if ( parentToken . Type == JTokenType . Null ||
956+ ( parentToken . Type == JTokenType . String && string . IsNullOrEmpty ( parentToken . ToString ( ) ) ) )
957+ {
958+ // Explicit null parent - move to root
959+ duplicatedGo . transform . SetParent ( null ) ;
960+ }
961+ else
962+ {
963+ GameObject newParent = FindObjectInternal ( parentToken , "by_id_or_name_or_path" ) ;
964+ if ( newParent != null )
965+ {
966+ duplicatedGo . transform . SetParent ( newParent . transform , true ) ;
967+ }
968+ else
969+ {
970+ Debug . LogWarning ( $ "[ManageGameObject.Duplicate] Parent '{ parentToken } ' not found. Keeping original parent.") ;
971+ }
972+ }
973+ }
974+ else
975+ {
976+ // Default: same parent as source
977+ duplicatedGo . transform . SetParent ( sourceGo . transform . parent , true ) ;
978+ }
979+
980+ // Mark scene dirty
981+ EditorUtility . SetDirty ( duplicatedGo ) ;
982+ EditorSceneManager . MarkSceneDirty ( EditorSceneManager . GetActiveScene ( ) ) ;
983+
984+ Selection . activeGameObject = duplicatedGo ;
985+
986+ return new SuccessResponse (
987+ $ "Duplicated '{ sourceGo . name } ' as '{ duplicatedGo . name } '.",
988+ new
989+ {
990+ originalName = sourceGo . name ,
991+ originalId = sourceGo . GetInstanceID ( ) ,
992+ duplicatedObject = Helpers . GameObjectSerializer . GetGameObjectData ( duplicatedGo )
993+ }
994+ ) ;
995+ }
996+
997+ /// <summary>
998+ /// Moves a GameObject relative to another reference object.
999+ /// Supports directional offsets (left, right, up, down, forward, back) and distance.
1000+ /// </summary>
1001+ private static object MoveRelativeToObject ( JObject @params , JToken targetToken , string searchMethod )
1002+ {
1003+ GameObject targetGo = FindObjectInternal ( targetToken , searchMethod ) ;
1004+ if ( targetGo == null )
1005+ {
1006+ return new ErrorResponse (
1007+ $ "Target GameObject ('{ targetToken } ') not found using method '{ searchMethod ?? "default" } '."
1008+ ) ;
1009+ }
1010+
1011+ // Get reference object (required for relative movement)
1012+ JToken referenceToken = @params [ "reference_object" ] ;
1013+ if ( referenceToken == null )
1014+ {
1015+ return new ErrorResponse ( "'reference_object' parameter is required for 'move_relative' action." ) ;
1016+ }
1017+
1018+ GameObject referenceGo = FindObjectInternal ( referenceToken , "by_id_or_name_or_path" ) ;
1019+ if ( referenceGo == null )
1020+ {
1021+ return new ErrorResponse ( $ "Reference object '{ referenceToken } ' not found.") ;
1022+ }
1023+
1024+ // Get movement parameters
1025+ string direction = @params [ "direction" ] ? . ToString ( ) ? . ToLower ( ) ;
1026+ float distance = @params [ "distance" ] ? . ToObject < float > ( ) ?? 1f ;
1027+ Vector3 ? customOffset = ParseVector3 ( @params [ "offset" ] as JArray ) ;
1028+ bool useWorldSpace = @params [ "world_space" ] ? . ToObject < bool > ( ) ?? true ;
1029+
1030+ // Record for undo
1031+ Undo . RecordObject ( targetGo . transform , $ "Move { targetGo . name } relative to { referenceGo . name } ") ;
1032+
1033+ Vector3 newPosition ;
1034+
1035+ if ( customOffset . HasValue )
1036+ {
1037+ // Custom offset vector provided
1038+ if ( useWorldSpace )
1039+ {
1040+ newPosition = referenceGo . transform . position + customOffset . Value ;
1041+ }
1042+ else
1043+ {
1044+ // Offset in reference object's local space
1045+ newPosition = referenceGo . transform . TransformPoint ( customOffset . Value ) ;
1046+ }
1047+ }
1048+ else if ( ! string . IsNullOrEmpty ( direction ) )
1049+ {
1050+ // Directional movement
1051+ Vector3 directionVector = GetDirectionVector ( direction , referenceGo . transform , useWorldSpace ) ;
1052+ newPosition = referenceGo . transform . position + directionVector * distance ;
1053+ }
1054+ else
1055+ {
1056+ return new ErrorResponse ( "Either 'direction' or 'offset' parameter is required for 'move_relative' action." ) ;
1057+ }
1058+
1059+ targetGo . transform . position = newPosition ;
1060+
1061+ // Mark scene dirty
1062+ EditorUtility . SetDirty ( targetGo ) ;
1063+ EditorSceneManager . MarkSceneDirty ( EditorSceneManager . GetActiveScene ( ) ) ;
1064+
1065+ return new SuccessResponse (
1066+ $ "Moved '{ targetGo . name } ' relative to '{ referenceGo . name } '.",
1067+ new
1068+ {
1069+ movedObject = targetGo . name ,
1070+ referenceObject = referenceGo . name ,
1071+ newPosition = new [ ] { targetGo . transform . position . x , targetGo . transform . position . y , targetGo . transform . position . z } ,
1072+ direction = direction ,
1073+ distance = distance ,
1074+ gameObject = Helpers . GameObjectSerializer . GetGameObjectData ( targetGo )
1075+ }
1076+ ) ;
1077+ }
1078+
1079+ /// <summary>
1080+ /// Converts a direction string to a Vector3.
1081+ /// </summary>
1082+ private static Vector3 GetDirectionVector ( string direction , Transform referenceTransform , bool useWorldSpace )
1083+ {
1084+ if ( useWorldSpace )
1085+ {
1086+ // World space directions
1087+ switch ( direction )
1088+ {
1089+ case "right" : return Vector3 . right ;
1090+ case "left" : return Vector3 . left ;
1091+ case "up" : return Vector3 . up ;
1092+ case "down" : return Vector3 . down ;
1093+ case "forward" : case "front" : return Vector3 . forward ;
1094+ case "back" : case "backward" : case "behind" : return Vector3 . back ;
1095+ default :
1096+ Debug . LogWarning ( $ "[ManageGameObject.MoveRelative] Unknown direction '{ direction } ', defaulting to forward.") ;
1097+ return Vector3 . forward ;
1098+ }
1099+ }
1100+ else
1101+ {
1102+ // Reference object's local space directions
1103+ switch ( direction )
1104+ {
1105+ case "right" : return referenceTransform . right ;
1106+ case "left" : return - referenceTransform . right ;
1107+ case "up" : return referenceTransform . up ;
1108+ case "down" : return - referenceTransform . up ;
1109+ case "forward" : case "front" : return referenceTransform . forward ;
1110+ case "back" : case "backward" : case "behind" : return - referenceTransform . forward ;
1111+ default :
1112+ Debug . LogWarning ( $ "[ManageGameObject.MoveRelative] Unknown direction '{ direction } ', defaulting to forward.") ;
1113+ return referenceTransform . forward ;
1114+ }
1115+ }
1116+ }
1117+
9011118 private static object DeleteGameObject ( JToken targetToken , string searchMethod )
9021119 {
9031120 // Find potentially multiple objects if name/tag search is used without find_all=false implicitly
0 commit comments