small behavior fixes

This commit is contained in:
DazedAnon 2026-07-14 14:23:11 -05:00
parent f76788db69
commit 26081f8e6a
5 changed files with 150 additions and 12 deletions

View file

@ -108,4 +108,20 @@ public static class InterestCollector
Pending.Clear();
}
}
public static bool HasPendingAtLeast(InterestTier minTier)
{
lock (Gate)
{
for (int i = 0; i < Pending.Count; i++)
{
if (Pending[i] != null && Pending[i].Tier >= minTier)
{
return true;
}
}
return false;
}
}
}

View file

@ -10,6 +10,7 @@ public static class InterestDirector
{
public const float MinDwellSeconds = 10f;
public const float CuriosityDwellSeconds = 6f;
public const float CuriosityRotateSeconds = 10f;
public const float SwitchCooldownSeconds = 5f;
public const float HighTierInterruptAfterSeconds = 3f;
public const float QuietWorldAmbientAfterSeconds = 8f;
@ -97,6 +98,13 @@ public static class InterestDirector
}
}
// Queued discovery tips beat ambient, but never stall ambient under Action/Story.
if (InterestCollector.HasPendingAtLeast(InterestTier.Curiosity)
&& (_current == null || _current.Tier <= InterestTier.Curiosity))
{
return;
}
bool quiet = now - _lastInterestingAt >= QuietWorldAmbientAfterSeconds;
bool dwellDone = _current == null || onCurrent >= DwellFor(_current);
bool ambientDue = now - _lastAmbientAt >= AmbientRotateSeconds;
@ -110,6 +118,24 @@ public static class InterestDirector
}
}
/// <summary>
/// True when a drained New species tip would be allowed to take the camera.
/// </summary>
public static bool WouldAcceptCuriosity()
{
if (_current == null)
{
return true;
}
float onCurrent = Time.unscaledTime - _currentStartedAt;
float sinceSwitch = Time.unscaledTime - _lastSwitchAt;
return CanSwitchTo(
new InterestEvent { Tier = InterestTier.Curiosity },
onCurrent,
sinceSwitch);
}
private static bool PlayerTookManualControl()
{
// Rising-edge style checks only - avoid sticky flags like already_used_power
@ -165,7 +191,8 @@ public static class InterestDirector
return true;
}
// Curiosity is a footnote: never interrupt Action/Story/Epic, and rarely replace another curiosity.
// Curiosity is a footnote: never interrupt Action/Story/Epic.
// Curiosity→curiosity uses a shorter rotate so discovery drains can surface.
if (candidate.Tier == InterestTier.Curiosity)
{
if (_current.Tier >= InterestTier.Action)
@ -173,7 +200,7 @@ public static class InterestDirector
return false;
}
if (_current.Tier == InterestTier.Curiosity && onCurrent < AmbientRotateSeconds)
if (_current.Tier == InterestTier.Curiosity && onCurrent < CuriosityRotateSeconds)
{
return false;
}

View file

