Live HUD for character
This commit is contained in:
parent
3d6d8f27f3
commit
343c4c686e
12 changed files with 1470 additions and 147 deletions
|
|
@ -55,6 +55,8 @@ Use harness `fast_timing` + `director_run` / `age_current` for scheduling tests.
|
|||
- `current_tier` / `would_accept_curiosity` - director scheduling
|
||||
- `focus_same` - after `remember_focus` (ghost guard)
|
||||
- `presented` / `pending_discovery` - species discovery buffer
|
||||
- `caption_layout_ok` - dossier panel size/layout bounds (catches off-panel text)
|
||||
- `screenshot` harness action writes `.harness/<name>.png` via Unity ScreenCapture
|
||||
|
||||
## Extending scenarios
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ public static class AgentHarness
|
|||
private static string _lastSpawnedAssetId = "";
|
||||
private static bool _directorRunActive;
|
||||
private static string _rememberedFocusKey = "";
|
||||
private static string LastScreenshotPath = "";
|
||||
private static readonly string[] PreferredSpawnAssets =
|
||||
{
|
||||
"sheep", "cow", "pig", "chicken", "dog", "cat", "rabbit", "monkey", "fox", "crab"
|
||||
|
|
@ -437,6 +438,22 @@ public static class AgentHarness
|
|||
break;
|
||||
}
|
||||
|
||||
case "screenshot":
|
||||
{
|
||||
bool ok = CaptureHarnessScreenshot(cmd.value);
|
||||
if (ok)
|
||||
{
|
||||
_cmdOk++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cmdFail++;
|
||||
}
|
||||
|
||||
Emit(cmd, ok, detail: $"path={LastScreenshotPath} layoutOk={WatchCaption.LastLayoutOk} size={WatchCaption.LastPanelSize}");
|
||||
break;
|
||||
}
|
||||
|
||||
case "spawn":
|
||||
DoSpawn(cmd);
|
||||
break;
|
||||
|
|
@ -1100,6 +1117,16 @@ public static class AgentHarness
|
|||
detail = $"latest='{line}' needle='{needle}'";
|
||||
break;
|
||||
}
|
||||
case "caption_layout_ok":
|
||||
{
|
||||
pass = WatchCaption.LastLayoutOk
|
||||
&& WatchCaption.LastPanelSize.y > 20f
|
||||
&& WatchCaption.LastPanelSize.y < 120f
|
||||
&& WatchCaption.LastPanelSize.x <= 280f;
|
||||
detail =
|
||||
$"layoutOk={WatchCaption.LastLayoutOk} size={WatchCaption.LastPanelSize} caption='{(WatchCaption.LastCaptionText ?? "").Replace("\n", " | ")}'";
|
||||
break;
|
||||
}
|
||||
case "chronicle_world_latest_contains":
|
||||
{
|
||||
string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value;
|
||||
|
|
@ -1772,6 +1799,39 @@ public static class AgentHarness
|
|||
return true;
|
||||
}
|
||||
|
||||
private static bool CaptureHarnessScreenshot(string fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
string dir = HarnessDir();
|
||||
if (string.IsNullOrEmpty(dir))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(dir);
|
||||
string name = string.IsNullOrEmpty(fileName) ? "hud.png" : fileName;
|
||||
if (!name.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
name += ".png";
|
||||
}
|
||||
|
||||
string path = Path.Combine(dir, name);
|
||||
// Absolute path required so Unity does not write under the game cwd.
|
||||
ScreenCapture.CaptureScreenshot(path);
|
||||
LastScreenshotPath = path;
|
||||
LogService.LogInfo(
|
||||
$"[IdleSpectator][HARNESS] screenshot queued path={path} layoutOk={WatchCaption.LastLayoutOk} size={WatchCaption.LastPanelSize}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LastScreenshotPath = "";
|
||||
LogService.LogInfo("[IdleSpectator][HARNESS] screenshot failed: " + ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string HarnessDir()
|
||||
{
|
||||
if (string.IsNullOrEmpty(ModClass.ModFolder))
|
||||
|
|
|
|||
|
|
@ -52,6 +52,18 @@ public static class Chronicle
|
|||
private static readonly HashSet<long> DeathLogged = new HashSet<long>();
|
||||
private static bool _hudReady;
|
||||
private static long _lastHistorySubjectId;
|
||||
private static object _boundWorld;
|
||||
|
||||
public static void OnSpectatorEnabled()
|
||||
{
|
||||
object world = World.world;
|
||||
if (!ReferenceEquals(world, _boundWorld))
|
||||
{
|
||||
ClearSession();
|
||||
_boundWorld = world;
|
||||
LogService.LogInfo("[IdleSpectator] Chronicle cleared for new world session");
|
||||
}
|
||||
}
|
||||
|
||||
public static int Count => HistoryCountFor(CurrentHistorySubjectId()) + WorldCount;
|
||||
|
||||
|
|
@ -227,6 +239,7 @@ public static class Chronicle
|
|||
FriendPairKeys.Clear();
|
||||
DeathLogged.Clear();
|
||||
_lastHistorySubjectId = 0;
|
||||
// Keep _boundWorld so re-enable on same map does not loop-clear.
|
||||
}
|
||||
|
||||
public static void PollInput()
|
||||
|
|
|
|||
|
|
@ -7,18 +7,21 @@ using UnityEngine.UI;
|
|||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>
|
||||
/// Compact bottom-right chronicle card with History | World tabs.
|
||||
/// Compact bottom-right chronicle card with History | World tabs and vanilla icons.
|
||||
/// </summary>
|
||||
public static class ChronicleHud
|
||||
{
|
||||
private const float PanelWidth = 248f;
|
||||
private const float PanelHeight = 178f;
|
||||
private const float PanelWidth = 260f;
|
||||
private const float PanelHeight = 186f;
|
||||
private const int MaxVisibleRows = 5;
|
||||
private const float RowHeight = 20f;
|
||||
private const float RowHeight = 24f;
|
||||
private const float KindIconSize = 14f;
|
||||
private const float TitleIconSize = 16f;
|
||||
|
||||
private static GameObject _root;
|
||||
private static RectTransform _content;
|
||||
private static Text _titleText;
|
||||
private static Image _titleIcon;
|
||||
private static Text _historyTabLabel;
|
||||
private static Text _worldTabLabel;
|
||||
private static Image _historyTabBg;
|
||||
|
|
@ -61,20 +64,20 @@ public static class ChronicleHud
|
|||
rt.anchorMax = new Vector2(1f, 0f);
|
||||
rt.pivot = new Vector2(1f, 0f);
|
||||
rt.sizeDelta = new Vector2(PanelWidth, PanelHeight);
|
||||
rt.anchoredPosition = new Vector2(-12f, 12f);
|
||||
rt.anchoredPosition = new Vector2(-12f, 56f);
|
||||
|
||||
Image bg = _root.GetComponent<Image>();
|
||||
bg.color = new Color(0.05f, 0.06f, 0.08f, 0.65f);
|
||||
bg.raycastTarget = true;
|
||||
HudCanvas.StylePanel(bg, _root.transform, new Color(0.07f, 0.08f, 0.1f, 0.82f));
|
||||
|
||||
CanvasGroup group = _root.GetComponent<CanvasGroup>();
|
||||
group.blocksRaycasts = true;
|
||||
group.interactable = true;
|
||||
|
||||
BuildTabButton(_root.transform, "HistoryTab", "History", -6f, 0f, out _historyTabBg, out _historyTabLabel,
|
||||
() => SetTab(ChronicleTab.History));
|
||||
BuildTabButton(_root.transform, "WorldTab", "World", -6f, 72f, out _worldTabBg, out _worldTabLabel,
|
||||
() => SetTab(ChronicleTab.World));
|
||||
BuildTabButton(_root.transform, "HistoryTab", "History", "iconBooks", -6f, 0f,
|
||||
out _historyTabBg, out _historyTabLabel, () => SetTab(ChronicleTab.History));
|
||||
BuildTabButton(_root.transform, "WorldTab", "World", "iconWar", -6f, 88f,
|
||||
out _worldTabBg, out _worldTabLabel, () => SetTab(ChronicleTab.World));
|
||||
|
||||
Text f9 = HudCanvas.MakeText(_root.transform, "F9", "F9", 9);
|
||||
RectTransform f9Rt = f9.GetComponent<RectTransform>();
|
||||
|
|
@ -86,13 +89,20 @@ public static class ChronicleHud
|
|||
f9.alignment = TextAnchor.MiddleRight;
|
||||
f9.color = new Color(0.55f, 0.58f, 0.62f, 1f);
|
||||
|
||||
_titleIcon = HudCanvas.MakeIcon(_root.transform, "TitleIcon", TitleIconSize);
|
||||
RectTransform titleIconRt = _titleIcon.GetComponent<RectTransform>();
|
||||
titleIconRt.anchorMin = new Vector2(0f, 1f);
|
||||
titleIconRt.anchorMax = new Vector2(0f, 1f);
|
||||
titleIconRt.pivot = new Vector2(0.5f, 0.5f);
|
||||
titleIconRt.anchoredPosition = new Vector2(16f, -34f);
|
||||
|
||||
_titleText = HudCanvas.MakeText(_root.transform, "Title", "", 9);
|
||||
RectTransform titleRt = _titleText.GetComponent<RectTransform>();
|
||||
titleRt.anchorMin = new Vector2(0f, 1f);
|
||||
titleRt.anchorMax = new Vector2(1f, 1f);
|
||||
titleRt.pivot = new Vector2(0.5f, 1f);
|
||||
titleRt.sizeDelta = new Vector2(-14f, 16f);
|
||||
titleRt.anchoredPosition = new Vector2(0f, -26f);
|
||||
titleRt.offsetMin = new Vector2(28f, -44f);
|
||||
titleRt.offsetMax = new Vector2(-10f, -28f);
|
||||
_titleText.alignment = TextAnchor.MiddleLeft;
|
||||
_titleText.color = new Color(0.78f, 0.8f, 0.84f, 1f);
|
||||
|
||||
|
|
@ -102,9 +112,9 @@ public static class ChronicleHud
|
|||
scrollRt.anchorMin = new Vector2(0f, 0f);
|
||||
scrollRt.anchorMax = new Vector2(1f, 1f);
|
||||
scrollRt.offsetMin = new Vector2(6f, 6f);
|
||||
scrollRt.offsetMax = new Vector2(-6f, -44f);
|
||||
scrollRt.offsetMax = new Vector2(-6f, -48f);
|
||||
Image scrollBg = scrollGo.GetComponent<Image>();
|
||||
scrollBg.color = new Color(0f, 0f, 0f, 0.05f);
|
||||
scrollBg.color = new Color(0f, 0f, 0f, 0.08f);
|
||||
Mask mask = scrollGo.GetComponent<Mask>();
|
||||
mask.showMaskGraphic = false;
|
||||
|
||||
|
|
@ -123,7 +133,7 @@ public static class ChronicleHud
|
|||
layout.childControlWidth = true;
|
||||
layout.childForceExpandHeight = false;
|
||||
layout.childForceExpandWidth = true;
|
||||
layout.spacing = 1f;
|
||||
layout.spacing = 2f;
|
||||
layout.padding = new RectOffset(0, 0, 0, 0);
|
||||
|
||||
ContentSizeFitter fitter = contentGo.GetComponent<ContentSizeFitter>();
|
||||
|
|
@ -140,7 +150,7 @@ public static class ChronicleHud
|
|||
RefreshTabChrome();
|
||||
_root.SetActive(false);
|
||||
_visible = false;
|
||||
LogService.LogInfo("[IdleSpectator] Chronicle HUD ready (F9, History|World)");
|
||||
LogService.LogInfo("[IdleSpectator] Chronicle HUD ready (F9, History|World + icons)");
|
||||
}
|
||||
|
||||
public static void SetTab(ChronicleTab tab)
|
||||
|
|
@ -213,6 +223,8 @@ public static class ChronicleHud
|
|||
_titleText.text = "World events";
|
||||
}
|
||||
|
||||
HudIcons.Apply(_titleIcon, HudIcons.FromUiIcon("iconWar") ?? HudIcons.FromUiIcon("iconBooks"));
|
||||
|
||||
if (entries.Count == 0)
|
||||
{
|
||||
AddEmpty("No world events yet");
|
||||
|
|
@ -231,6 +243,16 @@ public static class ChronicleHud
|
|||
: title;
|
||||
}
|
||||
|
||||
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||
if (focus != null)
|
||||
{
|
||||
HudIcons.Apply(_titleIcon, HudIcons.FromActor(focus));
|
||||
}
|
||||
else
|
||||
{
|
||||
HudIcons.Apply(_titleIcon, HudIcons.FromUiIcon("iconBooks"));
|
||||
}
|
||||
|
||||
if (subjectId == 0)
|
||||
{
|
||||
AddEmpty("Focus a creature to see history");
|
||||
|
|
@ -269,6 +291,7 @@ public static class ChronicleHud
|
|||
Transform parent,
|
||||
string name,
|
||||
string label,
|
||||
string iconName,
|
||||
float yFromTop,
|
||||
float x,
|
||||
out Image bg,
|
||||
|
|
@ -281,19 +304,27 @@ public static class ChronicleHud
|
|||
rt.anchorMin = new Vector2(0f, 1f);
|
||||
rt.anchorMax = new Vector2(0f, 1f);
|
||||
rt.pivot = new Vector2(0f, 1f);
|
||||
rt.sizeDelta = new Vector2(68f, 16f);
|
||||
rt.sizeDelta = new Vector2(82f, 18f);
|
||||
rt.anchoredPosition = new Vector2(8f + x, yFromTop);
|
||||
|
||||
bg = go.GetComponent<Image>();
|
||||
bg.color = new Color(0.15f, 0.16f, 0.2f, 0.9f);
|
||||
|
||||
Image icon = HudCanvas.MakeIcon(go.transform, "Icon", 12f);
|
||||
RectTransform iconRt = icon.GetComponent<RectTransform>();
|
||||
iconRt.anchorMin = new Vector2(0f, 0.5f);
|
||||
iconRt.anchorMax = new Vector2(0f, 0.5f);
|
||||
iconRt.pivot = new Vector2(0.5f, 0.5f);
|
||||
iconRt.anchoredPosition = new Vector2(10f, 0f);
|
||||
HudIcons.Apply(icon, HudIcons.FromUiIcon(iconName));
|
||||
|
||||
text = HudCanvas.MakeText(go.transform, "Label", label, 9);
|
||||
RectTransform textRt = text.GetComponent<RectTransform>();
|
||||
textRt.anchorMin = Vector2.zero;
|
||||
textRt.anchorMax = Vector2.one;
|
||||
textRt.offsetMin = Vector2.zero;
|
||||
textRt.offsetMax = Vector2.zero;
|
||||
text.alignment = TextAnchor.MiddleCenter;
|
||||
textRt.offsetMin = new Vector2(20f, 0f);
|
||||
textRt.offsetMax = new Vector2(-2f, 0f);
|
||||
text.alignment = TextAnchor.MiddleLeft;
|
||||
text.raycastTarget = false;
|
||||
|
||||
Button button = go.GetComponent<Button>();
|
||||
|
|
@ -302,7 +333,7 @@ public static class ChronicleHud
|
|||
|
||||
private static void RefreshTabChrome()
|
||||
{
|
||||
Color active = new Color(0.28f, 0.32f, 0.4f, 0.95f);
|
||||
Color active = new Color(0.32f, 0.38f, 0.48f, 0.95f);
|
||||
Color idle = new Color(0.12f, 0.13f, 0.16f, 0.7f);
|
||||
Color activeText = new Color(0.95f, 0.93f, 0.88f, 1f);
|
||||
Color idleText = new Color(0.65f, 0.68f, 0.72f, 1f);
|
||||
|
|
@ -353,7 +384,7 @@ public static class ChronicleHud
|
|||
le.preferredHeight = RowHeight;
|
||||
|
||||
Text label = HudCanvas.MakeText(go.transform, "Text", text, 9);
|
||||
Stretch(label.GetComponent<RectTransform>(), 2f);
|
||||
Stretch(label.GetComponent<RectTransform>(), 4f);
|
||||
label.alignment = TextAnchor.MiddleLeft;
|
||||
label.color = new Color(0.65f, 0.68f, 0.72f, 0.95f);
|
||||
return go;
|
||||
|
|
@ -369,10 +400,22 @@ public static class ChronicleHud
|
|||
le.preferredHeight = RowHeight;
|
||||
|
||||
Image bg = go.GetComponent<Image>();
|
||||
bg.color = new Color(1f, 1f, 1f, 0.02f);
|
||||
bg.color = new Color(1f, 1f, 1f, 0.03f);
|
||||
|
||||
Text label = HudCanvas.MakeText(go.transform, "Text", Truncate(entry.Line, 44), 9);
|
||||
Stretch(label.GetComponent<RectTransform>(), 3f);
|
||||
Image kindIcon = HudCanvas.MakeIcon(go.transform, "Kind", KindIconSize);
|
||||
RectTransform kindRt = kindIcon.GetComponent<RectTransform>();
|
||||
kindRt.anchorMin = new Vector2(0f, 0.5f);
|
||||
kindRt.anchorMax = new Vector2(0f, 0.5f);
|
||||
kindRt.pivot = new Vector2(0.5f, 0.5f);
|
||||
kindRt.anchoredPosition = new Vector2(10f, 0f);
|
||||
HudIcons.Apply(kindIcon, HudIcons.ForChronicleKind(entry.Kind));
|
||||
|
||||
Text label = HudCanvas.MakeText(go.transform, "Text", Truncate(entry.Line, 40), 9);
|
||||
RectTransform labelRt = label.GetComponent<RectTransform>();
|
||||
labelRt.anchorMin = Vector2.zero;
|
||||
labelRt.anchorMax = Vector2.one;
|
||||
labelRt.offsetMin = new Vector2(22f, 1f);
|
||||
labelRt.offsetMax = new Vector2(-4f, -1f);
|
||||
label.alignment = TextAnchor.MiddleLeft;
|
||||
label.color = ColorForKind(entry.Kind);
|
||||
|
||||
|
|
@ -398,15 +441,15 @@ public static class ChronicleHud
|
|||
switch (kind)
|
||||
{
|
||||
case ChronicleKind.Death:
|
||||
return new Color(0.75f, 0.7f, 0.75f, 1f);
|
||||
return new Color(0.78f, 0.72f, 0.78f, 1f);
|
||||
case ChronicleKind.Kill:
|
||||
return new Color(0.95f, 0.55f, 0.45f, 1f);
|
||||
return new Color(0.95f, 0.58f, 0.48f, 1f);
|
||||
case ChronicleKind.Lover:
|
||||
return new Color(0.95f, 0.55f, 0.75f, 1f);
|
||||
return new Color(0.95f, 0.58f, 0.78f, 1f);
|
||||
case ChronicleKind.Friend:
|
||||
return new Color(0.55f, 0.85f, 0.95f, 1f);
|
||||
return new Color(0.58f, 0.85f, 0.95f, 1f);
|
||||
case ChronicleKind.World:
|
||||
return new Color(0.95f, 0.85f, 0.45f, 1f);
|
||||
return new Color(0.95f, 0.85f, 0.48f, 1f);
|
||||
default:
|
||||
return new Color(0.88f, 0.88f, 0.9f, 1f);
|
||||
}
|
||||
|
|
|
|||
371
IdleSpectator/DossierAvatar.cs
Normal file
371
IdleSpectator/DossierAvatar.cs
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
using NeoModLoader.services;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>
|
||||
/// Compact dossier portrait that reuses vanilla <see cref="UiUnitAvatarElement"/>
|
||||
/// (stone frame + floor tile), falling back to a plain live sprite if needed.
|
||||
/// </summary>
|
||||
public static class DossierAvatar
|
||||
{
|
||||
private static UiUnitAvatarElement _template;
|
||||
private static UiUnitAvatarElement _instance;
|
||||
private static RectTransform _host;
|
||||
private static Image _fallbackBg;
|
||||
private static Image _fallbackSprite;
|
||||
private static bool _usingVanilla;
|
||||
private static bool _loggedMode;
|
||||
private static long _shownActorId = -1;
|
||||
|
||||
public static bool UsingVanilla => _usingVanilla;
|
||||
|
||||
public static void EnsureHost(Transform parent, float size)
|
||||
{
|
||||
if (_host != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject hostGo = new GameObject("DossierAvatarHost", typeof(RectTransform));
|
||||
hostGo.transform.SetParent(parent, false);
|
||||
_host = hostGo.GetComponent<RectTransform>();
|
||||
_host.anchorMin = new Vector2(0f, 1f);
|
||||
_host.anchorMax = new Vector2(0f, 1f);
|
||||
_host.pivot = new Vector2(0f, 1f);
|
||||
_host.sizeDelta = new Vector2(size, size);
|
||||
|
||||
TrySpawnVanilla(size);
|
||||
if (!_usingVanilla)
|
||||
{
|
||||
BuildFallback(size);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Place(float padX, float yFromTop, float size)
|
||||
{
|
||||
if (_host == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_host.anchoredPosition = new Vector2(padX, -yFromTop);
|
||||
_host.sizeDelta = new Vector2(size, size);
|
||||
if (_instance != null)
|
||||
{
|
||||
RectTransform rt = _instance.GetComponent<RectTransform>();
|
||||
if (rt != null)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero;
|
||||
rt.offsetMax = Vector2.zero;
|
||||
rt.localScale = Vector3.one;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetActive(bool active)
|
||||
{
|
||||
if (_host != null)
|
||||
{
|
||||
_host.gameObject.SetActive(active);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Show(Actor actor)
|
||||
{
|
||||
if (_host == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (actor == null || !actor.isAlive())
|
||||
{
|
||||
ClearActor();
|
||||
return;
|
||||
}
|
||||
|
||||
long id = actor.getID();
|
||||
if (_usingVanilla && _instance != null && _shownActorId == id && _instance.gameObject.activeSelf)
|
||||
{
|
||||
try
|
||||
{
|
||||
_instance.updateTileSprite();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore tile refresh failures
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_usingVanilla && _instance != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_instance.show_banner_kingdom = false;
|
||||
_instance.show_banner_clan = false;
|
||||
if (_instance.kingdomBanner != null)
|
||||
{
|
||||
_instance.kingdomBanner.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
if (_instance.clanBanner != null)
|
||||
{
|
||||
_instance.clanBanner.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
if (_instance.avatarLoader != null)
|
||||
{
|
||||
// Keep unit readable inside the compact frame.
|
||||
_instance.avatarLoader.avatarSize = 0.55f;
|
||||
}
|
||||
|
||||
_instance.show(actor);
|
||||
_shownActorId = id;
|
||||
DisableInteractions(_instance.gameObject);
|
||||
return;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
LogService.LogInfo("[IdleSpectator] Vanilla avatar show failed, using fallback: " + ex.Message);
|
||||
FallbackToSprite(actor);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
FallbackToSprite(actor);
|
||||
_shownActorId = id;
|
||||
}
|
||||
|
||||
public static void ClearActor()
|
||||
{
|
||||
_shownActorId = -1;
|
||||
if (_usingVanilla && _instance != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_instance.gameObject.SetActive(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (_fallbackSprite != null)
|
||||
{
|
||||
_fallbackSprite.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void TrySpawnVanilla(float size)
|
||||
{
|
||||
UiUnitAvatarElement template = FindTemplate();
|
||||
if (template == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
GameObject clone = Object.Instantiate(template.gameObject, _host, false);
|
||||
clone.name = "VanillaUnitAvatar";
|
||||
clone.SetActive(true);
|
||||
|
||||
_instance = clone.GetComponent<UiUnitAvatarElement>();
|
||||
if (_instance == null)
|
||||
{
|
||||
Object.Destroy(clone);
|
||||
return;
|
||||
}
|
||||
|
||||
_template = template;
|
||||
_usingVanilla = true;
|
||||
_instance.show_banner_kingdom = false;
|
||||
_instance.show_banner_clan = false;
|
||||
|
||||
RectTransform rt = clone.GetComponent<RectTransform>();
|
||||
if (rt != null)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero;
|
||||
rt.offsetMax = Vector2.zero;
|
||||
rt.localScale = Vector3.one;
|
||||
rt.sizeDelta = Vector2.zero;
|
||||
}
|
||||
|
||||
DisableInteractions(clone);
|
||||
if (!_loggedMode)
|
||||
{
|
||||
_loggedMode = true;
|
||||
LogService.LogInfo("[IdleSpectator] Dossier portrait using vanilla UiUnitAvatarElement frame");
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
_usingVanilla = false;
|
||||
_instance = null;
|
||||
LogService.LogInfo("[IdleSpectator] Could not clone unit avatar: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static UiUnitAvatarElement FindTemplate()
|
||||
{
|
||||
if (_template != null)
|
||||
{
|
||||
return _template;
|
||||
}
|
||||
|
||||
UiUnitAvatarElement prefab = null;
|
||||
UiUnitAvatarElement scene = null;
|
||||
UiUnitAvatarElement[] all = Resources.FindObjectsOfTypeAll<UiUnitAvatarElement>();
|
||||
for (int i = 0; i < all.Length; i++)
|
||||
{
|
||||
UiUnitAvatarElement el = all[i];
|
||||
if (el == null || el.gameObject == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip our own clone if already present.
|
||||
if (el.gameObject.name.StartsWith("VanillaUnitAvatar"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!el.gameObject.scene.IsValid())
|
||||
{
|
||||
prefab = el;
|
||||
break;
|
||||
}
|
||||
|
||||
if (scene == null)
|
||||
{
|
||||
scene = el;
|
||||
}
|
||||
}
|
||||
|
||||
_template = prefab != null ? prefab : scene;
|
||||
return _template;
|
||||
}
|
||||
|
||||
private static void BuildFallback(float size)
|
||||
{
|
||||
_fallbackBg = new GameObject("FallbackBg", typeof(RectTransform), typeof(Image)).GetComponent<Image>();
|
||||
_fallbackBg.transform.SetParent(_host, false);
|
||||
RectTransform bgRt = _fallbackBg.GetComponent<RectTransform>();
|
||||
bgRt.anchorMin = Vector2.zero;
|
||||
bgRt.anchorMax = Vector2.one;
|
||||
bgRt.offsetMin = Vector2.zero;
|
||||
bgRt.offsetMax = Vector2.zero;
|
||||
_fallbackBg.color = new Color(0.04f, 0.045f, 0.055f, 0.95f);
|
||||
_fallbackBg.raycastTarget = false;
|
||||
|
||||
_fallbackSprite = HudCanvas.MakeIcon(_host, "FallbackLive", size);
|
||||
RectTransform spRt = _fallbackSprite.GetComponent<RectTransform>();
|
||||
spRt.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
spRt.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
spRt.pivot = new Vector2(0.5f, 0.5f);
|
||||
spRt.anchoredPosition = Vector2.zero;
|
||||
_fallbackSprite.preserveAspect = true;
|
||||
|
||||
if (!_loggedMode)
|
||||
{
|
||||
_loggedMode = true;
|
||||
LogService.LogInfo("[IdleSpectator] Dossier portrait using fallback live sprite (no vanilla avatar template)");
|
||||
}
|
||||
}
|
||||
|
||||
private static void FallbackToSprite(Actor actor)
|
||||
{
|
||||
if (_fallbackSprite == null && _host != null)
|
||||
{
|
||||
BuildFallback(_host.sizeDelta.x);
|
||||
}
|
||||
|
||||
if (_instance != null)
|
||||
{
|
||||
_instance.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
if (_fallbackBg != null)
|
||||
{
|
||||
_fallbackBg.gameObject.SetActive(true);
|
||||
// Prefer the unit's current floor tile as portrait ground, like vanilla.
|
||||
try
|
||||
{
|
||||
if (actor != null && actor.current_tile != null && actor.current_tile.Type != null
|
||||
&& actor.current_tile.Type.sprites != null && actor.current_tile.Type.sprites.main != null)
|
||||
{
|
||||
Sprite tile = actor.current_tile.Type.sprites.main.sprite;
|
||||
if (tile != null)
|
||||
{
|
||||
_fallbackBg.sprite = tile;
|
||||
_fallbackBg.color = Color.white;
|
||||
_fallbackBg.type = Image.Type.Simple;
|
||||
_fallbackBg.preserveAspect = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// keep flat fill
|
||||
}
|
||||
}
|
||||
|
||||
if (_fallbackSprite != null)
|
||||
{
|
||||
Sprite live = HudIcons.FromActorLive(actor);
|
||||
HudIcons.Apply(_fallbackSprite, live, preserveAspect: true);
|
||||
if (live != null)
|
||||
{
|
||||
float w = Mathf.Max(1f, live.rect.width);
|
||||
float h = Mathf.Max(1f, live.rect.height);
|
||||
float max = _host != null ? _host.sizeDelta.x : 44f;
|
||||
float scale = (max * 0.85f) / Mathf.Max(w, h);
|
||||
_fallbackSprite.rectTransform.sizeDelta = new Vector2(w * scale, h * scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DisableInteractions(GameObject root)
|
||||
{
|
||||
if (root == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Graphic[] graphics = root.GetComponentsInChildren<Graphic>(true);
|
||||
for (int i = 0; i < graphics.Length; i++)
|
||||
{
|
||||
if (graphics[i] != null)
|
||||
{
|
||||
graphics[i].raycastTarget = false;
|
||||
}
|
||||
}
|
||||
|
||||
Behaviour[] tips = root.GetComponentsInChildren<TipButton>(true);
|
||||
for (int i = 0; i < tips.Length; i++)
|
||||
{
|
||||
if (tips[i] != null)
|
||||
{
|
||||
tips[i].enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
Button[] buttons = root.GetComponentsInChildren<Button>(true);
|
||||
for (int i = 0; i < buttons.Length; i++)
|
||||
{
|
||||
if (buttons[i] != null)
|
||||
{
|
||||
buttons[i].enabled = false;
|
||||
buttons[i].interactable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -406,6 +406,8 @@ internal static class HarnessScenarios
|
|||
Step("ch10", "assert", expect: "health"),
|
||||
Step("ch11", "assert", expect: "dossier_contains", value: "auto"),
|
||||
Step("ch12", "assert", expect: "caption_contains", value: "auto"),
|
||||
Step("ch12b", "assert", expect: "caption_layout_ok"),
|
||||
Step("ch12c", "assert", expect: "caption_contains", value: "age"),
|
||||
|
||||
// History inject
|
||||
Step("ch13", "chronicle_force", label: "Killed"),
|
||||
|
|
@ -431,12 +433,15 @@ internal static class HarnessScenarios
|
|||
|
||||
// Tabs
|
||||
Step("ch18", "chronicle_open"),
|
||||
Step("ch18a", "screenshot", value: "hud-chronicle_smoke.png"),
|
||||
Step("ch18a2", "wait", wait: 0.6f),
|
||||
Step("ch18b", "chronicle_tab", value: "history"),
|
||||
Step("ch18c", "assert", expect: "chronicle_tab", value: "history"),
|
||||
Step("ch18d", "chronicle_tab", value: "world"),
|
||||
Step("ch18e", "assert", expect: "chronicle_tab", value: "world"),
|
||||
Step("ch18f", "chronicle_tab", value: "history"),
|
||||
Step("ch18g", "wait", wait: 0.25f),
|
||||
Step("ch18c", "assert", expect: "caption_layout_ok"),
|
||||
Step("ch18d", "assert", expect: "chronicle_tab", value: "history"),
|
||||
Step("ch18e", "chronicle_tab", value: "world"),
|
||||
Step("ch18f", "assert", expect: "chronicle_tab", value: "world"),
|
||||
Step("ch18g", "chronicle_tab", value: "history"),
|
||||
Step("ch18h", "wait", wait: 0.25f),
|
||||
Step("ch19", "assert", expect: "focus", value: "true"),
|
||||
Step("ch20", "assert", expect: "health"),
|
||||
Step("ch21", "chronicle_jump"),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ using UnityEngine.UI;
|
|||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>
|
||||
/// Shared canvas / text helpers for compact non-modal HUD cards.
|
||||
/// Shared canvas / text / panel helpers for compact non-modal HUD cards.
|
||||
/// </summary>
|
||||
public static class HudCanvas
|
||||
{
|
||||
|
|
@ -51,4 +51,43 @@ public static class HudCanvas
|
|||
text.verticalOverflow = VerticalWrapMode.Truncate;
|
||||
return text;
|
||||
}
|
||||
|
||||
public static Image MakeIcon(Transform parent, string name, float size)
|
||||
{
|
||||
Image image = new GameObject(name, typeof(RectTransform), typeof(Image)).GetComponent<Image>();
|
||||
image.transform.SetParent(parent, false);
|
||||
RectTransform rt = image.GetComponent<RectTransform>();
|
||||
rt.sizeDelta = new Vector2(size, size);
|
||||
image.raycastTarget = false;
|
||||
image.preserveAspect = true;
|
||||
image.color = Color.white;
|
||||
image.enabled = false;
|
||||
return image;
|
||||
}
|
||||
|
||||
/// <summary>Dark fill + lighter 1px edge so panels read as WorldBox chrome.</summary>
|
||||
public static void StylePanel(Image fill, Transform root, Color fillColor)
|
||||
{
|
||||
if (fill != null)
|
||||
{
|
||||
fill.color = fillColor;
|
||||
}
|
||||
|
||||
if (root == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject edge = new GameObject("Edge", typeof(RectTransform), typeof(Image));
|
||||
edge.transform.SetParent(root, false);
|
||||
edge.transform.SetAsFirstSibling();
|
||||
RectTransform edgeRt = edge.GetComponent<RectTransform>();
|
||||
edgeRt.anchorMin = Vector2.zero;
|
||||
edgeRt.anchorMax = Vector2.one;
|
||||
edgeRt.offsetMin = new Vector2(-1f, -1f);
|
||||
edgeRt.offsetMax = new Vector2(1f, 1f);
|
||||
Image edgeImg = edge.GetComponent<Image>();
|
||||
edgeImg.color = new Color(0.42f, 0.45f, 0.5f, 0.55f);
|
||||
edgeImg.raycastTarget = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
198
IdleSpectator/HudIcons.cs
Normal file
198
IdleSpectator/HudIcons.cs
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>
|
||||
/// Loads WorldBox ui/Icons sprites for HUD chrome.
|
||||
/// </summary>
|
||||
public static class HudIcons
|
||||
{
|
||||
public static Sprite FromUiIcon(string iconName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(iconName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Sprite sprite = SpriteTextureLoader.getSprite("ui/Icons/" + iconName);
|
||||
if (sprite != null)
|
||||
{
|
||||
return sprite;
|
||||
}
|
||||
|
||||
// Some paths are lower-case in older content.
|
||||
return SpriteTextureLoader.getSprite("ui/icons/" + iconName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Sprite FromAsset(ActorAsset asset)
|
||||
{
|
||||
if (asset == null)
|
||||
{
|
||||
return FromUiIcon("iconQuestionMark");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Sprite sprite = asset.getSpriteIcon();
|
||||
if (sprite != null)
|
||||
{
|
||||
return sprite;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// fall through
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(asset.icon))
|
||||
{
|
||||
return FromUiIcon(asset.icon);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
return FromUiIcon("iconQuestionMark");
|
||||
}
|
||||
|
||||
public static Sprite FromActor(Actor actor)
|
||||
{
|
||||
if (actor == null)
|
||||
{
|
||||
return FromUiIcon("iconQuestionMark");
|
||||
}
|
||||
|
||||
return FromAsset(actor.asset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Live unit sprite as shown in inspect UI (phenotype/kingdom tint), not the species icon.
|
||||
/// </summary>
|
||||
public static Sprite FromActorLive(Actor actor)
|
||||
{
|
||||
if (actor == null || !actor.isAlive())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Sprite main = actor.calculateMainSprite();
|
||||
if (main == null)
|
||||
{
|
||||
return FromActor(actor);
|
||||
}
|
||||
|
||||
Sprite ui = DynamicActorSpriteCreatorUI.getUnitSpriteForUI(actor, main);
|
||||
if (ui != null)
|
||||
{
|
||||
return ui;
|
||||
}
|
||||
|
||||
if (actor.hasColoredSprite())
|
||||
{
|
||||
Sprite colored = actor.calculateColoredSprite(main);
|
||||
if (colored != null)
|
||||
{
|
||||
return colored;
|
||||
}
|
||||
}
|
||||
|
||||
return main;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return FromActor(actor);
|
||||
}
|
||||
}
|
||||
|
||||
public static Sprite ForChronicleKind(ChronicleKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case ChronicleKind.Death:
|
||||
return FromUiIcon("iconDead") ?? FromUiIcon("iconSkulls");
|
||||
case ChronicleKind.Kill:
|
||||
return FromUiIcon("iconKills") ?? FromUiIcon("iconAttack");
|
||||
case ChronicleKind.Lover:
|
||||
return FromUiIcon("iconArrowLover");
|
||||
case ChronicleKind.Friend:
|
||||
return FromUiIcon("iconFavoriteStar");
|
||||
case ChronicleKind.World:
|
||||
return FromUiIcon("iconWar") ?? FromUiIcon("iconBooks");
|
||||
default:
|
||||
return FromUiIcon("iconClock");
|
||||
}
|
||||
}
|
||||
|
||||
public static Sprite Age() => FromUiIcon("iconAge");
|
||||
|
||||
public static Sprite Level() => FromUiIcon("iconLevels");
|
||||
|
||||
public static Sprite Kills() => FromUiIcon("iconKills") ?? FromUiIcon("iconAttack");
|
||||
|
||||
public static Sprite Task() => FromUiIcon("iconShowTasks") ?? FromUiIcon("iconWalker");
|
||||
|
||||
public static Sprite SexMale() => FromUiIcon("IconMale");
|
||||
|
||||
public static Sprite SexFemale() => FromUiIcon("IconFemale");
|
||||
|
||||
public static Sprite FromTrait(ActorTrait trait)
|
||||
{
|
||||
if (trait == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Sprite sprite = trait.getSprite();
|
||||
if (sprite != null)
|
||||
{
|
||||
return sprite;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// fall through
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(trait.path_icon))
|
||||
{
|
||||
return SpriteTextureLoader.getSprite(trait.path_icon);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void Apply(UnityEngine.UI.Image image, Sprite sprite, bool preserveAspect = true)
|
||||
{
|
||||
if (image == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
image.sprite = sprite;
|
||||
image.enabled = sprite != null;
|
||||
image.preserveAspect = preserveAspect;
|
||||
image.color = Color.white;
|
||||
}
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ public static class SpectatorMode
|
|||
{
|
||||
InterestDirector.OnSpectatorEnabled();
|
||||
SpeciesDiscovery.OnSpectatorEnabled();
|
||||
Chronicle.OnSpectatorEnabled();
|
||||
WorldTip.showNowTop("Idle Spectator ON (I or any input to stop; F9 chronicle)", pTranslate: false);
|
||||
LogService.LogInfo("[IdleSpectator] Spectator mode enabled");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ public sealed class UnitDossier
|
|||
public float Score;
|
||||
public int Kills;
|
||||
public int Age;
|
||||
public int Level;
|
||||
public int Renown;
|
||||
public bool IsFavorite;
|
||||
public bool IsKing;
|
||||
|
|
@ -25,9 +26,20 @@ public sealed class UnitDossier
|
|||
public bool IsFighting;
|
||||
public bool IsSpectacle;
|
||||
public bool IsLoneSpecies;
|
||||
public bool IsMale;
|
||||
public bool IsFemale;
|
||||
public string TaskText = "";
|
||||
public string KingdomName = "";
|
||||
public string CityName = "";
|
||||
public readonly List<string> ScoreReasons = new List<string>();
|
||||
public readonly List<TraitChip> TopTraits = new List<TraitChip>();
|
||||
|
||||
public sealed class TraitChip
|
||||
{
|
||||
public string Id = "";
|
||||
public string Name = "";
|
||||
public ActorTrait Trait;
|
||||
}
|
||||
|
||||
public static UnitDossier FromActor(Actor actor, InterestTier? watchTier = null, string watchLabel = null)
|
||||
{
|
||||
|
|
@ -47,6 +59,7 @@ public sealed class UnitDossier
|
|||
d.SpeciesId = actor.asset != null ? actor.asset.id : "creature";
|
||||
d.Kills = actor.data != null ? actor.data.kills : 0;
|
||||
d.Age = SafeAge(actor);
|
||||
d.Level = SafeLevel(actor);
|
||||
d.Renown = actor.renown;
|
||||
d.IsFavorite = actor.isFavorite();
|
||||
d.IsKing = actor.isKing();
|
||||
|
|
@ -57,9 +70,15 @@ public sealed class UnitDossier
|
|||
d.KingdomName = actor.kingdom != null ? actor.kingdom.name : "";
|
||||
d.CityName = actor.city != null ? actor.city.name : "";
|
||||
d.Score = WorldActivityScanner.ScoreActorPublic(actor, speciesCounts);
|
||||
FillSex(d, actor);
|
||||
d.TaskText = SafeTask(actor);
|
||||
FillTopTraits(d, actor);
|
||||
CollectReasons(d, actor, speciesCounts);
|
||||
|
||||
d.Headline = $"{d.Name} ({d.SpeciesId})";
|
||||
// Species icon is shown separately; keep id in the title for scanability.
|
||||
d.Headline = string.IsNullOrEmpty(d.SpeciesId)
|
||||
? d.Name
|
||||
: $"{d.Name} ({d.SpeciesId})";
|
||||
d.ReasonLine = BuildReasonLine(d, watchTier, watchLabel, actor, speciesCounts);
|
||||
d.DetailLine = BuildDetailLine(d);
|
||||
d.CaptionText = JoinCaption(d.Headline, d.ReasonLine, d.DetailLine);
|
||||
|
|
@ -73,12 +92,41 @@ public sealed class UnitDossier
|
|||
return false;
|
||||
}
|
||||
|
||||
return (CaptionText ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (Headline ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (ReasonLine ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (DetailLine ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (Name ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (SpeciesId ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
if ((CaptionText ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (Headline ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (ReasonLine ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (DetailLine ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (Name ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (SpeciesId ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (TaskText ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Harness probes "age" after we moved age onto an icon chip.
|
||||
if (needle.Equals("age", System.StringComparison.OrdinalIgnoreCase) && Age > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < TopTraits.Count; i++)
|
||||
{
|
||||
TraitChip chip = TopTraits[i];
|
||||
if (chip == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((!string.IsNullOrEmpty(chip.Name)
|
||||
&& chip.Name.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
|| (!string.IsNullOrEmpty(chip.Id)
|
||||
&& chip.Id.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string JoinCaption(string headline, string reason, string detail)
|
||||
|
|
@ -132,7 +180,7 @@ public sealed class UnitDossier
|
|||
}
|
||||
}
|
||||
|
||||
if (IsRedundantWithHeadline(why, d))
|
||||
if (IsRedundantWithHeadline(why, d) || IsShownTraitName(d, why))
|
||||
{
|
||||
why = "";
|
||||
}
|
||||
|
|
@ -144,7 +192,7 @@ public sealed class UnitDossier
|
|||
|
||||
if (string.IsNullOrEmpty(why) || why.IndexOf(tierPart, System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return tierPart;
|
||||
return IsShownTraitName(d, tierPart) ? "" : tierPart;
|
||||
}
|
||||
|
||||
return tierPart + " · " + why;
|
||||
|
|
@ -165,12 +213,45 @@ public sealed class UnitDossier
|
|||
continue;
|
||||
}
|
||||
|
||||
// Traits already render as icon chips; don't repeat them as the reason line.
|
||||
if (IsShownTraitName(d, reason))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return reason;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private static bool IsShownTraitName(UnitDossier d, string text)
|
||||
{
|
||||
if (d == null || string.IsNullOrEmpty(text))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < d.TopTraits.Count; i++)
|
||||
{
|
||||
TraitChip chip = d.TopTraits[i];
|
||||
if (chip == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((!string.IsNullOrEmpty(chip.Name)
|
||||
&& string.Equals(text, chip.Name, System.StringComparison.OrdinalIgnoreCase))
|
||||
|| (!string.IsNullOrEmpty(chip.Id)
|
||||
&& string.Equals(text, Humanize(chip.Id), System.StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsRedundantWithHeadline(string text, UnitDossier d)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text) || d == null)
|
||||
|
|
@ -295,49 +376,16 @@ public sealed class UnitDossier
|
|||
d.ScoreReasons.Add("renown " + d.Renown);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (actor.hasTraits())
|
||||
{
|
||||
int n = 0;
|
||||
foreach (ActorTrait trait in actor.getTraits())
|
||||
{
|
||||
if (trait == null || string.IsNullOrEmpty(trait.id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
d.ScoreReasons.Add(Humanize(trait.id));
|
||||
n++;
|
||||
if (n >= 2)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore trait access failures on exotic units
|
||||
}
|
||||
// Trait names are shown as dossier chips; keep ScoreReasons for non-trait flavor only.
|
||||
}
|
||||
|
||||
private static string BuildDetailLine(UnitDossier d)
|
||||
{
|
||||
// Compact text fallback for logs/harness: task + optional kingdom.
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (d.Age > 0)
|
||||
if (!string.IsNullOrEmpty(d.TaskText))
|
||||
{
|
||||
sb.Append("age ").Append(d.Age);
|
||||
}
|
||||
|
||||
if (d.Kills > 0)
|
||||
{
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
sb.Append(" · ");
|
||||
}
|
||||
|
||||
sb.Append(d.Kills).Append(" kills");
|
||||
sb.Append(d.TaskText);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(d.KingdomName)
|
||||
|
|
@ -363,39 +411,113 @@ public sealed class UnitDossier
|
|||
sb.Append(d.CityName);
|
||||
}
|
||||
|
||||
int tags = 0;
|
||||
for (int i = 0; i < d.ScoreReasons.Count && tags < 2; i++)
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static void FillSex(UnitDossier d, Actor actor)
|
||||
{
|
||||
try
|
||||
{
|
||||
string reason = d.ScoreReasons[i];
|
||||
if (string.IsNullOrEmpty(reason))
|
||||
d.IsMale = actor.isSexMale();
|
||||
d.IsFemale = actor.isSexFemale();
|
||||
}
|
||||
catch
|
||||
{
|
||||
d.IsMale = false;
|
||||
d.IsFemale = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void FillTopTraits(UnitDossier d, Actor actor)
|
||||
{
|
||||
d.TopTraits.Clear();
|
||||
try
|
||||
{
|
||||
if (!actor.hasTraits())
|
||||
{
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
if (reason.IndexOf("kills", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
foreach (ActorTrait trait in actor.getTraits())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (trait == null || string.IsNullOrEmpty(trait.id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string shown = Humanize(reason);
|
||||
if (IsRedundantWithHeadline(shown, d)
|
||||
|| sb.ToString().IndexOf(shown, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (!string.IsNullOrEmpty(d.ReasonLine)
|
||||
&& d.ReasonLine.IndexOf(shown, System.StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
d.TopTraits.Add(new TraitChip
|
||||
{
|
||||
Id = trait.id,
|
||||
Name = TraitDisplayName(trait),
|
||||
Trait = trait
|
||||
});
|
||||
if (d.TopTraits.Count >= 3)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore trait access failures on exotic units
|
||||
}
|
||||
}
|
||||
|
||||
private static string TraitDisplayName(ActorTrait trait)
|
||||
{
|
||||
try
|
||||
{
|
||||
string locale = trait.getLocaleID();
|
||||
if (!string.IsNullOrEmpty(locale))
|
||||
{
|
||||
continue;
|
||||
string localized = LocalizedTextManager.getText(locale);
|
||||
if (!string.IsNullOrEmpty(localized) && localized != locale)
|
||||
{
|
||||
return localized;
|
||||
}
|
||||
}
|
||||
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
sb.Append(" · ");
|
||||
}
|
||||
|
||||
sb.Append(shown);
|
||||
tags++;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// fall through
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
return Humanize(trait.id);
|
||||
}
|
||||
|
||||
private static string SafeTask(Actor actor)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!actor.hasTask() || actor.ai == null || actor.ai.task == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
string text = actor.ai.task.getLocalizedText();
|
||||
if (!string.IsNullOrEmpty(text) && text != "-")
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
return Humanize(actor.ai.task.id);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
if (actor.hasTask() && actor.ai?.task != null)
|
||||
{
|
||||
return Humanize(actor.ai.task.id);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static string Humanize(string raw)
|
||||
|
|
@ -437,4 +559,16 @@ public sealed class UnitDossier
|
|||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int SafeLevel(Actor actor)
|
||||
{
|
||||
try
|
||||
{
|
||||
return actor.data != null ? actor.data.level : 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using NeoModLoader.General;
|
||||
using NeoModLoader.services;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
|
@ -6,21 +5,58 @@ using UnityEngine.UI;
|
|||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>
|
||||
/// Persistent compact dossier card (top-left) while Idle Spectator is active.
|
||||
/// Replaces WorldTip for dossier text so AFK watching does not spam giant orange toasts.
|
||||
/// Persistent compact dossier (Option A / vanilla strip):
|
||||
/// header species+name+sex, live portrait + level/task, full-width trait strip, reason.
|
||||
/// </summary>
|
||||
public static class WatchCaption
|
||||
{
|
||||
private const float PanelWidth = 260f;
|
||||
private const float PanelHeight = 72f;
|
||||
private const float PanelWidth = 220f;
|
||||
private const float SpeciesSize = 16f;
|
||||
private const float SexSize = 14f;
|
||||
private const float LiveMax = 44f;
|
||||
private const float ChipIcon = 14f;
|
||||
private const float TraitIcon = 12f;
|
||||
private const float PadX = 4f;
|
||||
private const float PadTop = 4f;
|
||||
private const float HeaderH = 18f;
|
||||
private const float BodyH = 44f;
|
||||
private const float TraitsH = 16f;
|
||||
private const float ReasonH = 12f;
|
||||
private const float Gap = 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 GameObject _root;
|
||||
private static RectTransform _rootRt;
|
||||
private static Image _speciesIcon;
|
||||
private static Image _sexIcon;
|
||||
private static Text _nameText;
|
||||
private static Text _reasonText;
|
||||
private static Text _metaText;
|
||||
|
||||
private static Image _levelIcon;
|
||||
private static Text _levelValue;
|
||||
private static Image _taskIcon;
|
||||
private static GameObject _metaCol;
|
||||
|
||||
private static readonly TraitSlot[] _traitSlots = new TraitSlot[3];
|
||||
private static GameObject _traitsRow;
|
||||
|
||||
private static UnitDossier _current;
|
||||
private static Actor _boundActor;
|
||||
private static bool _visible;
|
||||
|
||||
private sealed class TraitSlot
|
||||
{
|
||||
public GameObject Root;
|
||||
public Image Icon;
|
||||
public Text Label;
|
||||
}
|
||||
|
||||
public static UnitDossier Current => _current;
|
||||
|
||||
public static string LastHeadline { get; private set; } = "";
|
||||
|
|
@ -29,12 +65,20 @@ public static class WatchCaption
|
|||
|
||||
public static string LastCaptionText { get; private set; } = "";
|
||||
|
||||
public static Vector2 LastPanelSize { get; private set; }
|
||||
|
||||
public static bool LastLayoutOk { get; private set; }
|
||||
|
||||
public static void Clear()
|
||||
{
|
||||
_current = null;
|
||||
_boundActor = null;
|
||||
LastHeadline = "";
|
||||
LastDetail = "";
|
||||
LastCaptionText = "";
|
||||
LastPanelSize = Vector2.zero;
|
||||
LastLayoutOk = false;
|
||||
ApplyVisual(null, null);
|
||||
SetVisible(false);
|
||||
}
|
||||
|
||||
|
|
@ -52,8 +96,15 @@ public static class WatchCaption
|
|||
LastDetail = "";
|
||||
LastCaptionText = LastHeadline;
|
||||
_current = null;
|
||||
_boundActor = null;
|
||||
EnsureBuilt();
|
||||
ApplyLines(LastHeadline, "", "");
|
||||
ApplyVisual(null, null);
|
||||
if (_nameText != null)
|
||||
{
|
||||
_nameText.text = LastHeadline;
|
||||
}
|
||||
|
||||
Relayout(false, 0, false, false);
|
||||
SetVisible(ModSettings.ShowDossierCaption && SpectatorMode.Active);
|
||||
}
|
||||
|
||||
|
|
@ -67,11 +118,12 @@ public static class WatchCaption
|
|||
{
|
||||
UnitDossier dossier = UnitDossier.FromActor(actor, tier, label);
|
||||
_current = dossier;
|
||||
_boundActor = actor;
|
||||
LastHeadline = dossier.Headline ?? "";
|
||||
LastDetail = dossier.DetailLine ?? "";
|
||||
LastCaptionText = dossier.CaptionText ?? "";
|
||||
EnsureBuilt();
|
||||
ApplyLines(dossier.Headline, dossier.ReasonLine, dossier.DetailLine);
|
||||
ApplyVisual(actor, dossier);
|
||||
SetVisible(ModSettings.ShowDossierCaption && SpectatorMode.Active);
|
||||
LogService.LogInfo("[IdleSpectator][CAPTION] " + LastCaptionText.Replace("\n", " | "));
|
||||
}
|
||||
|
|
@ -91,35 +143,397 @@ public static class WatchCaption
|
|||
if (_root == null && !string.IsNullOrEmpty(LastCaptionText))
|
||||
{
|
||||
EnsureBuilt();
|
||||
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||
if (_current != null)
|
||||
{
|
||||
ApplyLines(_current.Headline, _current.ReasonLine, _current.DetailLine);
|
||||
ApplyVisual(focus ?? _boundActor, _current);
|
||||
}
|
||||
else
|
||||
else if (_nameText != null)
|
||||
{
|
||||
ApplyLines(LastHeadline, "", LastDetail);
|
||||
_nameText.text = LastHeadline;
|
||||
Relayout(false, 0, false, false);
|
||||
}
|
||||
|
||||
SetVisible(true);
|
||||
}
|
||||
|
||||
RefreshLivePortrait();
|
||||
}
|
||||
|
||||
private static void ApplyLines(string name, string reason, string meta)
|
||||
private static void RefreshLivePortrait()
|
||||
{
|
||||
if (_nameText != null)
|
||||
if (!_visible)
|
||||
{
|
||||
_nameText.text = string.IsNullOrEmpty(name) ? "" : name;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_reasonText != null)
|
||||
Actor actor = _boundActor;
|
||||
if (actor == null || !actor.isAlive())
|
||||
{
|
||||
_reasonText.text = string.IsNullOrEmpty(reason) ? "" : reason;
|
||||
if (MoveCamera.hasFocusUnit())
|
||||
{
|
||||
actor = MoveCamera._focus_unit;
|
||||
}
|
||||
}
|
||||
|
||||
if (actor == null || !actor.isAlive())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_current != null && actor.getID() != _current.UnitId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Tile floor can change as the unit moves; keep portrait in sync.
|
||||
DossierAvatar.Show(actor);
|
||||
}
|
||||
|
||||
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);
|
||||
if (_metaCol != null)
|
||||
{
|
||||
_metaCol.SetActive(hasBody);
|
||||
}
|
||||
|
||||
if (hasBody)
|
||||
{
|
||||
DossierAvatar.Show(actor);
|
||||
HudIcons.Apply(_levelIcon, HudIcons.Level());
|
||||
if (_levelValue != null)
|
||||
{
|
||||
_levelValue.text = dossier.Level.ToString();
|
||||
}
|
||||
}
|
||||
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 (_metaText != null)
|
||||
{
|
||||
_metaText.text = string.IsNullOrEmpty(meta) ? "" : meta;
|
||||
_metaText.text = hasTask ? dossier.TaskText : "";
|
||||
_metaText.gameObject.SetActive(hasTask);
|
||||
}
|
||||
|
||||
int traitCount = 0;
|
||||
if (_traitsRow != null)
|
||||
{
|
||||
if (dossier != null && dossier.TopTraits.Count > 0)
|
||||
{
|
||||
_traitsRow.SetActive(true);
|
||||
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));
|
||||
if (slot.Label != null)
|
||||
{
|
||||
string label = chip.Name ?? "";
|
||||
if (label.Length > 11)
|
||||
{
|
||||
label = label.Substring(0, 10) + "...";
|
||||
}
|
||||
|
||||
slot.Label.text = label;
|
||||
}
|
||||
|
||||
traitCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
slot.Root.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_traitsRow.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);
|
||||
}
|
||||
|
||||
private static void Relayout(bool hasBody, int traitCount, bool hasTask, bool hasReason)
|
||||
{
|
||||
float y = PadTop;
|
||||
PlaceHeader(y);
|
||||
y += HeaderH + Gap;
|
||||
|
||||
if (hasBody)
|
||||
{
|
||||
PlaceBody(y);
|
||||
y += BodyH + Gap;
|
||||
}
|
||||
|
||||
if (traitCount > 0)
|
||||
{
|
||||
PlaceTraits(y, traitCount);
|
||||
y += TraitsH + Gap;
|
||||
}
|
||||
|
||||
if (hasReason)
|
||||
{
|
||||
PlaceLine(_reasonText != null ? _reasonText.GetComponent<RectTransform>() : null, y, ReasonH, PadX);
|
||||
y += ReasonH + Gap;
|
||||
}
|
||||
|
||||
float height = Mathf.Max(HeaderH + PadTop * 2f, y + PadTop - Gap);
|
||||
if (_rootRt != null)
|
||||
{
|
||||
_rootRt.sizeDelta = new Vector2(PanelWidth, height);
|
||||
LastPanelSize = _rootRt.sizeDelta;
|
||||
}
|
||||
|
||||
LastLayoutOk = ProbeLayoutInside(height);
|
||||
if (!LastLayoutOk)
|
||||
{
|
||||
LogService.LogInfo(
|
||||
$"[IdleSpectator][CAPTION][LAYOUT_BAD] size={LastPanelSize} body={hasBody} traits={traitCount} task={hasTask} reason={hasReason}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void PlaceHeader(float yFromTop)
|
||||
{
|
||||
if (_speciesIcon != null)
|
||||
{
|
||||
RectTransform rt = _speciesIcon.GetComponent<RectTransform>();
|
||||
rt.anchorMin = new Vector2(0f, 1f);
|
||||
rt.anchorMax = new Vector2(0f, 1f);
|
||||
rt.pivot = new Vector2(0f, 1f);
|
||||
rt.anchoredPosition = new Vector2(PadX, -yFromTop);
|
||||
rt.sizeDelta = new Vector2(SpeciesSize, SpeciesSize);
|
||||
}
|
||||
|
||||
if (_sexIcon != null)
|
||||
{
|
||||
RectTransform rt = _sexIcon.GetComponent<RectTransform>();
|
||||
rt.anchorMin = new Vector2(1f, 1f);
|
||||
rt.anchorMax = new Vector2(1f, 1f);
|
||||
rt.pivot = new Vector2(1f, 1f);
|
||||
rt.anchoredPosition = new Vector2(-PadX, -yFromTop - 1f);
|
||||
rt.sizeDelta = new Vector2(SexSize, SexSize);
|
||||
}
|
||||
|
||||
if (_nameText != null)
|
||||
{
|
||||
RectTransform rt = _nameText.GetComponent<RectTransform>();
|
||||
rt.anchorMin = new Vector2(0f, 1f);
|
||||
rt.anchorMax = new Vector2(1f, 1f);
|
||||
rt.pivot = new Vector2(0.5f, 1f);
|
||||
float right = (_sexIcon != null && _sexIcon.gameObject.activeSelf) ? (PadX + SexSize + 2f) : PadX;
|
||||
rt.offsetMin = new Vector2(PadX + SpeciesSize + 4f, -(yFromTop + HeaderH));
|
||||
rt.offsetMax = new Vector2(-right, -yFromTop);
|
||||
}
|
||||
}
|
||||
|
||||
private static void PlaceBody(float yFromTop)
|
||||
{
|
||||
DossierAvatar.Place(PadX, yFromTop, LiveMax);
|
||||
|
||||
if (_metaCol != null)
|
||||
{
|
||||
RectTransform col = _metaCol.GetComponent<RectTransform>();
|
||||
col.anchorMin = new Vector2(0f, 1f);
|
||||
col.anchorMax = new Vector2(1f, 1f);
|
||||
col.pivot = new Vector2(0.5f, 1f);
|
||||
col.offsetMin = new Vector2(PadX + LiveMax + 4f, -(yFromTop + BodyH));
|
||||
col.offsetMax = new Vector2(-PadX, -yFromTop);
|
||||
}
|
||||
|
||||
// Level row at top of meta col, task under it.
|
||||
if (_levelIcon != null)
|
||||
{
|
||||
RectTransform irt = _levelIcon.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, -2f);
|
||||
irt.sizeDelta = new Vector2(ChipIcon, ChipIcon);
|
||||
}
|
||||
|
||||
if (_levelValue != null)
|
||||
{
|
||||
RectTransform vrt = _levelValue.GetComponent<RectTransform>();
|
||||
vrt.anchorMin = new Vector2(0f, 1f);
|
||||
vrt.anchorMax = new Vector2(1f, 1f);
|
||||
vrt.pivot = new Vector2(0.5f, 1f);
|
||||
vrt.offsetMin = new Vector2(ChipIcon + 3f, -18f);
|
||||
vrt.offsetMax = new Vector2(0f, -2f);
|
||||
}
|
||||
|
||||
if (_taskIcon != null)
|
||||
{
|
||||
RectTransform irt = _taskIcon.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, -22f);
|
||||
irt.sizeDelta = new Vector2(ChipIcon, ChipIcon);
|
||||
}
|
||||
|
||||
if (_metaText != null)
|
||||
{
|
||||
RectTransform trt = _metaText.GetComponent<RectTransform>();
|
||||
trt.anchorMin = new Vector2(0f, 1f);
|
||||
trt.anchorMax = new Vector2(1f, 1f);
|
||||
trt.pivot = new Vector2(0.5f, 1f);
|
||||
trt.offsetMin = new Vector2(ChipIcon + 3f, -38f);
|
||||
trt.offsetMax = new Vector2(0f, -22f);
|
||||
}
|
||||
}
|
||||
|
||||
private static void PlaceTraits(float yFromTop, int traitCount)
|
||||
{
|
||||
if (_traitsRow == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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 innerW = PanelWidth - PadX * 2f;
|
||||
float slotW = innerW / Mathf.Max(1, traitCount);
|
||||
for (int i = 0; i < _traitSlots.Length; i++)
|
||||
{
|
||||
TraitSlot slot = _traitSlots[i];
|
||||
if (slot == null || slot.Root == null || !slot.Root.activeSelf)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RectTransform rt = slot.Root.GetComponent<RectTransform>();
|
||||
rt.anchorMin = new Vector2(0f, 0f);
|
||||
rt.anchorMax = new Vector2(0f, 1f);
|
||||
rt.pivot = new Vector2(0f, 0.5f);
|
||||
rt.anchoredPosition = new Vector2(i * slotW, 0f);
|
||||
rt.sizeDelta = new Vector2(slotW - 2f, TraitsH);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return top >= 0f && bottom <= panelHeight + 0.5f && top < bottom;
|
||||
}
|
||||
|
||||
private static void SetVisible(bool visible)
|
||||
|
|
@ -150,48 +564,91 @@ public static class WatchCaption
|
|||
_root = new GameObject("IdleSpectatorDossierHud", typeof(RectTransform), typeof(Image), typeof(CanvasGroup));
|
||||
_root.transform.SetParent(canvas.transform, false);
|
||||
|
||||
RectTransform rt = _root.GetComponent<RectTransform>();
|
||||
rt.anchorMin = new Vector2(0f, 1f);
|
||||
rt.anchorMax = new Vector2(0f, 1f);
|
||||
rt.pivot = new Vector2(0f, 1f);
|
||||
rt.sizeDelta = new Vector2(PanelWidth, PanelHeight);
|
||||
rt.anchoredPosition = new Vector2(12f, -12f);
|
||||
_rootRt = _root.GetComponent<RectTransform>();
|
||||
_rootRt.anchorMin = new Vector2(0f, 1f);
|
||||
_rootRt.anchorMax = new Vector2(0f, 1f);
|
||||
_rootRt.pivot = new Vector2(0f, 1f);
|
||||
_rootRt.sizeDelta = new Vector2(PanelWidth, HeaderH + PadTop * 2f);
|
||||
_rootRt.anchoredPosition = new Vector2(12f, -12f);
|
||||
|
||||
Image bg = _root.GetComponent<Image>();
|
||||
bg.color = new Color(0.05f, 0.06f, 0.08f, 0.72f);
|
||||
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;
|
||||
|
||||
_nameText = HudCanvas.MakeText(_root.transform, "Name", "", 12);
|
||||
PlaceLine(_nameText.GetComponent<RectTransform>(), -6f, 22f);
|
||||
_nameText.fontStyle = FontStyle.Bold;
|
||||
_nameText.color = new Color(0.95f, 0.93f, 0.88f, 1f);
|
||||
_nameText.alignment = TextAnchor.MiddleLeft;
|
||||
_speciesIcon = HudCanvas.MakeIcon(_root.transform, "SpeciesIcon", SpeciesSize);
|
||||
_sexIcon = HudCanvas.MakeIcon(_root.transform, "SexIcon", SexSize);
|
||||
|
||||
_reasonText = HudCanvas.MakeText(_root.transform, "Reason", "", 10);
|
||||
PlaceLine(_reasonText.GetComponent<RectTransform>(), -28f, 18f);
|
||||
_reasonText.color = new Color(0.95f, 0.72f, 0.38f, 1f);
|
||||
_nameText = HudCanvas.MakeText(_root.transform, "Name", "", 11);
|
||||
_nameText.fontStyle = FontStyle.Bold;
|
||||
_nameText.color = NameColor;
|
||||
_nameText.alignment = TextAnchor.MiddleLeft;
|
||||
_nameText.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||||
|
||||
DossierAvatar.EnsureHost(_root.transform, LiveMax);
|
||||
|
||||
_metaCol = new GameObject("MetaCol", typeof(RectTransform));
|
||||
_metaCol.transform.SetParent(_root.transform, false);
|
||||
_levelIcon = HudCanvas.MakeIcon(_metaCol.transform, "LevelIcon", ChipIcon);
|
||||
_levelValue = HudCanvas.MakeText(_metaCol.transform, "LevelValue", "", 10);
|
||||
_levelValue.color = StatValueColor;
|
||||
_levelValue.fontStyle = FontStyle.Bold;
|
||||
_levelValue.alignment = TextAnchor.MiddleLeft;
|
||||
_levelValue.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||||
_levelValue.resizeTextMinSize = 8;
|
||||
_levelValue.resizeTextMaxSize = 10;
|
||||
|
||||
_taskIcon = HudCanvas.MakeIcon(_metaCol.transform, "TaskIcon", ChipIcon);
|
||||
_metaText = HudCanvas.MakeText(_metaCol.transform, "Task", "", 9);
|
||||
_metaText.color = TaskColor;
|
||||
_metaText.alignment = TextAnchor.MiddleLeft;
|
||||
_metaText.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||||
|
||||
_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;
|
||||
|
||||
_metaText = HudCanvas.MakeText(_root.transform, "Meta", "", 9);
|
||||
PlaceLine(_metaText.GetComponent<RectTransform>(), -46f, 18f);
|
||||
_metaText.color = new Color(0.72f, 0.75f, 0.78f, 1f);
|
||||
_metaText.alignment = TextAnchor.MiddleLeft;
|
||||
|
||||
DossierAvatar.SetActive(false);
|
||||
_metaCol.SetActive(false);
|
||||
_traitsRow.SetActive(false);
|
||||
_reasonText.gameObject.SetActive(false);
|
||||
Relayout(false, 0, false, false);
|
||||
_root.SetActive(false);
|
||||
_visible = false;
|
||||
LogService.LogInfo("[IdleSpectator] Dossier HUD ready (top-left card)");
|
||||
LogService.LogInfo("[IdleSpectator] Dossier HUD ready (vanilla strip + avatar frame)");
|
||||
}
|
||||
|
||||
private static void PlaceLine(RectTransform lineRt, float yFromTop, float height)
|
||||
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.sizeDelta = new Vector2(-16f, height);
|
||||
lineRt.anchoredPosition = new Vector2(0f, yFromTop);
|
||||
lineRt.offsetMin = new Vector2(left, -(yFromTop + height));
|
||||
lineRt.offsetMax = new Vector2(-PadX, -yFromTop);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "IdleSpectator",
|
||||
"author": "dazed",
|
||||
"version": "0.9.1",
|
||||
"description": "AFK Idle Spectator (I) + Chronicle History|World (F9). Character death causes, life events.",
|
||||
"version": "0.9.10",
|
||||
"description": "AFK Idle Spectator (I) + Chronicle (F9). Vanilla-strip dossier with unit avatar frame.",
|
||||
"GUID": "com.dazed.idlespectator"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue