using Animancer; using BrewMonster; using UnityEngine; using UnityEngine.UI; namespace BrewMonster.Scripts.UI.Inventory { /// /// 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; [Tooltip("Copy the preview root layer onto the cloned hierarchy so the preview camera can isolate it.")] [SerializeField] private bool inheritPreviewLayer = true; [Header("Camera Output")] [SerializeField] private Camera previewCamera; [SerializeField] private RawImage previewFrame; [SerializeField] private RenderTexture previewRenderTexture; [SerializeField] private bool autoFocusCamera = true; [SerializeField] private Vector3 cameraFocusOffset = new Vector3(0f, 1.5f, 0f); [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 void Awake() { if (previewRoot == null) { previewRoot = transform; } TryBindHostPlayer(); EnsureCameraBindings(); } private void OnEnable() { QueueRefresh(); } private void OnDisable() { DestroyPreviewInstance(); } private void LateUpdate() { 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 var currentOriginRoot = ResolveSourceModelRoot(); if (currentOriginRoot != null) { int currentChildCount = CountAllChildren(currentOriginRoot); // Refresh if origin model changed or child count changed (equipment added/removed) if (_lastOriginModelRoot != currentOriginRoot || _lastOriginModelChildCount != currentChildCount) { _lastOriginModelRoot = currentOriginRoot; _lastOriginModelChildCount = currentChildCount; QueueRefresh(); } } if (_refreshQueued) { _refreshQueued = false; BuildPreviewModel(); } } /// 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 TryBindHostPlayer() { if (!autoBindHostPlayer || hostPlayer != null) { return; } hostPlayer = FindFirstObjectByType(); if (hostPlayer != null) { QueueRefresh(); } } private void EnsureCameraBindings() { if (previewCamera != null && previewRenderTexture != null) { previewCamera.targetTexture = previewRenderTexture; } if (previewFrame != null && previewCamera != null) { previewFrame.texture = previewCamera.targetTexture; } } private void BuildPreviewModel() { if (previewRoot == null) { return; } var sourceRoot = ResolveSourceModelRoot(); if (sourceRoot == null) { return; } DestroyPreviewInstance(); _previewInstance = Instantiate(sourceRoot.gameObject, previewRoot, false); _previewInstance.name = $"{sourceRoot.name}_Preview"; var instanceTransform = _previewInstance.transform; instanceTransform.localPosition = previewLocalPosition; instanceTransform.localRotation = Quaternion.Euler(previewLocalEuler); instanceTransform.localScale = previewLocalScale; if (inheritPreviewLayer) { ApplyLayerRecursive(instanceTransform, previewRoot.gameObject.layer); } if (freezeAnimation) { DisableComponentInChildren(_previewInstance); DisableComponentInChildren(_previewInstance); DisableComponentInChildren(_previewInstance); FreezeAnimators(_previewInstance); } if (cloneWholeHostHierarchy && stripRuntimeComponents) { StripRuntimeScripts(_previewInstance); } EnsureCameraFocus(instanceTransform); } private Transform ResolveSourceModelRoot() { if (sourceModelRoot != null) { return sourceModelRoot; } if (hostPlayer == null) { return null; } if (cloneWholeHostHierarchy) { sourceModelRoot = hostPlayer.transform; return sourceModelRoot; } var playerVisual = hostPlayer.GetComponentInChildren(true); sourceModelRoot = playerVisual != null ? playerVisual.transform : hostPlayer.transform; return sourceModelRoot; } 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 (!autoFocusCamera || previewCamera == null || modelRoot == null) { return; } var focusPoint = modelRoot.TransformPoint(cameraFocusOffset); } 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 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; } } }