Implement Lore panel functionality, replacing Chronicle with a new tabbed interface for World Memory and Character History. Enhanced HUD interactions, added search and favorites features, and updated key bindings. Adjusted test scenarios to validate new Lore features and behaviors.
This commit is contained in:
parent
0f30f18bcb
commit
b40f3f8621
13 changed files with 2082 additions and 239 deletions
|
|
@ -390,10 +390,20 @@ public static class AgentHarness
|
|||
}
|
||||
|
||||
case "chronicle_tab":
|
||||
case "lore_tab":
|
||||
case "world_memory_open":
|
||||
{
|
||||
// Tabs removed - World Memory is a single panel.
|
||||
Chronicle.ShowHud();
|
||||
string tabRaw = (cmd.value ?? cmd.label ?? "world").Trim().ToLowerInvariant();
|
||||
if (tabRaw == "characters" || tabRaw == "character" || tabRaw == "chars" || tabRaw == "history")
|
||||
{
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.Characters);
|
||||
}
|
||||
else
|
||||
{
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.World);
|
||||
}
|
||||
|
||||
bool open = Chronicle.HudVisible;
|
||||
if (open)
|
||||
{
|
||||
|
|
@ -404,13 +414,138 @@ public static class AgentHarness
|
|||
_cmdFail++;
|
||||
}
|
||||
|
||||
Emit(cmd, open, detail: $"hud={open} memory={Chronicle.MemoryCount} age={Chronicle.CurrentAgeName}");
|
||||
Emit(cmd, open,
|
||||
detail: $"hud={open} tab={ChronicleHud.ActiveTab} memory={Chronicle.MemoryCount} recent={Chronicle.RecentFocusCount}");
|
||||
break;
|
||||
}
|
||||
|
||||
case "lore_search":
|
||||
{
|
||||
Chronicle.ShowHud();
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.Characters);
|
||||
ChronicleHud.SetSearch(cmd.value ?? cmd.label ?? "");
|
||||
_cmdOk++;
|
||||
Emit(cmd, ok: true, detail: $"search='{ChronicleHud.SearchText}'");
|
||||
break;
|
||||
}
|
||||
|
||||
case "lore_favorites":
|
||||
{
|
||||
Chronicle.ShowHud();
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.Characters);
|
||||
string v = (cmd.value ?? "true").Trim().ToLowerInvariant();
|
||||
bool on = v != "false" && v != "0" && v != "off";
|
||||
ChronicleHud.SetFavoritesOnly(on);
|
||||
_cmdOk++;
|
||||
Emit(cmd, ok: true, detail: $"favoritesOnly={ChronicleHud.FavoritesOnly}");
|
||||
break;
|
||||
}
|
||||
|
||||
case "lore_pick":
|
||||
{
|
||||
Chronicle.ShowHud();
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.Characters);
|
||||
string needle = cmd.value ?? cmd.label ?? "";
|
||||
if (needle == "auto")
|
||||
{
|
||||
needle = "";
|
||||
}
|
||||
|
||||
bool ok = ChronicleHud.PickFirstCharacter(needle);
|
||||
if (ok)
|
||||
{
|
||||
_cmdOk++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cmdFail++;
|
||||
}
|
||||
|
||||
Emit(cmd, ok,
|
||||
detail: $"picked={ok} focus={MoveCamera.hasFocusUnit()} dossier={WatchCaption.LastHeadline} recent={Chronicle.RecentFocusCount}");
|
||||
break;
|
||||
}
|
||||
|
||||
case "lore_open_focus":
|
||||
case "chronicle_open_focus":
|
||||
{
|
||||
ChronicleHud.OpenSyncedFromFocus();
|
||||
bool open = Chronicle.HudVisible;
|
||||
if (open)
|
||||
{
|
||||
_cmdOk++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cmdFail++;
|
||||
}
|
||||
|
||||
Emit(cmd, open,
|
||||
detail:
|
||||
$"lore={Chronicle.HudVisible} detail={ChronicleHud.DetailUnitId} dossierId={WatchCaption.CurrentUnitId} idle={SpectatorMode.Active} follow={ChronicleHud.FollowFocus}");
|
||||
break;
|
||||
}
|
||||
|
||||
case "lore_pick_fallen":
|
||||
{
|
||||
Chronicle.ShowHud();
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.Characters);
|
||||
string needle = cmd.value ?? cmd.label ?? "";
|
||||
if (needle == "auto")
|
||||
{
|
||||
needle = "";
|
||||
}
|
||||
|
||||
bool ok = ChronicleHud.PickFirstFallen(needle);
|
||||
if (ok)
|
||||
{
|
||||
_cmdOk++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cmdFail++;
|
||||
}
|
||||
|
||||
Emit(cmd, ok,
|
||||
detail:
|
||||
$"picked={ok} detail={ChronicleHud.DetailUnitId} follow={ChronicleHud.FollowFocus} hist={Chronicle.HistoryCountFor(ChronicleHud.DetailUnitId)}");
|
||||
break;
|
||||
}
|
||||
|
||||
case "chronicle_orphan":
|
||||
{
|
||||
int n = Math.Max(1, cmd.count);
|
||||
string name = !string.IsNullOrEmpty(cmd.label) ? cmd.label : "Orphan";
|
||||
string species = !string.IsNullOrEmpty(cmd.asset) ? cmd.asset : "creature";
|
||||
string prefix = !string.IsNullOrEmpty(cmd.value) ? cmd.value : "Fallen";
|
||||
long id = Chronicle.ForceOrphanHistory(name, species, n, prefix);
|
||||
bool ok = id != 0 && Chronicle.HistoryCountFor(id) >= n;
|
||||
if (ok)
|
||||
{
|
||||
_cmdOk++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cmdFail++;
|
||||
}
|
||||
|
||||
Emit(cmd, ok, detail: $"orphanId={id} hist={Chronicle.HistoryCountFor(id)} want={n}");
|
||||
break;
|
||||
}
|
||||
|
||||
case "lore_close":
|
||||
case "chronicle_close":
|
||||
{
|
||||
Chronicle.HideHud();
|
||||
_cmdOk++;
|
||||
Emit(cmd, ok: true, detail: $"lore={Chronicle.HudVisible}");
|
||||
break;
|
||||
}
|
||||
|
||||
case "chronicle_open":
|
||||
{
|
||||
Chronicle.ShowHud();
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.World);
|
||||
bool open = Chronicle.HudVisible;
|
||||
if (open)
|
||||
{
|
||||
|
|
@ -481,11 +616,29 @@ public static class AgentHarness
|
|||
}
|
||||
|
||||
case "dossier_expand":
|
||||
case "dossier_history_open":
|
||||
{
|
||||
// Expand control removed: history is always full-list + scroll when tall.
|
||||
WatchCaption.OpenFullHistoryInLore();
|
||||
bool open = Chronicle.HudVisible && ChronicleHud.DetailUnitId != 0;
|
||||
if (open)
|
||||
{
|
||||
_cmdOk++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cmdFail++;
|
||||
}
|
||||
|
||||
Emit(cmd, open,
|
||||
detail: $"lore={Chronicle.HudVisible} detail={ChronicleHud.DetailUnitId} idle={SpectatorMode.Active} shown={WatchCaption.LastHistoryShown}");
|
||||
break;
|
||||
}
|
||||
|
||||
case "lore_back":
|
||||
{
|
||||
ChronicleHud.ClearUnitDetail();
|
||||
_cmdOk++;
|
||||
Emit(cmd, ok: true,
|
||||
detail: $"expand_removed shown={WatchCaption.LastHistoryShown} scrollable={WatchCaption.LastHistoryScrollable}");
|
||||
Emit(cmd, ok: true, detail: $"detail={ChronicleHud.DetailUnitId} tab={ChronicleHud.ActiveTab}");
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -1211,9 +1364,10 @@ public static class AgentHarness
|
|||
&& WatchCaption.LastPanelSize.y > 20f
|
||||
&& WatchCaption.LastPanelSize.y < maxH
|
||||
&& WatchCaption.LastPanelSize.x <= 440f
|
||||
&& (WatchCaption.LastHistoryShown <= 0 || WatchCaption.LastHistoryFillsBody);
|
||||
&& (WatchCaption.LastHistoryShown <= 0 || WatchCaption.LastHistoryFillsBody)
|
||||
&& WatchCaption.LastHistoryShown <= 3;
|
||||
detail =
|
||||
$"layoutOk={WatchCaption.LastLayoutOk} size={WatchCaption.LastPanelSize} histFill={WatchCaption.LastHistoryFillsBody} scrollable={WatchCaption.LastHistoryScrollable} caption='{(WatchCaption.LastCaptionText ?? "").Replace("\n", " | ")}' hist='{(WatchCaption.LastHistoryPreview ?? "").Replace("\n", " ")}'";
|
||||
$"layoutOk={WatchCaption.LastLayoutOk} size={WatchCaption.LastPanelSize} histFill={WatchCaption.LastHistoryFillsBody} shown={WatchCaption.LastHistoryShown} caption='{(WatchCaption.LastCaptionText ?? "").Replace("\n", " | ")}' hist='{(WatchCaption.LastHistoryPreview ?? "").Replace("\n", " ")}'";
|
||||
break;
|
||||
}
|
||||
case "dossier_traits_ok":
|
||||
|
|
@ -1313,11 +1467,114 @@ public static class AgentHarness
|
|||
}
|
||||
case "dossier_history_scrollable":
|
||||
{
|
||||
pass = WatchCaption.LastHistoryScrollable && WatchCaption.LastHistoryShown > 3;
|
||||
// Dossier history is no longer scrollable; full history lives in Lore.
|
||||
pass = !WatchCaption.LastHistoryScrollable && WatchCaption.LastHistoryShown <= 3;
|
||||
detail =
|
||||
$"scrollable={WatchCaption.LastHistoryScrollable} shown={WatchCaption.LastHistoryShown}";
|
||||
break;
|
||||
}
|
||||
case "lore_detail":
|
||||
{
|
||||
bool want = ParseBool(cmd.value, defaultValue: true);
|
||||
bool have = ChronicleHud.DetailUnitId != 0;
|
||||
pass = Chronicle.HudVisible && have == want;
|
||||
detail =
|
||||
$"detailId={ChronicleHud.DetailUnitId} want={want} tab={ChronicleHud.ActiveTab} follow={ChronicleHud.FollowFocus}";
|
||||
break;
|
||||
}
|
||||
case "lore_follow":
|
||||
{
|
||||
bool want = ParseBool(cmd.value, defaultValue: true);
|
||||
bool have = ChronicleHud.FollowFocus;
|
||||
pass = Chronicle.HudVisible && ChronicleHud.DetailUnitId != 0 && have == want;
|
||||
detail =
|
||||
$"follow={have} want={want} detailId={ChronicleHud.DetailUnitId}";
|
||||
break;
|
||||
}
|
||||
case "lore_follows_focus":
|
||||
{
|
||||
long focusId = WatchCaption.CurrentUnitId;
|
||||
if (focusId == 0)
|
||||
{
|
||||
focusId = Chronicle.CurrentHistorySubjectId();
|
||||
}
|
||||
|
||||
long loreId = ChronicleHud.DetailUnitId;
|
||||
pass = Chronicle.HudVisible
|
||||
&& ChronicleHud.FollowFocus
|
||||
&& SpectatorMode.Active
|
||||
&& focusId != 0
|
||||
&& loreId == focusId;
|
||||
detail =
|
||||
$"follow={ChronicleHud.FollowFocus} idle={SpectatorMode.Active} focusId={focusId} loreId={loreId}";
|
||||
break;
|
||||
}
|
||||
case "lore_history_shown":
|
||||
{
|
||||
int want = ParseCountExpect(cmd, defaultValue: 1);
|
||||
long detailId = ChronicleHud.DetailUnitId;
|
||||
int haveData = Chronicle.HistoryCountFor(detailId);
|
||||
int haveUi = ChronicleHud.LastDetailHistoryRows;
|
||||
string cmp = (cmd.label ?? "min").Trim().ToLowerInvariant();
|
||||
bool dataOk = cmp == "exact" || cmp == "==" ? haveData == want : haveData >= want;
|
||||
bool uiOk = cmp == "exact" || cmp == "==" ? haveUi == want : haveUi >= want;
|
||||
pass = Chronicle.HudVisible && detailId != 0 && dataOk && uiOk;
|
||||
detail =
|
||||
$"loreHist={haveData} uiRows={haveUi} want={want} detailId={detailId} dossierId={WatchCaption.CurrentUnitId}";
|
||||
break;
|
||||
}
|
||||
case "dossier_lore_history_match":
|
||||
{
|
||||
long dossierId = WatchCaption.CurrentUnitId;
|
||||
long loreId = ChronicleHud.DetailUnitId;
|
||||
int dossierShown = WatchCaption.LastHistoryShown;
|
||||
int loreData = Chronicle.HistoryCountFor(loreId);
|
||||
int loreUi = ChronicleHud.LastDetailHistoryRows;
|
||||
bool idsMatch = dossierId != 0 && dossierId == loreId;
|
||||
bool countsAgree = dossierShown <= 0
|
||||
? loreData == 0 && loreUi == 0
|
||||
: loreData >= dossierShown && loreUi >= Mathf.Min(dossierShown, 24);
|
||||
pass = Chronicle.HudVisible && idsMatch && countsAgree;
|
||||
detail =
|
||||
$"dossierId={dossierId} loreId={loreId} dossierShown={dossierShown} loreData={loreData} loreUi={loreUi} preview={WatchCaption.LastHistoryPreview}";
|
||||
break;
|
||||
}
|
||||
case "dossier_pinned":
|
||||
{
|
||||
bool want = ParseBool(cmd.value, defaultValue: true);
|
||||
bool have = WatchCaption.PinnedWhilePaused && WatchCaption.Visible;
|
||||
pass = have == want;
|
||||
detail =
|
||||
$"pinned={WatchCaption.PinnedWhilePaused} visible={WatchCaption.Visible} idle={SpectatorMode.Active} want={want}";
|
||||
break;
|
||||
}
|
||||
case "lore_zoom_blocked":
|
||||
{
|
||||
MoveCamera cam = MoveCamera.instance;
|
||||
if (cam == null)
|
||||
{
|
||||
pass = false;
|
||||
detail = "no camera";
|
||||
break;
|
||||
}
|
||||
|
||||
float before = cam.getTargetZoom();
|
||||
CameraZoomPatches.ForceAbsorbForHarness = true;
|
||||
try
|
||||
{
|
||||
MoveCamera.zoomInWheel(null);
|
||||
MoveCamera.zoomOutWheel(null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CameraZoomPatches.ForceAbsorbForHarness = false;
|
||||
}
|
||||
|
||||
float after = cam.getTargetZoom();
|
||||
pass = Mathf.Abs(after - before) < 0.001f;
|
||||
detail = $"zoomBefore={before} zoomAfter={after} absorb={ChronicleHud.ShouldAbsorbScrollZoom()}";
|
||||
break;
|
||||
}
|
||||
case "dossier_history_fills_body":
|
||||
{
|
||||
pass = WatchCaption.LastHistoryShown > 0 && WatchCaption.LastHistoryFillsBody;
|
||||
|
|
@ -1359,10 +1616,24 @@ public static class AgentHarness
|
|||
break;
|
||||
}
|
||||
case "chronicle_tab":
|
||||
case "lore_tab":
|
||||
{
|
||||
// Deprecated: panel is always World Memory when open.
|
||||
pass = Chronicle.HudVisible;
|
||||
detail = $"hud={Chronicle.HudVisible} memory={Chronicle.MemoryCount} (tabs removed)";
|
||||
string wantRaw = (cmd.value ?? cmd.label ?? "world").Trim().ToLowerInvariant();
|
||||
ChronicleHud.LoreTab want = wantRaw == "characters" || wantRaw == "character"
|
||||
|| wantRaw == "chars" || wantRaw == "history"
|
||||
? ChronicleHud.LoreTab.Characters
|
||||
: ChronicleHud.LoreTab.World;
|
||||
pass = Chronicle.HudVisible && ChronicleHud.ActiveTab == want;
|
||||
detail =
|
||||
$"hud={Chronicle.HudVisible} tab={ChronicleHud.ActiveTab} want={want} recent={Chronicle.RecentFocusCount}";
|
||||
break;
|
||||
}
|
||||
case "lore_recent":
|
||||
{
|
||||
int want = ParseCountExpect(cmd, defaultValue: 1);
|
||||
int have = Chronicle.RecentFocusCount;
|
||||
pass = have >= want;
|
||||
detail = $"recent={have} min={want}";
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ public static class CameraDirector
|
|||
|
||||
bool firstFocus = !MoveCamera.hasFocusUnit();
|
||||
MoveCamera.setFocusUnit(actor);
|
||||
Chronicle.NoteFocus(actor);
|
||||
|
||||
MoveCamera cam = MoveCamera.instance;
|
||||
if (cam == null)
|
||||
|
|
|
|||
34
IdleSpectator/CameraZoomPatches.cs
Normal file
34
IdleSpectator/CameraZoomPatches.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using HarmonyLib;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>
|
||||
/// WorldBox zooms on mouse wheel even over UI while a unit is focused
|
||||
/// (<c>!isOverUI || MoveCamera.inSpectatorMode()</c>). Absorb wheel zoom when
|
||||
/// the cursor is over Lore or the dossier so ScrollRect history can scroll.
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
internal static class CameraZoomPatches
|
||||
{
|
||||
/// <summary>Harness: force the zoom block without needing a real mouse position.</summary>
|
||||
public static bool ForceAbsorbForHarness;
|
||||
|
||||
private static bool Absorb()
|
||||
{
|
||||
return ForceAbsorbForHarness || ChronicleHud.ShouldAbsorbScrollZoom();
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(MoveCamera), nameof(MoveCamera.zoomInWheel))]
|
||||
[HarmonyPrefix]
|
||||
private static bool PrefixZoomInWheel()
|
||||
{
|
||||
return !Absorb();
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(MoveCamera), nameof(MoveCamera.zoomOutWheel))]
|
||||
[HarmonyPrefix]
|
||||
private static bool PrefixZoomOutWheel()
|
||||
{
|
||||
return !Absorb();
|
||||
}
|
||||
}
|
||||
|
|
@ -92,7 +92,7 @@ public sealed class ChronicleEntry
|
|||
/// </summary>
|
||||
public static class Chronicle
|
||||
{
|
||||
public const KeyCode ToggleKey = KeyCode.F9;
|
||||
public const KeyCode ToggleKey = KeyCode.L;
|
||||
public const int MaxHistoryPerUnit = 40;
|
||||
private const int MaxLandmarks = 80;
|
||||
private const int MaxLandmarksPerAge = 8;
|
||||
|
|
@ -105,9 +105,13 @@ public static class Chronicle
|
|||
private static readonly HashSet<long> DeathLogged = new HashSet<long>();
|
||||
private static readonly HashSet<string> LandmarkDedupe = new HashSet<string>();
|
||||
private static readonly Dictionary<string, int> LandmarksPerAge = new Dictionary<string, int>();
|
||||
private static readonly List<long> RecentFocusIds = new List<long>();
|
||||
|
||||
public const int MaxRecentFocus = 24;
|
||||
|
||||
private static bool _hudReady;
|
||||
private static long _lastHistorySubjectId;
|
||||
private static long _trackedFocusId;
|
||||
private static object _boundWorld;
|
||||
private static string _currentAgeId = "";
|
||||
private static string _currentAgeName = "";
|
||||
|
|
@ -145,6 +149,17 @@ public static class Chronicle
|
|||
|
||||
public static long LastHistorySubjectId => _lastHistorySubjectId;
|
||||
|
||||
public static int RecentFocusCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (RecentFocusIds)
|
||||
{
|
||||
return RecentFocusIds.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static string CurrentAgeId => _currentAgeId;
|
||||
|
||||
public static string CurrentAgeName => _currentAgeName;
|
||||
|
|
@ -311,7 +326,7 @@ public static class Chronicle
|
|||
if (ChronicleHud.IsReady)
|
||||
{
|
||||
_hudReady = true;
|
||||
LogService.LogInfo("[IdleSpectator] World Memory HUD ready (F9)");
|
||||
LogService.LogInfo("[IdleSpectator] World Memory HUD ready (L)");
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
|
|
@ -337,7 +352,9 @@ public static class Chronicle
|
|||
DeathLogged.Clear();
|
||||
LandmarkDedupe.Clear();
|
||||
LandmarksPerAge.Clear();
|
||||
RecentFocusIds.Clear();
|
||||
_lastHistorySubjectId = 0;
|
||||
_trackedFocusId = 0;
|
||||
_currentAgeId = "";
|
||||
_currentAgeName = "";
|
||||
// Keep _boundWorld so re-enable on same map does not loop-clear.
|
||||
|
|
@ -351,6 +368,11 @@ public static class Chronicle
|
|||
}
|
||||
|
||||
EnsureWindow();
|
||||
if (ChronicleHud.IsTypingSearch)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Input.GetKeyDown(ToggleKey))
|
||||
{
|
||||
ToggleWindow();
|
||||
|
|
@ -371,6 +393,11 @@ public static class Chronicle
|
|||
public static void ShowHud()
|
||||
{
|
||||
EnsureWindow();
|
||||
if (ChronicleHud.DetailUnitId == 0)
|
||||
{
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.Characters);
|
||||
}
|
||||
|
||||
ChronicleHud.SetVisible(true);
|
||||
}
|
||||
|
||||
|
|
@ -401,9 +428,7 @@ public static class Chronicle
|
|||
Actor unit = FindUnitById(entry.SubjectId);
|
||||
if (unit != null && unit.isAlive())
|
||||
{
|
||||
CameraDirector.FocusUnit(unit);
|
||||
WatchCaption.SetFromActor(unit);
|
||||
return true;
|
||||
return FocusSubject(unit);
|
||||
}
|
||||
|
||||
if (entry.OtherId != 0)
|
||||
|
|
@ -411,9 +436,7 @@ public static class Chronicle
|
|||
Actor other = FindUnitById(entry.OtherId);
|
||||
if (other != null && other.isAlive())
|
||||
{
|
||||
CameraDirector.FocusUnit(other);
|
||||
WatchCaption.SetFromActor(other);
|
||||
return true;
|
||||
return FocusSubject(other);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -426,6 +449,356 @@ public static class Chronicle
|
|||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Record a focus for the Character History "recent" list.</summary>
|
||||
public static void NoteFocus(Actor actor)
|
||||
{
|
||||
if (actor == null || !actor.isAlive())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long id;
|
||||
try
|
||||
{
|
||||
id = actor.getID();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (id == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NoteFocusId(id);
|
||||
}
|
||||
|
||||
private static void NoteFocusId(long id)
|
||||
{
|
||||
if (id == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastHistorySubjectId = id;
|
||||
lock (RecentFocusIds)
|
||||
{
|
||||
RecentFocusIds.Remove(id);
|
||||
RecentFocusIds.Insert(0, id);
|
||||
while (RecentFocusIds.Count > MaxRecentFocus)
|
||||
{
|
||||
RecentFocusIds.RemoveAt(RecentFocusIds.Count - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Focus a living unit and show its dossier (enables spectator if needed).</summary>
|
||||
public static string TitleForSubject(long subjectId)
|
||||
{
|
||||
if (subjectId == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
Actor live = FindUnitById(subjectId);
|
||||
if (live != null)
|
||||
{
|
||||
return SafeName(live) + " (" + SpeciesOf(live) + ")";
|
||||
}
|
||||
|
||||
IReadOnlyList<ChronicleEntry> snap = SnapshotForSubject(subjectId);
|
||||
if (snap == null || snap.Count == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
ChronicleEntry last = snap[snap.Count - 1];
|
||||
if (last == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
string name = last.Name ?? "";
|
||||
string species = last.SpeciesId ?? "";
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return species;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(species))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
return name + " (" + species + ")";
|
||||
}
|
||||
|
||||
/// <summary>Focus camera + dossier while idle is paused (does not re-enable spectator).</summary>
|
||||
public static bool FocusCameraForBrowse(long unitId)
|
||||
{
|
||||
Actor unit = FindUnitById(unitId);
|
||||
if (unit == null || !unit.isAlive())
|
||||
{
|
||||
NoteFocusId(unitId);
|
||||
return false;
|
||||
}
|
||||
|
||||
WatchCaption.PinWhilePaused();
|
||||
CameraDirector.FocusUnit(unit);
|
||||
WatchCaption.SetFromActor(unit);
|
||||
NoteFocus(unit);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool FocusSubject(long unitId)
|
||||
{
|
||||
return FocusSubject(FindUnitById(unitId));
|
||||
}
|
||||
|
||||
public static bool FocusSubject(Actor unit)
|
||||
{
|
||||
if (unit == null || !unit.isAlive())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SpectatorMode.Active && ModSettings.Enabled)
|
||||
{
|
||||
SpectatorMode.SetActive(true);
|
||||
}
|
||||
|
||||
CameraDirector.FocusUnit(unit);
|
||||
WatchCaption.SetFromActor(unit);
|
||||
NoteFocus(unit);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Character History rows: recent focuses first, then other history subjects / favorites.
|
||||
/// </summary>
|
||||
public static List<ChronicleCharacterSummary> ListCharacters(
|
||||
string search = "",
|
||||
bool favoritesOnly = false,
|
||||
int max = 40)
|
||||
{
|
||||
var byId = new Dictionary<long, ChronicleCharacterSummary>();
|
||||
string needle = (search ?? "").Trim().ToLowerInvariant();
|
||||
|
||||
void Upsert(Actor actor, bool recent, int recentIndex)
|
||||
{
|
||||
if (actor == null || !actor.isAlive())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long id;
|
||||
try
|
||||
{
|
||||
id = actor.getID();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (id == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool fav = false;
|
||||
try
|
||||
{
|
||||
fav = actor.isFavorite();
|
||||
}
|
||||
catch
|
||||
{
|
||||
fav = false;
|
||||
}
|
||||
|
||||
if (favoritesOnly && !fav)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string name = SafeName(actor);
|
||||
string species = SpeciesOf(actor);
|
||||
if (!string.IsNullOrEmpty(needle))
|
||||
{
|
||||
string hay = ((name ?? "") + " " + (species ?? "")).ToLowerInvariant();
|
||||
if (hay.IndexOf(needle, System.StringComparison.Ordinal) < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!byId.TryGetValue(id, out ChronicleCharacterSummary row))
|
||||
{
|
||||
row = new ChronicleCharacterSummary
|
||||
{
|
||||
UnitId = id,
|
||||
Name = name,
|
||||
SpeciesId = species,
|
||||
HistoryCount = HistoryCountFor(id),
|
||||
IsFavorite = fav,
|
||||
IsAlive = true
|
||||
};
|
||||
byId[id] = row;
|
||||
}
|
||||
|
||||
if (recent)
|
||||
{
|
||||
row.IsRecent = true;
|
||||
if (row.RecentIndex < 0 || recentIndex < row.RecentIndex)
|
||||
{
|
||||
row.RecentIndex = recentIndex;
|
||||
}
|
||||
}
|
||||
|
||||
row.IsFavorite = fav;
|
||||
row.HistoryCount = HistoryCountFor(id);
|
||||
}
|
||||
|
||||
List<long> recentCopy;
|
||||
lock (RecentFocusIds)
|
||||
{
|
||||
recentCopy = new List<long>(RecentFocusIds);
|
||||
}
|
||||
|
||||
for (int i = 0; i < recentCopy.Count; i++)
|
||||
{
|
||||
Upsert(FindUnitById(recentCopy[i]), recent: true, recentIndex: i);
|
||||
}
|
||||
|
||||
foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic())
|
||||
{
|
||||
if (actor == null || !actor.isAlive())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool fav = false;
|
||||
try
|
||||
{
|
||||
fav = actor.isFavorite();
|
||||
}
|
||||
catch
|
||||
{
|
||||
fav = false;
|
||||
}
|
||||
|
||||
long id = 0;
|
||||
try
|
||||
{
|
||||
id = actor.getID();
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool hasHist = HistoryCountFor(id) > 0;
|
||||
if (!fav && !hasHist && !byId.ContainsKey(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Upsert(actor, recent: false, recentIndex: -1);
|
||||
}
|
||||
|
||||
// History subjects that may still be named from entries even if not matched above.
|
||||
List<long> histIds;
|
||||
lock (Histories)
|
||||
{
|
||||
histIds = new List<long>(Histories.Keys);
|
||||
}
|
||||
|
||||
for (int i = 0; i < histIds.Count; i++)
|
||||
{
|
||||
long id = histIds[i];
|
||||
if (byId.ContainsKey(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Actor live = FindUnitById(id);
|
||||
if (live != null)
|
||||
{
|
||||
Upsert(live, recent: false, recentIndex: -1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (favoritesOnly)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IReadOnlyList<ChronicleEntry> snap = SnapshotForSubject(id);
|
||||
if (snap == null || snap.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ChronicleEntry last = snap[snap.Count - 1];
|
||||
string name = last?.Name ?? "";
|
||||
string species = last?.SpeciesId ?? "";
|
||||
if (!string.IsNullOrEmpty(needle))
|
||||
{
|
||||
string hay = (name + " " + species).ToLowerInvariant();
|
||||
if (hay.IndexOf(needle, System.StringComparison.Ordinal) < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
byId[id] = new ChronicleCharacterSummary
|
||||
{
|
||||
UnitId = id,
|
||||
Name = name,
|
||||
SpeciesId = species,
|
||||
HistoryCount = snap.Count,
|
||||
IsFavorite = false,
|
||||
IsAlive = false,
|
||||
IsRecent = false
|
||||
};
|
||||
}
|
||||
|
||||
var list = new List<ChronicleCharacterSummary>(byId.Values);
|
||||
list.Sort((a, b) =>
|
||||
{
|
||||
if (a.IsAlive != b.IsAlive)
|
||||
{
|
||||
return a.IsAlive ? -1 : 1;
|
||||
}
|
||||
|
||||
if (a.IsRecent != b.IsRecent)
|
||||
{
|
||||
return a.IsRecent ? -1 : 1;
|
||||
}
|
||||
|
||||
if (a.IsRecent && b.IsRecent)
|
||||
{
|
||||
return a.RecentIndex.CompareTo(b.RecentIndex);
|
||||
}
|
||||
|
||||
if (a.IsFavorite != b.IsFavorite)
|
||||
{
|
||||
return a.IsFavorite ? -1 : 1;
|
||||
}
|
||||
|
||||
return string.Compare(a.Title, b.Title, System.StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
|
||||
if (max > 0 && list.Count > max)
|
||||
{
|
||||
list.RemoveRange(max, list.Count - max);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public static void Update()
|
||||
{
|
||||
if (!ModSettings.ChronicleEnabled || !Config.game_loaded || World.world == null)
|
||||
|
|
@ -440,9 +813,10 @@ public static class Chronicle
|
|||
try
|
||||
{
|
||||
long id = MoveCamera._focus_unit.getID();
|
||||
if (id != 0)
|
||||
if (id != 0 && id != _trackedFocusId)
|
||||
{
|
||||
_lastHistorySubjectId = id;
|
||||
_trackedFocusId = id;
|
||||
NoteFocusId(id);
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
|
@ -597,6 +971,64 @@ public static class Chronicle
|
|||
return true;
|
||||
}
|
||||
|
||||
private static long _orphanSeq = -1000;
|
||||
|
||||
/// <summary>
|
||||
/// Harness: inject history for a subject id with no living unit (Fallen list).
|
||||
/// </summary>
|
||||
public static long ForceOrphanHistory(string name, string species, int count, string labelPrefix = "Fallen")
|
||||
{
|
||||
count = Mathf.Clamp(count, 1, MaxHistoryPerUnit);
|
||||
long id;
|
||||
lock (Histories)
|
||||
{
|
||||
id = _orphanSeq--;
|
||||
}
|
||||
|
||||
string who = string.IsNullOrEmpty(name) ? "Nameless" : name;
|
||||
string sp = string.IsNullOrEmpty(species) ? "creature" : species;
|
||||
string prefix = string.IsNullOrEmpty(labelPrefix) ? "Fallen" : labelPrefix;
|
||||
RefreshAgeChapter(force: false);
|
||||
StampWorldDate(out double worldTime, out string dateLabel);
|
||||
|
||||
lock (Histories)
|
||||
{
|
||||
if (!Histories.TryGetValue(id, out List<ChronicleEntry> list))
|
||||
{
|
||||
list = new List<ChronicleEntry>();
|
||||
Histories[id] = list;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
string line = count > 1 ? $"{prefix} #{i + 1}" : prefix;
|
||||
list.Add(new ChronicleEntry
|
||||
{
|
||||
CreatedAt = Time.unscaledTime,
|
||||
WorldTime = worldTime,
|
||||
DateLabel = dateLabel,
|
||||
Kind = ChronicleKind.Death,
|
||||
SubjectId = id,
|
||||
Name = who,
|
||||
SpeciesId = sp,
|
||||
Line = line,
|
||||
LoreLine = line,
|
||||
AgeId = _currentAgeId,
|
||||
AgeName = _currentAgeName
|
||||
});
|
||||
}
|
||||
|
||||
while (list.Count > MaxHistoryPerUnit)
|
||||
{
|
||||
list.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
NoteFocusId(id);
|
||||
LogService.LogInfo($"[IdleSpectator][CHRONICLE] orphan history id={id} count={count} name={who}");
|
||||
return id;
|
||||
}
|
||||
|
||||
/// <summary>Harness: inject a World Memory landmark (does not touch character History).</summary>
|
||||
public static bool ForceWorldNote(string label)
|
||||
{
|
||||
|
|
|
|||
46
IdleSpectator/ChronicleCharacterSummary.cs
Normal file
46
IdleSpectator/ChronicleCharacterSummary.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>One row in the Lore panel Character History tab.</summary>
|
||||
public sealed class ChronicleCharacterSummary
|
||||
{
|
||||
public long UnitId;
|
||||
public string Name = "";
|
||||
public string SpeciesId = "";
|
||||
public int HistoryCount;
|
||||
public bool IsFavorite;
|
||||
public bool IsAlive;
|
||||
public bool IsRecent;
|
||||
public int RecentIndex = -1;
|
||||
|
||||
public string Title
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(Name))
|
||||
{
|
||||
return SpeciesId ?? "creature";
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(SpeciesId))
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
|
||||
return Name + " (" + SpeciesId + ")";
|
||||
}
|
||||
}
|
||||
|
||||
public string Detail
|
||||
{
|
||||
get
|
||||
{
|
||||
string hist = HistoryCount <= 0 ? "no history" : HistoryCount + " events";
|
||||
if (IsRecent)
|
||||
{
|
||||
return "recent · " + hist;
|
||||
}
|
||||
|
||||
return hist;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -461,23 +461,96 @@ internal static class HarnessScenarios
|
|||
Step("ch18f", "assert", expect: "is_favorite", value: "true"),
|
||||
Step("ch18g", "dossier_favorite", value: "false"),
|
||||
Step("ch18h", "assert", expect: "is_favorite", value: "false"),
|
||||
// Full per-unit buffer: auto-scroll + history column must fill the dossier body.
|
||||
// Dossier peeks 3 lines; books button opens full history in Lore and pauses idle.
|
||||
Step("ch18h1", "chronicle_force", label: "Hist", count: 40),
|
||||
Step("ch18h2", "wait", wait: 0.25f),
|
||||
Step("ch18k", "assert", expect: "dossier_history_shown", value: "40", label: "exact"),
|
||||
Step("ch18k2", "assert", expect: "dossier_history_scrollable"),
|
||||
Step("ch18k", "assert", expect: "dossier_history_shown", value: "3", label: "exact"),
|
||||
Step("ch18k3", "assert", expect: "dossier_history_fills_body"),
|
||||
Step("ch18l", "assert", expect: "caption_layout_ok"),
|
||||
Step("ch18l1", "assert", expect: "hud_no_overlap"),
|
||||
Step("ch18l2", "screenshot", value: "hud-dossier-expanded.png"),
|
||||
Step("ch18l3", "wait", wait: 0.55f),
|
||||
Step("ch18l2", "screenshot", value: "hud-dossier-peek.png"),
|
||||
Step("ch18l3", "wait", wait: 0.45f),
|
||||
// L shortcut syncs to the focused dossier unit (Follow mode) and locks idle.
|
||||
Step("ch18m", "lore_open_focus"),
|
||||
Step("ch18n", "wait", wait: 0.3f),
|
||||
Step("ch18o", "assert", expect: "idle", value: "false"),
|
||||
Step("ch18o2", "assert", expect: "lore_tab", value: "characters"),
|
||||
Step("ch18o3", "assert", expect: "lore_detail", value: "true"),
|
||||
Step("ch18o3b", "assert", expect: "lore_follow", value: "true"),
|
||||
Step("ch18o4", "assert", expect: "lore_history_shown", value: "40", label: "min"),
|
||||
Step("ch18o4b", "assert", expect: "dossier_lore_history_match"),
|
||||
Step("ch18o4c", "assert", expect: "dossier_pinned", value: "true"),
|
||||
Step("ch18o4d", "assert", expect: "lore_zoom_blocked"),
|
||||
Step("ch18o5", "screenshot", value: "hud-lore-unit-history.png"),
|
||||
Step("ch18o6", "wait", wait: 0.55f),
|
||||
// Banner used to expire at 5s and hide the dossier - pin must outlive that.
|
||||
Step("ch18o7", "wait", wait: 5.5f),
|
||||
Step("ch18o8", "assert", expect: "dossier_pinned", value: "true"),
|
||||
Step("ch18o9", "assert", expect: "idle", value: "false"),
|
||||
Step("ch18o10", "assert", expect: "dossier_lore_history_match"),
|
||||
Step("ch18p", "assert", expect: "hud_no_overlap"),
|
||||
// Resume idle: Follow mode Lore must retarget when dossier focus changes.
|
||||
Step("ch18f1", "spectator", value: "on"),
|
||||
Step("ch18f2", "wait", wait: 0.2f),
|
||||
Step("ch18f3", "spawn", asset: "rabbit", count: 1),
|
||||
Step("ch18f4", "focus", asset: "auto"),
|
||||
Step("ch18f5", "wait", wait: 0.35f),
|
||||
Step("ch18f6", "assert", expect: "lore_follows_focus"),
|
||||
Step("ch18f7", "assert", expect: "lore_follow", value: "true"),
|
||||
// Books button still opens Follow mode.
|
||||
Step("ch18p0", "lore_close"),
|
||||
Step("ch18p0b", "dossier_history_open"),
|
||||
Step("ch18p0c", "wait", wait: 0.2f),
|
||||
Step("ch18p0d", "assert", expect: "dossier_lore_history_match"),
|
||||
Step("ch18p0e", "assert", expect: "idle", value: "false"),
|
||||
Step("ch18p0f", "assert", expect: "lore_follow", value: "true"),
|
||||
// Empty-history subject must not keep a previous unit's peek lines.
|
||||
Step("ch18p1", "lore_back"),
|
||||
Step("ch18p2", "spawn", asset: "cow", count: 1),
|
||||
Step("ch18p3", "focus", asset: "auto"),
|
||||
Step("ch18p4", "wait", wait: 0.25f),
|
||||
Step("ch18p5", "dossier_history_open"),
|
||||
Step("ch18p6", "wait", wait: 0.25f),
|
||||
Step("ch18p7", "assert", expect: "idle", value: "false"),
|
||||
Step("ch18p8", "assert", expect: "lore_detail", value: "true"),
|
||||
Step("ch18p9", "assert", expect: "dossier_history_shown", value: "0", label: "exact"),
|
||||
Step("ch18p10", "assert", expect: "lore_history_shown", value: "0", label: "exact"),
|
||||
Step("ch18p11", "assert", expect: "dossier_lore_history_match"),
|
||||
Step("ch18p12", "screenshot", value: "hud-lore-empty-history.png"),
|
||||
Step("ch18p13", "wait", wait: 0.55f),
|
||||
// Fallen (dead) archive: pinned, not follow-mode.
|
||||
Step("ch18d1", "lore_back"),
|
||||
Step("ch18d2", "chronicle_orphan", label: "Oldbone", asset: "skeleton", value: "Fallen", count: 5),
|
||||
Step("ch18d3", "lore_pick_fallen", value: "Oldbone"),
|
||||
Step("ch18d4", "wait", wait: 0.25f),
|
||||
Step("ch18d5", "assert", expect: "lore_detail", value: "true"),
|
||||
Step("ch18d6", "assert", expect: "lore_follow", value: "false"),
|
||||
Step("ch18d7", "assert", expect: "lore_history_shown", value: "5", label: "exact"),
|
||||
Step("ch18d8", "screenshot", value: "hud-lore-fallen.png"),
|
||||
Step("ch18d9", "wait", wait: 0.55f),
|
||||
// Character list browse (pinned archive).
|
||||
Step("ch18q", "lore_tab", value: "characters"),
|
||||
Step("ch18q0", "lore_back"),
|
||||
Step("ch18q2", "assert", expect: "lore_tab", value: "characters"),
|
||||
Step("ch18q3", "assert", expect: "lore_recent", value: "1", label: "min"),
|
||||
Step("ch18q4", "lore_pick", value: "auto"),
|
||||
Step("ch18q5", "wait", wait: 0.25f),
|
||||
Step("ch18q6", "assert", expect: "idle", value: "false"),
|
||||
Step("ch18q7", "assert", expect: "lore_detail", value: "true"),
|
||||
Step("ch18q7b", "assert", expect: "lore_follow", value: "false"),
|
||||
Step("ch18q8", "screenshot", value: "hud-lore-characters.png"),
|
||||
Step("ch18q9", "wait", wait: 0.55f),
|
||||
// Resume idle: pinned list pick flips back to Follow mode.
|
||||
Step("ch18q10", "spectator", value: "on"),
|
||||
Step("ch18q11", "wait", wait: 0.25f),
|
||||
Step("ch18q11b", "assert", expect: "lore_follow", value: "true"),
|
||||
Step("ch18q11c", "spawn", asset: "sheep", count: 1),
|
||||
Step("ch18q11d", "focus", asset: "auto"),
|
||||
Step("ch18q11e", "wait", wait: 0.35f),
|
||||
Step("ch18q11f", "assert", expect: "lore_follows_focus"),
|
||||
Step("ch18q12", "lore_tab", value: "world"),
|
||||
Step("ch19", "assert", expect: "focus", value: "true"),
|
||||
Step("ch20", "assert", expect: "health"),
|
||||
Step("ch21", "chronicle_jump"),
|
||||
Step("ch22", "wait", wait: 0.25f),
|
||||
Step("ch23", "assert", expect: "focus", value: "true"),
|
||||
Step("ch24", "assert", expect: "dossier_contains", value: "auto"),
|
||||
Step("ch25", "assert", expect: "no_bad"),
|
||||
Step("ch99", "snapshot"),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -113,6 +113,26 @@ public static class InterestDirector
|
|||
InterestCollector.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop idle auto-follow the same way manual input does (quiet), for Lore history browsing.
|
||||
/// Prefer <see cref="ChronicleHud.OpenUnitHistory"/> which focuses first then banners.
|
||||
/// </summary>
|
||||
public static void PauseForBrowsing(string banner = "Paused (viewing history)")
|
||||
{
|
||||
if (SpectatorMode.Active)
|
||||
{
|
||||
SpectatorMode.SetActive(false, quiet: true);
|
||||
}
|
||||
|
||||
WatchCaption.PinWhilePaused();
|
||||
if (!string.IsNullOrEmpty(banner))
|
||||
{
|
||||
WatchCaption.ShowStatusBanner(banner);
|
||||
}
|
||||
|
||||
LogService.LogInfo("[IdleSpectator] Spectator paused (viewing history)");
|
||||
}
|
||||
|
||||
public static void Update()
|
||||
{
|
||||
if (!SpectatorMode.Active || !Config.game_loaded || World.world == null)
|
||||
|
|
@ -120,6 +140,8 @@ public static class InterestDirector
|
|||
return;
|
||||
}
|
||||
|
||||
WatchCaption.ClearPausePin();
|
||||
|
||||
// During harness batches, freeze unless director_run explicitly unfreezes.
|
||||
if (AgentHarness.FreezeDirector)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"enabled": "Enable Idle Spectator (I)",
|
||||
"show_watch_reasons": "Show brief watch tips",
|
||||
"show_dossier_caption": "Show creature dossier caption while watching",
|
||||
"chronicle_enabled": "Enable World Memory panel (F9) - ages, landmarks, legends",
|
||||
"chronicle_enabled": "Enable Lore panel (L) - World Memory + Character History",
|
||||
"debug_state_probe": "Log focus/power-bar state (for debugging)",
|
||||
"IdleSpectator Chronicle": "World Memory"
|
||||
"IdleSpectator Chronicle": "Lore"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ public class ModClass : MonoBehaviour, IMod, IConfigurable, ILocalizable
|
|||
_harmony = new Harmony(pModDecl.UID);
|
||||
_harmony.PatchAll(typeof(ModClass).Assembly);
|
||||
LogService.LogInfo($"[{pModDecl.Name}]: Harmony patches applied");
|
||||
LogService.LogInfo($"[{pModDecl.Name}]: Press I for Idle Spectator, F9 for Chronicle");
|
||||
LogService.LogInfo($"[{pModDecl.Name}]: Press I for Idle Spectator, L for Lore");
|
||||
Chronicle.EnsureWindow();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,14 +33,16 @@ public static class SpectatorMode
|
|||
}
|
||||
|
||||
Active = active;
|
||||
if (Active)
|
||||
{
|
||||
InterestDirector.OnSpectatorEnabled();
|
||||
SpeciesDiscovery.OnSpectatorEnabled();
|
||||
Chronicle.OnSpectatorEnabled();
|
||||
FocusRelationshipArrows.OnSpectatorEnabled();
|
||||
LogService.LogInfo("[IdleSpectator] Spectator mode enabled (I or any input to stop; F9 chronicle)");
|
||||
}
|
||||
if (Active)
|
||||
{
|
||||
WatchCaption.ClearPausePin();
|
||||
InterestDirector.OnSpectatorEnabled();
|
||||
SpeciesDiscovery.OnSpectatorEnabled();
|
||||
Chronicle.OnSpectatorEnabled();
|
||||
ChronicleHud.OnIdleResumed();
|
||||
FocusRelationshipArrows.OnSpectatorEnabled();
|
||||
LogService.LogInfo("[IdleSpectator] Spectator mode enabled (I or any input to stop; L for Lore)");
|
||||
}
|
||||
else
|
||||
{
|
||||
InterestDirector.OnSpectatorDisabled();
|
||||
|
|
|
|||
|
|
@ -30,14 +30,9 @@ public static class WatchCaption
|
|||
private const float HistoryLineMinH = 13f;
|
||||
private const float HistoryLineMaxH = 72f;
|
||||
private const float Gap = 2f;
|
||||
private const int HistoryMaxSlots = Chronicle.MaxHistoryPerUnit;
|
||||
private const int HistoryPeekMax = 3;
|
||||
private const float BtnSize = 16f;
|
||||
private const float StatusBannerSeconds = 5f;
|
||||
/// <summary>
|
||||
/// Visible history peek (~3 lines). Full buffer stays in the scroll content.
|
||||
/// </summary>
|
||||
private const int HistoryPeekLines = 3;
|
||||
private const float HistoryBodyMax = HistoryLineMinH * HistoryPeekLines;
|
||||
|
||||
/// <summary>History column grows with content until the dossier hits <see cref="PanelWidthMax"/>.</summary>
|
||||
private static float HistoryColMaxW =>
|
||||
|
|
@ -68,18 +63,18 @@ public static class WatchCaption
|
|||
private static readonly TraitSlot[] _traitSlots = new TraitSlot[3];
|
||||
private static GameObject _traitsRow;
|
||||
|
||||
private static GameObject _historyViewport;
|
||||
private static ScrollRect _historyScroll;
|
||||
private static GameObject _historyScrollbar;
|
||||
private static GameObject _historyCol;
|
||||
private static readonly HistorySlot[] _historySlots = new HistorySlot[HistoryMaxSlots];
|
||||
private static readonly float[] _historySlotHeights = new float[HistoryMaxSlots];
|
||||
private static readonly HistorySlot[] _historySlots = new HistorySlot[HistoryPeekMax];
|
||||
private static readonly float[] _historySlotHeights = new float[HistoryPeekMax];
|
||||
private static float _historyColW = HistoryColMinW;
|
||||
private static int _lastHistoryCount = -1;
|
||||
private static long _lastHistorySubjectId;
|
||||
|
||||
private static Button _favoriteBtn;
|
||||
private static Image _favoriteIcon;
|
||||
private static Button _historyBtn;
|
||||
private static Image _historyIcon;
|
||||
private static bool _pinnedWhilePaused;
|
||||
|
||||
private static UnitDossier _current;
|
||||
private static Actor _boundActor;
|
||||
|
|
@ -115,14 +110,20 @@ public static class WatchCaption
|
|||
/// <summary>Harness: how many history lines are currently visible on the dossier.</summary>
|
||||
public static int LastHistoryShown { get; private set; }
|
||||
|
||||
/// <summary>True when history content is taller than the viewport (ScrollRect active).</summary>
|
||||
public static bool LastHistoryScrollable { get; private set; }
|
||||
/// <summary>Current dossier subject id (0 if none).</summary>
|
||||
public static long CurrentUnitId => _current != null ? _current.UnitId : 0;
|
||||
|
||||
/// <summary>True when dossier is shown while idle is paused for Lore browsing.</summary>
|
||||
public static bool PinnedWhilePaused => _pinnedWhilePaused;
|
||||
|
||||
/// <summary>True when the history column fills the body width (no empty strip beside it).</summary>
|
||||
public static bool LastHistoryFillsBody { get; private set; }
|
||||
|
||||
/// <summary>Deprecated: expand control removed; kept as alias of <see cref="LastHistoryScrollable"/>.</summary>
|
||||
public static bool HistoryExpanded => LastHistoryScrollable;
|
||||
/// <summary>Legacy alias; dossier history is no longer scrollable.</summary>
|
||||
public static bool LastHistoryScrollable => false;
|
||||
|
||||
/// <summary>Legacy alias for harness compatibility.</summary>
|
||||
public static bool HistoryExpanded => false;
|
||||
|
||||
/// <summary>Harness: comma-joined top trait labels currently shown.</summary>
|
||||
public static string LastTraitsPreview { get; private set; } = "";
|
||||
|
|
@ -131,6 +132,9 @@ public static class WatchCaption
|
|||
|
||||
public static bool LastLayoutOk { get; private set; }
|
||||
|
||||
/// <summary>True when the dossier card GameObject is active.</summary>
|
||||
public static bool Visible => _visible && _root != null && _root.activeSelf;
|
||||
|
||||
/// <summary>Screen-pixel rect of the dossier card (Unity bottom-left origin), if visible.</summary>
|
||||
public static bool TryGetScreenPixelRect(out Rect pixelRect)
|
||||
{
|
||||
|
|
@ -188,7 +192,7 @@ public static class WatchCaption
|
|||
return false;
|
||||
}
|
||||
|
||||
if (IsPointerOverButton(_favoriteBtn))
|
||||
if (IsPointerOverButton(_favoriteBtn) || IsPointerOverButton(_historyBtn))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
@ -239,6 +243,39 @@ public static class WatchCaption
|
|||
$"[IdleSpectator] Favorite toggled name={SafeActorName(actor)} favorite={actor.isFavorite()}");
|
||||
}
|
||||
|
||||
public static void PinWhilePaused()
|
||||
{
|
||||
_pinnedWhilePaused = true;
|
||||
if (ModSettings.ShowDossierCaption)
|
||||
{
|
||||
SetVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ClearPausePin()
|
||||
{
|
||||
_pinnedWhilePaused = false;
|
||||
}
|
||||
|
||||
/// <summary>Open this unit's full history in the Lore panel and pause idle auto-follow.</summary>
|
||||
public static void OpenFullHistoryInLore()
|
||||
{
|
||||
// Prefer the live camera focus so a pinned dossier cannot open the wrong unit's lore.
|
||||
long id = Chronicle.CurrentHistorySubjectId();
|
||||
if (id == 0 && _current != null)
|
||||
{
|
||||
id = _current.UnitId;
|
||||
}
|
||||
|
||||
if (id == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ChronicleHud.OpenUnitHistory(id, pauseIdle: true, followFocus: true);
|
||||
LogService.LogInfo($"[IdleSpectator] Open full history in Lore unitId={id}");
|
||||
}
|
||||
|
||||
private static string SafeActorName(Actor actor)
|
||||
{
|
||||
try
|
||||
|
|
@ -266,8 +303,8 @@ public static class WatchCaption
|
|||
LastCaptionText = "";
|
||||
LastHistoryPreview = "";
|
||||
LastHistoryShown = 0;
|
||||
LastHistoryScrollable = false;
|
||||
LastHistoryFillsBody = false;
|
||||
_pinnedWhilePaused = false;
|
||||
LastTraitsPreview = "";
|
||||
LastPanelSize = Vector2.zero;
|
||||
LastLayoutOk = false;
|
||||
|
|
@ -312,7 +349,9 @@ public static class WatchCaption
|
|||
}
|
||||
|
||||
LastCaptionText = (LastHeadline ?? "Idle Spectator") + " | " + message;
|
||||
int hist = CountActiveHistorySlots();
|
||||
// Re-fill from the current subject so Relayout cannot resurrect stale history slots
|
||||
// from a previously focused unit (activeSelf stays true while the column is hidden).
|
||||
int hist = _current != null ? FillHistory(_current.UnitId) : 0;
|
||||
int traits = CountActiveTraitSlots();
|
||||
bool hasTask = _taskText != null && _taskText.gameObject.activeSelf;
|
||||
bool hasBody = _boundActor != null || hist > 0 || _current != null;
|
||||
|
|
@ -356,7 +395,11 @@ public static class WatchCaption
|
|||
|
||||
public static void SetFromActor(Actor actor, InterestTier? tier = null, string label = null)
|
||||
{
|
||||
ClearStatusBanner();
|
||||
if (!_pinnedWhilePaused)
|
||||
{
|
||||
ClearStatusBanner();
|
||||
}
|
||||
|
||||
UnitDossier dossier = UnitDossier.FromActor(actor, tier, label);
|
||||
_current = dossier;
|
||||
_boundActor = actor;
|
||||
|
|
@ -365,12 +408,39 @@ public static class WatchCaption
|
|||
LastCaptionText = dossier.CaptionText ?? "";
|
||||
EnsureBuilt();
|
||||
ApplyVisual(actor, dossier);
|
||||
SetVisible(ModSettings.ShowDossierCaption && SpectatorMode.Active);
|
||||
SetVisible(ModSettings.ShowDossierCaption && (SpectatorMode.Active || _pinnedWhilePaused));
|
||||
LogService.LogInfo("[IdleSpectator][CAPTION] " + LastCaptionText.Replace("\n", " | "));
|
||||
}
|
||||
|
||||
public static void Update()
|
||||
{
|
||||
// History / manual pause: keep dossier pinned even after the banner timer expires.
|
||||
if (_pinnedWhilePaused && ModSettings.ShowDossierCaption)
|
||||
{
|
||||
if (SpectatorMode.Active)
|
||||
{
|
||||
ClearPausePin();
|
||||
ClearStatusBanner();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_visible)
|
||||
{
|
||||
SetVisible(true);
|
||||
}
|
||||
|
||||
// Keep the pause reason visible while browsing.
|
||||
if (!string.IsNullOrEmpty(_statusBanner))
|
||||
{
|
||||
_statusBannerUntil = Time.unscaledTime + 1f;
|
||||
}
|
||||
|
||||
RefreshLivePortrait();
|
||||
RefreshHistoryIfChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (HasStatusBanner())
|
||||
{
|
||||
if (!_visible && ModSettings.ShowDossierCaption)
|
||||
|
|
@ -435,6 +505,11 @@ public static class WatchCaption
|
|||
|
||||
private static int CountActiveHistorySlots()
|
||||
{
|
||||
if (_historyCol == null || !_historyCol.activeSelf)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int n = 0;
|
||||
for (int i = 0; i < _historySlots.Length; i++)
|
||||
{
|
||||
|
|
@ -510,6 +585,7 @@ public static class WatchCaption
|
|||
Front(_taskText);
|
||||
Front(_sexIcon);
|
||||
Front(_favoriteBtn);
|
||||
Front(_historyBtn);
|
||||
}
|
||||
|
||||
private static void RefreshHistoryIfChanged()
|
||||
|
|
@ -691,10 +767,10 @@ public static class WatchCaption
|
|||
{
|
||||
LastHistoryPreview = "";
|
||||
LastHistoryShown = 0;
|
||||
LastHistoryScrollable = false;
|
||||
if (_historyViewport != null)
|
||||
LastHistoryFillsBody = false;
|
||||
if (_historyCol != null)
|
||||
{
|
||||
_historyViewport.SetActive(false);
|
||||
_historyCol.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -712,32 +788,49 @@ public static class WatchCaption
|
|||
private static int FillHistory(long unitId)
|
||||
{
|
||||
IReadOnlyList<ChronicleEntry> entries =
|
||||
Chronicle.LatestForSubject(unitId, HistoryMaxSlots);
|
||||
bool subjectChanged = _lastHistorySubjectId != unitId;
|
||||
Chronicle.LatestForSubject(unitId, HistoryPeekMax);
|
||||
_lastHistorySubjectId = unitId;
|
||||
_lastHistoryCount = Chronicle.HistoryCountFor(unitId);
|
||||
|
||||
LastHistoryPreview = "";
|
||||
LastHistoryShown = 0;
|
||||
LastHistoryScrollable = false;
|
||||
_historyColW = HistoryColMinW;
|
||||
for (int i = 0; i < _historySlotHeights.Length; i++)
|
||||
{
|
||||
_historySlotHeights[i] = HistoryLineMinH;
|
||||
}
|
||||
|
||||
if (_historyViewport == null || _historyCol == null)
|
||||
if (_historyCol == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (entries == null || entries.Count == 0)
|
||||
{
|
||||
_historyViewport.SetActive(false);
|
||||
for (int i = 0; i < _historySlots.Length; i++)
|
||||
{
|
||||
HistorySlot slot = _historySlots[i];
|
||||
if (slot?.Root != null)
|
||||
{
|
||||
slot.Root.SetActive(false);
|
||||
}
|
||||
|
||||
if (slot?.Label != null)
|
||||
{
|
||||
slot.Label.text = "";
|
||||
}
|
||||
|
||||
_historySlotHeights[i] = 0f;
|
||||
}
|
||||
|
||||
_historyCol.SetActive(false);
|
||||
LastHistoryShown = 0;
|
||||
LastHistoryFillsBody = false;
|
||||
LastHistoryPreview = "";
|
||||
return 0;
|
||||
}
|
||||
|
||||
_historyViewport.SetActive(true);
|
||||
_historyCol.SetActive(true);
|
||||
|
||||
// Content-based minimum width; Relayout stretches to fill the panel body.
|
||||
float widestLabel = 0f;
|
||||
|
|
@ -773,11 +866,6 @@ public static class WatchCaption
|
|||
HistoryColMaxW);
|
||||
ApplyHistorySlotContents(entries, _historyColW);
|
||||
LastHistoryShown = CountActiveHistorySlots();
|
||||
if (subjectChanged && _historyScroll != null)
|
||||
{
|
||||
_historyScroll.verticalNormalizedPosition = 1f;
|
||||
}
|
||||
|
||||
return LastHistoryShown;
|
||||
}
|
||||
|
||||
|
|
@ -889,8 +977,7 @@ public static class WatchCaption
|
|||
|
||||
private static float MeasureHistoryColumnHeight(int historyCount)
|
||||
{
|
||||
float contentH = MeasureHistoryContentHeight(historyCount);
|
||||
return Mathf.Min(contentH, HistoryBodyMax);
|
||||
return MeasureHistoryContentHeight(historyCount);
|
||||
}
|
||||
|
||||
private static void Relayout(bool hasBody, int traitCount, bool hasTask, bool hasReason, int historyCount)
|
||||
|
|
@ -1063,6 +1150,8 @@ public static class WatchCaption
|
|||
|
||||
x += 6f;
|
||||
PlaceHeaderButton(_favoriteBtn, x, yFromTop);
|
||||
x += BtnSize + 2f;
|
||||
PlaceHeaderButton(_historyBtn, x, yFromTop);
|
||||
x += BtnSize;
|
||||
|
||||
RefreshFavoriteVisual(_boundActor);
|
||||
|
|
@ -1100,7 +1189,7 @@ public static class WatchCaption
|
|||
x += SexSize;
|
||||
}
|
||||
|
||||
x += 6f + BtnSize;
|
||||
x += 6f + BtnSize + 2f + BtnSize;
|
||||
return Mathf.Max(0f, x - PadX);
|
||||
}
|
||||
|
||||
|
|
@ -1179,65 +1268,24 @@ public static class WatchCaption
|
|||
{
|
||||
DossierAvatar.Place(PadX, yFromTop, LiveMax);
|
||||
|
||||
if (_historyViewport == null || _historyCol == null)
|
||||
if (_historyCol == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (historyCount <= 0)
|
||||
{
|
||||
_historyViewport.SetActive(false);
|
||||
if (_historyScrollbar != null)
|
||||
{
|
||||
_historyScrollbar.SetActive(false);
|
||||
}
|
||||
|
||||
LastHistoryScrollable = false;
|
||||
_historyCol.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_historyViewport.SetActive(true);
|
||||
RectTransform viewport = _historyViewport.GetComponent<RectTransform>();
|
||||
viewport.anchorMin = new Vector2(0f, 1f);
|
||||
viewport.anchorMax = new Vector2(0f, 1f);
|
||||
viewport.pivot = new Vector2(0f, 1f);
|
||||
viewport.anchoredPosition = new Vector2(PadX + LiveMax + 4f, -yFromTop);
|
||||
viewport.sizeDelta = new Vector2(_historyColW, bodyH);
|
||||
|
||||
float contentH = MeasureHistoryContentHeight(historyCount);
|
||||
_historyCol.SetActive(true);
|
||||
RectTransform col = _historyCol.GetComponent<RectTransform>();
|
||||
col.anchorMin = new Vector2(0f, 1f);
|
||||
col.anchorMax = new Vector2(1f, 1f);
|
||||
col.pivot = new Vector2(0.5f, 1f);
|
||||
col.anchoredPosition = Vector2.zero;
|
||||
col.sizeDelta = new Vector2(0f, contentH);
|
||||
|
||||
bool scrollable = contentH > bodyH + 0.5f;
|
||||
LastHistoryScrollable = scrollable;
|
||||
float scrollGutter = scrollable ? 5f : 0f;
|
||||
if (_historyScroll != null)
|
||||
{
|
||||
_historyScroll.enabled = true;
|
||||
_historyScroll.vertical = scrollable;
|
||||
if (!scrollable)
|
||||
{
|
||||
_historyScroll.verticalNormalizedPosition = 1f;
|
||||
}
|
||||
}
|
||||
|
||||
if (_historyScrollbar != null)
|
||||
{
|
||||
_historyScrollbar.SetActive(scrollable);
|
||||
if (scrollable)
|
||||
{
|
||||
RectTransform barRt = _historyScrollbar.GetComponent<RectTransform>();
|
||||
barRt.anchorMin = new Vector2(0f, 1f);
|
||||
barRt.anchorMax = new Vector2(0f, 1f);
|
||||
barRt.pivot = new Vector2(1f, 1f);
|
||||
barRt.anchoredPosition = new Vector2(PadX + LiveMax + 4f + _historyColW, -yFromTop);
|
||||
barRt.sizeDelta = new Vector2(4f, bodyH);
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
float top = 0f;
|
||||
for (int i = 0; i < _historySlots.Length; i++)
|
||||
|
|
@ -1254,7 +1302,7 @@ public static class WatchCaption
|
|||
rt.anchorMax = new Vector2(1f, 1f);
|
||||
rt.pivot = new Vector2(0.5f, 1f);
|
||||
rt.offsetMin = new Vector2(0f, -(top + lineH));
|
||||
rt.offsetMax = new Vector2(-scrollGutter, -top);
|
||||
rt.offsetMax = new Vector2(0f, -top);
|
||||
top += lineH;
|
||||
|
||||
if (slot.Icon != null)
|
||||
|
|
@ -1403,8 +1451,8 @@ public static class WatchCaption
|
|||
|| LineInside(_reasonText.GetComponent<RectTransform>(), panelHeight))
|
||||
&& (_traitsRow == null || !_traitsRow.activeSelf
|
||||
|| LineInside(_traitsRow.GetComponent<RectTransform>(), panelHeight))
|
||||
&& (_historyViewport == null || !_historyViewport.activeSelf
|
||||
|| LineInside(_historyViewport.GetComponent<RectTransform>(), panelHeight));
|
||||
&& (_historyCol == null || !_historyCol.activeSelf
|
||||
|| LineInside(_historyCol.GetComponent<RectTransform>(), panelHeight));
|
||||
}
|
||||
|
||||
private static bool LineInside(RectTransform line, float panelHeight)
|
||||
|
|
@ -1449,9 +1497,9 @@ public static class WatchCaption
|
|||
private static void EnsureBuilt()
|
||||
{
|
||||
if (_root != null && (_nameText == null || _nameText.transform.parent != _root.transform
|
||||
|| _historyViewport == null || _historyScroll == null
|
||||
|| _historyScrollbar == null
|
||||
|| _root.transform.Find("ExpandHistBtn") != null))
|
||||
|| _historyCol == null
|
||||
|| _historyBtn == null
|
||||
|| _root.transform.Find("HistoryScroll") != null))
|
||||
{
|
||||
// Stale HUD from an older build (nested nametag row / non-scroll history) - rebuild.
|
||||
try
|
||||
|
|
@ -1474,12 +1522,11 @@ public static class WatchCaption
|
|||
_taskIcon = null;
|
||||
_taskText = null;
|
||||
_traitsRow = null;
|
||||
_historyViewport = null;
|
||||
_historyScroll = null;
|
||||
_historyScrollbar = null;
|
||||
_historyCol = null;
|
||||
_favoriteBtn = null;
|
||||
_favoriteIcon = null;
|
||||
_historyBtn = null;
|
||||
_historyIcon = null;
|
||||
DossierAvatar.ResetHost();
|
||||
}
|
||||
|
||||
|
|
@ -1513,56 +1560,8 @@ public static class WatchCaption
|
|||
// Avatar/history/traits first so nametag chips can SetAsLastSibling on top.
|
||||
DossierAvatar.EnsureHost(_root.transform, LiveMax);
|
||||
|
||||
_historyViewport = new GameObject(
|
||||
"HistoryScroll",
|
||||
typeof(RectTransform),
|
||||
typeof(Image),
|
||||
typeof(ScrollRect),
|
||||
typeof(RectMask2D));
|
||||
_historyViewport.transform.SetParent(_root.transform, false);
|
||||
Image historyVpBg = _historyViewport.GetComponent<Image>();
|
||||
historyVpBg.color = new Color(0f, 0f, 0f, 0.01f);
|
||||
historyVpBg.raycastTarget = true;
|
||||
_historyScroll = _historyViewport.GetComponent<ScrollRect>();
|
||||
_historyScroll.horizontal = false;
|
||||
_historyScroll.vertical = true;
|
||||
_historyScroll.movementType = ScrollRect.MovementType.Clamped;
|
||||
_historyScroll.scrollSensitivity = 24f;
|
||||
_historyScroll.viewport = _historyViewport.GetComponent<RectTransform>();
|
||||
|
||||
_historyCol = new GameObject("HistoryCol", typeof(RectTransform));
|
||||
_historyCol.transform.SetParent(_historyViewport.transform, false);
|
||||
_historyScroll.content = _historyCol.GetComponent<RectTransform>();
|
||||
|
||||
_historyScrollbar = new GameObject(
|
||||
"HistoryScrollbar",
|
||||
typeof(RectTransform),
|
||||
typeof(Image),
|
||||
typeof(Scrollbar));
|
||||
_historyScrollbar.transform.SetParent(_root.transform, false);
|
||||
Image barBg = _historyScrollbar.GetComponent<Image>();
|
||||
barBg.color = new Color(0.12f, 0.13f, 0.15f, 0.55f);
|
||||
barBg.raycastTarget = true;
|
||||
|
||||
GameObject handleGo = new GameObject("Handle", typeof(RectTransform), typeof(Image));
|
||||
handleGo.transform.SetParent(_historyScrollbar.transform, false);
|
||||
RectTransform handleRt = handleGo.GetComponent<RectTransform>();
|
||||
handleRt.anchorMin = Vector2.zero;
|
||||
handleRt.anchorMax = Vector2.one;
|
||||
handleRt.offsetMin = new Vector2(0.5f, 0.5f);
|
||||
handleRt.offsetMax = new Vector2(-0.5f, -0.5f);
|
||||
Image handleImg = handleGo.GetComponent<Image>();
|
||||
handleImg.color = new Color(0.72f, 0.74f, 0.78f, 0.85f);
|
||||
handleImg.raycastTarget = true;
|
||||
|
||||
Scrollbar scrollbar = _historyScrollbar.GetComponent<Scrollbar>();
|
||||
scrollbar.direction = Scrollbar.Direction.BottomToTop;
|
||||
scrollbar.handleRect = handleRt;
|
||||
scrollbar.targetGraphic = handleImg;
|
||||
scrollbar.numberOfSteps = 0;
|
||||
_historyScroll.verticalScrollbar = scrollbar;
|
||||
_historyScroll.verticalScrollbarVisibility = ScrollRect.ScrollbarVisibility.Permanent;
|
||||
_historyScrollbar.SetActive(false);
|
||||
_historyCol.transform.SetParent(_root.transform, false);
|
||||
|
||||
for (int i = 0; i < _historySlots.Length; i++)
|
||||
{
|
||||
|
|
@ -1625,7 +1624,7 @@ public static class WatchCaption
|
|||
BuildHeaderButtons();
|
||||
|
||||
DossierAvatar.SetActive(false);
|
||||
_historyViewport.SetActive(false);
|
||||
_historyCol.SetActive(false);
|
||||
_traitsRow.SetActive(false);
|
||||
_reasonText.gameObject.SetActive(false);
|
||||
_levelIcon.gameObject.SetActive(false);
|
||||
|
|
@ -1642,6 +1641,12 @@ public static class WatchCaption
|
|||
{
|
||||
_favoriteBtn = BuildIconButton(_root.transform, "FavoriteBtn", HudIcons.Favorite(), ToggleFavorite);
|
||||
_favoriteIcon = _favoriteBtn.transform.Find("Icon")?.GetComponent<Image>();
|
||||
_historyBtn = BuildIconButton(_root.transform, "HistoryBtn", HudIcons.ExpandHistory(), OpenFullHistoryInLore);
|
||||
_historyIcon = _historyBtn.transform.Find("Icon")?.GetComponent<Image>();
|
||||
if (_historyIcon != null)
|
||||
{
|
||||
_historyIcon.color = new Color(0.75f, 0.78f, 0.85f, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
private static Button BuildIconButton(Transform parent, string name, Sprite icon, UnityAction onClick)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "IdleSpectator",
|
||||
"author": "dazed",
|
||||
"version": "0.10.10",
|
||||
"description": "AFK Idle Spectator (I) + World Memory (F9). Dossier favorite + 3-line scrollable history.",
|
||||
"version": "0.12.6",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). L/books follow focus; Characters list is a pinned archive including Fallen.",
|
||||
"GUID": "com.dazed.idlespectator"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue