diff --git a/IdleSpectator/AgentHarness.cs b/IdleSpectator/AgentHarness.cs index 25b52b0..9c607df 100644 --- a/IdleSpectator/AgentHarness.cs +++ b/IdleSpectator/AgentHarness.cs @@ -702,7 +702,50 @@ public static class AgentHarness _cmdFail++; } - Emit(cmd, ok, detail: $"jumped={ok} focus={MoveCamera.hasFocusUnit()}"); + Emit( + cmd, + ok, + detail: + $"jumped={ok} detail={Chronicle.LastJumpDetail} focus={MoveCamera.hasFocusUnit()} pos={Chronicle.LastJumpPosition}"); + break; + } + + case "lore_jump_world": + { + Chronicle.ShowHud(); + ChronicleHud.SetTab(ChronicleHud.LoreTab.World); + ChronicleEntry target = null; + IReadOnlyList snap = Chronicle.SnapshotMemory(); + for (int i = snap.Count - 1; i >= 0; i--) + { + ChronicleEntry e = snap[i]; + if (e == null || e.Kind == ChronicleKind.AgeChapter) + { + continue; + } + + if (e.Kind == ChronicleKind.World || e.IsLegend) + { + target = e; + break; + } + } + + bool ok = target != null && Chronicle.JumpTo(target); + if (ok) + { + _cmdOk++; + } + else + { + _cmdFail++; + } + + Emit( + cmd, + ok, + detail: + $"jumped={ok} detail={Chronicle.LastJumpDetail} focus={MoveCamera.hasFocusUnit()} line={(target != null ? target.Line : "")}"); break; } @@ -1700,6 +1743,15 @@ public static class AgentHarness $"fallenFocus={Chronicle.LastFallenFocusDetail} hasFocus={MoveCamera.hasFocusUnit()} pos={Chronicle.LastFallenFocusPosition}"; break; } + case "world_jump_place": + { + pass = Chronicle.LastJumpDetail == "place" + && !MoveCamera.hasFocusUnit() + && Chronicle.LastJumpPosition.sqrMagnitude > 0.0001f; + detail = + $"jump={Chronicle.LastJumpDetail} hasFocus={MoveCamera.hasFocusUnit()} pos={Chronicle.LastJumpPosition}"; + break; + } case "fallen_killer_camera": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; diff --git a/IdleSpectator/Chronicle.cs b/IdleSpectator/Chronicle.cs index aa65a51..b3dd8c0 100644 --- a/IdleSpectator/Chronicle.cs +++ b/IdleSpectator/Chronicle.cs @@ -449,6 +449,12 @@ public static class Chronicle ChronicleHud.SetVisible(false); } + /// Harness: how the last JumpTo resolved (unit:id | place | none). + public static string LastJumpDetail { get; private set; } = ""; + + /// Harness: world position used by the last place-style JumpTo. + public static Vector3 LastJumpPosition { get; private set; } = Vector3.zero; + public static bool JumpToLatest() { // Prefer a Memory landmark when it can resolve a unit/position; else focus history. @@ -463,15 +469,37 @@ public static class Chronicle public static bool JumpTo(ChronicleEntry entry) { + LastJumpDetail = "none"; + LastJumpPosition = Vector3.zero; if (entry == null || entry.Kind == ChronicleKind.AgeChapter) { return false; } + // World Memory landmarks: always go to the recorded place when we have one, + // even if the related unit is gone or has moved on. + bool memoryPlaceFirst = entry.Kind == ChronicleKind.World || entry.IsLegend; + if (memoryPlaceFirst && EntryHasWorldPosition(entry)) + { + CameraDirector.ClearFollow(); + CameraDirector.PanTo(entry.Position); + LastJumpDetail = "place"; + LastJumpPosition = entry.Position; + LogService.LogInfo( + $"[IdleSpectator][MEMORY] jump place pos={entry.Position} line={entry.DisplayLine}"); + return true; + } + Actor unit = FindUnitById(entry.SubjectId); if (unit != null && unit.isAlive()) { - return FocusSubject(unit); + bool ok = FocusSubject(unit); + if (ok) + { + LastJumpDetail = "unit:" + entry.SubjectId; + } + + return ok; } if (entry.OtherId != 0) @@ -479,13 +507,24 @@ public static class Chronicle Actor other = FindUnitById(entry.OtherId); if (other != null && other.isAlive()) { - return FocusSubject(other); + bool ok = FocusSubject(other); + if (ok) + { + LastJumpDetail = "unit:" + entry.OtherId; + } + + return ok; } } - if (entry.Position != Vector3.zero) + if (EntryHasWorldPosition(entry)) { + CameraDirector.ClearFollow(); CameraDirector.PanTo(entry.Position); + LastJumpDetail = "place"; + LastJumpPosition = entry.Position; + LogService.LogInfo( + $"[IdleSpectator][MEMORY] jump place pos={entry.Position} line={entry.DisplayLine}"); return true; } @@ -1180,13 +1219,37 @@ public static class Chronicle return false; } + Vector3 pos = Vector3.zero; + if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) + { + try + { + pos = MoveCamera._focus_unit.current_position; + } + catch + { + pos = Vector3.zero; + } + } + + if (pos.sqrMagnitude < 0.0001f && MoveCamera.instance != null) + { + pos = MoveCamera.instance.transform.position; + pos.z = 0f; + } + + if (pos.sqrMagnitude < 0.0001f) + { + pos = new Vector3(48f, 48f, 0f); + } + AppendLandmark( ChronicleKind.World, null, null, label, LoreProse.Rewrite(ChronicleKind.World, label), - Vector3.zero, + pos, "harness", isLegend: false); return true; diff --git a/IdleSpectator/ChronicleHud.cs b/IdleSpectator/ChronicleHud.cs index 9aa50eb..77d51de 100644 --- a/IdleSpectator/ChronicleHud.cs +++ b/IdleSpectator/ChronicleHud.cs @@ -1549,8 +1549,19 @@ public static class ChronicleHud ChronicleEntry captured = entry; button.onClick.AddListener(new UnityAction(() => { + if (SpectatorMode.Active) + { + SpectatorMode.SetActive(false, quiet: true); + WatchCaption.PinWhilePaused(); + WatchCaption.ShowStatusBanner( + captured.Kind == ChronicleKind.World || captured.IsLegend + ? "Paused (viewing world)" + : "Paused (viewing landmark)"); + } + bool ok = Chronicle.JumpTo(captured); - LogService.LogInfo($"[IdleSpectator][MEMORY] jump ok={ok} line={captured.DisplayLine}"); + LogService.LogInfo( + $"[IdleSpectator][MEMORY] jump ok={ok} detail={Chronicle.LastJumpDetail} line={captured.DisplayLine}"); })); return go; diff --git a/IdleSpectator/HarnessScenarios.cs b/IdleSpectator/HarnessScenarios.cs index 3c3d60f..02786c3 100644 --- a/IdleSpectator/HarnessScenarios.cs +++ b/IdleSpectator/HarnessScenarios.cs @@ -447,6 +447,15 @@ internal static class HarnessScenarios Step("ch17c", "assert", expect: "world_memory_contains", value: "harness_alpha"), Step("ch17d", "assert", expect: "chronicle_latest_contains", value: "Burned"), Step("ch17e", "assert", expect: "world_memory_age"), + // Selecting a world event pans to its recorded place (even with no unit there). + Step("ch17j", "lore_jump_world"), + Step("ch17k", "wait", wait: 0.2f), + Step("ch17l", "assert", expect: "world_jump_place"), + Step("ch17m", "assert", expect: "focus", value: "false"), + // Restore a living focus for the rest of the scenario. + Step("ch17n", "focus", asset: "auto"), + Step("ch17o", "wait", wait: 0.2f), + Step("ch17p", "assert", expect: "focus", value: "true"), // World Memory panel + dossier controls Step("ch18", "chronicle_open"), diff --git a/IdleSpectator/mod.json b/IdleSpectator/mod.json index 77bb180..efce34e 100644 --- a/IdleSpectator/mod.json +++ b/IdleSpectator/mod.json @@ -1,7 +1,7 @@ { "name": "IdleSpectator", "author": "dazed", - "version": "0.12.14", - "description": "AFK Idle Spectator (I) + Lore (L). Tabs: World / Living / Fallen (newest first).", + "version": "0.12.16", + "description": "AFK Idle Spectator (I) + Lore (L). World events pan to location; Living/Fallen tabs.", "GUID": "com.dazed.idlespectator" }