@ -11,11 +11,14 @@ namespace IdleSpectator;
public static class SpeciesDiscovery
{
private const float DrainIntervalSeconds = 12f;
private const float NotReadyBackoffSeconds = 5f;
private const int MaxBuffered = 24;
private const int MaxDrainPerSession = 8;
private const int MaxNotReadyAttempts = 8;
private static readonly Queue<PendingSpecies> Pending = new Queue<PendingSpecies>();
private static readonly List<PendingSpecies> Pending = new List<PendingSpecies>();
private static readonly HashSet<string> SeenAssets = new HashSet<string>();
private static readonly HashSet<string> PresentedAssets = new HashSet<string>();
private static float _lastDrainAt = -999f;
private static int _drainedThisSession;
@ -24,6 +27,7 @@ public static class SpeciesDiscovery
public string AssetId;
public Vector3 Position;
public Subspecies Subspecies;
public int NotReadyAttempts;
}
public static void OnSpectatorEnabled()
@ -50,20 +54,53 @@ public static class SpeciesDiscovery
return;
}
// Already shown via live path before buffer ran - nothing to queue.
if (PresentedAssets.Contains(asset.id))
{
return;
}
if (Pending.Count >= MaxBuffered)
{
Pending.Dequeue();
Pending.RemoveAt(0);
}
Vector3 pos = tile != null ? tile.posV3 : Vector3.zero;
Pending.Enqueue(new PendingSpecies
Pending.Add(new PendingSpecies
{
AssetId = asset.id,
Position = pos,
Subspecies = subspecies
Subspecies = subspecies,
NotReadyAttempts = 0
});
}
/// <summary>
/// Live newSpecies path already tipped this asset - drop any buffered copy.
/// </summary>
public static void MarkPresented(string assetId)
{
if (string.IsNullOrEmpty(assetId))
{
return;
}
PresentedAssets.Add(assetId);
SeenAssets.Add(assetId);
for (int i = Pending.Count - 1; i >= 0; i--)
{
if (Pending[i].AssetId == assetId)
{
Pending.RemoveAt(i);
}
}
}
public static bool WasPresented(string assetId)
{
return !string.IsNullOrEmpty(assetId) && PresentedAssets.Contains(assetId);
}
public static void Update()
{
if (!SpectatorMode.Active || !Config.game_loaded || World.world == null)
@ -87,23 +124,43 @@ public static class SpeciesDiscovery
return;
}
PendingSpecies next = Pending.Dequeue();
// Wait until the director can actually show a curiosity tip.
if (!InterestDirector.WouldAcceptCuriosity())
{
return;
}
int index = FindNextReadyIndex();
if (index < 0)
{
// Front items not spawnable yet - retry later without dequeue flap.
_lastDrainAt = now - DrainIntervalSeconds + NotReadyBackoffSeconds;
return;
}
PendingSpecies next = Pending[index];
ActorAsset asset = AssetManager.actor_library?.get(next.AssetId);
if (asset == null)
{
Pending.RemoveAt(index);
return;
}
InterestEvent curiosity = WorldActivityScanner.FromNewSpecies(next.Subspecies, asset, null);
if (curiosity == null)
{
// Unit may not exist yet - put back once, try later.
if (Pending.Count < MaxBuffered)
next.NotReadyAttempts++;
if (next.NotReadyAttempts >= MaxNotReadyAttempts)
{
Pending.Enqueue(next);
Pending.RemoveAt(index);
LogService.LogInfo($"[IdleSpectator] Discovery skip (never spawned): {next.AssetId}");
}
else
{
Pending[index] = next;
}
_lastDrainAt = now - DrainIntervalSeconds + 3f;
_lastDrainAt = now - DrainIntervalSeconds + NotReadyBackoffSeconds;
return;
}
@ -113,9 +170,40 @@ public static class SpeciesDiscovery
curiosity.Position = next.Position;
}
Pending.RemoveAt(index);
PresentedAssets.Add(next.AssetId);
_lastDrainAt = now;
_drainedThisSession++;
InterestCollector.EnqueueDirect(curiosity);
LogService.LogInfo($"[IdleSpectator] Discovery drain: {next.AssetId} ({_drainedThisSession}/{MaxDrainPerSession}, queued={Pending.Count})");
}
private static int FindNextReadyIndex()
{
for (int i = 0; i < Pending.Count; i++)
{
PendingSpecies next = Pending[i];
if (PresentedAssets.Contains(next.AssetId))
{
Pending.RemoveAt(i);
i--;
continue;
}
ActorAsset asset = AssetManager.actor_library?.get(next.AssetId);
if (asset == null)
{
Pending.RemoveAt(i);
i--;
continue;
}
if (WorldActivityScanner.FromNewSpecies(next.Subspecies, asset, null) != null)
{
return i;
}
}
return -1;
}
}

View file

@ -32,6 +32,12 @@ public static class SubspeciesNewSpeciesPatch
return;
}
// Drain already tipped this species (or live tip already fired).
if (SpeciesDiscovery.WasPresented(assetId))
{
return;
}
float now = Time.unscaledTime;
if (LastLiveEnqueuedAt.TryGetValue(assetId, out float last) && now - last < PerSpeciesCooldownSeconds)
{
@ -45,6 +51,7 @@ public static class SubspeciesNewSpeciesPatch
}
LastLiveEnqueuedAt[assetId] = now;
SpeciesDiscovery.MarkPresented(assetId);
InterestCollector.EnqueueDirect(curiosity);
}
}

View file

@ -1,7 +1,7 @@
{
"name": "IdleSpectator",
"author": "dazed",
"version": "0.3.9",
"version": "0.3.10",
"description": "AFK Idle Spectator (I): follows wars, disasters, battles, creatures, and new species from fresh worlds to empires.",
"GUID": "com.dazed.idlespectator"
}