From 94de8a3b5bc733c852aa17d9944e56f4dbb98f71 Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Tue, 14 Jul 2026 22:36:06 -0500 Subject: [PATCH] Implement favorites filtering for Fallen characters in Chronicle and HUD. Added functionality to track and manage favorite subjects across death events, enhancing user experience. Updated test scenarios to validate new favorite handling and interactions. --- IdleSpectator/AgentHarness.cs | 79 +++++++++++++---- IdleSpectator/Chronicle.cs | 140 +++++++++++++++++++++++++++--- IdleSpectator/ChronicleHud.cs | 17 ++-- IdleSpectator/HarnessScenarios.cs | 15 ++++ IdleSpectator/WatchCaption.cs | 65 +++++++++++--- IdleSpectator/mod.json | 4 +- 6 files changed, 266 insertions(+), 54 deletions(-) diff --git a/IdleSpectator/AgentHarness.cs b/IdleSpectator/AgentHarness.cs index 3daf8ad..8a47769 100644 --- a/IdleSpectator/AgentHarness.cs +++ b/IdleSpectator/AgentHarness.cs @@ -453,12 +453,20 @@ public static class AgentHarness case "lore_favorites": { Chronicle.ShowHud(); - ChronicleHud.SetTab(ChronicleHud.LoreTab.Living); string v = (cmd.value ?? "true").Trim().ToLowerInvariant(); bool on = v != "false" && v != "0" && v != "off"; ChronicleHud.SetFavoritesOnly(on); + // Filter is a list view - leave detail if we were in one. + if (ChronicleHud.DetailUnitId != 0) + { + ChronicleHud.ClearUnitDetail(); + } + _cmdOk++; - Emit(cmd, ok: true, detail: $"favoritesOnly={ChronicleHud.FavoritesOnly}"); + Emit( + cmd, + ok: true, + detail: $"favoritesOnly={ChronicleHud.FavoritesOnly} tab={ChronicleHud.ActiveTab}"); break; } @@ -549,34 +557,44 @@ public static class AgentHarness long killerId = 0; Vector3 deathPos = Vector3.zero; string mode = (cmd.tier ?? "").Trim().ToLowerInvariant(); + bool favorite = mode.IndexOf("fav", System.StringComparison.Ordinal) >= 0; Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; - if (mode == "killer" && focus != null && focus.isAlive()) + if (mode.IndexOf("killer", System.StringComparison.Ordinal) >= 0 + && focus != null + && focus.isAlive()) { killerId = focus.getID(); deathPos = focus.current_position; } - else if (mode == "place") + else if (mode.IndexOf("place", System.StringComparison.Ordinal) >= 0 + || favorite + || string.IsNullOrEmpty(mode)) { - if (focus != null) + if (mode.IndexOf("place", System.StringComparison.Ordinal) >= 0 || favorite) { - deathPos = focus.current_position; - } - else if (MoveCamera.instance != null) - { - deathPos = MoveCamera.instance.transform.position; - deathPos.z = 0f; - } + if (focus != null) + { + deathPos = focus.current_position; + } + else if (MoveCamera.instance != null) + { + deathPos = MoveCamera.instance.transform.position; + deathPos.z = 0f; + } - if (deathPos == Vector3.zero) - { - deathPos = new Vector3(48f, 48f, 0f); + if (deathPos == Vector3.zero) + { + deathPos = new Vector3(48f, 48f, 0f); + } } } long id = Chronicle.ForceOrphanHistory( - name, species, n, prefix, attackType, killerId, deathPos); + name, species, n, prefix, attackType, killerId, deathPos, favorite); DeathManner manner = Chronicle.LatestDeathMannerFor(id); - bool ok = id != 0 && Chronicle.HistoryCountFor(id) >= n; + bool ok = id != 0 + && Chronicle.HistoryCountFor(id) >= n + && (!favorite || Chronicle.IsFavoriteSubject(id)); if (ok) { _cmdOk++; @@ -590,7 +608,7 @@ public static class AgentHarness cmd, ok, detail: - $"orphanId={id} hist={Chronicle.HistoryCountFor(id)} want={n} manner={manner} killer={killerId} mode={mode}"); + $"orphanId={id} hist={Chronicle.HistoryCountFor(id)} want={n} manner={manner} killer={killerId} mode={mode} fav={Chronicle.IsFavoriteSubject(id)}"); break; } @@ -1742,13 +1760,36 @@ public static class AgentHarness bool fav = false; try { - fav = unit != null && unit.isFavorite(); + fav = unit != null && unit.isAlive() && unit.isFavorite(); } catch { // ignore } + if (!fav) + { + long id = WatchCaption.CurrentUnitId; + if (id == 0) + { + id = ChronicleHud.DetailUnitId; + } + + if (id == 0 && unit != null) + { + try + { + id = unit.getID(); + } + catch + { + id = 0; + } + } + + fav = Chronicle.IsFavoriteSubject(id); + } + string wantRaw = (cmd.value ?? "true").Trim().ToLowerInvariant(); bool want = wantRaw != "false" && wantRaw != "0" && wantRaw != "off"; pass = fav == want; diff --git a/IdleSpectator/Chronicle.cs b/IdleSpectator/Chronicle.cs index 0072c70..556a542 100644 --- a/IdleSpectator/Chronicle.cs +++ b/IdleSpectator/Chronicle.cs @@ -151,6 +151,7 @@ public static class Chronicle private static readonly HashSet LoverPairKeys = new HashSet(); private static readonly HashSet FriendPairKeys = new HashSet(); private static readonly HashSet DeathLogged = new HashSet(); + private static readonly HashSet FavoriteIds = new HashSet(); private static readonly Dictionary FinalDossiers = new Dictionary(); private static readonly HashSet LandmarkDedupe = new HashSet(); @@ -244,6 +245,82 @@ public static class Chronicle public static int FinalDossierCount => FinalDossiers.Count; + public static bool IsFavoriteSubject(long unitId) + { + return unitId != 0 && FavoriteIds.Contains(unitId); + } + + /// Track favorites across death so Fallen can filter like Living. + public static void SetFavoriteSubject(long unitId, bool favorite) + { + if (unitId == 0) + { + return; + } + + bool had = FavoriteIds.Contains(unitId); + if (favorite == had) + { + if (FinalDossiers.TryGetValue(unitId, out UnitDossier existing) && existing != null) + { + existing.IsFavorite = favorite; + } + + return; + } + + if (favorite) + { + FavoriteIds.Add(unitId); + } + else + { + FavoriteIds.Remove(unitId); + } + + if (FinalDossiers.TryGetValue(unitId, out UnitDossier dossier) && dossier != null) + { + dossier.IsFavorite = favorite; + } + + BumpRevision(); + } + + public static void SyncFavoriteFromActor(Actor actor) + { + if (actor == null) + { + return; + } + + long id = 0; + try + { + id = actor.getID(); + } + catch + { + return; + } + + if (id == 0) + { + return; + } + + bool fav = false; + try + { + fav = actor.isAlive() && actor.isFavorite(); + } + catch + { + fav = false; + } + + SetFavoriteSubject(id, fav); + } + /// Harness / tuning: change subject cap and prune immediately. public static void SetMaxHistorySubjects(int max) { @@ -534,6 +611,7 @@ public static class Chronicle LoverPairKeys.Clear(); FriendPairKeys.Clear(); DeathLogged.Clear(); + FavoriteIds.Clear(); FinalDossiers.Clear(); LandmarkDedupe.Clear(); LandmarksPerAge.Clear(); @@ -587,7 +665,8 @@ public static class Chronicle public static void ShowHud() { EnsureWindow(); - if (ChronicleHud.DetailUnitId == 0) + // Default into Living only when opening from World with no detail pinned. + if (ChronicleHud.DetailUnitId == 0 && ChronicleHud.ActiveTab == ChronicleHud.LoreTab.World) { ChronicleHud.SetTab(ChronicleHud.LoreTab.Living); } @@ -706,6 +785,7 @@ public static class Chronicle } NoteFocusId(id); + SyncFavoriteFromActor(actor); } private static void NoteFocusId(long id) @@ -867,6 +947,15 @@ public static class Chronicle return; } + if (fav) + { + FavoriteIds.Add(id); + } + else + { + FavoriteIds.Remove(id); + } + string name = SafeName(actor); string species = SpeciesOf(actor); if (!string.IsNullOrEmpty(needle)) @@ -982,7 +1071,13 @@ public static class Chronicle if (favoritesOnly) { - continue; + if (!IsFavoriteSubject(id) + && !(FinalDossiers.TryGetValue(id, out UnitDossier favDossier) + && favDossier != null + && favDossier.IsFavorite)) + { + continue; + } } IReadOnlyList snap = SnapshotForSubject(id); @@ -1003,13 +1098,21 @@ public static class Chronicle } } + bool fallenFav = IsFavoriteSubject(id); + if (!fallenFav + && FinalDossiers.TryGetValue(id, out UnitDossier archived) + && archived != null) + { + fallenFav = archived.IsFavorite; + } + byId[id] = new ChronicleCharacterSummary { UnitId = id, Name = name, SpeciesId = species, HistoryCount = snap.Count, - IsFavorite = false, + IsFavorite = fallenFav, IsAlive = false, IsRecent = false, DeathManner = LatestDeathMannerFor(id), @@ -1021,6 +1124,12 @@ public static class Chronicle var list = new List(byId.Values); list.Sort((a, b) => { + // Favorites first on both Living and Fallen. + if (a.IsFavorite != b.IsFavorite) + { + return a.IsFavorite ? -1 : 1; + } + if (wantFallen || (!a.IsAlive && !b.IsAlive)) { // Fallen: most recent event first. @@ -1037,12 +1146,7 @@ public static class Chronicle return string.Compare(a.Title, b.Title, System.StringComparison.OrdinalIgnoreCase); } - // Living: favorites, recently watched, event count, name. - if (a.IsFavorite != b.IsFavorite) - { - return a.IsFavorite ? -1 : 1; - } - + // Living: recently watched, event count, name. int ra = a.IsRecent && a.RecentIndex >= 0 ? a.RecentIndex : int.MaxValue; int rb = b.IsRecent && b.RecentIndex >= 0 ? b.RecentIndex : int.MaxValue; if (ra != rb) @@ -1270,7 +1374,8 @@ public static class Chronicle string labelPrefix = "Fallen", AttackType attackType = AttackType.Age, long killerId = 0, - Vector3 deathPos = default) + Vector3 deathPos = default, + bool favorite = false) { count = Mathf.Clamp(count, 1, MaxHistoryPerUnit); long id; @@ -1362,7 +1467,14 @@ public static class Chronicle BumpRevision(); - FinalDossiers[id] = UnitDossier.FromFallenArchive(id, who, sp, manner); + UnitDossier archived = UnitDossier.FromFallenArchive(id, who, sp, manner); + archived.IsFavorite = favorite; + FinalDossiers[id] = archived; + if (favorite) + { + FavoriteIds.Add(id); + } + PruneFinalDossiers(); return id; } @@ -1615,6 +1727,11 @@ public static class Chronicle } FinalDossiers[id] = dossier; + if (dossier.IsFavorite) + { + FavoriteIds.Add(id); + } + PruneFinalDossiers(); } @@ -1946,6 +2063,7 @@ public static class Chronicle Histories.Remove(id); DeathLogged.Remove(id); FinalDossiers.Remove(id); + FavoriteIds.Remove(id); need--; } } diff --git a/IdleSpectator/ChronicleHud.cs b/IdleSpectator/ChronicleHud.cs index 5e4c651..7783581 100644 --- a/IdleSpectator/ChronicleHud.cs +++ b/IdleSpectator/ChronicleHud.cs @@ -848,20 +848,17 @@ public static class ChronicleHud if (_favFilterBtn != null) { - // Favorites filter is Living-only. - _favFilterBtn.gameObject.SetActive(_tab == LoreTab.Living && !detail); + _favFilterBtn.gameObject.SetActive(chars && !detail); } if (_refreshBtn != null) { _refreshBtn.gameObject.SetActive(chars && !detail); - // On Fallen (no fav), park refresh on the far right. RectTransform refreshRt = _refreshBtn.GetComponent(); if (refreshRt != null) { - refreshRt.anchoredPosition = _tab == LoreTab.Living - ? new Vector2(-19f, 0f) - : Vector2.zero; + // Search | Refresh | Fav on both Living and Fallen. + refreshRt.anchoredPosition = new Vector2(-19f, 0f); } } @@ -1165,7 +1162,7 @@ public static class ChronicleHud Chronicle.CharacterListScope scope = _tab == LoreTab.Fallen ? Chronicle.CharacterListScope.Fallen : Chronicle.CharacterListScope.Living; - bool favOnly = scope == Chronicle.CharacterListScope.Living && _favoritesOnly; + bool favOnly = _favoritesOnly; List list = Chronicle.ListCharacters(_searchText, favOnly, scope: scope); _lastCharFingerprint = CharacterFingerprint(); _builtAtRevision = Chronicle.HistoryRevision; @@ -1175,7 +1172,7 @@ public static class ChronicleHud { if (scope == Chronicle.CharacterListScope.Fallen) { - AddEmpty("No fallen yet"); + AddEmpty(favOnly ? "No favorite fallen" : "No fallen yet"); } else { @@ -1197,9 +1194,7 @@ public static class ChronicleHud continue; } - if (scope == Chronicle.CharacterListScope.Living - && (favOnly || row.IsFavorite) - && !wroteFavoritesHeader) + if ((favOnly || row.IsFavorite) && !wroteFavoritesHeader) { AddSection("Favorites"); wroteFavoritesHeader = true; diff --git a/IdleSpectator/HarnessScenarios.cs b/IdleSpectator/HarnessScenarios.cs index 64a0107..09feb58 100644 --- a/IdleSpectator/HarnessScenarios.cs +++ b/IdleSpectator/HarnessScenarios.cs @@ -689,6 +689,21 @@ internal static class HarnessScenarios Step("ch18ft5f", "lore_refresh"), Step("ch18ft5g", "assert", expect: "lore_list_stale", value: "false"), Step("ch18ft5h", "assert", expect: "fallen_list_top", value: "FreshFallen"), + // Favorite filter on Fallen (same star control as Living). + Step( + "ch18ft5i", + "chronicle_orphan", + label: "FavFallen", + asset: "human", + value: "Fav", + count: 1, + expect: "Age", + tier: "place+fav"), + Step("ch18ft5j", "lore_favorites", value: "true"), + Step("ch18ft5k", "lore_refresh"), + Step("ch18ft5l", "assert", expect: "fallen_list_top", value: "FavFallen"), + Step("ch18ft5m", "lore_favorites", value: "false"), + Step("ch18ft5n", "lore_refresh"), Step("ch18ft6", "screenshot", value: "hud-lore-fallen-tab.png"), Step("ch18ft7", "wait", wait: 0.55f), Step("ch18q12", "lore_tab", value: "world"), diff --git a/IdleSpectator/WatchCaption.cs b/IdleSpectator/WatchCaption.cs index a0fca77..26bd973 100644 --- a/IdleSpectator/WatchCaption.cs +++ b/IdleSpectator/WatchCaption.cs @@ -224,23 +224,45 @@ public static class WatchCaption } } - if (actor == null || !actor.isAlive()) + if (actor != null && actor.isAlive()) + { + try + { + actor.switchFavorite(); + } + catch + { + return; + } + + Chronicle.SyncFavoriteFromActor(actor); + RefreshFavoriteVisual(actor); + LogService.LogInfo( + $"[IdleSpectator] Favorite toggled name={SafeActorName(actor)} favorite={actor.isFavorite()}"); + return; + } + + // Fallen / archive: track in Chronicle so the Fallen filter stays consistent. + long id = _current != null ? _current.UnitId : 0; + if (id == 0) + { + id = Chronicle.CurrentHistorySubjectId(); + } + + if (id == 0) { return; } - try + bool next = !Chronicle.IsFavoriteSubject(id); + Chronicle.SetFavoriteSubject(id, next); + if (_current != null && _current.UnitId == id) { - actor.switchFavorite(); - } - catch - { - return; + _current.IsFavorite = next; } - RefreshFavoriteVisual(actor); - LogService.LogInfo( - $"[IdleSpectator] Favorite toggled name={SafeActorName(actor)} favorite={actor.isFavorite()}"); + RefreshFavoriteVisual(null); + LogService.LogInfo($"[IdleSpectator] Favorite toggled fallen id={id} favorite={next}"); } public static void PinWhilePaused() @@ -1266,13 +1288,34 @@ public static class WatchCaption bool fav = false; try { - fav = actor != null && actor.isAlive() && actor.isFavorite(); + if (actor != null && actor.isAlive() && actor.isFavorite()) + { + fav = true; + } } catch { fav = false; } + if (!fav) + { + long id = _current != null ? _current.UnitId : 0; + if (id == 0 && actor != null) + { + try + { + id = actor.getID(); + } + catch + { + id = 0; + } + } + + fav = Chronicle.IsFavoriteSubject(id) || (_current != null && _current.IsFavorite); + } + HudIcons.Apply(_favoriteIcon, HudIcons.Favorite()); _favoriteIcon.color = fav ? new Color(1f, 0.85f, 0.25f, 1f) diff --git a/IdleSpectator/mod.json b/IdleSpectator/mod.json index 33f10cb..a44b2ff 100644 --- a/IdleSpectator/mod.json +++ b/IdleSpectator/mod.json @@ -1,7 +1,7 @@ { "name": "IdleSpectator", "author": "dazed", - "version": "0.12.22", - "description": "AFK Idle Spectator (I) + Lore (L). Subject soft cap 100k.", + "version": "0.12.24", + "description": "AFK Idle Spectator (I) + Lore (L). Fallen favorites filter.", "GUID": "com.dazed.idlespectator" }