Enhance Lore interactions and improve dismiss behavior.
- Allow multiple named entries for grave force command on the same tile - Update dismiss behavior to clear Lore and dossier while keeping idle off - Introduce new methods for managing dismiss grace period - Refactor focus handling for fallen subjects and their killers - Increment version to 0.28.13 in mod.json
This commit is contained in:
parent
c11b87aa2e
commit
8a4314be6f
7 changed files with 181 additions and 55 deletions
|
|
@ -1846,9 +1846,16 @@ public static class AgentHarness
|
|||
|
||||
case "grave_force":
|
||||
{
|
||||
// value=name, count=entries on same tile, tier="dx,dy" tile offset from focus/camera
|
||||
string gName = !string.IsNullOrEmpty(cmd.value) ? cmd.value : "GraveDummy";
|
||||
int n = Math.Max(1, cmd.count);
|
||||
// value=name or "A,B,C" (same tile), count=entries, tier="dx,dy" offset from focus/camera.
|
||||
// Comma names / count>1 share one captured tile so a walking focus cannot split the stack.
|
||||
string rawName = !string.IsNullOrEmpty(cmd.value) ? cmd.value : "GraveDummy";
|
||||
string[] named = rawName.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
for (int i = 0; i < named.Length; i++)
|
||||
{
|
||||
named[i] = named[i].Trim();
|
||||
}
|
||||
|
||||
int n = named.Length > 1 ? named.Length : Math.Max(1, cmd.count);
|
||||
Vector3 pos = Vector3.zero;
|
||||
if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null)
|
||||
{
|
||||
|
|
@ -1882,7 +1889,9 @@ public static class AgentHarness
|
|||
int before = GraveMarkers.EntryCountAt(tx, ty);
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
string nm = n > 1 ? gName + i : gName;
|
||||
string nm = named.Length > 1
|
||||
? named[i]
|
||||
: (n > 1 ? rawName + i : rawName);
|
||||
long id = Chronicle.ForceOrphanHistory(
|
||||
nm,
|
||||
!string.IsNullOrEmpty(cmd.asset) ? cmd.asset : "human",
|
||||
|
|
@ -2143,12 +2152,25 @@ public static class AgentHarness
|
|||
case "inspect_dismiss":
|
||||
{
|
||||
InspectUi.DismissAll();
|
||||
_cmdOk++;
|
||||
bool cleared = !SpectatorMode.Active
|
||||
&& !Chronicle.HudVisible
|
||||
&& !WatchCaption.Visible
|
||||
&& !ChronicleHud.GraveStackScopeActive
|
||||
&& !InspectUi.GraveSessionActive;
|
||||
if (cleared)
|
||||
{
|
||||
_cmdOk++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cmdFail++;
|
||||
}
|
||||
|
||||
Emit(
|
||||
cmd,
|
||||
ok: true,
|
||||
cleared,
|
||||
detail:
|
||||
$"graveSession={InspectUi.GraveSessionActive} lore={Chronicle.HudVisible} scope={ChronicleHud.GraveStackScopeActive}");
|
||||
$"cleared={cleared} idle={SpectatorMode.Active} lore={Chronicle.HudVisible} dossier={WatchCaption.Visible} scope={ChronicleHud.GraveStackScopeActive} graveSession={InspectUi.GraveSessionActive}");
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -6127,6 +6149,17 @@ public static class AgentHarness
|
|||
|
||||
switch (expect)
|
||||
{
|
||||
case "inspect_cleared":
|
||||
{
|
||||
pass = !SpectatorMode.Active
|
||||
&& !Chronicle.HudVisible
|
||||
&& !WatchCaption.Visible
|
||||
&& !ChronicleHud.GraveStackScopeActive
|
||||
&& !InspectUi.GraveSessionActive;
|
||||
detail =
|
||||
$"idle={SpectatorMode.Active} lore={Chronicle.HudVisible} dossier={WatchCaption.Visible} scope={ChronicleHud.GraveStackScopeActive} graveSession={InspectUi.GraveSessionActive}";
|
||||
break;
|
||||
}
|
||||
case "idle":
|
||||
case "spectator":
|
||||
{
|
||||
|
|
@ -7160,10 +7193,32 @@ public static class AgentHarness
|
|||
ChronicleEntry latest = Chronicle.Latest;
|
||||
string line = latest != null ? latest.Line : "";
|
||||
string display = latest != null ? latest.DisplayLine : "";
|
||||
pass = latest != null
|
||||
&& !string.IsNullOrEmpty(needle)
|
||||
&& line.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
detail = $"latest='{display}' needle='{needle}' date='{(latest != null ? latest.DateLabel : "")}'";
|
||||
bool inLatest = latest != null
|
||||
&& !string.IsNullOrEmpty(needle)
|
||||
&& line.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
// Live lover/friend patches can append after a death sample in the same wait;
|
||||
// still pass when the needle exists on the focus subject's recent history.
|
||||
bool inRecent = false;
|
||||
if (!inLatest && !string.IsNullOrEmpty(needle))
|
||||
{
|
||||
IReadOnlyList<ChronicleEntry> snap = Chronicle.SnapshotForFocus();
|
||||
for (int i = snap.Count - 1; i >= 0 && i >= snap.Count - 8; i--)
|
||||
{
|
||||
ChronicleEntry e = snap[i];
|
||||
if (e != null
|
||||
&& !string.IsNullOrEmpty(e.Line)
|
||||
&& e.Line.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
inRecent = true;
|
||||
display = e.DisplayLine;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pass = inLatest || inRecent;
|
||||
detail =
|
||||
$"latest='{(latest != null ? latest.DisplayLine : "")}' hit='{display}' needle='{needle}' date='{(latest != null ? latest.DateLabel : "")}' recent={inRecent}";
|
||||
break;
|
||||
}
|
||||
case "chronicle_latest_dated":
|
||||
|
|
@ -9096,10 +9151,10 @@ public static class AgentHarness
|
|||
pass = killerTag
|
||||
&& killerId != 0
|
||||
&& focusId == killerId
|
||||
&& WatchCaption.CurrentUnitId != 0
|
||||
&& WatchCaption.CurrentUnitId != focusId;
|
||||
&& WatchCaption.CurrentUnitId == focusId
|
||||
&& DossierAvatar.ShowingLiveAvatar;
|
||||
detail =
|
||||
$"fallenFocus={detailStr} focusId={focusId} dossierId={WatchCaption.CurrentUnitId}";
|
||||
$"fallenFocus={detailStr} focusId={focusId} dossierId={WatchCaption.CurrentUnitId} avatarLive={DossierAvatar.ShowingLiveAvatar}";
|
||||
break;
|
||||
}
|
||||
case "lore_zoom_blocked":
|
||||
|
|
|
|||
|
|
@ -2163,9 +2163,8 @@ public static class Chronicle
|
|||
|
||||
/// <summary>
|
||||
/// Camera for a dead subject.
|
||||
/// When <paramref name="preferPlace"/> is true (default), pan to death place / skull (no unit follow).
|
||||
/// Killer follow is only via <see cref="FocusFallenKiller"/>.
|
||||
/// Dossier always shows the fallen unit's last living state (not the killer).
|
||||
/// When <paramref name="preferPlace"/> is true (default), pan to death place / skull (no unit follow)
|
||||
/// and bind the dossier to the fallen unit. Killer follow is only via <see cref="FocusFallenKiller"/>.
|
||||
/// </summary>
|
||||
public static bool FocusFallenSubject(long unitId, bool preferPlace = true)
|
||||
{
|
||||
|
|
@ -2176,27 +2175,23 @@ public static class Chronicle
|
|||
return false;
|
||||
}
|
||||
|
||||
UnitDossier fallen = ResolveFallenDossier(unitId);
|
||||
if (fallen != null)
|
||||
{
|
||||
WatchCaption.PinWhilePaused();
|
||||
WatchCaption.SetFromDossier(fallen);
|
||||
}
|
||||
|
||||
ChronicleEntry death = LatestDeathEntry(unitId);
|
||||
if (!preferPlace && death != null && death.OtherId != 0)
|
||||
{
|
||||
Actor killer = FindUnitById(death.OtherId);
|
||||
if (killer != null && killer.isAlive())
|
||||
{
|
||||
CameraDirector.FocusUnit(killer, updateCaption: false);
|
||||
LastFallenFocusDetail = "killer:" + death.OtherId;
|
||||
LogService.LogInfo(
|
||||
$"[IdleSpectator][LORE] fallen focus killer id={death.OtherId} victim={unitId}");
|
||||
return true;
|
||||
return FocusFallenKiller(unitId);
|
||||
}
|
||||
}
|
||||
|
||||
UnitDossier fallen = ResolveFallenDossier(unitId);
|
||||
if (fallen != null)
|
||||
{
|
||||
WatchCaption.PinWhilePaused();
|
||||
WatchCaption.SetFromDossier(fallen);
|
||||
}
|
||||
|
||||
// Place / skull path: never keep following a living unit (e.g. prior killer focus).
|
||||
CameraDirector.ClearFollow();
|
||||
if (TryResolveFallenPlace(unitId, death, out Vector3 pos))
|
||||
|
|
@ -2235,7 +2230,10 @@ public static class Chronicle
|
|||
return killer;
|
||||
}
|
||||
|
||||
/// <summary>Camera follow on the living killer of a fallen Lore subject.</summary>
|
||||
/// <summary>
|
||||
/// Camera follow + dossier on the living killer of a fallen Lore subject.
|
||||
/// Lore detail stays on the fallen archive; only the idle dossier retargets.
|
||||
/// </summary>
|
||||
public static bool FocusFallenKiller(long unitId)
|
||||
{
|
||||
Actor killer = FindLivingKillerFor(unitId);
|
||||
|
|
@ -2244,14 +2242,9 @@ public static class Chronicle
|
|||
return false;
|
||||
}
|
||||
|
||||
UnitDossier fallen = ResolveFallenDossier(unitId);
|
||||
if (fallen != null)
|
||||
{
|
||||
WatchCaption.PinWhilePaused();
|
||||
WatchCaption.SetFromDossier(fallen);
|
||||
}
|
||||
|
||||
CameraDirector.FocusUnit(killer, updateCaption: false);
|
||||
WatchCaption.PinWhilePaused();
|
||||
WatchCaption.SetFromActor(killer);
|
||||
LastFallenFocusDetail = "killer:" + killer.getID();
|
||||
LastFallenFocusPosition = Vector3.zero;
|
||||
LogService.LogInfo(
|
||||
|
|
|
|||
|
|
@ -161,9 +161,15 @@ public static class ChronicleHud
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Ignore outside-click dismiss briefly after skull open / Keep.</summary>
|
||||
/// <summary>Ignore outside-click dismiss briefly after Lore open / Keep.</summary>
|
||||
public static float IgnoreDismissUntil => _ignoreDismissUntil;
|
||||
|
||||
/// <summary>Arm a short grace so the open click does not instantly dismiss Lore.</summary>
|
||||
public static void ArmDismissGrace(float seconds = 0.4f)
|
||||
{
|
||||
_ignoreDismissUntil = Time.unscaledTime + Mathf.Max(0.05f, seconds);
|
||||
}
|
||||
|
||||
private static bool IsCastTab => _tab == LoreTab.Living || _tab == LoreTab.Fallen;
|
||||
|
||||
/// <summary>True when Lore detail was opened via L/books and should track idle focus.</summary>
|
||||
|
|
@ -748,6 +754,7 @@ public static class ChronicleHud
|
|||
_root.SetActive(visible);
|
||||
if (visible)
|
||||
{
|
||||
ArmDismissGrace();
|
||||
Rebuild(force: true);
|
||||
}
|
||||
else
|
||||
|
|
@ -871,7 +878,7 @@ public static class ChronicleHud
|
|||
_graveStackActive = true;
|
||||
_graveTileX = tileX;
|
||||
_graveTileY = tileY;
|
||||
_ignoreDismissUntil = Time.unscaledTime + 0.4f;
|
||||
ArmDismissGrace();
|
||||
_searchText = "";
|
||||
if (_searchField != null)
|
||||
{
|
||||
|
|
@ -944,7 +951,7 @@ public static class ChronicleHud
|
|||
return;
|
||||
}
|
||||
|
||||
_ignoreDismissUntil = Time.unscaledTime + 0.4f;
|
||||
ArmDismissGrace();
|
||||
RefreshKeepGraveVisual();
|
||||
}
|
||||
|
||||
|
|
@ -963,7 +970,7 @@ public static class ChronicleHud
|
|||
return false;
|
||||
}
|
||||
|
||||
_ignoreDismissUntil = Time.unscaledTime + 0.4f;
|
||||
ArmDismissGrace();
|
||||
LogService.LogInfo($"[IdleSpectator][LORE] deleted grave tile={tx},{ty}");
|
||||
InspectUi.DismissAll();
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -721,6 +721,16 @@ internal static class HarnessScenarios
|
|||
Step("ie23", "assert", expect: "idle", value: "false"),
|
||||
Step("ie24", "assert", expect: "no_bad"),
|
||||
|
||||
// Outside dismiss closes Lore + dossier and keeps idle off (no auto-resume).
|
||||
Step("ie30", "spectator", value: "on"),
|
||||
Step("ie31", "focus", asset: "auto"),
|
||||
Step("ie32", "lore_open_focus"),
|
||||
Step("ie33", "wait", wait: 0.5f),
|
||||
Step("ie34", "assert", expect: "idle", value: "false"),
|
||||
Step("ie35", "inspect_dismiss"),
|
||||
Step("ie36", "assert", expect: "inspect_cleared"),
|
||||
Step("ie37", "assert", expect: "idle", value: "false"),
|
||||
|
||||
Step("ie90", "fast_timing", value: "false"),
|
||||
Step("ie99", "snapshot"),
|
||||
};
|
||||
|
|
@ -2575,6 +2585,7 @@ internal static class HarnessScenarios
|
|||
Step("ch18k11g", "wait", wait: 0.2f),
|
||||
Step("ch18k11h", "assert", expect: "fallen_focus", value: "killer"),
|
||||
Step("ch18k11i", "assert", expect: "fallen_killer_camera"),
|
||||
Step("ch18k11i2", "assert", expect: "dossier_matches_focus"),
|
||||
Step("ch18k11j", "assert", expect: "fallen_killer_banner"),
|
||||
Step("ch18k12", "screenshot", value: "hud-lore-fallen-slain.png"),
|
||||
Step("ch18k13", "wait", wait: 0.55f),
|
||||
|
|
@ -2691,10 +2702,8 @@ internal static class HarnessScenarios
|
|||
Step("gv7", "spawn", asset: "human", count: 1),
|
||||
Step("gv8", "focus", asset: "auto"),
|
||||
Step("gv9", "wait", wait: 0.25f),
|
||||
// Stack three named deaths on the focus tile.
|
||||
Step("gv10", "grave_force", value: "GraveAlice", count: 1),
|
||||
Step("gv11", "grave_force", value: "GraveBob", count: 1),
|
||||
Step("gv12", "grave_force", value: "GraveCara", count: 1),
|
||||
// Stack three named deaths on one captured tile (unit can walk between steps).
|
||||
Step("gv10", "grave_force", value: "GraveAlice,GraveBob,GraveCara"),
|
||||
Step("gv13", "assert", expect: "grave_stacks", count: 1, label: "eq"),
|
||||
Step("gv14", "assert", expect: "grave_entries", count: 3),
|
||||
Step("gv15", "grave_open"),
|
||||
|
|
@ -2714,10 +2723,12 @@ internal static class HarnessScenarios
|
|||
Step("gv24", "assert", expect: "lore_detail", value: "true"),
|
||||
Step("gv25", "assert", expect: "fallen_focus", value: "place"),
|
||||
Step("gv26", "assert", expect: "fallen_place_no_follow"),
|
||||
// Outside click / dismiss closes Lore (+ grave scope) together.
|
||||
// Outside click / dismiss closes Lore + dossier and turns idle off.
|
||||
Step("gv26b", "inspect_dismiss"),
|
||||
Step("gv26c", "assert", expect: "grave_stack_scope", value: "false"),
|
||||
Step("gv26d", "assert", expect: "lore_detail", value: "false"),
|
||||
Step("gv26d1", "assert", expect: "idle", value: "false"),
|
||||
Step("gv26d2", "assert", expect: "inspect_cleared"),
|
||||
// Delete removes the skull and closes Lore.
|
||||
Step("gv26e0", "grave_clear"),
|
||||
Step("gv26e", "grave_force", value: "GraveDel", count: 1),
|
||||
|
|
@ -2750,6 +2761,7 @@ internal static class HarnessScenarios
|
|||
Step("gv37", "wait", wait: 0.2f),
|
||||
Step("gv38", "assert", expect: "fallen_focus", value: "killer"),
|
||||
Step("gv39", "assert", expect: "fallen_killer_camera"),
|
||||
Step("gv39b", "assert", expect: "dossier_matches_focus"),
|
||||
// Cap eviction: shrink max stacks and flood tiles via orphans at offset positions.
|
||||
Step("gv40", "lore_close"),
|
||||
Step("gv41", "set_setting", expect: "gravestone_max_stacks", value: "2"),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ using UnityEngine;
|
|||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>
|
||||
/// Shared dismiss for skull-opened Lore + paused dossier pin.
|
||||
/// Outside-click dismiss for Lore + idle dossier. Always turns Idle Spectator off.
|
||||
/// </summary>
|
||||
public static class InspectUi
|
||||
{
|
||||
|
|
@ -15,9 +15,20 @@ public static class InspectUi
|
|||
GraveSessionActive = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close Lore / dossier pin, clear grave scope, and force Idle Spectator off.
|
||||
/// </summary>
|
||||
public static void DismissAll()
|
||||
{
|
||||
GraveSessionActive = false;
|
||||
// Outside dismiss must not auto-resume idle when Lore was opened via L.
|
||||
ChronicleHud.NotifyManualIdleToggle();
|
||||
|
||||
if (SpectatorMode.Active)
|
||||
{
|
||||
SpectatorMode.SetActive(false, quiet: true);
|
||||
}
|
||||
|
||||
ChronicleHud.ClearGraveStackScope();
|
||||
if (ChronicleHud.DetailUnitId != 0)
|
||||
{
|
||||
|
|
@ -29,13 +40,15 @@ public static class InspectUi
|
|||
ChronicleHud.SetVisible(false);
|
||||
}
|
||||
|
||||
WatchCaption.ClearPausePin();
|
||||
WatchCaption.ClearStatusBanner();
|
||||
WatchCaption.HideForDismiss();
|
||||
}
|
||||
|
||||
public static void Update()
|
||||
{
|
||||
if (!GraveSessionActive)
|
||||
bool loreOpen = ChronicleHud.Visible;
|
||||
bool dossierOpen = WatchCaption.Visible;
|
||||
bool idleOn = SpectatorMode.Active;
|
||||
if (!loreOpen && !dossierOpen && !idleOn && !GraveSessionActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -50,13 +63,51 @@ public static class InspectUi
|
|||
return;
|
||||
}
|
||||
|
||||
if (ChronicleHud.IsPointerOverPanel()
|
||||
|| WatchCaption.IsPointerOverPanel())
|
||||
if (ChronicleHud.IsTypingSearch)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ChronicleHud.IsPointerOverPanel() || WatchCaption.IsPointerOverPanel())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Skull click opens Lore this frame; leave the new session alone.
|
||||
if (IsClickingGraveSkull())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Click outside Lore / dossier closes the skull inspect session.
|
||||
DismissAll();
|
||||
}
|
||||
|
||||
private static bool IsClickingGraveSkull()
|
||||
{
|
||||
if (!ModSettings.GravestonesEnabled || GraveMarkers.StackCount <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (World.world != null && World.world.isOverUI())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
WorldTile tile = World.world.getMouseTilePosCachedFrame() ?? World.world.getMouseTilePos();
|
||||
if (tile != null && GraveMarkers.TryGetStack(tile.x, tile.y, out _))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Vector2 mouse = World.world.getMousePos();
|
||||
return GraveMarkers.FindNearestStack(mouse, 1.35f) != null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -350,6 +350,14 @@ public static class WatchCaption
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Hide dossier after outside-click / inspect dismiss (idle already off).</summary>
|
||||
public static void HideForDismiss()
|
||||
{
|
||||
_pinnedWhilePaused = false;
|
||||
ClearStatusBanner();
|
||||
SetVisible(false);
|
||||
}
|
||||
|
||||
public static void ClearPausePin()
|
||||
{
|
||||
_pinnedWhilePaused = false;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "IdleSpectator",
|
||||
"author": "dazed",
|
||||
"version": "0.28.11",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Flavored death lines, full-width grave search, no skull counts.",
|
||||
"version": "0.28.13",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Killer button retargets dossier to the killer.",
|
||||
"GUID": "com.dazed.idlespectator"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue