using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.XR; namespace BlockSpace.Voxels { [DisallowMultipleComponent] public sealed class AvatarCustomizerController : MonoBehaviour { private const float AvatarVoxelSizeScale = 0.5f; private const float ButtonPressCooldown = 0.2f; private const float ButtonShrinkDuration = 0.08f; private const float ButtonSettleDuration = 0.12f; private const float ButtonPressedScale = 0.84f; private const float ButtonOvershootScale = 1.04f; private const string ButtonThockResourcePath = "Thock"; private static readonly string[] SuccessClipAssetPaths = new string[3] { "Assets/success.wav", "Assets/success.mp3", "Assets/success.ogg" }; private const string UiRootPath = "UI"; private const string EditHeadPath = "UI/Edit Head"; private const string EditLeftPath = "UI/Edit Left"; private const string EditRightPath = "UI/Edit Right"; private const string StopEditingPath = "UI/Stop Editing"; private const string ApplyAndSavePath = "UI/Apply and Save"; private const string HeadGroupPath = "Origins/Head/Head Group"; private const string LeftGroupPath = "Origins/Left/Left Group"; private const string RightGroupPath = "Origins/Right/Right Group"; [SerializeField] private VoxelGroupManager groupManager; [SerializeField] private VRVoxelBrush voxelBrush; [SerializeField] private Collider editHeadButton; [SerializeField] private Collider editLeftButton; [SerializeField] private Collider editRightButton; [SerializeField] private Collider stopEditingButton; [SerializeField] private Collider applyAndSaveButton; [SerializeField] private Transform headGroupTransform; [SerializeField] private Transform leftGroupTransform; [SerializeField] private Transform rightGroupTransform; [SerializeField] private AudioClip buttonThockClip; [SerializeField] private AudioClip applySuccessClip; private VoxelGroup headEditorGroup; private VoxelGroup leftEditorGroup; private VoxelGroup rightEditorGroup; private VoxelGroup activeEditorGroup; private readonly Dictionary buttonBaseScales = new Dictionary(); private readonly Dictionary buttonAnimationCoroutines = new Dictionary(); private bool editorGroupsInitialized; private bool loadedPersistedAvatar; private bool editHeadInsidePrevious; private bool editLeftInsidePrevious; private bool editRightInsidePrevious; private bool stopEditingInsidePrevious; private bool applyAndSaveInsidePrevious; private float nextButtonPressTime; public void Configure(VoxelGroupManager manager, VRVoxelBrush brush) { if (manager != null) { groupManager = manager; } if (brush != null) { voxelBrush = brush; } ResolveSceneReferences(); EnsureButtonAudioAssigned(); EnsureApplySuccessAudioAssigned(); TryInitializeEditorGroups(); } private void Awake() { ResolveSceneReferences(); EnsureButtonAudioAssigned(); EnsureApplySuccessAudioAssigned(); } private void OnDisable() { StopAllCoroutines(); StopEditing(); ResetAllButtonScales(); } private void Update() { ResolveSceneReferences(); if (TryInitializeEditorGroups()) { HandleButton(editHeadButton, ref editHeadInsidePrevious, delegate { StartEditing(headEditorGroup); }); HandleButton(editLeftButton, ref editLeftInsidePrevious, delegate { StartEditing(leftEditorGroup); }); HandleButton(editRightButton, ref editRightInsidePrevious, delegate { StartEditing(rightEditorGroup); }); HandleButton(stopEditingButton, ref stopEditingInsidePrevious, StopEditing); HandleButton(applyAndSaveButton, ref applyAndSaveInsidePrevious, ApplyAndSave); } } private void ResolveSceneReferences() { if (groupManager == null) { groupManager = ((VoxelGroupManager.Instance != null) ? VoxelGroupManager.Instance : UnityEngine.Object.FindObjectOfType()); } if (voxelBrush == null) { voxelBrush = ((VRVoxelBrush.Instance != null) ? VRVoxelBrush.Instance : UnityEngine.Object.FindObjectOfType()); } if (editHeadButton == null) { editHeadButton = FindCollider("UI/Edit Head"); } if (editLeftButton == null) { editLeftButton = FindCollider("UI/Edit Left"); } if (editRightButton == null) { editRightButton = FindCollider("UI/Edit Right"); } if (stopEditingButton == null) { stopEditingButton = FindCollider("UI/Stop Editing"); } if (applyAndSaveButton == null) { applyAndSaveButton = FindCollider("UI/Apply and Save"); } if (headGroupTransform == null) { headGroupTransform = base.transform.Find("Origins/Head/Head Group"); } if (leftGroupTransform == null) { leftGroupTransform = base.transform.Find("Origins/Left/Left Group"); } if (rightGroupTransform == null) { rightGroupTransform = base.transform.Find("Origins/Right/Right Group"); } } private bool TryInitializeEditorGroups() { if (editorGroupsInitialized) { return true; } if (groupManager == null || headGroupTransform == null || leftGroupTransform == null || rightGroupTransform == null) { return false; } headEditorGroup = PrepareEditorGroup(headGroupTransform.gameObject, headEditorGroup, 61000); leftEditorGroup = PrepareEditorGroup(leftGroupTransform.gameObject, leftEditorGroup, 61001); rightEditorGroup = PrepareEditorGroup(rightGroupTransform.gameObject, rightEditorGroup, 61002); if (headEditorGroup == null || leftEditorGroup == null || rightEditorGroup == null) { return false; } editorGroupsInitialized = true; if (!loadedPersistedAvatar) { AvatarVoxelStorage.ApplyPayload(AvatarVoxelStorage.LoadLocalOrDefault(), headEditorGroup, leftEditorGroup, rightEditorGroup, groupManager, useLocalGridTransform: true); loadedPersistedAvatar = true; } return true; } private VoxelGroup PrepareEditorGroup(GameObject groupObject, VoxelGroup existingGroup, ushort groupId) { if (groupObject == null || groupManager == null) { return null; } VoxelGroup voxelGroup = ((existingGroup != null) ? existingGroup : groupObject.GetComponent()); if (voxelGroup == null) { voxelGroup = groupObject.AddComponent(); } AvatarVoxelStorage.ApplyGroupData(voxelGroup, AvatarVoxelStorage.CaptureGroup(voxelGroup), groupManager, useLocalGridTransform: true); voxelGroup.SetVoxelSizeScale(0.5f); voxelGroup.id = groupId; return voxelGroup; } private void StartEditing(VoxelGroup group) { if (!(group == null)) { activeEditorGroup = group; VRHaptics.Pulse(XRNode.RightHand, 0.18f, 0.035f); VRGroupSelector.Instance?.ClearSelectionForToolUse(); VRGroupSelector.Instance?.SetInteractionSuppressed(suppressed: true); voxelBrush?.SetAvatarEditorTarget(group); } } private void StopEditing() { activeEditorGroup = null; voxelBrush?.ClearAvatarEditorTarget(); VRGroupSelector.Instance?.SetInteractionSuppressed(suppressed: false); } private void ApplyAndSave() { if (TryInitializeEditorGroups()) { voxelBrush?.FinalizeAvatarEditorSessions(); AvatarVoxelPayload payload = AvatarVoxelStorage.CapturePayload(headEditorGroup, leftEditorGroup, rightEditorGroup); AvatarVoxelStorage.SaveLocal(payload); PhotonVoxelAvatar localOwnedInstance = PhotonVoxelAvatar.LocalOwnedInstance; if (localOwnedInstance != null) { localOwnedInstance.ApplyAndBroadcastAvatar(payload); } VRHaptics.Pulse(XRNode.RightHand, 0.32f, 0.055f); PlayApplySuccessFeedback(); } } public bool TryGetEditorGroups(out VoxelGroup headBody, out VoxelGroup left, out VoxelGroup right) { headBody = null; left = null; right = null; if (!TryInitializeEditorGroups()) { return false; } headBody = headEditorGroup; left = leftEditorGroup; right = rightEditorGroup; if (headBody != null && left != null) { return right != null; } return false; } private void HandleButton(Collider button, ref bool insidePrevious, Action action) { bool flag = IsHandInside(button, (voxelBrush != null) ? voxelBrush.LeftHandAnchor : null) || IsHandInside(button, (voxelBrush != null) ? voxelBrush.RightHandAnchor : null); if (flag && !insidePrevious) { PlayButtonEnterAnimation(button); if (Time.unscaledTime >= nextButtonPressTime) { nextButtonPressTime = Time.unscaledTime + 0.2f; PlayButtonClickFeedback(button); action?.Invoke(); } } insidePrevious = flag; } private void PlayButtonEnterAnimation(Collider button) { if (!(button == null)) { Transform transform = button.transform; if (!buttonBaseScales.TryGetValue(transform, out var value)) { value = transform.localScale; buttonBaseScales.Add(transform, value); } if (buttonAnimationCoroutines.TryGetValue(transform, out var value2) && value2 != null) { StopCoroutine(value2); } transform.localScale = value; buttonAnimationCoroutines[transform] = StartCoroutine(AnimateButtonScale(transform, value)); } } private void PlayButtonClickFeedback(Collider button) { if (!(buttonThockClip == null) && !(button == null)) { AudioSource.PlayClipAtPoint(buttonThockClip, button.bounds.center, 1f); } } private IEnumerator AnimateButtonScale(Transform buttonTransform, Vector3 baseScale) { Vector3 pressedScale = baseScale * 0.84f; Vector3 overshootScale = baseScale * 1.04f; float elapsed = 0f; while (elapsed < 0.08f) { if (buttonTransform == null) { yield break; } elapsed += Time.unscaledDeltaTime; float t = Mathf.Clamp01(elapsed / 0.08f); buttonTransform.localScale = Vector3.LerpUnclamped(baseScale, pressedScale, t); yield return null; } elapsed = 0f; while (elapsed < 0.12f) { if (buttonTransform == null) { yield break; } elapsed += Time.unscaledDeltaTime; float num = Mathf.Clamp01(elapsed / 0.12f); float num2 = num * num * (3f - 2f * num); Vector3 localScale = ((num2 < 0.5f) ? Vector3.LerpUnclamped(pressedScale, overshootScale, num2 / 0.5f) : Vector3.LerpUnclamped(overshootScale, baseScale, (num2 - 0.5f) / 0.5f)); buttonTransform.localScale = localScale; yield return null; } if (buttonTransform != null) { buttonTransform.localScale = baseScale; buttonAnimationCoroutines[buttonTransform] = null; } } private void ResetAllButtonScales() { foreach (KeyValuePair buttonBaseScale in buttonBaseScales) { if (buttonBaseScale.Key != null) { buttonBaseScale.Key.localScale = buttonBaseScale.Value; } } buttonAnimationCoroutines.Clear(); } private void EnsureButtonAudioAssigned() { if (buttonThockClip == null) { buttonThockClip = Resources.Load("Thock"); } } private void EnsureApplySuccessAudioAssigned() { _ = applySuccessClip != null; } private void PlayApplySuccessFeedback() { if (!(applySuccessClip == null)) { Vector3 position = ((applyAndSaveButton != null) ? applyAndSaveButton.bounds.center : base.transform.position); AudioSource.PlayClipAtPoint(applySuccessClip, position, 1f); } } private Collider FindCollider(string relativePath) { Transform transform = base.transform.Find(relativePath); if (!(transform != null)) { return null; } return transform.GetComponent(); } private static bool IsHandInside(Collider button, Transform handAnchor) { if (button == null || handAnchor == null) { return false; } Vector3 position = handAnchor.position; return (button.ClosestPoint(position) - position).sqrMagnitude <= 1E-06f; } } }