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.
This commit is contained in:
parent
0b23a15618
commit
94de8a3b5b
6 changed files with 266 additions and 54 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ public static class Chronicle
|
|||
private static readonly HashSet<string> LoverPairKeys = new HashSet<string>();
|
||||
private static readonly HashSet<string> FriendPairKeys = new HashSet<string>();
|
||||
private static readonly HashSet<long> DeathLogged = new HashSet<long>();
|
||||
private static readonly HashSet<long> FavoriteIds = new HashSet<long>();
|
||||
private static readonly Dictionary<long, UnitDossier> FinalDossiers =
|
||||
new Dictionary<long, UnitDossier>();
|
||||
private static readonly HashSet<string> LandmarkDedupe = new HashSet<string>();
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>Track favorites across death so Fallen can filter like Living.</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Harness / tuning: change subject cap and prune immediately.</summary>
|
||||
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<ChronicleEntry> 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<ChronicleCharacterSummary>(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--;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<RectTransform>();
|
||||
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<ChronicleCharacterSummary> 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;
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue