From 7b9c0d5c8acd475a7c261d00f95b691396003546 Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Tue, 14 Jul 2026 19:08:00 -0500 Subject: [PATCH] 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. --- .cursor/skills/idle-spectator-e2e/SKILL.md | 2 +- IdleSpectator/AgentHarness.cs | 17 +- IdleSpectator/CameraDirector.cs | 5 - IdleSpectator/Chronicle.cs | 111 ++++++++- IdleSpectator/ChronicleHud.cs | 81 ++++--- IdleSpectator/HarnessScenarios.cs | 11 + IdleSpectator/InterestDirector.cs | 4 +- IdleSpectator/SpectatorMode.cs | 21 +- IdleSpectator/WatchCaption.cs | 247 ++++++++++++++++++--- IdleSpectator/mod.json | 4 +- 10 files changed, 415 insertions(+), 88 deletions(-) diff --git a/.cursor/skills/idle-spectator-e2e/SKILL.md b/.cursor/skills/idle-spectator-e2e/SKILL.md index 5167bb5..829e538 100644 --- a/.cursor/skills/idle-spectator-e2e/SKILL.md +++ b/.cursor/skills/idle-spectator-e2e/SKILL.md @@ -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 diff --git a/IdleSpectator/AgentHarness.cs b/IdleSpectator/AgentHarness.cs index f626f68..699a4bb 100644 --- a/IdleSpectator/AgentHarness.cs +++ b/IdleSpectator/AgentHarness.cs @@ -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; diff --git a/IdleSpectator/CameraDirector.cs b/IdleSpectator/CameraDirector.cs index cadb01b..20bc467 100644 --- a/IdleSpectator/CameraDirector.cs +++ b/IdleSpectator/CameraDirector.cs @@ -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); diff --git a/IdleSpectator/Chronicle.cs b/IdleSpectator/Chronicle.cs index b22785a..1cb2342 100644 --- a/IdleSpectator/Chronicle.cs +++ b/IdleSpectator/Chronicle.cs @@ -23,6 +23,10 @@ public enum ChronicleTab public sealed class ChronicleEntry { public float CreatedAt; + /// WorldBox sim time when the event was recorded (). + public double WorldTime; + /// In-world month + year, e.g. January 1 (no brackets). + public string DateLabel = ""; public ChronicleKind Kind; public long SubjectId; public long OtherId; @@ -33,6 +37,46 @@ public sealed class ChronicleEntry /// Back-compat for harness / jump helpers. public long UnitId => SubjectId; + + /// Plain date-prefixed line for logs / asserts. + public string DisplayLine + { + get + { + if (string.IsNullOrEmpty(DateLabel)) + { + return Line ?? ""; + } + + if (string.IsNullOrEmpty(Line)) + { + return "[" + DateLabel + "]"; + } + + return "[" + DateLabel + "] " + Line; + } + } + + /// HUD line with a muted colored date in brackets (Unity rich text). + public string DisplayLineRich + { + get + { + if (string.IsNullOrEmpty(DateLabel)) + { + return Line ?? ""; + } + + const string color = "#A8B4C8"; + string stamped = "[" + DateLabel + "]"; + if (string.IsNullOrEmpty(Line)) + { + return stamped; + } + + return stamped + " " + Line; + } + } } /// @@ -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); + } + + /// + /// Capture current WorldBox calendar as month + year (e.g. January 1). + /// + 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) diff --git a/IdleSpectator/ChronicleHud.cs b/IdleSpectator/ChronicleHud.cs index ee83e87..32f3aa4 100644 --- a/IdleSpectator/ChronicleHud.cs +++ b/IdleSpectator/ChronicleHud.cs @@ -11,10 +11,10 @@ namespace IdleSpectator; /// 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(); 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(); @@ -378,46 +378,69 @@ public static class ChronicleHud { GameObject go = new GameObject("Empty", typeof(RectTransform), typeof(LayoutElement)); RectTransform rt = go.GetComponent(); - rt.sizeDelta = new Vector2(PanelWidth - 16f, RowHeight); + rt.sizeDelta = new Vector2(PanelWidth - 16f, RowHeightMin); LayoutElement le = go.GetComponent(); - le.minHeight = RowHeight; - le.preferredHeight = RowHeight; + le.minHeight = RowHeightMin; + le.preferredHeight = RowHeightMin; Text label = HudCanvas.MakeText(go.transform, "Text", text, 9); Stretch(label.GetComponent(), 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(); - rt.sizeDelta = new Vector2(PanelWidth - 16f, RowHeight); + rt.sizeDelta = new Vector2(PanelWidth - 16f, RowHeightMin); + LayoutElement le = go.GetComponent(); - le.minHeight = RowHeight; - le.preferredHeight = RowHeight; + le.minHeight = RowHeightMin; + le.preferredHeight = -1f; + + ContentSizeFitter rowFit = go.GetComponent(); + rowFit.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + rowFit.horizontalFit = ContentSizeFitter.FitMode.Unconstrained; Image bg = go.GetComponent(); bg.color = new Color(1f, 1f, 1f, 0.03f); Image kindIcon = HudCanvas.MakeIcon(go.transform, "Kind", KindIconSize); RectTransform kindRt = kindIcon.GetComponent(); - 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(); - 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(); + textFit.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + textFit.horizontalFit = ContentSizeFitter.FitMode.Unconstrained; Button button = go.GetComponent