diff --git a/.cursor/skills/idle-spectator-e2e/reference.md b/.cursor/skills/idle-spectator-e2e/reference.md index ee38d27..9ab09e5 100644 --- a/.cursor/skills/idle-spectator-e2e/reference.md +++ b/.cursor/skills/idle-spectator-e2e/reference.md @@ -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/.png` via Unity ScreenCapture ## Extending scenarios diff --git a/IdleSpectator/AgentHarness.cs b/IdleSpectator/AgentHarness.cs index a0b29ea..b6c328c 100644 --- a/IdleSpectator/AgentHarness.cs +++ b/IdleSpectator/AgentHarness.cs @@ -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)) diff --git a/IdleSpectator/Chronicle.cs b/IdleSpectator/Chronicle.cs index ebc81a2..c0011b8 100644 --- a/IdleSpectator/Chronicle.cs +++ b/IdleSpectator/Chronicle.cs @@ -52,6 +52,18 @@ public static class Chronicle private static readonly HashSet DeathLogged = new HashSet(); 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() diff --git a/IdleSpectator/ChronicleHud.cs b/IdleSpectator/ChronicleHud.cs index 8a711c8..ee83e87 100644 --- a/IdleSpectator/ChronicleHud.cs +++ b/IdleSpectator/ChronicleHud.cs @@ -7,18 +7,21 @@ using UnityEngine.UI; namespace IdleSpectator; /// -/// Compact bottom-right chronicle card with History | World tabs. +/// Compact bottom-right chronicle card with History | World tabs and vanilla icons. /// 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(); - 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(); 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(); @@ -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(); + 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(); 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(); - scrollBg.color = new Color(0f, 0f, 0f, 0.05f); + scrollBg.color = new Color(0f, 0f, 0f, 0.08f); Mask mask = scrollGo.GetComponent(); 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(); @@ -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(); bg.color = new Color(0.15f, 0.16f, 0.2f, 0.9f); + Image icon = HudCanvas.MakeIcon(go.transform, "Icon", 12f); + RectTransform iconRt = icon.GetComponent(); + 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(); 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