using Animancer; using BrewMonster; using UnityEngine; using UnityEngine.UI; namespace BrewMonster.Scripts.UI.Inventory { public enum PreviewDistanceScaleMode { AspectRatio, ViewportHeight } /// /// Clones the current host-player visual model into a dedicated preview rig for the inventory UI. /// [DisallowMultipleComponent] public class InventoryCharacterPreview : MonoBehaviour { [Header("Player Binding")] [SerializeField] private CECHostPlayer hostPlayer; [Tooltip("Auto locate the active host player if none is assigned.")] [SerializeField] private bool autoBindHostPlayer = true; [Tooltip("Optional manual override for the visual root. Leave empty to auto detect the PlayerVisual/host root.")] [SerializeField] private Transform sourceModelRoot; [Tooltip("Clone the entire CECHostPlayer hierarchy instead of just the PlayerVisual child.")] [SerializeField] private bool cloneWholeHostHierarchy = true; [Tooltip("Remove gameplay scripts/colliders from the preview clone when duplicating the full hierarchy.")] [SerializeField] private bool stripRuntimeComponents = true; [Header("Preview Rig")] [SerializeField] private Transform previewRoot; [SerializeField] private Vector3 previewLocalPosition = Vector3.zero; [SerializeField] private Vector3 previewLocalEuler; [SerializeField] private Vector3 previewLocalScale = Vector3.one; [Header("Camera Output")] [SerializeField] private RawImage previewFrame; [SerializeField] private RenderTexture previewRenderTexture; [SerializeField] private bool autoFocusCamera = true; [SerializeField] private Vector3 cameraFocusOffset = new Vector3(0f, 0f, 0f); [Header("Camera Framing")] [SerializeField] private float previewCameraDistance = 3.3f; [SerializeField] private Vector3 previewCameraOffset = new Vector3(0.15f, 0.8f, 0f); [SerializeField] private bool overrideFieldOfView = true; [SerializeField][Range(10f, 60f)] private float previewFieldOfView = 33f; [SerializeField] private bool autoFrameByBounds = true; [SerializeField][Range(1f, 2f)] private float autoFramePadding = 1.15f; [SerializeField] private float minAutoCameraDistance = 1.25f; [SerializeField] private float maxAutoCameraDistance = 8f; [Header("Device Framing")] [Tooltip("Design-time screen width (1920x1080 baseline); previewCameraDistance/offset tuned for this.")] [SerializeField] private float referenceScreenWidth = 1920f; [Tooltip("Design-time screen height (1920x1080 baseline); previewCameraDistance/offset tuned for this.")] [SerializeField] private float referenceScreenHeight = 1080f; [Tooltip("Use previewFrame pixel size when available; otherwise Screen.width/height.")] [SerializeField] private bool useRawImageViewport = true; [SerializeField] private PreviewDistanceScaleMode distanceScaleMode = PreviewDistanceScaleMode.AspectRatio; [Tooltip("Scale previewCameraOffset.y by viewport height vs 1080p reference (1.0 at 1920x1080).")] [SerializeField] private bool scaleCameraOffsetYByViewport = true; private bool _cameraStateCached; private Vector3 _cachedCameraPosition; private Quaternion _cachedCameraRotation; private float _cachedCameraFov; //[Header("Behaviour")] //[Tooltip("Disable PlayerVisual/Animancer components on the clone so it stays in a frozen idle pose.")] //[SerializeField] private bool freezeAnimation = true; private GameObject _previewInstance; private bool _refreshQueued; private int _lastOriginModelChildCount; private Transform _lastOriginModelRoot; private Camera _previewCamera; private PlayerModelPreview _playerModelPreview; private RenderTexture _cachedTargetTexture; private void Awake() { if (previewRoot == null) { previewRoot = transform; } TryBindPreviewSystem(); TryBindHostPlayer(); EnsureCameraBindings(); } private void OnEnable() { TryBindPreviewSystem(); EnsureCameraBindings(); QueueRefresh(); } private void OnDisable() { RestoreCameraTargetTexture(); RestoreSharedCameraState(); ReleasePreviewReference(); } private void LateUpdate() { TryBindPreviewSystem(); TryBindHostPlayer(); // Check if origin model structure has changed (equipment models are child objects) // This detects equipment changes in realtime since all equipment is attached as children Transform currentOriginRoot = ResolveSourceModelRoot(); if (currentOriginRoot == null) { if (_previewInstance == null) { QueueRefresh(); } if(_refreshQueued) { _refreshQueued = false; BuildPreviewModel(); } return; } int currentChildCount = CountAllChildren(currentOriginRoot); if (_lastOriginModelRoot != currentOriginRoot || _lastOriginModelChildCount != currentChildCount) { _lastOriginModelRoot = currentOriginRoot; _lastOriginModelChildCount = currentChildCount; QueueRefresh(); } if (_refreshQueued) { _refreshQueued = false; BuildPreviewModel(); } if(_previewInstance != null) { EnsureCameraFocus(_previewInstance.transform); } } /// Allows manual binding from external UI scripts. public void BindPlayer(CECHostPlayer player) { if (player == null || player == hostPlayer) { return; } hostPlayer = player; QueueRefresh(); } /// Forces an immediate rebuild of the preview model. public void ForceRefreshNow() { _refreshQueued = false; BuildPreviewModel(); } /// Marks the clone dirty so it gets recreated next LateUpdate. public void QueueRefresh() { _refreshQueued = true; } private void TryBindPreviewSystem() { if (_playerModelPreview == null) { _playerModelPreview = PlayerModelPreview.Instance; } if (_playerModelPreview == null) { return; } if (_previewCamera == null) { _previewCamera = _playerModelPreview.GetComponent(); } } private void TryBindHostPlayer() { if (!autoBindHostPlayer || hostPlayer != null) { return; } hostPlayer = FindFirstObjectByType(); if (hostPlayer != null) { QueueRefresh(); } } private void EnsureCameraBindings() { if (_previewCamera == null) { TryBindPreviewSystem(); } if (_previewCamera == null || previewFrame == null) { return; } if (_cachedTargetTexture == null) { _cachedTargetTexture = _previewCamera.targetTexture; } if (previewRenderTexture != null) { _previewCamera.targetTexture = previewRenderTexture; } if(previewFrame != null) previewFrame.texture = _previewCamera.targetTexture; } private void RestoreCameraTargetTexture() { if (_previewCamera == null) { return; } if (_cachedTargetTexture != null) { _previewCamera.targetTexture = _cachedTargetTexture; } } private void BuildPreviewModel() { if (previewRoot == null) { return; } Transform sourceRoot = ResolveSourceModelRoot(); if (sourceRoot == null) { _previewInstance = null; _lastOriginModelRoot = null; _lastOriginModelChildCount = 0; QueueRefresh(); return; } if (_previewInstance != null && _previewInstance != sourceRoot.gameObject) { DestroyPreviewInstance(); } if (!sourceRoot.gameObject.activeSelf) { sourceRoot.gameObject.SetActive(true); } // Use resolved source root, not sourceModelRoot field directly. _previewInstance = sourceRoot.gameObject; Transform instanceTransform = _previewInstance.transform; if (instanceTransform.parent != previewRoot) { instanceTransform.SetParent(previewRoot, false); } instanceTransform.localPosition = previewLocalPosition + Vector3.up; instanceTransform.localRotation = Quaternion.Euler(previewLocalEuler); instanceTransform.localScale = previewLocalScale; ApplyLayerRecursive(instanceTransform, previewRoot.gameObject.layer); EnsureCameraFocus(instanceTransform); EnsureCameraBindings(); } private void DestroyPreviewInstance() { if (_previewInstance != null) { Destroy(_previewInstance); _previewInstance = null; } } private Transform ResolveSourceModelRoot() { if (sourceModelRoot != null) { return sourceModelRoot; } if (_playerModelPreview == null) { return null; } // Prefer active model. for (int i = 0; i < _playerModelPreview.playerModels.Count; i++) { GameObject model = _playerModelPreview.playerModels[i]; if (model != null && model.activeSelf) { return model.transform; } } // Fallback to first available model. for (int i = 0; i < _playerModelPreview.playerModels.Count; i++) { GameObject model = _playerModelPreview.playerModels[i]; if (model != null) { return model.transform; } } return null; } private void ReleasePreviewReference() { _previewInstance = null; } //private void DestroyPreviewInstance() //{ // if (_previewInstance != null) // { // Destroy(_previewInstance); // _previewInstance = null; // } //} //private static void DisableComponentInChildren(GameObject root) where T : Behaviour //{ // if (root == null) // { // return; // } // var components = root.GetComponentsInChildren(true); // for (int i = 0; i < components.Length; i++) // { // var component = components[i]; // if (component == null) // continue; // component.enabled = false; // if (component is PlayerVisual) // { // Destroy(component); // } // } //} private void ApplyLayerRecursive(Transform root, int layer) { if (root == null) { return; } root.gameObject.layer = layer; for (int i = 0; i < root.childCount; i++) { ApplyLayerRecursive(root.GetChild(i), layer); } } private void EnsureCameraFocus(Transform modelRoot) { if (_previewCamera == null) { return; } if (!_cameraStateCached) { _cachedCameraPosition = _previewCamera.transform.position; _cachedCameraRotation = _previewCamera.transform.rotation; _cachedCameraFov = _previewCamera.fieldOfView; _cameraStateCached = true; } // Apply FOV override independently from auto focus. if (overrideFieldOfView) { _previewCamera.fieldOfView = previewFieldOfView; } if (!autoFocusCamera || modelRoot == null) { return; } Vector3 effectiveOffset = ComputeEffectiveCameraOffset(); Vector3 focusPoint = modelRoot.TransformPoint(cameraFocusOffset) + effectiveOffset; Vector3 viewDirection = modelRoot.forward; viewDirection.y = 0f; if (viewDirection.sqrMagnitude < 0.0001f) { viewDirection = Vector3.back; } viewDirection.Normalize(); float effectiveDistance = ComputeEffectiveCameraDistance(modelRoot, focusPoint, viewDirection); _previewCamera.transform.position = focusPoint + viewDirection * effectiveDistance; _previewCamera.transform.LookAt(focusPoint); } private bool TryGetViewportSize(out float width, out float height) { if (useRawImageViewport && previewFrame != null) { RectTransform rt = previewFrame.rectTransform; Canvas canvas = previewFrame.canvas; float scale = canvas != null ? canvas.scaleFactor : 1f; width = rt.rect.width * scale; height = rt.rect.height * scale; if (width > 1f && height > 1f) { return true; } } width = Screen.width; height = Screen.height; return width > 0f && height > 0f; } private float ComputeAspectScaledDistance() { float distance = previewCameraDistance * ComputeViewportFramingScale(); return Mathf.Clamp(distance, minAutoCameraDistance, maxAutoCameraDistance); } private float ComputeViewportFramingScale() { if (!TryGetViewportSize(out float viewportWidth, out float viewportHeight)) { return 1f; } float refWidth = Mathf.Max(1f, referenceScreenWidth); float refHeight = Mathf.Max(1f, referenceScreenHeight); switch (distanceScaleMode) { case PreviewDistanceScaleMode.ViewportHeight: return viewportHeight / refHeight; default: float refAspect = refWidth / refHeight; float viewAspect = viewportWidth / viewportHeight; return viewAspect / refAspect; } } private float ComputeViewportHeightScale() { if (!TryGetViewportSize(out _, out float viewportHeight)) { return 1f; } float refHeight = Mathf.Max(1f, referenceScreenHeight); return viewportHeight / refHeight; } private Vector3 ComputeEffectiveCameraOffset() { if (!scaleCameraOffsetYByViewport) { return previewCameraOffset; } // Tuned at 1920x1080: scale is 1.0 so previewCameraOffset.y is unchanged on reference resolution. float offsetYScale = ComputeViewportHeightScale(); return new Vector3( previewCameraOffset.x, previewCameraOffset.y * offsetYScale, previewCameraOffset.z); } private float ComputeBoundsFitDistance(Transform modelRoot, Vector3 focusPoint, Vector3 viewDirection) { if (modelRoot == null) { return previewCameraDistance; } var renderers = modelRoot.GetComponentsInChildren(true); if (renderers == null || renderers.Length == 0) { return previewCameraDistance; } Bounds bounds = renderers[0].bounds; for (int i = 1; i < renderers.Length; i++) { if (renderers[i] != null) { bounds.Encapsulate(renderers[i].bounds); } } if (bounds.size.sqrMagnitude < 0.0001f) { return previewCameraDistance; } if (!TryGetViewportSize(out float viewportWidth, out float viewportHeight)) { return previewCameraDistance; } Vector3 cameraForward = -viewDirection; Vector3 cameraRight = Vector3.Cross(cameraForward, Vector3.up); if (cameraRight.sqrMagnitude < 0.0001f) { cameraRight = Vector3.Cross(cameraForward, Vector3.forward); } cameraRight.Normalize(); Vector3 cameraUp = Vector3.Cross(cameraRight, cameraForward).normalized; Vector3 boundsMin = bounds.min; Vector3 boundsMax = bounds.max; float maxHorizontal = 0f; float maxVertical = 0f; for (int x = 0; x <= 1; x++) { for (int y = 0; y <= 1; y++) { for (int z = 0; z <= 1; z++) { Vector3 corner = new Vector3( x == 0 ? boundsMin.x : boundsMax.x, y == 0 ? boundsMin.y : boundsMax.y, z == 0 ? boundsMin.z : boundsMax.z); Vector3 offset = corner - focusPoint; maxHorizontal = Mathf.Max(maxHorizontal, Mathf.Abs(Vector3.Dot(offset, cameraRight))); maxVertical = Mathf.Max(maxVertical, Mathf.Abs(Vector3.Dot(offset, cameraUp))); } } } float viewAspect = viewportWidth / viewportHeight; float verticalFovRad = previewFieldOfView * Mathf.Deg2Rad; float halfVerticalTan = Mathf.Tan(verticalFovRad * 0.5f); if (halfVerticalTan < 0.0001f) { return previewCameraDistance; } float halfHorizontalTan = halfVerticalTan * viewAspect; float distanceForHeight = maxVertical / halfVerticalTan; float distanceForWidth = maxHorizontal / Mathf.Max(halfHorizontalTan, 0.0001f); float boundsDistance = Mathf.Max(distanceForHeight, distanceForWidth); return boundsDistance * autoFramePadding; } private float ComputeEffectiveCameraDistance(Transform modelRoot, Vector3 focusPoint, Vector3 viewDirection) { float aspectScaledDistance = ComputeAspectScaledDistance(); float effectiveDistance = aspectScaledDistance; if (autoFrameByBounds) { float boundsDistance = ComputeBoundsFitDistance(modelRoot, focusPoint, viewDirection); effectiveDistance = Mathf.Max(aspectScaledDistance, boundsDistance); } return Mathf.Clamp(effectiveDistance, minAutoCameraDistance, maxAutoCameraDistance); } //private void StripRuntimeScripts(GameObject cloneRoot) //{ // if (cloneRoot == null) // { // return; // } // var monoBehaviours = cloneRoot.GetComponentsInChildren(true); // for (int i = 0; i < monoBehaviours.Length; i++) // { // var behaviour = monoBehaviours[i]; // if (behaviour == null) // continue; // Destroy(behaviour); // } // var colliders = cloneRoot.GetComponentsInChildren(true); // for (int i = 0; i < colliders.Length; i++) // { // var collider = colliders[i]; // if (collider == null) // continue; // Destroy(collider); // } // var rigidbodies = cloneRoot.GetComponentsInChildren(true); // for (int i = 0; i < rigidbodies.Length; i++) // { // var body = rigidbodies[i]; // if (body == null) // continue; // Destroy(body); // } //} private void RestoreSharedCameraState() { if (!_cameraStateCached || _previewCamera == null) { return; } _previewCamera.transform.position = _cachedCameraPosition; _previewCamera.transform.rotation = _cachedCameraRotation; _previewCamera.fieldOfView = _cachedCameraFov; _cameraStateCached = false; } //private void FreezeAnimators(GameObject cloneRoot) //{ // if (cloneRoot == null) // { // return; // } // var animators = cloneRoot.GetComponentsInChildren(true); // for (int i = 0; i < animators.Length; i++) // { // var animator = animators[i]; // if (animator == null) // continue; // animator.speed = 0f; // } //} /// /// Counts all children recursively in the transform hierarchy. /// Used to detect when equipment (child objects) are added or removed from the origin model. /// private int CountAllChildren(Transform root) { if (root == null) return 0; int count = root.childCount; for (int i = 0; i < root.childCount; i++) { count += CountAllChildren(root.GetChild(i)); } return count; } } }