// AvatarCloneControl.cs using IHXR.Simulation; public class AvatarCloneControl : ScriptBase { public SceneObject SpawnButton; public SceneObject EmoteButton; public SceneObject SitButton; public SceneObject DestroyButton; public SceneObject DropButton; // the clone falls over as a ragdoll public SceneObject StandButton; // the clone gets back up public Emote selectedEmote; public Sitpoint cloneSeat; // "*" or blank = the user who clicked SpawnButton. // Anything else = exact username to clone. public string targetUsername = "*"; // The name of one of that user's outfits, as they named it in the Avatar Editor (capitals don't matter). // Blank, or no outfit by that name = the outfit they are wearing. public string outfitName = ""; public Vector3 spawnOffset = new Vector3(0f, 0f, 2f); public Vector3 spawnRotation = Vector3.zero; public Vector3 cloneScale = Vector3.one; [Header("Ragdoll (Drop / Stand buttons)")] [Tooltip("Muscles: 0 = completely limp, 1 = stiff as a statue.")] [Range(0f, 1f)] public float ragdollStrength = 0.12f; [Tooltip("1 = normal gravity.")] [Range(-2f, 3f)] public float ragdollGravityScale = 1f; [Tooltip("How hard Drop pushes the clone backwards as it falls. 0 = it just collapses.")] public float dropPushPower = 2f; [Tooltip("Seconds until the clone gets up by itself. 0 = it stays down until Stand is clicked.")] public float dropAutoStandSeconds = 0f; [Tooltip("Seconds the clone takes to get up.")] public float standTransitionSeconds = 1.2f; public bool highlightButtons = true; public float interactDistance = 10f; AvatarClone clone; bool emotePlaying; bool seated; bool ragdolled; public override void Start() { if (SpawnButton != null && !SpawnButton.IsNull) MakeInteractable(SpawnButton, OnSpawnClicked, highlightButtons, null, true, interactDistance, "Spawn clone"); if (EmoteButton != null && !EmoteButton.IsNull) MakeInteractable(EmoteButton, OnEmoteClicked, highlightButtons, null, true, interactDistance, "Toggle clone emote"); if (SitButton != null && !SitButton.IsNull) MakeInteractable(SitButton, OnSitClicked, highlightButtons, null, true, interactDistance, "Toggle clone sit"); if (DestroyButton != null && !DestroyButton.IsNull) MakeInteractable(DestroyButton, OnDestroyClicked, highlightButtons, null, true, interactDistance, "Destroy clone"); if (DropButton != null && !DropButton.IsNull) MakeInteractable(DropButton, OnDropClicked, highlightButtons, null, true, interactDistance, "Drop clone"); if (StandButton != null && !StandButton.IsNull) MakeInteractable(StandButton, OnStandClicked, highlightButtons, null, true, interactDistance, "Clone stands up"); } void OnSpawnClicked(InteractionInfo info) { string username = ResolveTargetUsername(info); if (string.IsNullOrWhiteSpace(username)) { Log("[AvatarCloneControl] No username target for clone spawn."); return; } if (clone != null && !clone.IsNull) clone.Destroy(); Vector3 spawnPosition = GetSpawnPosition(); clone = SpawnAvatarClone(username, spawnPosition, spawnRotation, cloneScale, outfitName); emotePlaying = false; seated = false; ragdolled = false; Log("[AvatarCloneControl] Spawned clone of '" + username + "'" + (string.IsNullOrWhiteSpace(outfitName) ? "" : " in outfit '" + outfitName + "'") + " at " + spawnPosition); } void OnEmoteClicked(InteractionInfo info) { if (!HasClone()) return; if (selectedEmote == null || selectedEmote.IsNull) { Log("[AvatarCloneControl] Pick an emote in selectedEmote first."); return; } if (emotePlaying) { clone.StopEmote(); emotePlaying = false; } else { clone.PlayEmote(selectedEmote); emotePlaying = true; } Log("[AvatarCloneControl] clone emote='" + selectedEmote.EmoteId + "' playing=" + emotePlaying); } void OnSitClicked(InteractionInfo info) { if (!HasClone()) return; if (cloneSeat == null || cloneSeat.IsNull) { Log("[AvatarCloneControl] Slot a Sitpoint into cloneSeat first."); return; } if (seated) { clone.Stand(); seated = false; } else { clone.Sit(cloneSeat); seated = true; } Log("[AvatarCloneControl] clone seated=" + seated); } void OnDropClicked(InteractionInfo info) { if (!HasClone()) return; CharacterRagdollSettings settings = new CharacterRagdollSettings(); settings.Strength = ragdollStrength; settings.GravityScale = ragdollGravityScale; clone.Ragdoll(settings, dropAutoStandSeconds); if (dropPushPower > 0f) clone.ApplyImpact(new Vector3(0f, 0.3f, -1f), dropPushPower, 0.15f, dropAutoStandSeconds); ragdolled = true; seated = false; emotePlaying = false; Log("[AvatarCloneControl] clone ragdoll on"); } void OnStandClicked(InteractionInfo info) { if (!HasClone()) return; if (!ragdolled) Log("[AvatarCloneControl] The clone is not down (click Drop first)."); clone.StandUp(standTransitionSeconds); ragdolled = false; Log("[AvatarCloneControl] clone stands up"); } void OnDestroyClicked(InteractionInfo info) { if (!HasClone()) return; clone.Destroy(); clone = null; emotePlaying = false; seated = false; ragdolled = false; Log("[AvatarCloneControl] Destroyed clone."); } bool HasClone() { if (clone != null && !clone.IsNull) return true; Log("[AvatarCloneControl] Spawn a clone first."); return false; } string ResolveTargetUsername(InteractionInfo info) { string target = targetUsername == null ? "" : targetUsername.Trim(); if (target == "" || target == "*" || target == "**") return info != null ? info.Username : ""; return target; } Vector3 GetSpawnPosition() { if (SpawnButton != null && !SpawnButton.IsNull && SpawnButton.transform != null) return SpawnButton.transform.position + spawnOffset; return transform.position + spawnOffset; } }