- Add Dossier/Saga tabs, Design A panel, and hover preview with read-pause - Persist observed saga facts across roster churn; pin subject without stale fallback - Restore Saga after Open Lore; hide redundant Kind · Climax spine under matching tips - Densify dossier header/chips and keep the cast rail as stable top chrome - Extend harness coverage for hover restore, empty pick, Evidence, and Lore return
757 lines
24 KiB
C#
757 lines
24 KiB
C#
using System.Collections.Generic;
|
|
using System.Text;
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
using UnityEngine.UI;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>
|
|
/// Saga body matching Design A (cast + stakes): identity, stake, cast, evidence, legacy, Open Lore.
|
|
/// Width is fixed to the tabbed chrome so right-anchored hover cannot slide the rail.
|
|
/// Height sizes to content (capped) so lines wrap fully instead of ellipsis.
|
|
/// </summary>
|
|
public static class LifeSagaPanel
|
|
{
|
|
/// <summary>Matches WatchCaption tabbed chrome inner width (PanelWidthMax - pads).</summary>
|
|
public const float BodyWidthFixed = 232f;
|
|
/// <summary>Hard cap so Saga never eats the screen.</summary>
|
|
public const float BodyHeightMax = 148f;
|
|
|
|
public const float BodyWidthMin = BodyWidthFixed;
|
|
public const float BodyWidthMax = BodyWidthFixed;
|
|
public const float BodyHeightMin = 56f;
|
|
public const float BodyHeightFixed = BodyHeightMax;
|
|
|
|
private const float Pad = 3f;
|
|
private const float AvatarSize = 32f;
|
|
private const float ColGap = 6f;
|
|
private const float CastFace = 16f;
|
|
private const float CastSlotW = 54f;
|
|
private const float LoreH = 11f;
|
|
private const float SecLabelH = 10f;
|
|
private const float LineH = 10f;
|
|
private const int NameFont = 9;
|
|
private const int TitleFont = 7;
|
|
private const int StakeFont = 8;
|
|
private const int BodyFont = 7;
|
|
private const int MaxCast = 4;
|
|
private const int MaxLegacy = 5;
|
|
private const int BuildVersion = 7;
|
|
|
|
private static GameObject _root;
|
|
private static RectTransform _rootRt;
|
|
private static Text _nameText;
|
|
private static Text _titleText;
|
|
private static Text _stakeText;
|
|
private static Text _castHead;
|
|
private static Text _evidenceHead;
|
|
private static Text _evidenceText;
|
|
private static Text _legacyHead;
|
|
private static Text _legacyText;
|
|
private static Button _loreBtn;
|
|
private static Text _loreBtnLabel;
|
|
private static readonly CastSlot[] CastSlots = new CastSlot[MaxCast];
|
|
private static long _boundId;
|
|
private static string _speciesId = "";
|
|
private static string _fingerprint = "";
|
|
private static bool _ownsAvatar;
|
|
private static int _builtVersion;
|
|
private static float _laidOutHeight = BodyHeightMin;
|
|
|
|
private sealed class CastSlot
|
|
{
|
|
public GameObject Root;
|
|
public Image FaceBg;
|
|
public Image Face;
|
|
public Text Name;
|
|
public Text Relation;
|
|
public long UnitId;
|
|
}
|
|
|
|
public static bool Visible => _root != null && _root.activeSelf;
|
|
public static float BodyHeight => _laidOutHeight;
|
|
public static float BodyWidthPx => BodyWidthFixed;
|
|
public static long BoundUnitId => _boundId;
|
|
public static bool OwnsAvatar => _ownsAvatar && Visible;
|
|
public static string LastLayoutDebug { get; private set; } = "";
|
|
|
|
public static void EnsureBuilt(Transform parent)
|
|
{
|
|
if (parent == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_root != null && _builtVersion == BuildVersion)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_root != null)
|
|
{
|
|
Object.Destroy(_root);
|
|
_root = null;
|
|
_rootRt = null;
|
|
_nameText = null;
|
|
_titleText = null;
|
|
_stakeText = null;
|
|
_castHead = null;
|
|
_evidenceHead = null;
|
|
_evidenceText = null;
|
|
_legacyHead = null;
|
|
_legacyText = null;
|
|
_loreBtn = null;
|
|
_loreBtnLabel = null;
|
|
}
|
|
|
|
_root = new GameObject("LifeSagaPanel", typeof(RectTransform));
|
|
_root.transform.SetParent(parent, false);
|
|
_rootRt = _root.GetComponent<RectTransform>();
|
|
_rootRt.pivot = new Vector2(0f, 1f);
|
|
_rootRt.anchorMin = new Vector2(0f, 1f);
|
|
_rootRt.anchorMax = new Vector2(0f, 1f);
|
|
_rootRt.sizeDelta = new Vector2(BodyWidthFixed, BodyHeightMin);
|
|
|
|
_nameText = MakeLabel(_root.transform, "SagaName", NameFont, HudTheme.ValueOrange, FontStyle.Bold);
|
|
_titleText = MakeLabel(_root.transform, "SagaTitle", TitleFont, HudTheme.Muted, FontStyle.Normal);
|
|
_stakeText = MakeLabel(_root.transform, "StakeBody", StakeFont, HudTheme.ValueOrange, FontStyle.Bold);
|
|
_stakeText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
|
_stakeText.verticalOverflow = VerticalWrapMode.Overflow;
|
|
_castHead = MakeLabel(_root.transform, "CastHead", BodyFont, HudTheme.Muted, FontStyle.Bold);
|
|
_castHead.text = "Cast";
|
|
_evidenceHead = MakeLabel(_root.transform, "EvidenceHead", BodyFont, HudTheme.Muted, FontStyle.Bold);
|
|
_evidenceHead.text = "Evidence";
|
|
_evidenceText = MakeLabel(_root.transform, "EvidenceBody", BodyFont, HudTheme.Body, FontStyle.Normal);
|
|
_evidenceText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
|
_evidenceText.verticalOverflow = VerticalWrapMode.Overflow;
|
|
_legacyHead = MakeLabel(_root.transform, "LegacyHead", BodyFont, HudTheme.Muted, FontStyle.Bold);
|
|
_legacyHead.text = "Legacy";
|
|
_legacyText = MakeLabel(_root.transform, "LegacyBody", BodyFont, HudTheme.Body, FontStyle.Normal);
|
|
_legacyText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
|
_legacyText.verticalOverflow = VerticalWrapMode.Overflow;
|
|
|
|
for (int i = 0; i < MaxCast; i++)
|
|
{
|
|
CastSlots[i] = BuildCast(i);
|
|
}
|
|
|
|
GameObject loreGo = new GameObject("SagaLoreBtn", typeof(RectTransform), typeof(Image), typeof(Button));
|
|
loreGo.transform.SetParent(_root.transform, false);
|
|
Image loreBg = loreGo.GetComponent<Image>();
|
|
loreBg.color = new Color(0f, 0f, 0f, 0f);
|
|
loreBg.raycastTarget = true;
|
|
_loreBtn = loreGo.GetComponent<Button>();
|
|
_loreBtn.targetGraphic = loreBg;
|
|
_loreBtn.onClick.AddListener(new UnityAction(OpenLore));
|
|
_loreBtnLabel = MakeLabel(loreGo.transform, "Label", BodyFont, HudTheme.ValueOrange, FontStyle.Normal);
|
|
_loreBtnLabel.text = "Open Lore";
|
|
_loreBtnLabel.alignment = TextAnchor.MiddleLeft;
|
|
RectTransform loreRt = loreGo.GetComponent<RectTransform>();
|
|
loreRt.pivot = new Vector2(0f, 1f);
|
|
loreRt.anchorMin = new Vector2(0f, 1f);
|
|
loreRt.anchorMax = new Vector2(0f, 1f);
|
|
loreRt.sizeDelta = new Vector2(72f, LoreH);
|
|
|
|
_builtVersion = BuildVersion;
|
|
_root.SetActive(false);
|
|
}
|
|
|
|
public static void Clear()
|
|
{
|
|
_boundId = 0;
|
|
_speciesId = "";
|
|
_fingerprint = "";
|
|
_ownsAvatar = false;
|
|
_laidOutHeight = BodyHeightMin;
|
|
if (_root != null)
|
|
{
|
|
_root.SetActive(false);
|
|
}
|
|
}
|
|
|
|
public static void SetVisible(bool visible)
|
|
{
|
|
if (_root == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_root.SetActive(visible);
|
|
if (!visible)
|
|
{
|
|
_ownsAvatar = false;
|
|
}
|
|
}
|
|
|
|
public static void Bind(LifeSagaViewModel model, bool loreInteractive)
|
|
{
|
|
if (_root == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (model == null || model.UnitId == 0)
|
|
{
|
|
BindEmptyPickState();
|
|
return;
|
|
}
|
|
|
|
string fp = Fingerprint(model, loreInteractive);
|
|
bool same = string.Equals(fp, _fingerprint, System.StringComparison.Ordinal) && _boundId == model.UnitId;
|
|
_fingerprint = fp;
|
|
_boundId = model.UnitId;
|
|
_speciesId = model.SpeciesId ?? "";
|
|
|
|
if (!same)
|
|
{
|
|
_nameText.text = StripStar(model.Name);
|
|
_titleText.text = model.Title ?? "";
|
|
_stakeText.text = model.StakeLine ?? "";
|
|
|
|
for (int i = 0; i < CastSlots.Length; i++)
|
|
{
|
|
if (i < model.Cast.Count && model.Cast[i] != null)
|
|
{
|
|
BindCast(CastSlots[i], model.Cast[i]);
|
|
}
|
|
else
|
|
{
|
|
CastSlots[i].Root.SetActive(false);
|
|
CastSlots[i].UnitId = 0;
|
|
}
|
|
}
|
|
|
|
_evidenceText.text = FormatEvidence(model);
|
|
|
|
var legacyParts = new List<string>(MaxLegacy);
|
|
var seen = new HashSet<string>();
|
|
string stakeKey = (_stakeText.text ?? "").Trim().ToLowerInvariant();
|
|
for (int i = 0; i < model.Legacy.Count && legacyParts.Count < MaxLegacy; i++)
|
|
{
|
|
if (model.Legacy[i] == null || string.IsNullOrEmpty(model.Legacy[i].Line))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string line = model.Legacy[i].Line.Trim();
|
|
string key = line.ToLowerInvariant();
|
|
if (!string.IsNullOrEmpty(stakeKey)
|
|
&& string.Equals(key, stakeKey, System.StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!seen.Add(key))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
legacyParts.Add(line);
|
|
}
|
|
|
|
_legacyText.text = legacyParts.Count > 0 ? string.Join(" · ", legacyParts) : "";
|
|
}
|
|
|
|
if (_loreBtn != null)
|
|
{
|
|
_loreBtn.gameObject.SetActive(loreInteractive);
|
|
_loreBtn.interactable = loreInteractive;
|
|
}
|
|
|
|
RefreshAvatar(model);
|
|
Layout();
|
|
_root.SetActive(true);
|
|
}
|
|
|
|
public static float Place(float y, float padX)
|
|
{
|
|
if (!Visible || _rootRt == null)
|
|
{
|
|
return y;
|
|
}
|
|
|
|
_rootRt.anchoredPosition = new Vector2(padX, -y);
|
|
_rootRt.sizeDelta = new Vector2(BodyWidthFixed, _laidOutHeight);
|
|
if (_ownsAvatar)
|
|
{
|
|
DossierAvatar.Place(padX + Pad, y + Pad, AvatarSize);
|
|
DossierAvatar.DrawBehind(_root.transform);
|
|
DossierAvatar.ForceCompactFrame();
|
|
}
|
|
|
|
return y + _laidOutHeight + 2f;
|
|
}
|
|
|
|
private static string FormatEvidence(LifeSagaViewModel model)
|
|
{
|
|
if (model == null || string.IsNullOrEmpty(model.EvidenceLine))
|
|
{
|
|
return "";
|
|
}
|
|
|
|
var sb = new StringBuilder(96);
|
|
if (!string.IsNullOrEmpty(model.EvidenceTitle))
|
|
{
|
|
sb.Append(model.EvidenceTitle).Append(" · ");
|
|
}
|
|
|
|
sb.Append(model.EvidenceLine);
|
|
if (!string.IsNullOrEmpty(model.SecondaryEvidenceLine))
|
|
{
|
|
sb.Append(" · ");
|
|
if (!string.IsNullOrEmpty(model.SecondaryEvidenceTitle))
|
|
{
|
|
sb.Append(model.SecondaryEvidenceTitle).Append(" · ");
|
|
}
|
|
|
|
sb.Append(model.SecondaryEvidenceLine);
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static void RefreshAvatar(LifeSagaViewModel model)
|
|
{
|
|
if (model == null || model.UnitId == 0)
|
|
{
|
|
_ownsAvatar = false;
|
|
return;
|
|
}
|
|
|
|
DossierAvatar.EnsureHost(_root.transform.parent, AvatarSize);
|
|
DossierAvatar.SetHostVisible(true);
|
|
Actor live = EventFeedUtil.FindAliveById(model.UnitId);
|
|
if (live != null && live.isAlive())
|
|
{
|
|
DossierAvatar.Show(live);
|
|
DossierAvatar.ForceCompactFrame();
|
|
}
|
|
else
|
|
{
|
|
DossierAvatar.ShowSpecies(model.SpeciesId);
|
|
}
|
|
|
|
_ownsAvatar = true;
|
|
}
|
|
|
|
private static void Layout()
|
|
{
|
|
float colX = Pad + AvatarSize + ColGap;
|
|
float textW = BodyWidthFixed - colX - Pad;
|
|
float y = Pad;
|
|
|
|
PlaceText(_nameText, colX, y, textW, NameFont + 2f);
|
|
y += NameFont + 2f;
|
|
if (!string.IsNullOrEmpty(_titleText.text))
|
|
{
|
|
PlaceText(_titleText, colX, y, textW, LineH);
|
|
y += LineH;
|
|
}
|
|
else
|
|
{
|
|
Hide(_titleText);
|
|
}
|
|
|
|
y += 2f;
|
|
bool hasStake = !string.IsNullOrEmpty(_stakeText.text);
|
|
if (hasStake)
|
|
{
|
|
float stakeH = Mathf.Clamp(
|
|
EstimateWrapHeight(_stakeText.text, textW, StakeFont),
|
|
LineH,
|
|
LineH * 3f);
|
|
PlaceText(_stakeText, colX, y, textW, stakeH);
|
|
_stakeText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
|
_stakeText.verticalOverflow = VerticalWrapMode.Overflow;
|
|
y += stakeH + 2f;
|
|
}
|
|
else
|
|
{
|
|
Hide(_stakeText);
|
|
}
|
|
|
|
int castN = 0;
|
|
for (int i = 0; i < CastSlots.Length; i++)
|
|
{
|
|
if (CastSlots[i].Root.activeSelf)
|
|
{
|
|
castN++;
|
|
}
|
|
}
|
|
|
|
if (castN > 0)
|
|
{
|
|
PlaceText(_castHead, colX, y, textW, SecLabelH);
|
|
y += SecLabelH;
|
|
float castX = colX;
|
|
float castRowH = CastFace + 18f;
|
|
for (int i = 0; i < CastSlots.Length; i++)
|
|
{
|
|
if (!CastSlots[i].Root.activeSelf)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
RectTransform rt = CastSlots[i].Root.GetComponent<RectTransform>();
|
|
rt.anchoredPosition = new Vector2(castX, -y);
|
|
rt.sizeDelta = new Vector2(CastSlotW, castRowH);
|
|
CastSlots[i].Root.transform.SetAsLastSibling();
|
|
castX += CastSlotW + 2f;
|
|
}
|
|
|
|
y += castRowH + 2f;
|
|
}
|
|
else
|
|
{
|
|
Hide(_castHead);
|
|
}
|
|
|
|
bool hasEvidence = !string.IsNullOrEmpty(_evidenceText.text);
|
|
bool hasLegacy = !string.IsNullOrEmpty(_legacyText.text);
|
|
bool loreActive = _loreBtn != null && _loreBtn.gameObject.activeSelf;
|
|
float loreReserve = loreActive ? LoreH + 2f : 0f;
|
|
float yMax = BodyHeightMax - Pad - loreReserve;
|
|
|
|
if (hasEvidence && y + SecLabelH + LineH <= yMax)
|
|
{
|
|
_evidenceHead.text = "Evidence";
|
|
PlaceText(_evidenceHead, colX, y, textW, SecLabelH);
|
|
y += SecLabelH;
|
|
float evidenceH = Mathf.Clamp(
|
|
EstimateWrapHeight(_evidenceText.text, textW, BodyFont),
|
|
LineH,
|
|
LineH * 3f);
|
|
evidenceH = Mathf.Min(evidenceH, Mathf.Max(LineH, yMax - y - (hasLegacy ? SecLabelH + LineH : 0f)));
|
|
PlaceText(_evidenceText, colX, y, textW, evidenceH);
|
|
_evidenceText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
|
_evidenceText.verticalOverflow = VerticalWrapMode.Overflow;
|
|
y += evidenceH + 2f;
|
|
}
|
|
else
|
|
{
|
|
Hide(_evidenceHead);
|
|
Hide(_evidenceText);
|
|
}
|
|
|
|
if (hasLegacy && y + SecLabelH + LineH <= yMax)
|
|
{
|
|
PlaceText(_legacyHead, colX, y, textW, SecLabelH);
|
|
y += SecLabelH;
|
|
float legacyH = Mathf.Clamp(
|
|
EstimateWrapHeight(_legacyText.text, textW, BodyFont),
|
|
LineH,
|
|
LineH * 3f);
|
|
legacyH = Mathf.Min(legacyH, Mathf.Max(LineH, yMax - y));
|
|
PlaceText(_legacyText, colX, y, textW, legacyH);
|
|
_legacyText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
|
_legacyText.verticalOverflow = VerticalWrapMode.Overflow;
|
|
y += legacyH + 2f;
|
|
}
|
|
else
|
|
{
|
|
Hide(_legacyHead);
|
|
Hide(_legacyText);
|
|
}
|
|
|
|
if (loreActive)
|
|
{
|
|
y = Mathf.Max(y, AvatarSize + Pad + 2f);
|
|
RectTransform loreRt = _loreBtn.GetComponent<RectTransform>();
|
|
loreRt.anchoredPosition = new Vector2(colX, -y);
|
|
loreRt.sizeDelta = new Vector2(72f, LoreH);
|
|
_loreBtn.transform.SetAsLastSibling();
|
|
if (_loreBtnLabel != null)
|
|
{
|
|
RectTransform labelRt = _loreBtnLabel.rectTransform;
|
|
labelRt.anchoredPosition = Vector2.zero;
|
|
labelRt.sizeDelta = new Vector2(72f, LoreH);
|
|
}
|
|
|
|
y += LoreH;
|
|
}
|
|
|
|
y = Mathf.Max(y + Pad, AvatarSize + Pad * 2f);
|
|
_laidOutHeight = Mathf.Clamp(y, BodyHeightMin, BodyHeightMax);
|
|
|
|
if (_rootRt != null)
|
|
{
|
|
_rootRt.sizeDelta = new Vector2(BodyWidthFixed, _laidOutHeight);
|
|
}
|
|
|
|
LastLayoutDebug =
|
|
$"w={BodyWidthFixed} h={_laidOutHeight:0.#} name='{_nameText?.text}' stake='{Short(_stakeText?.text)}' cast={castN} evidence={(hasEvidence ? 1 : 0)} legacy={(hasLegacy ? 1 : 0)}";
|
|
}
|
|
|
|
private static float EstimateWrapHeight(string text, float width, float fontSize)
|
|
{
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
return LineH;
|
|
}
|
|
|
|
float charsPerLine = Mathf.Max(8f, width / Mathf.Max(3.8f, fontSize * 0.55f));
|
|
int lines = Mathf.Max(1, Mathf.CeilToInt(text.Length / charsPerLine));
|
|
return lines * (fontSize + 2f);
|
|
}
|
|
|
|
private static CastSlot BuildCast(int index)
|
|
{
|
|
GameObject root = new GameObject("Cast" + index, typeof(RectTransform));
|
|
root.transform.SetParent(_root.transform, false);
|
|
RectTransform rt = root.GetComponent<RectTransform>();
|
|
rt.pivot = new Vector2(0f, 1f);
|
|
rt.anchorMin = new Vector2(0f, 1f);
|
|
rt.anchorMax = new Vector2(0f, 1f);
|
|
|
|
GameObject bgGo = new GameObject("FaceBg", typeof(RectTransform), typeof(Image));
|
|
bgGo.transform.SetParent(root.transform, false);
|
|
RectTransform bgRt = bgGo.GetComponent<RectTransform>();
|
|
bgRt.pivot = new Vector2(0f, 1f);
|
|
bgRt.anchorMin = new Vector2(0f, 1f);
|
|
bgRt.anchorMax = new Vector2(0f, 1f);
|
|
bgRt.anchoredPosition = Vector2.zero;
|
|
bgRt.sizeDelta = new Vector2(CastFace, CastFace);
|
|
Image bg = bgGo.GetComponent<Image>();
|
|
bg.color = HudTheme.InsetFill;
|
|
bg.raycastTarget = false;
|
|
|
|
GameObject faceGo = new GameObject("Face", typeof(RectTransform), typeof(Image));
|
|
faceGo.transform.SetParent(root.transform, false);
|
|
RectTransform faceRt = faceGo.GetComponent<RectTransform>();
|
|
faceRt.pivot = new Vector2(0.5f, 0.5f);
|
|
faceRt.anchorMin = new Vector2(0f, 1f);
|
|
faceRt.anchorMax = new Vector2(0f, 1f);
|
|
faceRt.anchoredPosition = new Vector2(CastFace * 0.5f, -CastFace * 0.5f);
|
|
faceRt.sizeDelta = new Vector2(CastFace - 2f, CastFace - 2f);
|
|
Image face = faceGo.GetComponent<Image>();
|
|
face.color = Color.white;
|
|
face.preserveAspect = true;
|
|
face.raycastTarget = false;
|
|
|
|
Text name = MakeLabel(root.transform, "Name", BodyFont, HudTheme.Body, FontStyle.Bold);
|
|
name.alignment = TextAnchor.UpperLeft;
|
|
RectTransform nameRt = name.rectTransform;
|
|
nameRt.anchoredPosition = new Vector2(CastFace + 2f, 0f);
|
|
nameRt.sizeDelta = new Vector2(CastSlotW - CastFace - 3f, 9f);
|
|
|
|
Text relation = MakeLabel(root.transform, "Relation", BodyFont, HudTheme.Muted, FontStyle.Normal);
|
|
relation.alignment = TextAnchor.UpperLeft;
|
|
RectTransform relRt = relation.rectTransform;
|
|
relRt.anchoredPosition = new Vector2(CastFace + 2f, -9f);
|
|
relRt.sizeDelta = new Vector2(CastSlotW - CastFace - 3f, 9f);
|
|
|
|
root.SetActive(false);
|
|
return new CastSlot
|
|
{
|
|
Root = root,
|
|
FaceBg = bg,
|
|
Face = face,
|
|
Name = name,
|
|
Relation = relation
|
|
};
|
|
}
|
|
|
|
private static void BindCast(CastSlot slot, LifeSagaCastMember member)
|
|
{
|
|
slot.UnitId = member.UnitId;
|
|
slot.Name.text = FirstNonEmpty(member.Name, "Someone");
|
|
slot.Relation.text = FirstNonEmpty(member.Relation, "Linked");
|
|
|
|
Actor live = member.Alive ? EventFeedUtil.FindAliveById(member.UnitId) : null;
|
|
Sprite sprite = HudIcons.FromActorRailFace(live, member.SpeciesId)
|
|
?? HudIcons.FromSpeciesId(member.SpeciesId)
|
|
?? HudIcons.FromUiIcon("iconCitizen");
|
|
slot.Face.sprite = sprite;
|
|
slot.Face.enabled = sprite != null;
|
|
slot.Face.color = sprite != null ? Color.white : new Color(0.35f, 0.36f, 0.4f, 1f);
|
|
slot.Root.SetActive(true);
|
|
}
|
|
|
|
private static void BindEmptyPickState()
|
|
{
|
|
_boundId = 0;
|
|
_speciesId = "";
|
|
_fingerprint = "empty-pick";
|
|
_ownsAvatar = false;
|
|
if (_nameText != null)
|
|
{
|
|
_nameText.text = "Pick a life";
|
|
}
|
|
|
|
if (_titleText != null)
|
|
{
|
|
_titleText.text = "from the rail";
|
|
}
|
|
|
|
if (_stakeText != null)
|
|
{
|
|
_stakeText.text = "Hover or click a glyph to read their saga.";
|
|
}
|
|
|
|
if (_evidenceText != null)
|
|
{
|
|
_evidenceText.text = "";
|
|
}
|
|
|
|
if (_legacyText != null)
|
|
{
|
|
_legacyText.text = "";
|
|
}
|
|
|
|
for (int i = 0; i < CastSlots.Length; i++)
|
|
{
|
|
if (CastSlots[i]?.Root != null)
|
|
{
|
|
CastSlots[i].Root.SetActive(false);
|
|
CastSlots[i].UnitId = 0;
|
|
}
|
|
}
|
|
|
|
if (_loreBtn != null)
|
|
{
|
|
_loreBtn.gameObject.SetActive(false);
|
|
_loreBtn.interactable = false;
|
|
}
|
|
|
|
DossierAvatar.SetHostVisible(false);
|
|
Layout();
|
|
if (_root != null)
|
|
{
|
|
_root.SetActive(true);
|
|
}
|
|
}
|
|
|
|
/// <summary>Harness: invoke Open Lore for the bound saga life.</summary>
|
|
public static void HarnessOpenLore()
|
|
{
|
|
OpenLore();
|
|
}
|
|
|
|
private static void OpenLore()
|
|
{
|
|
if (_boundId == 0 || LifeSagaViewController.IsHoverPreview)
|
|
{
|
|
return;
|
|
}
|
|
|
|
LifeSagaViewController.StashLoreReturn(_boundId);
|
|
ChronicleHud.OpenUnitHistory(_boundId, pauseIdle: true, followFocus: false);
|
|
}
|
|
|
|
private static Text MakeLabel(Transform parent, string name, int size, Color color, FontStyle style)
|
|
{
|
|
int fitSize = size < 7 ? 7 : size;
|
|
Text text = HudCanvas.MakeText(parent, name, "", fitSize);
|
|
text.color = color;
|
|
text.fontStyle = style;
|
|
text.fontSize = fitSize;
|
|
text.resizeTextForBestFit = false;
|
|
text.alignment = TextAnchor.UpperLeft;
|
|
text.horizontalOverflow = HorizontalWrapMode.Wrap;
|
|
text.verticalOverflow = VerticalWrapMode.Overflow;
|
|
text.raycastTarget = false;
|
|
RectTransform rt = text.rectTransform;
|
|
rt.pivot = new Vector2(0f, 1f);
|
|
rt.anchorMin = new Vector2(0f, 1f);
|
|
rt.anchorMax = new Vector2(0f, 1f);
|
|
return text;
|
|
}
|
|
|
|
private static void PlaceText(Text text, float x, float y, float w, float h)
|
|
{
|
|
if (text == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
text.resizeTextForBestFit = false;
|
|
text.enabled = true;
|
|
Color c = text.color;
|
|
if (c.a < 0.99f)
|
|
{
|
|
c.a = 1f;
|
|
text.color = c;
|
|
}
|
|
|
|
RectTransform rt = text.rectTransform;
|
|
rt.anchoredPosition = new Vector2(x, -y);
|
|
rt.sizeDelta = new Vector2(w, h);
|
|
text.gameObject.SetActive(!string.IsNullOrEmpty(text.text));
|
|
}
|
|
|
|
private static void Hide(Text text)
|
|
{
|
|
if (text != null)
|
|
{
|
|
text.gameObject.SetActive(false);
|
|
}
|
|
}
|
|
|
|
private static string Fingerprint(LifeSagaViewModel model, bool loreInteractive)
|
|
{
|
|
var sb = new StringBuilder(192);
|
|
sb.Append(model.UnitId).Append('|').Append(model.Name).Append('|').Append(model.Title);
|
|
sb.Append('|').Append(model.StakeLine).Append('|').Append(model.EvidenceLine);
|
|
sb.Append('|').Append(model.SecondaryEvidenceLine);
|
|
sb.Append('|').Append(loreInteractive ? '1' : '0');
|
|
for (int i = 0; i < model.Cast.Count; i++)
|
|
{
|
|
var c = model.Cast[i];
|
|
if (c == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
sb.Append('|').Append(c.UnitId).Append(':').Append(c.Relation).Append(':').Append(c.Name);
|
|
}
|
|
|
|
for (int i = 0; i < model.Legacy.Count; i++)
|
|
{
|
|
if (model.Legacy[i] != null)
|
|
{
|
|
sb.Append('#').Append(model.Legacy[i].Line);
|
|
}
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string StripStar(string name)
|
|
{
|
|
if (string.IsNullOrEmpty(name))
|
|
{
|
|
return "";
|
|
}
|
|
|
|
return name.StartsWith("★ ") ? name.Substring(2) : name;
|
|
}
|
|
|
|
private static string FirstNonEmpty(params string[] parts)
|
|
{
|
|
if (parts == null)
|
|
{
|
|
return "";
|
|
}
|
|
|
|
for (int i = 0; i < parts.Length; i++)
|
|
{
|
|
if (!string.IsNullOrEmpty(parts[i]))
|
|
{
|
|
return parts[i];
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
private static string Short(string s)
|
|
{
|
|
if (string.IsNullOrEmpty(s))
|
|
{
|
|
return "";
|
|
}
|
|
|
|
return s.Length <= 28 ? s : s.Substring(0, 27) + "…";
|
|
}
|
|
}
|