using NeoModLoader.services; using UnityEngine; namespace IdleSpectator; public static class CameraDirector { public static string LastWatchLabel { get; private set; } = ""; public static string LastWatchAssetId { get; private set; } = ""; public static void FocusUnit(Actor actor, bool updateCaption = true) { if (actor != null && actor.isAlive()) { RetargetFollow(actor); if (updateCaption && SpectatorMode.Active) { WatchCaption.SetFromActor(actor); } } } public static void Watch(InterestEvent interest) { if (interest == null || !interest.HasValidPosition) { return; } if (!Config.game_loaded || World.world == null) { return; } string tip = FormatWatchTip(interest); // LastWatchLabel is tip/harness telemetry only - never dossier reason truth. LastWatchLabel = interest.Label ?? ""; LastWatchAssetId = interest.AssetId ?? ""; LogService.LogInfo($"[IdleSpectator] {tip}"); WatchCaption.SetFromInterest(interest); Actor follow = interest.HasFollowUnit ? interest.FollowUnit : null; // Species discovery: only attach a species-matched unit, never a random stranger. if (follow == null && !string.IsNullOrEmpty(interest.AssetId) && interest.AssetId != "live_battle" && interest.AssetId != "scored_unit" && interest.Label != null && interest.Label.StartsWith("New species:")) { follow = WorldActivityScanner.FindNearestAliveUnit(interest.Position, 24f, interest.AssetId); } // Location-only civic: pan only. Do not invent a stranger FollowUnit. if (follow != null && follow.isAlive()) { // If this is a species event, refuse mismatched units. if (interest.Label != null && interest.Label.StartsWith("New species:") && !string.IsNullOrEmpty(interest.AssetId) && follow.asset != null && follow.asset.id != interest.AssetId) { Actor matched = WorldActivityScanner.FindNearestAliveUnit(interest.Position, 24f, interest.AssetId); if (matched == null) { return; } follow = matched; } RetargetFollow(follow); return; } // No owned unit: pan only if we are not already following someone. // Never clear an existing focus unit here - that flashes the power bar. if (!MoveCamera.hasFocusUnit()) { PanCamera(interest.Position); } } public static string FormatWatchTip(InterestEvent interest) { string reason = string.IsNullOrEmpty(interest.Label) ? "something interesting" : interest.Label; return $"Watching: {reason}"; } public static void ClearFollow() { MoveCamera.clearFocusUnitOnly(); } public static void PanTo(Vector3 pos) { PanCamera(pos); } private static void RetargetFollow(Actor actor) { if (MoveCamera.isCameraFollowingUnit(actor)) { return; } bool firstFocus = !MoveCamera.hasFocusUnit(); MoveCamera.setFocusUnit(actor); Chronicle.NoteFocus(actor); MoveCamera cam = MoveCamera.instance; if (cam == null) { return; } // Same as vanilla killer handoff: reset ease so the camera lerps to the new unit. cam._focus_timer = 0f; if (firstFocus) { cam._target_zoom = 15f; cam._focus_zoom = 15f; } } private static void PanCamera(Vector3 pos) { MoveCamera cam = MoveCamera.instance; if (cam == null) { World.world.locatePosition(pos); return; } pos.z = cam.transform.position.z; cam.transform.position = pos; cam._target_zoom = 15f; cam._focus_zoom = 15f; } }