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;
}
}
}