1299 lines
42 KiB
C#
1299 lines
42 KiB
C#
using System.Collections.Generic;
|
|
using NeoModLoader.services;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>
|
|
/// Compact dossier: nametag (species/name/lv/task/sex), avatar + mini History, packed traits, reason.
|
|
/// </summary>
|
|
public static class WatchCaption
|
|
{
|
|
private const float PanelWidthMax = 420f;
|
|
private const float SpeciesSize = 16f;
|
|
private const float SexSize = 14f;
|
|
private const float LiveMax = 44f;
|
|
private const float ChipIcon = 12f;
|
|
private const float TraitIcon = 12f;
|
|
private const float HistoryIcon = 10f;
|
|
private const float HistoryColMinW = 118f;
|
|
private const float PadX = 4f;
|
|
/// <summary>Inset from the canvas top-right corner (grows left via pivot).</summary>
|
|
private const float ScreenInset = 12f;
|
|
private const float PadTop = 4f;
|
|
private const float HeaderH = 18f;
|
|
private const float BodyH = 44f;
|
|
private const float TraitsH = 14f;
|
|
private const float ReasonH = 14f;
|
|
private const float HistoryLineMinH = 13f;
|
|
private const float HistoryLineMaxH = 72f;
|
|
private const float Gap = 2f;
|
|
private const int HistoryMax = 3;
|
|
private const float StatusBannerSeconds = 5f;
|
|
|
|
/// <summary>History column grows with content until the dossier hits <see cref="PanelWidthMax"/>.</summary>
|
|
private static float HistoryColMaxW =>
|
|
Mathf.Max(HistoryColMinW, PanelWidthMax - PadX * 2f - LiveMax - 4f);
|
|
|
|
private static float _panelWidth = LiveMax + PadX * 2f;
|
|
|
|
private static readonly Color NameColor = new Color(0.95f, 0.93f, 0.88f, 1f);
|
|
private static readonly Color StatValueColor = new Color(1f, 0.62f, 0.28f, 1f);
|
|
private static readonly Color TaskColor = new Color(0.26f, 1f, 0.26f, 1f);
|
|
private static readonly Color ReasonColor = new Color(0.95f, 0.72f, 0.38f, 1f);
|
|
private static readonly Color TraitNameColor = new Color(0.78f, 0.8f, 0.84f, 1f);
|
|
private static readonly Color HistoryTextColor = new Color(0.82f, 0.84f, 0.86f, 1f);
|
|
private static readonly Color StatusBannerColor = new Color(1f, 0.55f, 0.18f, 1f);
|
|
|
|
private static GameObject _root;
|
|
private static RectTransform _rootRt;
|
|
private static Image _speciesIcon;
|
|
private static Image _sexIcon;
|
|
private static Text _nameText;
|
|
private static Text _reasonText;
|
|
|
|
private static Image _levelIcon;
|
|
private static Text _levelValue;
|
|
private static Image _taskIcon;
|
|
private static Text _taskText;
|
|
|
|
private static readonly TraitSlot[] _traitSlots = new TraitSlot[3];
|
|
private static GameObject _traitsRow;
|
|
|
|
private static GameObject _historyCol;
|
|
private static readonly HistorySlot[] _historySlots = new HistorySlot[HistoryMax];
|
|
private static readonly float[] _historySlotHeights = new float[HistoryMax];
|
|
private static float _historyColW = HistoryColMinW;
|
|
private static int _lastHistoryCount = -1;
|
|
private static long _lastHistorySubjectId;
|
|
|
|
private static UnitDossier _current;
|
|
private static Actor _boundActor;
|
|
private static bool _visible;
|
|
private static string _statusBanner = "";
|
|
private static float _statusBannerUntil;
|
|
|
|
private sealed class TraitSlot
|
|
{
|
|
public GameObject Root;
|
|
public Image Icon;
|
|
public Text Label;
|
|
}
|
|
|
|
private sealed class HistorySlot
|
|
{
|
|
public GameObject Root;
|
|
public Image Icon;
|
|
public Text Label;
|
|
}
|
|
|
|
public static UnitDossier Current => _current;
|
|
|
|
public static string LastHeadline { get; private set; } = "";
|
|
|
|
public static string LastDetail { get; private set; } = "";
|
|
|
|
public static string LastCaptionText { get; private set; } = "";
|
|
|
|
/// <summary>Harness: newest mini-history line currently shown (if any).</summary>
|
|
public static string LastHistoryPreview { get; private set; } = "";
|
|
|
|
/// <summary>Harness: comma-joined top trait labels currently shown.</summary>
|
|
public static string LastTraitsPreview { get; private set; } = "";
|
|
|
|
public static Vector2 LastPanelSize { get; private set; }
|
|
|
|
public static bool LastLayoutOk { get; private set; }
|
|
|
|
/// <summary>Screen-pixel rect of the dossier card (Unity bottom-left origin), if visible.</summary>
|
|
public static bool TryGetScreenPixelRect(out Rect pixelRect)
|
|
{
|
|
pixelRect = default;
|
|
if (_rootRt == null || _root == null || !_root.activeInHierarchy)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Vector3[] corners = new Vector3[4];
|
|
_rootRt.GetWorldCorners(corners);
|
|
float xMin = float.MaxValue;
|
|
float yMin = float.MaxValue;
|
|
float xMax = float.MinValue;
|
|
float yMax = float.MinValue;
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
Vector2 sp = RectTransformUtility.WorldToScreenPoint(null, corners[i]);
|
|
xMin = Mathf.Min(xMin, sp.x);
|
|
yMin = Mathf.Min(yMin, sp.y);
|
|
xMax = Mathf.Max(xMax, sp.x);
|
|
yMax = Mathf.Max(yMax, sp.y);
|
|
}
|
|
|
|
float pad = 4f;
|
|
pixelRect = Rect.MinMaxRect(xMin - pad, yMin - pad, xMax + pad, yMax + pad);
|
|
return pixelRect.width > 8f && pixelRect.height > 8f;
|
|
}
|
|
|
|
/// <summary>True if the mouse is over the dossier panel (including the live sprite).</summary>
|
|
public static bool IsPointerOverPanel()
|
|
{
|
|
if (_rootRt == null || _root == null || !_root.activeInHierarchy)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return RectTransformUtility.RectangleContainsScreenPoint(_rootRt, Input.mousePosition, null);
|
|
}
|
|
|
|
/// <summary>True if the mouse is over the dossier live sprite only.</summary>
|
|
public static bool IsPointerOverAvatar()
|
|
{
|
|
return DossierAvatar.ContainsScreenPoint(Input.mousePosition);
|
|
}
|
|
|
|
public static void Clear()
|
|
{
|
|
_current = null;
|
|
_boundActor = null;
|
|
LastHeadline = "";
|
|
LastDetail = "";
|
|
LastCaptionText = "";
|
|
LastHistoryPreview = "";
|
|
LastTraitsPreview = "";
|
|
LastPanelSize = Vector2.zero;
|
|
LastLayoutOk = false;
|
|
_lastHistoryCount = -1;
|
|
_lastHistorySubjectId = 0;
|
|
_historyColW = HistoryColMinW;
|
|
_statusBanner = "";
|
|
_statusBannerUntil = 0f;
|
|
ApplyVisual(null, null);
|
|
SetVisible(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Show a short status line on the dossier (replaces the top WorldTip for pause exits).
|
|
/// </summary>
|
|
public static void ShowStatusBanner(string message, float seconds = StatusBannerSeconds)
|
|
{
|
|
if (!ModSettings.ShowDossierCaption || string.IsNullOrEmpty(message))
|
|
{
|
|
return;
|
|
}
|
|
|
|
EnsureBuilt();
|
|
_statusBanner = message;
|
|
_statusBannerUntil = Time.unscaledTime + Mathf.Max(0.5f, seconds);
|
|
|
|
if (_nameText != null && (string.IsNullOrEmpty(_nameText.text) || _current == null))
|
|
{
|
|
_nameText.text = "Idle Spectator";
|
|
LastHeadline = "Idle Spectator";
|
|
}
|
|
|
|
if (_reasonText != null)
|
|
{
|
|
_reasonText.text = message;
|
|
_reasonText.color = StatusBannerColor;
|
|
_reasonText.supportRichText = false;
|
|
_reasonText.resizeTextForBestFit = false;
|
|
_reasonText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
|
_reasonText.verticalOverflow = VerticalWrapMode.Overflow;
|
|
_reasonText.gameObject.SetActive(true);
|
|
}
|
|
|
|
LastCaptionText = (LastHeadline ?? "Idle Spectator") + " | " + message;
|
|
int hist = CountActiveHistorySlots();
|
|
int traits = CountActiveTraitSlots();
|
|
bool hasTask = _taskText != null && _taskText.gameObject.activeSelf;
|
|
bool hasBody = _boundActor != null || hist > 0 || _current != null;
|
|
Relayout(hasBody, traits, hasTask, hasReason: true, hist);
|
|
SetVisible(true);
|
|
LogService.LogInfo("[IdleSpectator][CAPTION] status=" + message);
|
|
}
|
|
|
|
public static void SetFromInterest(InterestEvent interest)
|
|
{
|
|
Actor unit = interest != null && interest.HasFollowUnit
|
|
? interest.FollowUnit
|
|
: (MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null);
|
|
|
|
if (unit == null || !unit.isAlive())
|
|
{
|
|
if (interest != null)
|
|
{
|
|
LastHeadline = CameraDirector.FormatWatchTip(interest);
|
|
LastDetail = "";
|
|
LastCaptionText = LastHeadline;
|
|
LastHistoryPreview = "";
|
|
_current = null;
|
|
_boundActor = null;
|
|
EnsureBuilt();
|
|
ApplyVisual(null, null);
|
|
if (_nameText != null)
|
|
{
|
|
_nameText.text = LastHeadline;
|
|
}
|
|
|
|
Relayout(false, 0, false, false, 0);
|
|
SetVisible(ModSettings.ShowDossierCaption && SpectatorMode.Active);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
SetFromActor(unit, interest?.Tier, interest?.Label);
|
|
}
|
|
|
|
public static void SetFromActor(Actor actor, InterestTier? tier = null, string label = null)
|
|
{
|
|
ClearStatusBanner();
|
|
UnitDossier dossier = UnitDossier.FromActor(actor, tier, label);
|
|
_current = dossier;
|
|
_boundActor = actor;
|
|
LastHeadline = dossier.Headline ?? "";
|
|
LastDetail = dossier.DetailLine ?? "";
|
|
LastCaptionText = dossier.CaptionText ?? "";
|
|
EnsureBuilt();
|
|
ApplyVisual(actor, dossier);
|
|
SetVisible(ModSettings.ShowDossierCaption && SpectatorMode.Active);
|
|
LogService.LogInfo("[IdleSpectator][CAPTION] " + LastCaptionText.Replace("\n", " | "));
|
|
}
|
|
|
|
public static void Update()
|
|
{
|
|
if (HasStatusBanner())
|
|
{
|
|
if (!_visible && ModSettings.ShowDossierCaption)
|
|
{
|
|
SetVisible(true);
|
|
}
|
|
|
|
if (!SpectatorMode.Active)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Idle resumed - drop banner and restore normal reason styling next ApplyVisual.
|
|
ClearStatusBanner();
|
|
}
|
|
|
|
if (!SpectatorMode.Active || !ModSettings.ShowDossierCaption)
|
|
{
|
|
if (_visible)
|
|
{
|
|
SetVisible(false);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (_root == null && !string.IsNullOrEmpty(LastCaptionText))
|
|
{
|
|
EnsureBuilt();
|
|
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
|
if (_current != null)
|
|
{
|
|
ApplyVisual(focus ?? _boundActor, _current);
|
|
}
|
|
else if (_nameText != null)
|
|
{
|
|
_nameText.text = LastHeadline;
|
|
Relayout(false, 0, false, false, 0);
|
|
}
|
|
|
|
SetVisible(true);
|
|
}
|
|
|
|
RefreshLivePortrait();
|
|
RefreshHistoryIfChanged();
|
|
}
|
|
|
|
private static bool HasStatusBanner()
|
|
{
|
|
return !string.IsNullOrEmpty(_statusBanner) && Time.unscaledTime < _statusBannerUntil;
|
|
}
|
|
|
|
private static void ClearStatusBanner()
|
|
{
|
|
_statusBanner = "";
|
|
_statusBannerUntil = 0f;
|
|
if (_reasonText != null)
|
|
{
|
|
_reasonText.color = ReasonColor;
|
|
}
|
|
}
|
|
|
|
private static int CountActiveHistorySlots()
|
|
{
|
|
int n = 0;
|
|
for (int i = 0; i < _historySlots.Length; i++)
|
|
{
|
|
if (_historySlots[i]?.Root != null && _historySlots[i].Root.activeSelf)
|
|
{
|
|
n++;
|
|
}
|
|
}
|
|
|
|
return n;
|
|
}
|
|
|
|
private static int CountActiveTraitSlots()
|
|
{
|
|
int n = 0;
|
|
for (int i = 0; i < _traitSlots.Length; i++)
|
|
{
|
|
if (_traitSlots[i]?.Root != null && _traitSlots[i].Root.activeSelf)
|
|
{
|
|
n++;
|
|
}
|
|
}
|
|
|
|
return n;
|
|
}
|
|
|
|
private static void RefreshLivePortrait()
|
|
{
|
|
if (!_visible)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Actor actor = _boundActor;
|
|
if (actor == null || !actor.isAlive())
|
|
{
|
|
if (MoveCamera.hasFocusUnit())
|
|
{
|
|
actor = MoveCamera._focus_unit;
|
|
}
|
|
}
|
|
|
|
if (actor == null || !actor.isAlive())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_current != null && actor.getID() != _current.UnitId)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DossierAvatar.Show(actor);
|
|
BringHeaderFront();
|
|
}
|
|
|
|
private static void BringHeaderFront()
|
|
{
|
|
// Draw nametag above vanilla avatar chrome (which can paint outside its host).
|
|
void Front(Component c)
|
|
{
|
|
if (c != null)
|
|
{
|
|
c.transform.SetAsLastSibling();
|
|
}
|
|
}
|
|
|
|
Front(_speciesIcon);
|
|
Front(_nameText);
|
|
Front(_levelIcon);
|
|
Front(_levelValue);
|
|
Front(_taskIcon);
|
|
Front(_taskText);
|
|
Front(_sexIcon);
|
|
}
|
|
|
|
private static void RefreshHistoryIfChanged()
|
|
{
|
|
if (!_visible || _current == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
long id = _current.UnitId;
|
|
int count = Chronicle.HistoryCountFor(id);
|
|
if (id == _lastHistorySubjectId && count == _lastHistoryCount)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int shown = FillHistory(id);
|
|
// Re-run layout only when history presence flips or line count changes.
|
|
bool hasBody = _boundActor != null;
|
|
bool hasTask = _taskText != null && _taskText.gameObject.activeSelf;
|
|
bool hasReason = _reasonText != null && _reasonText.gameObject.activeSelf;
|
|
int traits = 0;
|
|
if (_traitsRow != null && _traitsRow.activeSelf)
|
|
{
|
|
for (int i = 0; i < _traitSlots.Length; i++)
|
|
{
|
|
if (_traitSlots[i] != null && _traitSlots[i].Root != null && _traitSlots[i].Root.activeSelf)
|
|
{
|
|
traits++;
|
|
}
|
|
}
|
|
}
|
|
|
|
Relayout(hasBody, traits, hasTask, hasReason, shown);
|
|
}
|
|
|
|
private static void ApplyVisual(Actor actor, UnitDossier dossier)
|
|
{
|
|
EnsureBuilt();
|
|
if (_root == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_boundActor = actor;
|
|
|
|
if (_speciesIcon != null)
|
|
{
|
|
HudIcons.Apply(_speciesIcon, actor != null ? HudIcons.FromActor(actor) : null);
|
|
}
|
|
|
|
bool hasSex = false;
|
|
if (_sexIcon != null)
|
|
{
|
|
Sprite sexSprite = null;
|
|
if (dossier != null)
|
|
{
|
|
if (dossier.IsMale)
|
|
{
|
|
sexSprite = HudIcons.SexMale();
|
|
}
|
|
else if (dossier.IsFemale)
|
|
{
|
|
sexSprite = HudIcons.SexFemale();
|
|
}
|
|
}
|
|
|
|
HudIcons.Apply(_sexIcon, sexSprite);
|
|
hasSex = sexSprite != null;
|
|
_sexIcon.gameObject.SetActive(hasSex);
|
|
}
|
|
|
|
if (_nameText != null)
|
|
{
|
|
_nameText.text = dossier != null && !string.IsNullOrEmpty(dossier.Headline)
|
|
? dossier.Headline
|
|
: "";
|
|
}
|
|
|
|
bool hasBody = dossier != null && actor != null;
|
|
DossierAvatar.SetActive(hasBody);
|
|
|
|
bool hasLevel = hasBody;
|
|
if (_levelIcon != null)
|
|
{
|
|
HudIcons.Apply(_levelIcon, hasLevel ? HudIcons.Level() : null);
|
|
_levelIcon.gameObject.SetActive(hasLevel);
|
|
}
|
|
|
|
if (_levelValue != null)
|
|
{
|
|
_levelValue.text = hasLevel ? dossier.Level.ToString() : "";
|
|
_levelValue.gameObject.SetActive(hasLevel);
|
|
}
|
|
|
|
if (hasBody)
|
|
{
|
|
DossierAvatar.Show(actor);
|
|
}
|
|
else
|
|
{
|
|
DossierAvatar.ClearActor();
|
|
}
|
|
|
|
bool hasTask = dossier != null && !string.IsNullOrEmpty(dossier.TaskText);
|
|
if (_taskIcon != null)
|
|
{
|
|
HudIcons.Apply(_taskIcon, hasTask ? HudIcons.Task() : null);
|
|
_taskIcon.gameObject.SetActive(hasTask);
|
|
}
|
|
|
|
if (_taskText != null)
|
|
{
|
|
string task = hasTask ? dossier.TaskText : "";
|
|
if (task.Length > 14)
|
|
{
|
|
task = task.Substring(0, 13) + "...";
|
|
}
|
|
|
|
_taskText.text = task;
|
|
_taskText.gameObject.SetActive(hasTask);
|
|
}
|
|
|
|
int traitCount = 0;
|
|
LastTraitsPreview = "";
|
|
if (_traitsRow != null)
|
|
{
|
|
if (dossier != null && dossier.TopTraits.Count > 0)
|
|
{
|
|
_traitsRow.SetActive(true);
|
|
System.Text.StringBuilder traitPreview = new System.Text.StringBuilder();
|
|
for (int i = 0; i < _traitSlots.Length; i++)
|
|
{
|
|
TraitSlot slot = _traitSlots[i];
|
|
if (slot == null || slot.Root == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (i < dossier.TopTraits.Count)
|
|
{
|
|
UnitDossier.TraitChip chip = dossier.TopTraits[i];
|
|
slot.Root.SetActive(true);
|
|
HudIcons.Apply(slot.Icon, HudIcons.FromTrait(chip.Trait));
|
|
string name = chip.Name ?? "";
|
|
if (slot.Label != null)
|
|
{
|
|
slot.Label.text = name;
|
|
}
|
|
|
|
if (traitPreview.Length > 0)
|
|
{
|
|
traitPreview.Append(", ");
|
|
}
|
|
|
|
traitPreview.Append(name);
|
|
traitCount++;
|
|
}
|
|
else
|
|
{
|
|
slot.Root.SetActive(false);
|
|
}
|
|
}
|
|
|
|
LastTraitsPreview = traitPreview.ToString();
|
|
}
|
|
else
|
|
{
|
|
_traitsRow.SetActive(false);
|
|
}
|
|
}
|
|
|
|
int historyCount = 0;
|
|
if (dossier != null)
|
|
{
|
|
historyCount = FillHistory(dossier.UnitId);
|
|
}
|
|
else
|
|
{
|
|
LastHistoryPreview = "";
|
|
if (_historyCol != null)
|
|
{
|
|
_historyCol.SetActive(false);
|
|
}
|
|
}
|
|
|
|
bool hasReason = dossier != null && !string.IsNullOrEmpty(dossier.ReasonLine);
|
|
if (_reasonText != null)
|
|
{
|
|
_reasonText.text = hasReason ? dossier.ReasonLine : "";
|
|
_reasonText.gameObject.SetActive(hasReason);
|
|
}
|
|
|
|
Relayout(hasBody, traitCount, hasTask, hasReason, historyCount);
|
|
}
|
|
|
|
private static int FillHistory(long unitId)
|
|
{
|
|
IReadOnlyList<ChronicleEntry> entries = Chronicle.LatestForSubject(unitId, HistoryMax);
|
|
_lastHistorySubjectId = unitId;
|
|
_lastHistoryCount = Chronicle.HistoryCountFor(unitId);
|
|
|
|
int shown = 0;
|
|
LastHistoryPreview = "";
|
|
_historyColW = HistoryColMinW;
|
|
for (int i = 0; i < _historySlotHeights.Length; i++)
|
|
{
|
|
_historySlotHeights[i] = HistoryLineMinH;
|
|
}
|
|
|
|
if (_historyCol == null)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (entries == null || entries.Count == 0)
|
|
{
|
|
_historyCol.SetActive(false);
|
|
return 0;
|
|
}
|
|
|
|
_historyCol.SetActive(true);
|
|
|
|
// Pass 1: grow the history column toward content width (capped by dossier max).
|
|
float widestLabel = 0f;
|
|
for (int i = 0; i < _historySlots.Length; i++)
|
|
{
|
|
HistorySlot slot = _historySlots[i];
|
|
if (slot?.Label == null || i >= entries.Count || entries[i] == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Text label = slot.Label;
|
|
label.supportRichText = true;
|
|
label.resizeTextForBestFit = false;
|
|
label.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
label.verticalOverflow = VerticalWrapMode.Overflow;
|
|
label.alignment = TextAnchor.UpperLeft;
|
|
label.text = entries[i].DisplayLineRich ?? "";
|
|
Canvas.ForceUpdateCanvases();
|
|
try
|
|
{
|
|
widestLabel = Mathf.Max(widestLabel, label.preferredWidth);
|
|
}
|
|
catch
|
|
{
|
|
widestLabel = Mathf.Max(widestLabel, (entries[i].DisplayLine ?? "").Length * 6.2f);
|
|
}
|
|
}
|
|
|
|
_historyColW = Mathf.Clamp(
|
|
widestLabel + HistoryIcon + 4f,
|
|
HistoryColMinW,
|
|
HistoryColMaxW);
|
|
float labelW = Mathf.Max(24f, _historyColW - HistoryIcon - 2f);
|
|
|
|
// Pass 2: wrap at the chosen column width and size each row height.
|
|
for (int i = 0; i < _historySlots.Length; i++)
|
|
{
|
|
HistorySlot slot = _historySlots[i];
|
|
if (slot == null || slot.Root == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (i < entries.Count && entries[i] != null)
|
|
{
|
|
ChronicleEntry e = entries[i];
|
|
slot.Root.SetActive(true);
|
|
HudIcons.Apply(slot.Icon, HudIcons.ForChronicleKind(e.Kind));
|
|
if (slot.Label != null)
|
|
{
|
|
slot.Label.horizontalOverflow = HorizontalWrapMode.Wrap;
|
|
slot.Label.verticalOverflow = VerticalWrapMode.Overflow;
|
|
slot.Label.text = e.DisplayLineRich ?? "";
|
|
RectTransform lrt = slot.Label.GetComponent<RectTransform>();
|
|
lrt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, labelW);
|
|
Canvas.ForceUpdateCanvases();
|
|
float needed = HistoryLineMinH;
|
|
try
|
|
{
|
|
needed = Mathf.Max(HistoryLineMinH, slot.Label.preferredHeight + 4f);
|
|
}
|
|
catch
|
|
{
|
|
int chars = (e.DisplayLine ?? "").Length;
|
|
int perLine = Mathf.Max(12, (int)(labelW / 6.2f));
|
|
int lines = Mathf.Max(1, (chars + perLine - 1) / perLine);
|
|
needed = HistoryLineMinH * lines;
|
|
}
|
|
|
|
_historySlotHeights[i] = Mathf.Clamp(needed, HistoryLineMinH, HistoryLineMaxH);
|
|
}
|
|
|
|
if (shown == 0)
|
|
{
|
|
LastHistoryPreview = e.DisplayLine ?? "";
|
|
}
|
|
|
|
shown++;
|
|
}
|
|
else
|
|
{
|
|
slot.Root.SetActive(false);
|
|
_historySlotHeights[i] = 0f;
|
|
}
|
|
}
|
|
|
|
return shown;
|
|
}
|
|
|
|
private static float MeasureHistoryColumnHeight(int historyCount)
|
|
{
|
|
if (historyCount <= 0)
|
|
{
|
|
return BodyH;
|
|
}
|
|
|
|
float total = 0f;
|
|
int used = 0;
|
|
for (int i = 0; i < _historySlotHeights.Length && used < historyCount; i++)
|
|
{
|
|
if (_historySlotHeights[i] <= 0.01f)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
total += _historySlotHeights[i];
|
|
used++;
|
|
}
|
|
|
|
return Mathf.Max(BodyH, total);
|
|
}
|
|
|
|
private static void Relayout(bool hasBody, int traitCount, bool hasTask, bool hasReason, int historyCount)
|
|
{
|
|
float y = PadTop;
|
|
float headerW = PlaceHeader(y, hasTask);
|
|
y += HeaderH + Gap;
|
|
|
|
float bodyW = 0f;
|
|
float traitsW = 0f;
|
|
bool traitsBeside = hasBody && historyCount <= 0 && traitCount > 0;
|
|
|
|
if (hasBody)
|
|
{
|
|
float bodyH = historyCount > 0 ? MeasureHistoryColumnHeight(historyCount) : BodyH;
|
|
PlaceBody(y, historyCount, bodyH);
|
|
if (historyCount > 0)
|
|
{
|
|
bodyW = LiveMax + 4f + _historyColW;
|
|
y += bodyH + Gap;
|
|
}
|
|
else if (traitsBeside)
|
|
{
|
|
// Fill the body-right void with a vertical trait stack beside the portrait.
|
|
traitsW = PlaceTraitsBesideAvatar(y, traitCount);
|
|
bodyW = LiveMax + 4f + traitsW;
|
|
y += BodyH + Gap;
|
|
}
|
|
else
|
|
{
|
|
bodyW = LiveMax;
|
|
y += BodyH + Gap;
|
|
}
|
|
}
|
|
|
|
if (traitCount > 0 && !traitsBeside)
|
|
{
|
|
traitsW = PlaceTraits(y, traitCount);
|
|
y += TraitsH + Gap;
|
|
}
|
|
|
|
if (hasReason)
|
|
{
|
|
PlaceLine(_reasonText != null ? _reasonText.GetComponent<RectTransform>() : null, y, ReasonH, PadX);
|
|
y += ReasonH + Gap;
|
|
}
|
|
|
|
_panelWidth = Mathf.Clamp(
|
|
PadX * 2f + Mathf.Max(headerW, bodyW, traitsW, LiveMax),
|
|
LiveMax + PadX * 2f,
|
|
PanelWidthMax);
|
|
|
|
float height = Mathf.Max(HeaderH + PadTop * 2f, y + PadTop - Gap);
|
|
if (_rootRt != null)
|
|
{
|
|
ApplyRootPlacement();
|
|
_rootRt.sizeDelta = new Vector2(_panelWidth, height);
|
|
LastPanelSize = _rootRt.sizeDelta;
|
|
}
|
|
|
|
BringHeaderFront();
|
|
|
|
LastLayoutOk = ProbeLayoutInside(height) && headerW > SpeciesSize;
|
|
if (!LastLayoutOk)
|
|
{
|
|
LogService.LogInfo(
|
|
$"[IdleSpectator][CAPTION][LAYOUT_BAD] size={LastPanelSize} body={hasBody} traits={traitCount} hist={historyCount} headerW={headerW}");
|
|
}
|
|
}
|
|
|
|
/// <summary>Left-pack nametag on the root: [species] Name [lv]n [task] [sex].</summary>
|
|
private static float PlaceHeader(float yFromTop, bool hasTask)
|
|
{
|
|
float x = PadX;
|
|
|
|
if (_speciesIcon != null)
|
|
{
|
|
PlaceLeftChip(_speciesIcon.rectTransform, x, yFromTop + 1f, SpeciesSize, SpeciesSize);
|
|
x += SpeciesSize + 3f;
|
|
}
|
|
|
|
if (_nameText != null)
|
|
{
|
|
float nameW = Mathf.Clamp(MeasureTextWidth(_nameText, 72f), 28f, 120f);
|
|
PlaceLeftChip(_nameText.rectTransform, x, yFromTop, nameW, HeaderH);
|
|
x += nameW + 5f;
|
|
}
|
|
|
|
if (_levelIcon != null && _levelIcon.gameObject.activeSelf)
|
|
{
|
|
PlaceLeftChip(_levelIcon.rectTransform, x, yFromTop + 3f, ChipIcon, ChipIcon);
|
|
x += ChipIcon + 1f;
|
|
}
|
|
|
|
if (_levelValue != null && _levelValue.gameObject.activeSelf)
|
|
{
|
|
float lvW = Mathf.Clamp(MeasureTextWidth(_levelValue, 10f), 8f, 24f);
|
|
PlaceLeftChip(_levelValue.rectTransform, x, yFromTop, lvW, HeaderH);
|
|
x += lvW + 4f;
|
|
}
|
|
|
|
if (hasTask && _taskIcon != null && _taskText != null)
|
|
{
|
|
_taskIcon.gameObject.SetActive(true);
|
|
_taskText.gameObject.SetActive(true);
|
|
PlaceLeftChip(_taskIcon.rectTransform, x, yFromTop + 3f, ChipIcon, ChipIcon);
|
|
x += ChipIcon + 1f;
|
|
float taskW = Mathf.Clamp(MeasureTextWidth(_taskText, 36f), 20f, 72f);
|
|
PlaceLeftChip(_taskText.rectTransform, x, yFromTop, taskW, HeaderH);
|
|
x += taskW + 4f;
|
|
}
|
|
else
|
|
{
|
|
if (_taskText != null)
|
|
{
|
|
_taskText.gameObject.SetActive(false);
|
|
}
|
|
|
|
if (_taskIcon != null)
|
|
{
|
|
_taskIcon.gameObject.SetActive(false);
|
|
}
|
|
}
|
|
|
|
if (_sexIcon != null && _sexIcon.gameObject.activeSelf)
|
|
{
|
|
PlaceLeftChip(_sexIcon.rectTransform, x, yFromTop + 2f, SexSize, SexSize);
|
|
x += SexSize;
|
|
}
|
|
|
|
BringHeaderFront();
|
|
return Mathf.Max(0f, x - PadX);
|
|
}
|
|
|
|
private static void PlaceLeftChip(RectTransform rt, float x, float yFromTop, float width, float height)
|
|
{
|
|
if (rt == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
rt.anchorMin = new Vector2(0f, 1f);
|
|
rt.anchorMax = new Vector2(0f, 1f);
|
|
rt.pivot = new Vector2(0f, 1f);
|
|
rt.anchoredPosition = new Vector2(x, -yFromTop);
|
|
rt.sizeDelta = new Vector2(width, height);
|
|
}
|
|
|
|
private static float MeasureTextWidth(Text text, float fallback)
|
|
{
|
|
if (text == null || string.IsNullOrEmpty(text.text))
|
|
{
|
|
return fallback;
|
|
}
|
|
|
|
try
|
|
{
|
|
float w = text.preferredWidth;
|
|
if (w > 1f)
|
|
{
|
|
return w + 2f;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// fall through
|
|
}
|
|
|
|
return fallback;
|
|
}
|
|
|
|
private static void PlaceBody(float yFromTop, int historyCount, float bodyH)
|
|
{
|
|
DossierAvatar.Place(PadX, yFromTop, LiveMax);
|
|
|
|
if (_historyCol == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (historyCount <= 0)
|
|
{
|
|
_historyCol.SetActive(false);
|
|
return;
|
|
}
|
|
|
|
_historyCol.SetActive(true);
|
|
RectTransform col = _historyCol.GetComponent<RectTransform>();
|
|
col.anchorMin = new Vector2(0f, 1f);
|
|
col.anchorMax = new Vector2(0f, 1f);
|
|
col.pivot = new Vector2(0f, 1f);
|
|
col.anchoredPosition = new Vector2(PadX + LiveMax + 4f, -yFromTop);
|
|
col.sizeDelta = new Vector2(_historyColW, bodyH);
|
|
|
|
float top = 0f;
|
|
for (int i = 0; i < _historySlots.Length; i++)
|
|
{
|
|
HistorySlot slot = _historySlots[i];
|
|
if (slot == null || slot.Root == null || !slot.Root.activeSelf)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
float lineH = _historySlotHeights[i] > 0.01f ? _historySlotHeights[i] : HistoryLineMinH;
|
|
RectTransform rt = slot.Root.GetComponent<RectTransform>();
|
|
rt.anchorMin = new Vector2(0f, 1f);
|
|
rt.anchorMax = new Vector2(1f, 1f);
|
|
rt.pivot = new Vector2(0.5f, 1f);
|
|
rt.offsetMin = new Vector2(0f, -(top + lineH));
|
|
rt.offsetMax = new Vector2(0f, -top);
|
|
top += lineH;
|
|
|
|
if (slot.Icon != null)
|
|
{
|
|
RectTransform irt = slot.Icon.GetComponent<RectTransform>();
|
|
irt.anchorMin = new Vector2(0f, 1f);
|
|
irt.anchorMax = new Vector2(0f, 1f);
|
|
irt.pivot = new Vector2(0f, 1f);
|
|
irt.anchoredPosition = new Vector2(0f, -1f);
|
|
irt.sizeDelta = new Vector2(HistoryIcon, HistoryIcon);
|
|
}
|
|
|
|
if (slot.Label != null)
|
|
{
|
|
RectTransform lrt = slot.Label.GetComponent<RectTransform>();
|
|
lrt.anchorMin = new Vector2(0f, 0f);
|
|
lrt.anchorMax = new Vector2(1f, 1f);
|
|
lrt.offsetMin = new Vector2(HistoryIcon + 2f, 0f);
|
|
lrt.offsetMax = new Vector2(0f, -1f);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static float PlaceTraits(float yFromTop, int traitCount)
|
|
{
|
|
if (_traitsRow == null)
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
RectTransform row = _traitsRow.GetComponent<RectTransform>();
|
|
row.anchorMin = new Vector2(0f, 1f);
|
|
row.anchorMax = new Vector2(1f, 1f);
|
|
row.pivot = new Vector2(0.5f, 1f);
|
|
row.offsetMin = new Vector2(PadX, -(yFromTop + TraitsH));
|
|
row.offsetMax = new Vector2(-PadX, -yFromTop);
|
|
|
|
float x = 0f;
|
|
for (int i = 0; i < _traitSlots.Length; i++)
|
|
{
|
|
TraitSlot slot = _traitSlots[i];
|
|
if (slot == null || slot.Root == null || !slot.Root.activeSelf)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
float labelW = slot.Label != null
|
|
? Mathf.Max(MeasureTextWidth(slot.Label, 28f), 18f)
|
|
: 28f;
|
|
float slotW = TraitIcon + 2f + labelW;
|
|
PlaceTraitSlot(slot, x, 0f, slotW, TraitsH);
|
|
x += slotW + 3f;
|
|
}
|
|
|
|
return x > 0f ? x - 3f : 0f;
|
|
}
|
|
|
|
/// <summary>Vertical trait stack in the body-right slot when History is empty.</summary>
|
|
private static float PlaceTraitsBesideAvatar(float yFromTop, int traitCount)
|
|
{
|
|
if (_traitsRow == null)
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
float colW = 0f;
|
|
for (int i = 0; i < _traitSlots.Length; i++)
|
|
{
|
|
TraitSlot slot = _traitSlots[i];
|
|
if (slot == null || slot.Root == null || !slot.Root.activeSelf || slot.Label == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
float labelW = Mathf.Max(MeasureTextWidth(slot.Label, 36f), 24f);
|
|
colW = Mathf.Max(colW, TraitIcon + 2f + labelW);
|
|
}
|
|
|
|
if (colW < 1f)
|
|
{
|
|
colW = 56f;
|
|
}
|
|
|
|
RectTransform row = _traitsRow.GetComponent<RectTransform>();
|
|
row.anchorMin = new Vector2(0f, 1f);
|
|
row.anchorMax = new Vector2(0f, 1f);
|
|
row.pivot = new Vector2(0f, 1f);
|
|
row.anchoredPosition = new Vector2(PadX + LiveMax + 4f, -yFromTop);
|
|
row.sizeDelta = new Vector2(colW, BodyH);
|
|
|
|
float line = Mathf.Min(HistoryLineMinH, BodyH / Mathf.Max(1, traitCount));
|
|
int shown = 0;
|
|
for (int i = 0; i < _traitSlots.Length; i++)
|
|
{
|
|
TraitSlot slot = _traitSlots[i];
|
|
if (slot == null || slot.Root == null || !slot.Root.activeSelf)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
PlaceTraitSlot(slot, 0f, -(shown * line), colW, line);
|
|
shown++;
|
|
}
|
|
|
|
return colW;
|
|
}
|
|
|
|
private static void PlaceTraitSlot(TraitSlot slot, float x, float y, float slotW, float slotH)
|
|
{
|
|
RectTransform rt = slot.Root.GetComponent<RectTransform>();
|
|
rt.anchorMin = new Vector2(0f, 1f);
|
|
rt.anchorMax = new Vector2(0f, 1f);
|
|
rt.pivot = new Vector2(0f, 1f);
|
|
rt.anchoredPosition = new Vector2(x, y);
|
|
rt.sizeDelta = new Vector2(slotW, slotH);
|
|
|
|
if (slot.Icon != null)
|
|
{
|
|
RectTransform irt = slot.Icon.GetComponent<RectTransform>();
|
|
irt.anchorMin = new Vector2(0f, 0.5f);
|
|
irt.anchorMax = new Vector2(0f, 0.5f);
|
|
irt.pivot = new Vector2(0f, 0.5f);
|
|
irt.anchoredPosition = Vector2.zero;
|
|
irt.sizeDelta = new Vector2(TraitIcon, TraitIcon);
|
|
}
|
|
|
|
if (slot.Label != null)
|
|
{
|
|
RectTransform lrt = slot.Label.GetComponent<RectTransform>();
|
|
lrt.anchorMin = new Vector2(0f, 0f);
|
|
lrt.anchorMax = new Vector2(1f, 1f);
|
|
lrt.offsetMin = new Vector2(TraitIcon + 2f, 0f);
|
|
lrt.offsetMax = Vector2.zero;
|
|
}
|
|
}
|
|
|
|
private static bool ProbeLayoutInside(float panelHeight)
|
|
{
|
|
if (_nameText == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return LineInside(_nameText.GetComponent<RectTransform>(), panelHeight)
|
|
&& (_reasonText == null || !_reasonText.gameObject.activeSelf
|
|
|| LineInside(_reasonText.GetComponent<RectTransform>(), panelHeight))
|
|
&& (_traitsRow == null || !_traitsRow.activeSelf
|
|
|| LineInside(_traitsRow.GetComponent<RectTransform>(), panelHeight))
|
|
&& (_historyCol == null || !_historyCol.activeSelf
|
|
|| LineInside(_historyCol.GetComponent<RectTransform>(), panelHeight));
|
|
}
|
|
|
|
private static bool LineInside(RectTransform line, float panelHeight)
|
|
{
|
|
if (line == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
float top = -line.offsetMax.y;
|
|
float bottom = -line.offsetMin.y;
|
|
if (Mathf.Approximately(line.offsetMin.y, 0f) && Mathf.Approximately(line.offsetMax.y, 0f)
|
|
&& line.anchorMin.y == line.anchorMax.y)
|
|
{
|
|
float y = -line.anchoredPosition.y;
|
|
float h = line.sizeDelta.y > 0.1f ? line.sizeDelta.y : TraitsH;
|
|
return y >= 0f && y + h <= panelHeight + 0.5f;
|
|
}
|
|
|
|
if (line.anchorMin.y == line.anchorMax.y && line.anchorMin.x == line.anchorMax.x)
|
|
{
|
|
float y = -line.anchoredPosition.y;
|
|
float h = line.sizeDelta.y > 0.1f ? line.sizeDelta.y : HeaderH;
|
|
return y >= 0f && y + h <= panelHeight + 0.5f;
|
|
}
|
|
|
|
return top >= 0f && bottom <= panelHeight + 0.5f && top < bottom;
|
|
}
|
|
|
|
private static void SetVisible(bool visible)
|
|
{
|
|
EnsureBuilt();
|
|
if (_root == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_visible = visible;
|
|
_root.SetActive(visible);
|
|
}
|
|
|
|
private static void EnsureBuilt()
|
|
{
|
|
if (_root != null && (_nameText == null || _nameText.transform.parent != _root.transform))
|
|
{
|
|
// Stale HUD from an older build (nested nametag row) - rebuild.
|
|
try
|
|
{
|
|
Object.Destroy(_root);
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
_root = null;
|
|
_rootRt = null;
|
|
_speciesIcon = null;
|
|
_sexIcon = null;
|
|
_nameText = null;
|
|
_reasonText = null;
|
|
_levelIcon = null;
|
|
_levelValue = null;
|
|
_taskIcon = null;
|
|
_taskText = null;
|
|
_traitsRow = null;
|
|
_historyCol = null;
|
|
DossierAvatar.ResetHost();
|
|
}
|
|
|
|
if (_root != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Canvas canvas = HudCanvas.Resolve();
|
|
if (canvas == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_root = new GameObject("IdleSpectatorDossierHud", typeof(RectTransform), typeof(Image), typeof(CanvasGroup));
|
|
_root.transform.SetParent(canvas.transform, false);
|
|
|
|
_rootRt = _root.GetComponent<RectTransform>();
|
|
ApplyRootPlacement();
|
|
_rootRt.sizeDelta = new Vector2(LiveMax + PadX * 2f, HeaderH + PadTop * 2f);
|
|
|
|
Image bg = _root.GetComponent<Image>();
|
|
bg.raycastTarget = false;
|
|
HudCanvas.StylePanel(bg, _root.transform, new Color(0.07f, 0.08f, 0.1f, 0.9f));
|
|
|
|
CanvasGroup group = _root.GetComponent<CanvasGroup>();
|
|
group.blocksRaycasts = false;
|
|
group.interactable = false;
|
|
|
|
// Avatar/history/traits first so nametag chips can SetAsLastSibling on top.
|
|
DossierAvatar.EnsureHost(_root.transform, LiveMax);
|
|
|
|
_historyCol = new GameObject("HistoryCol", typeof(RectTransform));
|
|
_historyCol.transform.SetParent(_root.transform, false);
|
|
for (int i = 0; i < _historySlots.Length; i++)
|
|
{
|
|
GameObject slotGo = new GameObject("Hist" + i, typeof(RectTransform));
|
|
slotGo.transform.SetParent(_historyCol.transform, false);
|
|
Image icon = HudCanvas.MakeIcon(slotGo.transform, "Icon", HistoryIcon);
|
|
Text label = HudCanvas.MakeText(slotGo.transform, "Label", "", 8);
|
|
label.color = HistoryTextColor;
|
|
label.alignment = TextAnchor.MiddleLeft;
|
|
label.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
label.resizeTextMinSize = 6;
|
|
label.resizeTextMaxSize = 8;
|
|
_historySlots[i] = new HistorySlot { Root = slotGo, Icon = icon, Label = label };
|
|
slotGo.SetActive(false);
|
|
}
|
|
|
|
_traitsRow = new GameObject("TraitsRow", typeof(RectTransform));
|
|
_traitsRow.transform.SetParent(_root.transform, false);
|
|
for (int i = 0; i < _traitSlots.Length; i++)
|
|
{
|
|
GameObject slotGo = new GameObject("Trait" + i, typeof(RectTransform));
|
|
slotGo.transform.SetParent(_traitsRow.transform, false);
|
|
Image icon = HudCanvas.MakeIcon(slotGo.transform, "Icon", TraitIcon);
|
|
Text label = HudCanvas.MakeText(slotGo.transform, "Label", "", 8);
|
|
label.color = TraitNameColor;
|
|
label.alignment = TextAnchor.MiddleLeft;
|
|
label.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
label.resizeTextMinSize = 6;
|
|
label.resizeTextMaxSize = 8;
|
|
_traitSlots[i] = new TraitSlot { Root = slotGo, Icon = icon, Label = label };
|
|
slotGo.SetActive(false);
|
|
}
|
|
|
|
_reasonText = HudCanvas.MakeText(_root.transform, "Reason", "", 8);
|
|
_reasonText.color = ReasonColor;
|
|
_reasonText.alignment = TextAnchor.MiddleLeft;
|
|
|
|
_speciesIcon = HudCanvas.MakeIcon(_root.transform, "SpeciesIcon", SpeciesSize);
|
|
_sexIcon = HudCanvas.MakeIcon(_root.transform, "SexIcon", SexSize);
|
|
|
|
_nameText = HudCanvas.MakeText(_root.transform, "Name", "", 11);
|
|
_nameText.fontStyle = FontStyle.Bold;
|
|
_nameText.color = NameColor;
|
|
_nameText.alignment = TextAnchor.MiddleLeft;
|
|
_nameText.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
|
|
_levelIcon = HudCanvas.MakeIcon(_root.transform, "LevelIcon", ChipIcon);
|
|
_levelValue = HudCanvas.MakeText(_root.transform, "LevelValue", "", 9);
|
|
_levelValue.color = StatValueColor;
|
|
_levelValue.fontStyle = FontStyle.Bold;
|
|
_levelValue.alignment = TextAnchor.MiddleLeft;
|
|
_levelValue.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
|
|
_taskIcon = HudCanvas.MakeIcon(_root.transform, "TaskIcon", ChipIcon);
|
|
_taskText = HudCanvas.MakeText(_root.transform, "Task", "", 8);
|
|
_taskText.color = TaskColor;
|
|
_taskText.alignment = TextAnchor.MiddleLeft;
|
|
_taskText.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
|
|
DossierAvatar.SetActive(false);
|
|
_historyCol.SetActive(false);
|
|
_traitsRow.SetActive(false);
|
|
_reasonText.gameObject.SetActive(false);
|
|
_levelIcon.gameObject.SetActive(false);
|
|
_levelValue.gameObject.SetActive(false);
|
|
_taskIcon.gameObject.SetActive(false);
|
|
_taskText.gameObject.SetActive(false);
|
|
Relayout(false, 0, false, false, 0);
|
|
_root.SetActive(false);
|
|
_visible = false;
|
|
LogService.LogInfo("[IdleSpectator] Dossier HUD ready (nametag chips + mini history)");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pin the card to the canvas top-right. Pivot (1,1) so width growth expands left and stays on-screen.
|
|
/// </summary>
|
|
private static void ApplyRootPlacement()
|
|
{
|
|
if (_rootRt == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_rootRt.anchorMin = new Vector2(1f, 1f);
|
|
_rootRt.anchorMax = new Vector2(1f, 1f);
|
|
_rootRt.pivot = new Vector2(1f, 1f);
|
|
_rootRt.anchoredPosition = new Vector2(-ScreenInset, -ScreenInset);
|
|
}
|
|
|
|
private static void PlaceLine(RectTransform lineRt, float yFromTop, float height, float left)
|
|
{
|
|
if (lineRt == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
lineRt.anchorMin = new Vector2(0f, 1f);
|
|
lineRt.anchorMax = new Vector2(1f, 1f);
|
|
lineRt.pivot = new Vector2(0.5f, 1f);
|
|
lineRt.offsetMin = new Vector2(left, -(yFromTop + height));
|
|
lineRt.offsetMax = new Vector2(-PadX, -yFromTop);
|
|
}
|
|
}
|