worldbox-observer-mod/IdleSpectator/DossierAssetTips.cs
DazedAnon 90d5b1604f Enhance dossier interactions and improve Lore click handling.
- Add related unit tracking for activity logs and dossiers
- Implement colorized names for related units in reason lines
- Update harness scenarios to validate new interactions
- Increment version to 0.26.1 in mod.json
2026-07-17 13:36:33 -05:00

321 lines
7.8 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace IdleSpectator;
/// <summary>
/// Vanilla trait/status tooltips for dossier chips.
/// Tip types come from live <see cref="AssetManager.tooltips"/> (not invented keys).
/// </summary>
public static class DossierAssetTips
{
public const string TraitTipType = "trait";
public const string StatusTipType = "status";
private static HashSet<string> _liveTipTypes;
private static int _liveTipTypesCount = -1;
public static List<string> EnumerateLiveTooltipTypeIds()
{
var ids = new List<string>();
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);
}
/// <summary>
/// Harness audit: dump live tooltip ids; require trait + status tip types exist.
/// </summary>
public static bool RunTooltipInventoryAudit(string outputDirectory, out string detail)
{
List<string> ids = EnumerateLiveTooltipTypeIds();
var lines = new List<string>(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<string> ids = EnumerateLiveTooltipTypeIds();
if (_liveTipTypes != null && _liveTipTypesCount == ids.Count)
{
return;
}
_liveTipTypes = new HashSet<string>(StringComparer.Ordinal);
for (int i = 0; i < ids.Count; i++)
{
if (!string.IsNullOrEmpty(ids[i]))
{
_liveTipTypes.Add(ids[i]);
}
}
_liveTipTypesCount = ids.Count;
}
}
/// <summary>Pointer hover on a dossier trait/status chip root.</summary>
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<Image>();
if (hit == null)
{
hit = gameObject.AddComponent<Image>();
}
hit.color = new Color(1f, 1f, 1f, 0.001f);
hit.raycastTarget = true;
}
}