216 lines
8.2 KiB
C#
216 lines
8.2 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
namespace BrewMonster
|
|
{
|
|
public enum IconTaskType
|
|
{
|
|
QI_NONE = -1,
|
|
QI_OUT = 0, // task tu chan (nhan)
|
|
QI_IN = 1, // task tu chan (hoan thanh)
|
|
QI_OUT_N = 2, // chua du dieu kien nhan task
|
|
QI_IN_N = 3, // task nhan nhung chua lam (chua xong)
|
|
QI_OUT_K = 4, // chua biet
|
|
QI_IN_K = 5, // chua biet
|
|
|
|
QI_OUT_TYPE1 = 6, // task bang hoi (nhan)
|
|
QI_IN_TYPE1 = 7, // task bang hoi (hoan thanh)
|
|
QI_OUT_TYPE2 = 8, // chua biet
|
|
QI_IN_TYPE2 = 9, // chua biet
|
|
QI_OUT_TYPE3 = 10, // task daily (nhan)
|
|
QI_IN_TYPE3 = 11, // task daily (hoan thanh)
|
|
QI_OUT_TYPE4 = 12, // task chinh (nhan)
|
|
QI_IN_TYPE4 = 13, // task chinh (hoan thanh)
|
|
}
|
|
|
|
[RequireComponent(typeof(NameplateWorldAnchor))]
|
|
public class UINPC : MonoBehaviour
|
|
{
|
|
[SerializeField] private TextMeshProUGUI _nameText;
|
|
[SerializeField] private TextMeshProUGUI _healthText;
|
|
[SerializeField] private Image _healthImage;
|
|
|
|
[Header("List Icon Task")]
|
|
[SerializeField] private GameObject _iconTaskMain;
|
|
[SerializeField] private List<Sprite> _listIconTask;
|
|
|
|
[Header("World Nameplate")]
|
|
[SerializeField] private Transform _canvasRoot;
|
|
|
|
private NameplateWorldAnchor _nameplateAnchor;
|
|
private Image _cachedIconImage;
|
|
private Coroutine _initialNameplateRoutine;
|
|
|
|
private void Awake()
|
|
{
|
|
CacheRefs();
|
|
_nameplateAnchor = GetComponent<NameplateWorldAnchor>();
|
|
if (_iconTaskMain != null)
|
|
{
|
|
_cachedIconImage = _iconTaskMain.GetComponent<Image>();
|
|
if (_cachedIconImage == null)
|
|
{
|
|
BMLogger.LogError($"[UINPC] _iconTaskMain doesn't have Image component!");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
_initialNameplateRoutine = StartCoroutine(CoApplyInitialNameplateDeferred(maxFrames: 120));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update nameplate position every frame so it follows skeleton animations (e.g. floating/flying NPCs or monsters).
|
|
/// Only active after the initial anchor coroutine has resolved (i.e. _initialNameplateRoutine == null).
|
|
/// 每帧更新名牌位置,使其跟随骨骼动画(如浮空/飞行的NPC或怪物,例如小星星)。
|
|
/// 仅在初始锚点协程完成后(_initialNameplateRoutine == null)生效。
|
|
/// </summary>
|
|
private void LateUpdate()
|
|
{
|
|
if (_initialNameplateRoutine != null)
|
|
return;
|
|
if (_canvasRoot == null || _nameplateAnchor == null)
|
|
return;
|
|
if (_nameplateAnchor.TryGetWorldPosition(out var worldPos))
|
|
ApplyCanvasRootLocalPosition(worldPos);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Call after NPC model async load (e.g. <see cref="CECNPC.QueueLoadNPCModel"/>) so SMR bounds exist before anchoring.
|
|
/// 在NPC模型异步加载完成后调用(如 <see cref="CECNPC.QueueLoadNPCModel"/>),以便SMR包围盒已存在再定位名牌。
|
|
/// </summary>
|
|
public void RefreshWorldNameplatePosition()
|
|
{
|
|
if (_initialNameplateRoutine != null)
|
|
{
|
|
StopCoroutine(_initialNameplateRoutine);
|
|
_initialNameplateRoutine = null;
|
|
}
|
|
_initialNameplateRoutine = StartCoroutine(CoApplyInitialNameplateDeferred(maxFrames: 45));
|
|
}
|
|
|
|
/// <summary>
|
|
/// World position for the nameplate root is applied once SMR bounds exist, or after timeout with root fallback (then may log).
|
|
/// <see cref="NameplateWorldAnchor.TryGetWorldPosition"/> with <c>logNpcRootFallback: false</c> while retrying so early frames do not spam errors.
|
|
/// 名牌根节点世界坐标:优先等合并SMR包围盒就绪后再设一次;超时则用根回退(此时才打LogError)。
|
|
/// </summary>
|
|
private IEnumerator CoApplyInitialNameplateDeferred(int maxFrames)
|
|
{
|
|
if (_canvasRoot == null)
|
|
{
|
|
CacheRefs();
|
|
}
|
|
if (_canvasRoot == null)
|
|
{
|
|
yield break;
|
|
}
|
|
if (_nameplateAnchor == null)
|
|
{
|
|
BMLogger.LogError(
|
|
"[Cuong] [UINPC] ApplyInitialCanvasRootPosition: NameplateWorldAnchor missing. " +
|
|
"Expected formula: _canvasRoot.position = anchor.TryGetWorldPosition(out worldPos) ? worldPos : unchanged.");
|
|
_initialNameplateRoutine = null;
|
|
yield break;
|
|
}
|
|
|
|
for (var i = 0; i < maxFrames; i++)
|
|
{
|
|
if (_nameplateAnchor.TryGetWorldPosition(out var worldPos, out var fromSmr, logNpcRootFallback: false)
|
|
&& fromSmr)
|
|
{
|
|
ApplyCanvasRootLocalPosition(worldPos);
|
|
// Model + SMR ready: scan bones and cache the highest one for per-frame LateUpdate tracking.
|
|
// 模型和SMR就绪:扫描骨骼并缓存最高骨骼,供LateUpdate每帧跟踪。
|
|
_nameplateAnchor.RefreshNpcTopBone();
|
|
_initialNameplateRoutine = null;
|
|
yield break;
|
|
}
|
|
yield return null;
|
|
}
|
|
|
|
// Timeout: set position from whatever is available (hook / SMR / root fallback), then still cache top bone.
|
|
// 超时:使用当前可用方式设置位置(挂点/SMR/根节点回退),仍然缓存最高骨骼。
|
|
if (_nameplateAnchor.TryGetWorldPosition(out var finalPos, out _, logNpcRootFallback: true))
|
|
{
|
|
ApplyCanvasRootLocalPosition(finalPos);
|
|
}
|
|
else
|
|
{
|
|
BMLogger.LogError(
|
|
"[Cuong] [UINPC] ApplyInitialCanvasRootPosition: TryGetWorldPosition returned false (no CECPlayer/CECNPC parent?). " +
|
|
"Formula when OK: _canvasRoot.position = worldPos. " +
|
|
"NPC with SMR bounds: worldPos = (combinedBounds.center.x, combinedBounds.max.y, combinedBounds.center.z) + Vector3.up * extraWorldYOffset. " +
|
|
"NPC fallback: worldPos = hostNpc.transform.position + Vector3.up * (fallbackHeightAboveRoot + extraWorldYOffset).");
|
|
}
|
|
_nameplateAnchor.RefreshNpcTopBone();
|
|
_initialNameplateRoutine = null;
|
|
}
|
|
|
|
private void ApplyCanvasRootLocalPosition(Vector3 worldPos)
|
|
{
|
|
var parent = _canvasRoot.parent;
|
|
if (parent == null)
|
|
{
|
|
_canvasRoot.position = worldPos;
|
|
return;
|
|
}
|
|
|
|
_canvasRoot.localPosition = parent.InverseTransformPoint(worldPos);
|
|
}
|
|
|
|
public void SetName(string name)
|
|
{
|
|
_nameText.text = name;
|
|
}
|
|
public void SetHealthImage(float health)
|
|
{
|
|
if(_healthImage != null)
|
|
_healthImage.fillAmount = health;
|
|
}
|
|
public void SetHealthText(string healthText)
|
|
{
|
|
if(_healthText != null)
|
|
_healthText.text = healthText;
|
|
}
|
|
|
|
public void SetTaskIcon(IconTaskType iconType)
|
|
{
|
|
if (_iconTaskMain == null || _cachedIconImage == null)
|
|
{
|
|
return;
|
|
}
|
|
int iconIndex = (int)iconType;
|
|
|
|
if (iconIndex >= 0 && _listIconTask != null && iconIndex < _listIconTask.Count)
|
|
{
|
|
_cachedIconImage.sprite = _listIconTask[iconIndex];
|
|
_iconTaskMain.SetActive(true);
|
|
}
|
|
else
|
|
{
|
|
_iconTaskMain.SetActive(false);
|
|
}
|
|
}
|
|
|
|
public void SetTaskIconMain(bool isShow)
|
|
{
|
|
if (_iconTaskMain != null)
|
|
_iconTaskMain.SetActive(isShow);
|
|
}
|
|
|
|
private void CacheRefs()
|
|
{
|
|
if (_canvasRoot == null)
|
|
{
|
|
_canvasRoot = transform;
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|