small behavior fixes
This commit is contained in:
parent
f76788db69
commit
26081f8e6a
5 changed files with 150 additions and 12 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue