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:
DazedAnon 2026-07-14 19:08:00 -05:00
parent 9fdce47810
commit 7b9c0d5c8a
10 changed files with 415 additions and 88 deletions

View file

@ -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

View file

@ -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;

View file

@ -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);

View file

@ -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)

View file

@ -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) + "...";
}
}

View file

@ -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"),

View file

@ -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)");
}

View file

@ -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");
}
}
}

View file

@ -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++)
{

View file

@ -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"
}