using System.Diagnostics; using ABI_RC.Core; using NAK.AvatarScaleMod.AvatarScaling; using NAK.AvatarScaleMod.ScaledComponents; using UnityEngine; using UnityEngine.Animations; namespace NAK.AvatarScaleMod.Components; [DefaultExecutionOrder(-99999)] // before playersetup/puppetmaster but after animator public class BaseScaler : MonoBehaviour { #region Constants protected const string ScaleFactorParameterName = "ScaleFactor"; protected const string ScaleFactorParameterNameLocal = "#" + ScaleFactorParameterName; #endregion #region Events // OnAnimatedHeightChanged public delegate void AnimatedHeightChangedDelegate(BaseScaler scaler); public event AnimatedHeightChangedDelegate OnAnimatedHeightChanged; // OnAnimatedHeightOverride public delegate void AnimatedHeightOverrideDelegate(BaseScaler scaler); public event AnimatedHeightOverrideDelegate OnAnimatedHeightOverride; // OnTargetHeightChanged public delegate void TargetHeightChangedDelegate(BaseScaler scaler); public event TargetHeightChangedDelegate OnTargetHeightChanged; // OnHeightReset public delegate void HeightResetDelegate(BaseScaler scaler); public event HeightResetDelegate OnTargetHeightReset; // ------------------------------------------------ protected void InvokeAnimatedHeightChanged() => OnAnimatedHeightChanged?.Invoke(this); protected void InvokeAnimatedHeightOverride() => OnAnimatedHeightOverride?.Invoke(this); protected void InvokeTargetHeightChanged() => OnTargetHeightChanged?.Invoke(this); protected void InvokeTargetHeightReset() => OnTargetHeightReset?.Invoke(this); #endregion #region Variables // Height update requested public bool heightNeedsUpdate { get; internal set; } // Config variables public bool avatarIsHidden { get; set; } public bool useTargetHeight { get; set; } public bool overrideAnimationHeight { get; set; } // State variables internal bool _isAvatarInstantiated; internal bool _shouldForceHeight => useTargetHeight || avatarIsHidden; // universal or hidden avatar // Avatar info internal Transform _avatarTransform; internal CVRAnimatorManager _animatorManager; // Initial scaling internal float _initialHeight; internal Vector3 _initialScale; // Forced scaling (Universal & Hidden Avatar) internal float _targetHeight = -1; internal Vector3 _targetScale = Vector3.one; internal float _scaleFactor = 1f; // AnimationClip-based scaling (Local Avatar) internal float _animatedHeight; internal Vector3 _animatedScale; internal float _animatedScaleFactor = 1f; #endregion #region Avatar Events public virtual void OnAvatarInstantiated(GameObject avatarObject, float initialHeight, Vector3 initialScale) { if (_isAvatarInstantiated) return; _isAvatarInstantiated = true; _initialHeight = _animatedHeight = Mathf.Clamp(initialHeight, 0.01f, 100f); _initialScale = _animatedScale = initialScale; _animatedScaleFactor = 1f; if (!_shouldForceHeight) // not universal or hidden avatar { _targetHeight = _initialHeight; _targetScale = _initialScale; _scaleFactor = 1f; } _avatarTransform = avatarObject.transform; Stopwatch stopwatch = new(); stopwatch.Start(); FindComponentsOfType(scalableComponentTypes); stopwatch.Stop(); if (ModSettings.Debug_ComponentSearchTime.Value) AvatarScaleMod.Logger.Msg($"({typeof(LocalScaler)}) Component search time for {avatarObject}: {stopwatch.ElapsedMilliseconds}ms"); } public void OnAvatarDestroyed() { if (!_isAvatarInstantiated) return; _isAvatarInstantiated = false; _avatarTransform = null; heightNeedsUpdate = false; ClearComponentLists(); } #endregion #region Public Methods public float GetInitialHeight() => _initialHeight; public float GetTargetHeight() => _targetHeight; public float GetAnimatedHeight() => _animatedHeight; public bool IsForcingHeight() => _shouldForceHeight; public void SetTargetHeight(float height) { if (height < float.Epsilon) { ResetTargetHeight(); return; } _targetHeight = Mathf.Clamp(height, AvatarScaleManager.MinHeight, AvatarScaleManager.MaxHeight); _scaleFactor = Mathf.Max(_targetHeight / _initialHeight, 0.01f); //safety _targetScale = _initialScale * _scaleFactor; InvokeTargetHeightChanged(); } public void ResetTargetHeight() { // if (Math.Abs(_initialHeight - _targetHeight) < float.Epsilon) // return; // no need to change, is close enough useTargetHeight = false; _targetHeight = _animatedHeight; _targetScale = _animatedScale; _scaleFactor = _animatedScaleFactor; InvokeTargetHeightReset(); } public bool ApplyTargetHeight() { if (!_isAvatarInstantiated || _initialHeight == 0) return false; if (_avatarTransform == null) return false; heightNeedsUpdate = false; ScaleAvatarRoot(); UpdateAnimatorParameter(); ApplyComponentScaling(); return true; } #endregion #region Private Methods private void ScaleAvatarRoot() { if (_avatarTransform == null) return; _avatarTransform.localScale = _targetScale; } protected virtual void UpdateAnimatorParameter() { // empty } #endregion #region Unity Events public virtual void LateUpdate() { if (!_isAvatarInstantiated) return; // no avatar if (!_shouldForceHeight) return; // not universal scaling or hidden avatar ScaleAvatarRoot(); } internal virtual void OnDestroy() { ClearComponentLists(); } #endregion #region Component Scaling internal static readonly Type[] scalableComponentTypes = { typeof(Light), typeof(AudioSource), typeof(ParticleSystem), typeof(ParentConstraint), typeof(PositionConstraint), typeof(ScaleConstraint) }; private readonly List _scaledLights = new(); private readonly List _scaledAudioSources = new(); private readonly List _scaledParentConstraints = new(); private readonly List _scaledPositionConstraints = new(); private readonly List _scaledScaleConstraints = new(); private void ClearComponentLists() { _scaledLights.Clear(); _scaledAudioSources.Clear(); _scaledParentConstraints.Clear(); _scaledPositionConstraints.Clear(); _scaledScaleConstraints.Clear(); } internal void FindComponentsOfType(Type[] types) { var components = _avatarTransform.gameObject.GetComponentsInChildren(true); foreach (Component component in components) { if (this == null) break; if (component == null) continue; Type componentType = component.GetType(); if (types.Contains(componentType)) AddScaledComponent(componentType, component); } } private void AddScaledComponent(Type type, Component component) { switch (type) { case not null when type == typeof(AudioSource): _scaledAudioSources.Add(new ScaledAudioSource((AudioSource)component)); break; case not null when type == typeof(Light): _scaledLights.Add(new ScaledLight((Light)component)); break; case not null when type == typeof(ParentConstraint): _scaledParentConstraints.Add(new ScaledParentConstraint((ParentConstraint)component)); break; case not null when type == typeof(PositionConstraint): _scaledPositionConstraints.Add(new ScaledPositionConstraint((PositionConstraint)component)); break; case not null when type == typeof(ScaleConstraint): _scaledScaleConstraints.Add(new ScaledScaleConstraint((ScaleConstraint)component)); break; } } private void ApplyComponentScaling() { // UpdateLightScales(); // might break dps UpdateAudioSourceScales(); UpdateParentConstraintScales(); UpdatePositionConstraintScales(); UpdateScaleConstraintScales(); } private void UpdateLightScales() { // Update range of each light component foreach (ScaledLight light in _scaledLights) light.Scale(_scaleFactor); } private void UpdateAudioSourceScales() { // Update min and max distance of each audio source component foreach (ScaledAudioSource audioSource in _scaledAudioSources) audioSource.Scale(_scaleFactor); } private void UpdateParentConstraintScales() { // Update translationAtRest and translationOffsets of each parent constraint component foreach (ScaledParentConstraint parentConstraint in _scaledParentConstraints) parentConstraint.Scale(_scaleFactor); } private void UpdatePositionConstraintScales() { // Update translationAtRest and translationOffset of each position constraint component foreach (ScaledPositionConstraint positionConstraint in _scaledPositionConstraints) positionConstraint.Scale(_scaleFactor); } private void UpdateScaleConstraintScales() { // Update scaleAtRest and scaleOffset of each scale constraint component foreach (ScaledScaleConstraint scaleConstraint in _scaledScaleConstraints) scaleConstraint.Scale(_scaleFactor); } #endregion }