using System; using System.Collections.Generic; using System.IO; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace IdleSpectator; /// /// Vanilla trait/status tooltips for dossier chips. /// Tip types come from live (not invented keys). /// public static class DossierAssetTips { public const string TraitTipType = "trait"; public const string StatusTipType = "status"; private static HashSet _liveTipTypes; private static int _liveTipTypesCount = -1; public static List EnumerateLiveTooltipTypeIds() { var ids = new List(); try { TooltipLibrary lib = AssetManager.tooltips; if (lib?.list == null) { return ids; } for (int i = 0; i < lib.list.Count; i++) { TooltipAsset asset = lib.list[i]; if (asset != null && !string.IsNullOrEmpty(asset.id)) { ids.Add(asset.id); } } } catch { // Library can be unavailable during early boot. } return ids; } public static bool HasLiveTooltipType(string tipType) { if (string.IsNullOrEmpty(tipType)) { return false; } EnsureLiveTipTypes(); return _liveTipTypes != null && _liveTipTypes.Contains(tipType); } /// /// Harness audit: dump live tooltip ids; require trait + status tip types exist. /// public static bool RunTooltipInventoryAudit(string outputDirectory, out string detail) { List ids = EnumerateLiveTooltipTypeIds(); var lines = new List(ids.Count + 1) { "id" }; for (int i = 0; i < ids.Count; i++) { lines.Add(ids[i]); } try { if (!string.IsNullOrEmpty(outputDirectory)) { Directory.CreateDirectory(outputDirectory); File.WriteAllLines(Path.Combine(outputDirectory, "tooltip-library-ids.tsv"), lines); } } catch { // diagnostic dump only } bool hasTrait = HasLiveTooltipType(TraitTipType); bool hasStatus = HasLiveTooltipType(StatusTipType); bool pass = ids.Count > 0 && hasTrait && hasStatus; detail = $"tips={ids.Count} trait={hasTrait} status={hasStatus}"; return pass; } public static void ShowTraitTip(GameObject host, ActorTrait trait) { if (host == null || trait == null || !HasLiveTooltipType(TraitTipType)) { return; } try { if (!Config.tooltips_active) { return; } Tooltip.show(host, TraitTipType, new TooltipData { trait = trait }); } catch { // Tip surface can be unavailable mid-load. } } public static void ShowStatusTip(GameObject host, Actor actor, StatusAsset asset, string statusId) { if (host == null || !HasLiveTooltipType(StatusTipType)) { return; } StatusAsset resolved = asset; if (resolved == null && !string.IsNullOrEmpty(statusId)) { resolved = ActivityAssetCatalog.TryGetStatusAsset(statusId); } if (resolved == null) { return; } Status live = FindLiveStatus(actor, resolved.id ?? statusId); if (live == null) { // Vanilla status tip needs a Status instance (time bar + stats). return; } try { if (!Config.tooltips_active) { return; } string locale = ""; string description = ""; try { locale = resolved.getLocaleID() ?? ""; description = resolved.getDescriptionID() ?? ""; } catch { // fall through with empty locale keys - showStatus still uses status.asset stats } Tooltip.show(host, StatusTipType, new TooltipData { tip_name = locale, tip_description = description, status = live }); } catch { // Tip surface can be unavailable mid-load. } } public static void HideTip() { try { Tooltip.hideTooltip(); } catch { // ignore } } private static Status FindLiveStatus(Actor actor, string statusId) { if (actor == null || string.IsNullOrEmpty(statusId)) { return null; } try { if (!actor.isAlive()) { return null; } foreach (Status status in actor.getStatuses()) { if (status?.asset != null && string.Equals(status.asset.id, statusId, StringComparison.Ordinal)) { return status; } } } catch { return null; } return null; } private static void EnsureLiveTipTypes() { List ids = EnumerateLiveTooltipTypeIds(); if (_liveTipTypes != null && _liveTipTypesCount == ids.Count) { return; } _liveTipTypes = new HashSet(StringComparer.Ordinal); for (int i = 0; i < ids.Count; i++) { if (!string.IsNullOrEmpty(ids[i])) { _liveTipTypes.Add(ids[i]); } } _liveTipTypesCount = ids.Count; } } /// Pointer hover on a dossier trait/status chip root. public sealed class DossierChipTip : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler { public enum Kind { Trait, Status } public Kind ChipKind; public string AssetId = ""; public ActorTrait Trait; public StatusAsset Status; public void BindTrait(ActorTrait trait, string id) { ChipKind = Kind.Trait; Trait = trait; Status = null; AssetId = id ?? (trait != null ? trait.id : ""); EnsureHitTarget(); } public void BindStatus(StatusAsset status, string id) { ChipKind = Kind.Status; Status = status; Trait = null; AssetId = id ?? (status != null ? status.id : ""); EnsureHitTarget(); } public void Clear() { Trait = null; Status = null; AssetId = ""; } public void OnPointerEnter(PointerEventData eventData) { if (ChipKind == Kind.Trait) { ActorTrait trait = Trait; if (trait == null && !string.IsNullOrEmpty(AssetId)) { try { trait = AssetManager.traits?.get(AssetId); } catch { trait = null; } } DossierAssetTips.ShowTraitTip(gameObject, trait); return; } Actor actor = WatchCaption.BoundActor; DossierAssetTips.ShowStatusTip(gameObject, actor, Status, AssetId); } public void OnPointerExit(PointerEventData eventData) { DossierAssetTips.HideTip(); } private void OnDisable() { DossierAssetTips.HideTip(); } private void EnsureHitTarget() { Image hit = GetComponent(); if (hit == null) { hit = gameObject.AddComponent(); } hit.color = new Color(1f, 1f, 1f, 0.001f); hit.raycastTarget = true; } }