Enhance Chronicle functionality and HUD layout. Added date tracking to Chronicle entries, improved display formatting, and adjusted HUD dimensions for better visibility. Updated assertions in test scenarios to reflect new features.
This commit is contained in:
parent
9fdce47810
commit
7b9c0d5c8a
10 changed files with 415 additions and 88 deletions
|
|
@ -143,7 +143,7 @@ Use `fast_timing` / `director_run` instead.
|
|||
- `set_setting` (`expect=enabled|show_watch_reasons|show_dossier_caption|chronicle_enabled`)
|
||||
- `discovery_reset`, `discovery_note`, `discovery_mark`, `discovery_drain`
|
||||
- `chronicle_force`, `chronicle_open`, `chronicle_jump`
|
||||
- `assert` expects: `idle`, `health`, `power_bar`, `no_bad`, `tip_matches_unit`, `unit_asset`, `tip_contains`, `enabled`, `show_watch_reasons`, `current_tier`, `would_accept_curiosity`, `focus_same`, `presented`, `pending_discovery`, `pending_interest`, `in_grace`, `dossier_contains`, `caption_contains`, `dossier_traits_ok`, `caption_layout_ok`, `dossier_history_contains`, `chronicle_count`, `chronicle_latest_contains`
|
||||
- `assert` expects: `idle`, `health`, `power_bar`, `no_bad`, `tip_matches_unit`, `unit_asset`, `tip_contains`, `enabled`, `show_watch_reasons`, `current_tier`, `would_accept_curiosity`, `focus_same`, `presented`, `pending_discovery`, `pending_interest`, `in_grace`, `dossier_contains`, `caption_contains`, `dossier_traits_ok`, `caption_layout_ok`, `dossier_history_contains`, `chronicle_count`, `chronicle_latest_contains`, `chronicle_latest_dated`, `focus_arrows`
|
||||
- `snapshot`, `reset_counters`, `scenario`
|
||||
|
||||
## Process safety
|
||||
|
|
|
|||
|
|
@ -1121,18 +1121,29 @@ public static class AgentHarness
|
|||
|
||||
ChronicleEntry latest = Chronicle.Latest;
|
||||
string line = latest != null ? latest.Line : "";
|
||||
string display = latest != null ? latest.DisplayLine : "";
|
||||
pass = latest != null
|
||||
&& !string.IsNullOrEmpty(needle)
|
||||
&& line.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
detail = $"latest='{line}' needle='{needle}'";
|
||||
detail = $"latest='{display}' needle='{needle}' date='{(latest != null ? latest.DateLabel : "")}'";
|
||||
break;
|
||||
}
|
||||
case "chronicle_latest_dated":
|
||||
{
|
||||
ChronicleEntry latest = Chronicle.Latest;
|
||||
pass = latest != null
|
||||
&& !string.IsNullOrEmpty(latest.DateLabel)
|
||||
&& latest.WorldTime > 0;
|
||||
detail =
|
||||
$"date='{(latest != null ? latest.DateLabel : "")}' worldTime={(latest != null ? latest.WorldTime.ToString("0.###") : "0")} line='{(latest != null ? latest.Line : "")}'";
|
||||
break;
|
||||
}
|
||||
case "caption_layout_ok":
|
||||
{
|
||||
pass = WatchCaption.LastLayoutOk
|
||||
&& WatchCaption.LastPanelSize.y > 20f
|
||||
&& WatchCaption.LastPanelSize.y < 140f
|
||||
&& WatchCaption.LastPanelSize.x <= 300f;
|
||||
&& WatchCaption.LastPanelSize.y < 220f
|
||||
&& WatchCaption.LastPanelSize.x <= 440f;
|
||||
detail =
|
||||
$"layoutOk={WatchCaption.LastLayoutOk} size={WatchCaption.LastPanelSize} caption='{(WatchCaption.LastCaptionText ?? "").Replace("\n", " | ")}' hist='{(WatchCaption.LastHistoryPreview ?? "").Replace("\n", " ")}'";
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -35,11 +35,6 @@ public static class CameraDirector
|
|||
string tip = FormatWatchTip(interest);
|
||||
LastWatchLabel = interest.Label ?? "";
|
||||
LastWatchAssetId = interest.AssetId ?? "";
|
||||
if (ModSettings.ShowWatchReasons && !ModSettings.ShowDossierCaption)
|
||||
{
|
||||
WorldTip.showNowTop(tip, pTranslate: false);
|
||||
}
|
||||
|
||||
LogService.LogInfo($"[IdleSpectator] {tip}");
|
||||
WatchCaption.SetFromInterest(interest);
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ public enum ChronicleTab
|
|||
public sealed class ChronicleEntry
|
||||
{
|
||||
public float CreatedAt;
|
||||
/// <summary>WorldBox sim time when the event was recorded (<see cref="MapBox.getCurWorldTime"/>).</summary>
|
||||
public double WorldTime;
|
||||
/// <summary>In-world month + year, e.g. <c>January 1</c> (no brackets).</summary>
|
||||
public string DateLabel = "";
|
||||
public ChronicleKind Kind;
|
||||
public long SubjectId;
|
||||
public long OtherId;
|
||||
|
|
@ -33,6 +37,46 @@ public sealed class ChronicleEntry
|
|||
|
||||
/// <summary>Back-compat for harness / jump helpers.</summary>
|
||||
public long UnitId => SubjectId;
|
||||
|
||||
/// <summary>Plain date-prefixed line for logs / asserts.</summary>
|
||||
public string DisplayLine
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(DateLabel))
|
||||
{
|
||||
return Line ?? "";
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(Line))
|
||||
{
|
||||
return "[" + DateLabel + "]";
|
||||
}
|
||||
|
||||
return "[" + DateLabel + "] " + Line;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>HUD line with a muted colored date in brackets (Unity rich text).</summary>
|
||||
public string DisplayLineRich
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(DateLabel))
|
||||
{
|
||||
return Line ?? "";
|
||||
}
|
||||
|
||||
const string color = "#A8B4C8";
|
||||
string stamped = "<color=" + color + ">[" + DateLabel + "]</color>";
|
||||
if (string.IsNullOrEmpty(Line))
|
||||
{
|
||||
return stamped;
|
||||
}
|
||||
|
||||
return stamped + " " + Line;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -494,13 +538,21 @@ public static class Chronicle
|
|||
}
|
||||
|
||||
Actor unit = MoveCamera._focus_unit;
|
||||
UnitDossier dossier = UnitDossier.FromActor(unit);
|
||||
string kindPrefix = string.IsNullOrEmpty(prefix) ? "Killed" : prefix;
|
||||
AppendHistory(
|
||||
ChronicleKind.Other,
|
||||
unit,
|
||||
null,
|
||||
$"{kindPrefix}: {dossier.Name} ({dossier.SpeciesId}) - life event");
|
||||
// Short tags keep the old "Killed: Name (species) - life event" shape.
|
||||
// Long strings are treated as the full event line (dossier wrap tests).
|
||||
string line;
|
||||
if (kindPrefix.Length <= 24 && kindPrefix.IndexOf(' ') < 0)
|
||||
{
|
||||
UnitDossier dossier = UnitDossier.FromActor(unit);
|
||||
line = $"{kindPrefix}: {dossier.Name} ({dossier.SpeciesId}) - life event";
|
||||
}
|
||||
else
|
||||
{
|
||||
line = kindPrefix;
|
||||
}
|
||||
|
||||
AppendHistory(ChronicleKind.Other, unit, null, line);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -620,9 +672,12 @@ public static class Chronicle
|
|||
return;
|
||||
}
|
||||
|
||||
StampWorldDate(out double worldTime, out string dateLabel);
|
||||
ChronicleEntry entry = new ChronicleEntry
|
||||
{
|
||||
CreatedAt = Time.unscaledTime,
|
||||
WorldTime = worldTime,
|
||||
DateLabel = dateLabel,
|
||||
Kind = kind,
|
||||
SubjectId = subjectId,
|
||||
OtherId = other != null ? other.getID() : 0,
|
||||
|
|
@ -647,7 +702,7 @@ public static class Chronicle
|
|||
}
|
||||
}
|
||||
|
||||
LogService.LogInfo($"[IdleSpectator][CHRONICLE][History:{subjectId}] {line}");
|
||||
LogService.LogInfo($"[IdleSpectator][CHRONICLE][History:{subjectId}] {entry.DisplayLine}");
|
||||
}
|
||||
|
||||
private static void AppendWorld(Actor unit, string line, Vector3 fallbackPosition)
|
||||
|
|
@ -657,9 +712,12 @@ public static class Chronicle
|
|||
return;
|
||||
}
|
||||
|
||||
StampWorldDate(out double worldTime, out string dateLabel);
|
||||
ChronicleEntry entry = new ChronicleEntry
|
||||
{
|
||||
CreatedAt = Time.unscaledTime,
|
||||
WorldTime = worldTime,
|
||||
DateLabel = dateLabel,
|
||||
Kind = ChronicleKind.World,
|
||||
SubjectId = unit != null ? unit.getID() : 0,
|
||||
OtherId = 0,
|
||||
|
|
@ -678,7 +736,44 @@ public static class Chronicle
|
|||
}
|
||||
}
|
||||
|
||||
LogService.LogInfo("[IdleSpectator][CHRONICLE][World] " + line);
|
||||
LogService.LogInfo("[IdleSpectator][CHRONICLE][World] " + entry.DisplayLine);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Capture current WorldBox calendar as month + year (e.g. <c>January 1</c>).
|
||||
/// </summary>
|
||||
private static void StampWorldDate(out double worldTime, out string dateLabel)
|
||||
{
|
||||
worldTime = 0;
|
||||
dateLabel = "";
|
||||
try
|
||||
{
|
||||
if (World.world == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
worldTime = World.world.getCurWorldTime();
|
||||
int[] raw = Date.getRawDate(worldTime);
|
||||
if (raw == null || raw.Length < 3)
|
||||
{
|
||||
dateLabel = "y" + Date.getCurrentYear() + " m" + Date.getCurrentMonth();
|
||||
return;
|
||||
}
|
||||
|
||||
// raw: [day, month, year] - UI uses month + year only
|
||||
string monthName = Date.formatMonth(raw[1]);
|
||||
if (string.IsNullOrEmpty(monthName))
|
||||
{
|
||||
monthName = "m" + raw[1];
|
||||
}
|
||||
|
||||
dateLabel = monthName + " " + raw[2];
|
||||
}
|
||||
catch
|
||||
{
|
||||
dateLabel = "";
|
||||
}
|
||||
}
|
||||
|
||||
private static string PairKey(long a, long b)
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ namespace IdleSpectator;
|
|||
/// </summary>
|
||||
public static class ChronicleHud
|
||||
{
|
||||
private const float PanelWidth = 260f;
|
||||
private const float PanelHeight = 186f;
|
||||
private const int MaxVisibleRows = 5;
|
||||
private const float RowHeight = 24f;
|
||||
private const float PanelWidth = 380f;
|
||||
private const float PanelHeight = 280f;
|
||||
private const int MaxVisibleRows = 24;
|
||||
private const float RowHeightMin = 28f;
|
||||
private const float KindIconSize = 14f;
|
||||
private const float TitleIconSize = 16f;
|
||||
|
||||
|
|
@ -129,11 +129,11 @@ public static class ChronicleHud
|
|||
|
||||
VerticalLayoutGroup layout = contentGo.GetComponent<VerticalLayoutGroup>();
|
||||
layout.childAlignment = TextAnchor.UpperCenter;
|
||||
layout.childControlHeight = false;
|
||||
layout.childControlHeight = true;
|
||||
layout.childControlWidth = true;
|
||||
layout.childForceExpandHeight = false;
|
||||
layout.childForceExpandWidth = true;
|
||||
layout.spacing = 2f;
|
||||
layout.spacing = 3f;
|
||||
layout.padding = new RectOffset(0, 0, 0, 0);
|
||||
|
||||
ContentSizeFitter fitter = contentGo.GetComponent<ContentSizeFitter>();
|
||||
|
|
@ -378,46 +378,69 @@ public static class ChronicleHud
|
|||
{
|
||||
GameObject go = new GameObject("Empty", typeof(RectTransform), typeof(LayoutElement));
|
||||
RectTransform rt = go.GetComponent<RectTransform>();
|
||||
rt.sizeDelta = new Vector2(PanelWidth - 16f, RowHeight);
|
||||
rt.sizeDelta = new Vector2(PanelWidth - 16f, RowHeightMin);
|
||||
LayoutElement le = go.GetComponent<LayoutElement>();
|
||||
le.minHeight = RowHeight;
|
||||
le.preferredHeight = RowHeight;
|
||||
le.minHeight = RowHeightMin;
|
||||
le.preferredHeight = RowHeightMin;
|
||||
|
||||
Text label = HudCanvas.MakeText(go.transform, "Text", text, 9);
|
||||
Stretch(label.GetComponent<RectTransform>(), 4f);
|
||||
label.alignment = TextAnchor.MiddleLeft;
|
||||
label.color = new Color(0.65f, 0.68f, 0.72f, 0.95f);
|
||||
label.resizeTextForBestFit = false;
|
||||
label.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
label.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
return go;
|
||||
}
|
||||
|
||||
private static GameObject BuildClickableRow(ChronicleEntry entry, int index)
|
||||
{
|
||||
GameObject go = new GameObject($"Row_{index}", typeof(RectTransform), typeof(Image), typeof(Button), typeof(LayoutElement));
|
||||
GameObject go = new GameObject(
|
||||
$"Row_{index}",
|
||||
typeof(RectTransform),
|
||||
typeof(Image),
|
||||
typeof(Button),
|
||||
typeof(LayoutElement),
|
||||
typeof(ContentSizeFitter));
|
||||
RectTransform rt = go.GetComponent<RectTransform>();
|
||||
rt.sizeDelta = new Vector2(PanelWidth - 16f, RowHeight);
|
||||
rt.sizeDelta = new Vector2(PanelWidth - 16f, RowHeightMin);
|
||||
|
||||
LayoutElement le = go.GetComponent<LayoutElement>();
|
||||
le.minHeight = RowHeight;
|
||||
le.preferredHeight = RowHeight;
|
||||
le.minHeight = RowHeightMin;
|
||||
le.preferredHeight = -1f;
|
||||
|
||||
ContentSizeFitter rowFit = go.GetComponent<ContentSizeFitter>();
|
||||
rowFit.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
rowFit.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
|
||||
|
||||
Image bg = go.GetComponent<Image>();
|
||||
bg.color = new Color(1f, 1f, 1f, 0.03f);
|
||||
|
||||
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);
|
||||
kindRt.anchorMin = new Vector2(0f, 1f);
|
||||
kindRt.anchorMax = new Vector2(0f, 1f);
|
||||
kindRt.pivot = new Vector2(0.5f, 1f);
|
||||
kindRt.anchoredPosition = new Vector2(10f, -7f);
|
||||
HudIcons.Apply(kindIcon, HudIcons.ForChronicleKind(entry.Kind));
|
||||
|
||||
Text label = HudCanvas.MakeText(go.transform, "Text", Truncate(entry.Line, 40), 9);
|
||||
Text label = HudCanvas.MakeText(go.transform, "Text", entry.DisplayLineRich, 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;
|
||||
labelRt.anchorMin = new Vector2(0f, 0f);
|
||||
labelRt.anchorMax = new Vector2(1f, 1f);
|
||||
labelRt.offsetMin = new Vector2(22f, 4f);
|
||||
labelRt.offsetMax = new Vector2(-6f, -4f);
|
||||
label.alignment = TextAnchor.UpperLeft;
|
||||
label.color = ColorForKind(entry.Kind);
|
||||
label.supportRichText = true;
|
||||
label.resizeTextForBestFit = false;
|
||||
label.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
label.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
|
||||
// Drive row height from wrapped text preferred height.
|
||||
ContentSizeFitter textFit = label.gameObject.AddComponent<ContentSizeFitter>();
|
||||
textFit.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
textFit.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
|
||||
|
||||
Button button = go.GetComponent<Button>();
|
||||
ColorBlock colors = button.colors;
|
||||
|
|
@ -430,7 +453,7 @@ public static class ChronicleHud
|
|||
button.onClick.AddListener(new UnityAction(() =>
|
||||
{
|
||||
bool ok = Chronicle.JumpTo(captured);
|
||||
LogService.LogInfo($"[IdleSpectator][CHRONICLE] jump ok={ok} line={captured.Line}");
|
||||
LogService.LogInfo($"[IdleSpectator][CHRONICLE] jump ok={ok} line={captured.DisplayLine}");
|
||||
}));
|
||||
|
||||
return go;
|
||||
|
|
@ -462,14 +485,4 @@ public static class ChronicleHud
|
|||
rt.offsetMin = new Vector2(pad, 1f);
|
||||
rt.offsetMax = new Vector2(-pad, -1f);
|
||||
}
|
||||
|
||||
private static string Truncate(string s, int max)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s) || s.Length <= max)
|
||||
{
|
||||
return s ?? "";
|
||||
}
|
||||
|
||||
return s.Substring(0, max - 3) + "...";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ internal static class HarnessScenarios
|
|||
Step("c50", "simulate_input"),
|
||||
Step("c51", "wait", wait: 0.25f),
|
||||
Step("c52", "assert", expect: "idle", value: "false"),
|
||||
Step("c52b", "assert", expect: "caption_contains", value: "Paused"),
|
||||
Step("c53", "assert", expect: "no_bad"),
|
||||
|
||||
Step("c99", "snapshot"),
|
||||
|
|
@ -416,10 +417,20 @@ internal static class HarnessScenarios
|
|||
Step("ch13", "chronicle_force", label: "Killed"),
|
||||
Step("ch14", "assert", expect: "chronicle_count", value: "1", label: "min"),
|
||||
Step("ch15", "assert", expect: "chronicle_latest_contains", value: "Killed"),
|
||||
Step("ch15a2", "assert", expect: "chronicle_latest_dated"),
|
||||
Step("ch15b", "assert", expect: "chronicle_latest_contains", value: "auto"),
|
||||
Step("ch15c", "wait", wait: 0.2f),
|
||||
Step("ch15d", "assert", expect: "dossier_history_contains", value: "Killed"),
|
||||
|
||||
// Long event wraps in the narrow dossier history column (no wider panel).
|
||||
Step("ch15e", "chronicle_force",
|
||||
label: "Became legendary after surviving the great crab migration across three kingdoms, returning home with a stolen crown, and telling the tale for forty winters"),
|
||||
Step("ch15f", "wait", wait: 0.35f),
|
||||
Step("ch15g", "assert", expect: "dossier_history_contains", value: "crab migration"),
|
||||
Step("ch15h", "assert", expect: "caption_layout_ok"),
|
||||
Step("ch15i", "screenshot", value: "hud-dossier-long-hist.png"),
|
||||
Step("ch15j", "wait", wait: 0.65f),
|
||||
|
||||
// Death-cause wording samples (victim POV)
|
||||
Step("ch16a", "chronicle_death_sample", value: "Age"),
|
||||
Step("ch16b", "assert", expect: "chronicle_latest_contains", value: "old age"),
|
||||
|
|
|
|||
|
|
@ -241,8 +241,8 @@ public static class InterestDirector
|
|||
|
||||
private static void ExitFromManualInput()
|
||||
{
|
||||
SpectatorMode.SetActive(false);
|
||||
WorldTip.showNowTop("Idle Spectator paused (manual input)", pTranslate: false);
|
||||
SpectatorMode.SetActive(false, quiet: true);
|
||||
WatchCaption.ShowStatusBanner("Paused (manual input)");
|
||||
LogService.LogInfo("[IdleSpectator] Spectator paused (manual input)");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,11 +19,10 @@ public static class SpectatorMode
|
|||
SetActive(!Active);
|
||||
}
|
||||
|
||||
public static void SetActive(bool active)
|
||||
public static void SetActive(bool active, bool quiet = false)
|
||||
{
|
||||
if (active && !ModSettings.Enabled)
|
||||
{
|
||||
WorldTip.showNowTop("Idle Spectator disabled in mod settings", pTranslate: false);
|
||||
LogService.LogInfo("[IdleSpectator] Enable blocked: disabled in settings");
|
||||
return;
|
||||
}
|
||||
|
|
@ -40,8 +39,7 @@ public static class SpectatorMode
|
|||
SpeciesDiscovery.OnSpectatorEnabled();
|
||||
Chronicle.OnSpectatorEnabled();
|
||||
FocusRelationshipArrows.OnSpectatorEnabled();
|
||||
WorldTip.showNowTop("Idle Spectator ON (I or any input to stop; F9 chronicle)", pTranslate: false);
|
||||
LogService.LogInfo("[IdleSpectator] Spectator mode enabled");
|
||||
LogService.LogInfo("[IdleSpectator] Spectator mode enabled (I or any input to stop; F9 chronicle)");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -49,10 +47,17 @@ public static class SpectatorMode
|
|||
SpeciesDiscovery.OnSpectatorDisabled();
|
||||
FocusRelationshipArrows.OnSpectatorDisabled();
|
||||
CameraDirector.ClearFollow();
|
||||
WatchCaption.Clear();
|
||||
StateProbe.Reset();
|
||||
WorldTip.showNowTop("Idle Spectator OFF", pTranslate: false);
|
||||
LogService.LogInfo("[IdleSpectator] Spectator mode disabled");
|
||||
if (quiet)
|
||||
{
|
||||
// Manual-input pause: leave dossier up so ShowStatusBanner can reuse it.
|
||||
StateProbe.Reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
WatchCaption.Clear();
|
||||
StateProbe.Reset();
|
||||
LogService.LogInfo("[IdleSpectator] Spectator mode disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,23 +10,29 @@ namespace IdleSpectator;
|
|||
/// </summary>
|
||||
public static class WatchCaption
|
||||
{
|
||||
private const float PanelWidthMax = 340f;
|
||||
private const float PanelWidthMax = 420f;
|
||||
private const float SpeciesSize = 16f;
|
||||
private const float SexSize = 14f;
|
||||
private const float LiveMax = 44f;
|
||||
private const float ChipIcon = 12f;
|
||||
private const float TraitIcon = 12f;
|
||||
private const float HistoryIcon = 10f;
|
||||
private const float HistoryColW = 118f;
|
||||
private const float HistoryColMinW = 118f;
|
||||
private const float PadX = 4f;
|
||||
private const float PadTop = 4f;
|
||||
private const float HeaderH = 18f;
|
||||
private const float BodyH = 44f;
|
||||
private const float TraitsH = 14f;
|
||||
private const float ReasonH = 12f;
|
||||
private const float HistoryLineH = 13f;
|
||||
private const float ReasonH = 14f;
|
||||
private const float HistoryLineMinH = 13f;
|
||||
private const float HistoryLineMaxH = 72f;
|
||||
private const float Gap = 2f;
|
||||
private const int HistoryMax = 3;
|
||||
private const float StatusBannerSeconds = 5f;
|
||||
|
||||
/// <summary>History column grows with content until the dossier hits <see cref="PanelWidthMax"/>.</summary>
|
||||
private static float HistoryColMaxW =>
|
||||
Mathf.Max(HistoryColMinW, PanelWidthMax - PadX * 2f - LiveMax - 4f);
|
||||
|
||||
private static float _panelWidth = LiveMax + PadX * 2f;
|
||||
|
||||
|
|
@ -36,6 +42,7 @@ public static class WatchCaption
|
|||
private static readonly Color ReasonColor = new Color(0.95f, 0.72f, 0.38f, 1f);
|
||||
private static readonly Color TraitNameColor = new Color(0.78f, 0.8f, 0.84f, 1f);
|
||||
private static readonly Color HistoryTextColor = new Color(0.82f, 0.84f, 0.86f, 1f);
|
||||
private static readonly Color StatusBannerColor = new Color(1f, 0.55f, 0.18f, 1f);
|
||||
|
||||
private static GameObject _root;
|
||||
private static RectTransform _rootRt;
|
||||
|
|
@ -54,12 +61,16 @@ public static class WatchCaption
|
|||
|
||||
private static GameObject _historyCol;
|
||||
private static readonly HistorySlot[] _historySlots = new HistorySlot[HistoryMax];
|
||||
private static readonly float[] _historySlotHeights = new float[HistoryMax];
|
||||
private static float _historyColW = HistoryColMinW;
|
||||
private static int _lastHistoryCount = -1;
|
||||
private static long _lastHistorySubjectId;
|
||||
|
||||
private static UnitDossier _current;
|
||||
private static Actor _boundActor;
|
||||
private static bool _visible;
|
||||
private static string _statusBanner = "";
|
||||
private static float _statusBannerUntil;
|
||||
|
||||
private sealed class TraitSlot
|
||||
{
|
||||
|
|
@ -135,10 +146,54 @@ public static class WatchCaption
|
|||
LastLayoutOk = false;
|
||||
_lastHistoryCount = -1;
|
||||
_lastHistorySubjectId = 0;
|
||||
_historyColW = HistoryColMinW;
|
||||
_statusBanner = "";
|
||||
_statusBannerUntil = 0f;
|
||||
ApplyVisual(null, null);
|
||||
SetVisible(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show a short status line on the dossier (replaces the top WorldTip for pause exits).
|
||||
/// </summary>
|
||||
public static void ShowStatusBanner(string message, float seconds = StatusBannerSeconds)
|
||||
{
|
||||
if (!ModSettings.ShowDossierCaption || string.IsNullOrEmpty(message))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureBuilt();
|
||||
_statusBanner = message;
|
||||
_statusBannerUntil = Time.unscaledTime + Mathf.Max(0.5f, seconds);
|
||||
|
||||
if (_nameText != null && (string.IsNullOrEmpty(_nameText.text) || _current == null))
|
||||
{
|
||||
_nameText.text = "Idle Spectator";
|
||||
LastHeadline = "Idle Spectator";
|
||||
}
|
||||
|
||||
if (_reasonText != null)
|
||||
{
|
||||
_reasonText.text = message;
|
||||
_reasonText.color = StatusBannerColor;
|
||||
_reasonText.supportRichText = false;
|
||||
_reasonText.resizeTextForBestFit = false;
|
||||
_reasonText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
_reasonText.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
_reasonText.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
LastCaptionText = (LastHeadline ?? "Idle Spectator") + " | " + message;
|
||||
int hist = CountActiveHistorySlots();
|
||||
int traits = CountActiveTraitSlots();
|
||||
bool hasTask = _taskText != null && _taskText.gameObject.activeSelf;
|
||||
bool hasBody = _boundActor != null || hist > 0 || _current != null;
|
||||
Relayout(hasBody, traits, hasTask, hasReason: true, hist);
|
||||
SetVisible(true);
|
||||
LogService.LogInfo("[IdleSpectator][CAPTION] status=" + message);
|
||||
}
|
||||
|
||||
public static void SetFromInterest(InterestEvent interest)
|
||||
{
|
||||
Actor unit = interest != null && interest.HasFollowUnit
|
||||
|
|
@ -174,6 +229,7 @@ public static class WatchCaption
|
|||
|
||||
public static void SetFromActor(Actor actor, InterestTier? tier = null, string label = null)
|
||||
{
|
||||
ClearStatusBanner();
|
||||
UnitDossier dossier = UnitDossier.FromActor(actor, tier, label);
|
||||
_current = dossier;
|
||||
_boundActor = actor;
|
||||
|
|
@ -188,6 +244,22 @@ public static class WatchCaption
|
|||
|
||||
public static void Update()
|
||||
{
|
||||
if (HasStatusBanner())
|
||||
{
|
||||
if (!_visible && ModSettings.ShowDossierCaption)
|
||||
{
|
||||
SetVisible(true);
|
||||
}
|
||||
|
||||
if (!SpectatorMode.Active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Idle resumed - drop banner and restore normal reason styling next ApplyVisual.
|
||||
ClearStatusBanner();
|
||||
}
|
||||
|
||||
if (!SpectatorMode.Active || !ModSettings.ShowDossierCaption)
|
||||
{
|
||||
if (_visible)
|
||||
|
|
@ -219,6 +291,49 @@ public static class WatchCaption
|
|||
RefreshHistoryIfChanged();
|
||||
}
|
||||
|
||||
private static bool HasStatusBanner()
|
||||
{
|
||||
return !string.IsNullOrEmpty(_statusBanner) && Time.unscaledTime < _statusBannerUntil;
|
||||
}
|
||||
|
||||
private static void ClearStatusBanner()
|
||||
{
|
||||
_statusBanner = "";
|
||||
_statusBannerUntil = 0f;
|
||||
if (_reasonText != null)
|
||||
{
|
||||
_reasonText.color = ReasonColor;
|
||||
}
|
||||
}
|
||||
|
||||
private static int CountActiveHistorySlots()
|
||||
{
|
||||
int n = 0;
|
||||
for (int i = 0; i < _historySlots.Length; i++)
|
||||
{
|
||||
if (_historySlots[i]?.Root != null && _historySlots[i].Root.activeSelf)
|
||||
{
|
||||
n++;
|
||||
}
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
private static int CountActiveTraitSlots()
|
||||
{
|
||||
int n = 0;
|
||||
for (int i = 0; i < _traitSlots.Length; i++)
|
||||
{
|
||||
if (_traitSlots[i]?.Root != null && _traitSlots[i].Root.activeSelf)
|
||||
{
|
||||
n++;
|
||||
}
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
private static void RefreshLivePortrait()
|
||||
{
|
||||
if (!_visible)
|
||||
|
|
@ -471,6 +586,12 @@ public static class WatchCaption
|
|||
|
||||
int shown = 0;
|
||||
LastHistoryPreview = "";
|
||||
_historyColW = HistoryColMinW;
|
||||
for (int i = 0; i < _historySlotHeights.Length; i++)
|
||||
{
|
||||
_historySlotHeights[i] = HistoryLineMinH;
|
||||
}
|
||||
|
||||
if (_historyCol == null)
|
||||
{
|
||||
return 0;
|
||||
|
|
@ -483,6 +604,42 @@ public static class WatchCaption
|
|||
}
|
||||
|
||||
_historyCol.SetActive(true);
|
||||
|
||||
// Pass 1: grow the history column toward content width (capped by dossier max).
|
||||
float widestLabel = 0f;
|
||||
for (int i = 0; i < _historySlots.Length; i++)
|
||||
{
|
||||
HistorySlot slot = _historySlots[i];
|
||||
if (slot?.Label == null || i >= entries.Count || entries[i] == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Text label = slot.Label;
|
||||
label.supportRichText = true;
|
||||
label.resizeTextForBestFit = false;
|
||||
label.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||||
label.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
label.alignment = TextAnchor.UpperLeft;
|
||||
label.text = entries[i].DisplayLineRich ?? "";
|
||||
Canvas.ForceUpdateCanvases();
|
||||
try
|
||||
{
|
||||
widestLabel = Mathf.Max(widestLabel, label.preferredWidth);
|
||||
}
|
||||
catch
|
||||
{
|
||||
widestLabel = Mathf.Max(widestLabel, (entries[i].DisplayLine ?? "").Length * 6.2f);
|
||||
}
|
||||
}
|
||||
|
||||
_historyColW = Mathf.Clamp(
|
||||
widestLabel + HistoryIcon + 4f,
|
||||
HistoryColMinW,
|
||||
HistoryColMaxW);
|
||||
float labelW = Mathf.Max(24f, _historyColW - HistoryIcon - 2f);
|
||||
|
||||
// Pass 2: wrap at the chosen column width and size each row height.
|
||||
for (int i = 0; i < _historySlots.Length; i++)
|
||||
{
|
||||
HistorySlot slot = _historySlots[i];
|
||||
|
|
@ -496,20 +653,33 @@ public static class WatchCaption
|
|||
ChronicleEntry e = entries[i];
|
||||
slot.Root.SetActive(true);
|
||||
HudIcons.Apply(slot.Icon, HudIcons.ForChronicleKind(e.Kind));
|
||||
string line = e.Line ?? "";
|
||||
if (line.Length > 28)
|
||||
{
|
||||
line = line.Substring(0, 27) + "...";
|
||||
}
|
||||
|
||||
if (slot.Label != null)
|
||||
{
|
||||
slot.Label.text = line;
|
||||
slot.Label.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
slot.Label.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
slot.Label.text = e.DisplayLineRich ?? "";
|
||||
RectTransform lrt = slot.Label.GetComponent<RectTransform>();
|
||||
lrt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, labelW);
|
||||
Canvas.ForceUpdateCanvases();
|
||||
float needed = HistoryLineMinH;
|
||||
try
|
||||
{
|
||||
needed = Mathf.Max(HistoryLineMinH, slot.Label.preferredHeight + 4f);
|
||||
}
|
||||
catch
|
||||
{
|
||||
int chars = (e.DisplayLine ?? "").Length;
|
||||
int perLine = Mathf.Max(12, (int)(labelW / 6.2f));
|
||||
int lines = Mathf.Max(1, (chars + perLine - 1) / perLine);
|
||||
needed = HistoryLineMinH * lines;
|
||||
}
|
||||
|
||||
_historySlotHeights[i] = Mathf.Clamp(needed, HistoryLineMinH, HistoryLineMaxH);
|
||||
}
|
||||
|
||||
if (shown == 0)
|
||||
{
|
||||
LastHistoryPreview = e.Line ?? "";
|
||||
LastHistoryPreview = e.DisplayLine ?? "";
|
||||
}
|
||||
|
||||
shown++;
|
||||
|
|
@ -517,12 +687,36 @@ public static class WatchCaption
|
|||
else
|
||||
{
|
||||
slot.Root.SetActive(false);
|
||||
_historySlotHeights[i] = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
return shown;
|
||||
}
|
||||
|
||||
private static float MeasureHistoryColumnHeight(int historyCount)
|
||||
{
|
||||
if (historyCount <= 0)
|
||||
{
|
||||
return BodyH;
|
||||
}
|
||||
|
||||
float total = 0f;
|
||||
int used = 0;
|
||||
for (int i = 0; i < _historySlotHeights.Length && used < historyCount; i++)
|
||||
{
|
||||
if (_historySlotHeights[i] <= 0.01f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
total += _historySlotHeights[i];
|
||||
used++;
|
||||
}
|
||||
|
||||
return Mathf.Max(BodyH, total);
|
||||
}
|
||||
|
||||
private static void Relayout(bool hasBody, int traitCount, bool hasTask, bool hasReason, int historyCount)
|
||||
{
|
||||
float y = PadTop;
|
||||
|
|
@ -535,11 +729,12 @@ public static class WatchCaption
|
|||
|
||||
if (hasBody)
|
||||
{
|
||||
PlaceBody(y, historyCount);
|
||||
float bodyH = historyCount > 0 ? MeasureHistoryColumnHeight(historyCount) : BodyH;
|
||||
PlaceBody(y, historyCount, bodyH);
|
||||
if (historyCount > 0)
|
||||
{
|
||||
bodyW = LiveMax + 4f + HistoryColW;
|
||||
y += BodyH + Gap;
|
||||
bodyW = LiveMax + 4f + _historyColW;
|
||||
y += bodyH + Gap;
|
||||
}
|
||||
else if (traitsBeside)
|
||||
{
|
||||
|
|
@ -690,7 +885,7 @@ public static class WatchCaption
|
|||
return fallback;
|
||||
}
|
||||
|
||||
private static void PlaceBody(float yFromTop, int historyCount)
|
||||
private static void PlaceBody(float yFromTop, int historyCount, float bodyH)
|
||||
{
|
||||
DossierAvatar.Place(PadX, yFromTop, LiveMax);
|
||||
|
||||
|
|
@ -711,8 +906,9 @@ public static class WatchCaption
|
|||
col.anchorMax = new Vector2(0f, 1f);
|
||||
col.pivot = new Vector2(0f, 1f);
|
||||
col.anchoredPosition = new Vector2(PadX + LiveMax + 4f, -yFromTop);
|
||||
col.sizeDelta = new Vector2(HistoryColW, BodyH);
|
||||
col.sizeDelta = new Vector2(_historyColW, bodyH);
|
||||
|
||||
float top = 0f;
|
||||
for (int i = 0; i < _historySlots.Length; i++)
|
||||
{
|
||||
HistorySlot slot = _historySlots[i];
|
||||
|
|
@ -721,21 +917,22 @@ public static class WatchCaption
|
|||
continue;
|
||||
}
|
||||
|
||||
float lineH = _historySlotHeights[i] > 0.01f ? _historySlotHeights[i] : HistoryLineMinH;
|
||||
RectTransform rt = slot.Root.GetComponent<RectTransform>();
|
||||
rt.anchorMin = new Vector2(0f, 1f);
|
||||
rt.anchorMax = new Vector2(1f, 1f);
|
||||
rt.pivot = new Vector2(0.5f, 1f);
|
||||
float top = i * HistoryLineH;
|
||||
rt.offsetMin = new Vector2(0f, -(top + HistoryLineH));
|
||||
rt.offsetMin = new Vector2(0f, -(top + lineH));
|
||||
rt.offsetMax = new Vector2(0f, -top);
|
||||
top += lineH;
|
||||
|
||||
if (slot.Icon != null)
|
||||
{
|
||||
RectTransform irt = slot.Icon.GetComponent<RectTransform>();
|
||||
irt.anchorMin = new Vector2(0f, 0.5f);
|
||||
irt.anchorMax = new Vector2(0f, 0.5f);
|
||||
irt.pivot = new Vector2(0f, 0.5f);
|
||||
irt.anchoredPosition = Vector2.zero;
|
||||
irt.anchorMin = new Vector2(0f, 1f);
|
||||
irt.anchorMax = new Vector2(0f, 1f);
|
||||
irt.pivot = new Vector2(0f, 1f);
|
||||
irt.anchoredPosition = new Vector2(0f, -1f);
|
||||
irt.sizeDelta = new Vector2(HistoryIcon, HistoryIcon);
|
||||
}
|
||||
|
||||
|
|
@ -745,7 +942,7 @@ public static class WatchCaption
|
|||
lrt.anchorMin = new Vector2(0f, 0f);
|
||||
lrt.anchorMax = new Vector2(1f, 1f);
|
||||
lrt.offsetMin = new Vector2(HistoryIcon + 2f, 0f);
|
||||
lrt.offsetMax = Vector2.zero;
|
||||
lrt.offsetMax = new Vector2(0f, -1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -817,7 +1014,7 @@ public static class WatchCaption
|
|||
row.anchoredPosition = new Vector2(PadX + LiveMax + 4f, -yFromTop);
|
||||
row.sizeDelta = new Vector2(colW, BodyH);
|
||||
|
||||
float line = Mathf.Min(HistoryLineH, BodyH / Mathf.Max(1, traitCount));
|
||||
float line = Mathf.Min(HistoryLineMinH, BodyH / Mathf.Max(1, traitCount));
|
||||
int shown = 0;
|
||||
for (int i = 0; i < _traitSlots.Length; i++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "IdleSpectator",
|
||||
"author": "dazed",
|
||||
"version": "0.9.22",
|
||||
"description": "AFK Idle Spectator (I) + Chronicle (F9). Focus arrows respect vanilla toggles.",
|
||||
"version": "0.9.28",
|
||||
"description": "AFK Idle Spectator (I) + Chronicle (F9). Dossier grows wide before history wraps.",
|
||||
"GUID": "com.dazed.idlespectator"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue