876 lines
28 KiB
C#
876 lines
28 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>
|
|
/// Converts live streams into <see cref="InterestRegistry"/> candidates.
|
|
/// Sole path for WorldLog / happiness / scanner / activity / status / chronicle → registry.
|
|
/// </summary>
|
|
public static class InterestFeeds
|
|
{
|
|
private static ulong _happinessCursor;
|
|
private static readonly List<HappinessOccurrence> HappinessDrain = new List<HappinessOccurrence>(64);
|
|
private static float _lastScannerAt = -999f;
|
|
private const float ScannerInterval = 0.75f;
|
|
|
|
// Civic aggregate boosts keyed by kingdom+effect.
|
|
private static readonly Dictionary<string, float> CivicBoostUntil = new Dictionary<string, float>(32);
|
|
|
|
public static void Reset()
|
|
{
|
|
_happinessCursor = HappinessEventRouter.CurrentSequence;
|
|
HappinessDrain.Clear();
|
|
_lastScannerAt = -999f;
|
|
CivicBoostUntil.Clear();
|
|
}
|
|
|
|
public static ulong HappinessCursor => _happinessCursor;
|
|
|
|
public static int CivicBoostCount
|
|
{
|
|
get
|
|
{
|
|
int n = 0;
|
|
foreach (KeyValuePair<string, float> kv in CivicBoostUntil)
|
|
{
|
|
if (kv.Key != null && !kv.Key.EndsWith("|amt") && kv.Value > Time.unscaledTime)
|
|
{
|
|
n++;
|
|
}
|
|
}
|
|
|
|
return n;
|
|
}
|
|
}
|
|
|
|
public static bool HasCivicBoostContaining(string effectId)
|
|
{
|
|
if (string.IsNullOrEmpty(effectId))
|
|
{
|
|
return CivicBoostCount > 0;
|
|
}
|
|
|
|
foreach (KeyValuePair<string, float> kv in CivicBoostUntil)
|
|
{
|
|
if (kv.Key != null
|
|
&& !kv.Key.EndsWith("|amt")
|
|
&& kv.Key.IndexOf(effectId, System.StringComparison.OrdinalIgnoreCase) >= 0
|
|
&& kv.Value > Time.unscaledTime)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>Harness: drain happiness once; returns how many occurrences were ingested.</summary>
|
|
public static int HarnessDrainOnce()
|
|
{
|
|
DrainHappiness();
|
|
return HappinessDrain.Count;
|
|
}
|
|
|
|
/// <summary>Harness: stamp a civic boost as AggregateSummary would.</summary>
|
|
public static void HarnessStampCivicBoost(string kingdom, string effectId, float amount)
|
|
{
|
|
StampCivicBoost(kingdom ?? "harness", effectId ?? "", amount);
|
|
}
|
|
|
|
public static void Tick()
|
|
{
|
|
DrainHappiness();
|
|
TickScanner();
|
|
PruneCivicBoosts();
|
|
}
|
|
|
|
public static void OnWorldLogMessage(WorldLogMessage message)
|
|
{
|
|
if (message == null || AgentHarness.Busy)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!WorldLogInterestTable.TryGetTier(message.asset_id, out InterestTier tier))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Vector3 position = message.getLocation();
|
|
Actor unit = null;
|
|
if (message.hasFollowLocation())
|
|
{
|
|
unit = message.unit;
|
|
}
|
|
|
|
if (unit == null && position == Vector3.zero)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string assetId = message.asset_id ?? "worldlog";
|
|
string kingdom = message.special1 ?? "";
|
|
string key = "worldlog:" + assetId + ":" + kingdom;
|
|
float boost = PeekCivicBoost(kingdom, assetId);
|
|
|
|
var candidate = new InterestCandidate
|
|
{
|
|
Key = key,
|
|
Urgency = tier,
|
|
LeadKind = InterestLeadKind.EventLed,
|
|
Category = tier >= InterestTier.Epic ? "Politics" : "Settlement",
|
|
Source = "worldlog",
|
|
EventStrength = 60f + (int)tier * 20f + boost,
|
|
VisualConfidence = unit != null ? 0.7f : 0.4f,
|
|
Position = unit != null ? unit.current_position : position,
|
|
FollowUnit = unit,
|
|
SubjectId = SafeId(unit),
|
|
Label = WorldLogInterestTable.MakeLabel(message),
|
|
AssetId = assetId,
|
|
KingdomKey = kingdom,
|
|
SpeciesId = unit?.asset != null ? unit.asset.id : "",
|
|
CreatedAt = Time.unscaledTime,
|
|
LastSeenAt = Time.unscaledTime,
|
|
ExpiresAt = Time.unscaledTime + 40f,
|
|
MinWatch = tier >= InterestTier.Epic ? 8f : 6f,
|
|
MaxWatch = tier >= InterestTier.Epic ? 35f : 28f,
|
|
Completion = InterestCompletionKind.FixedDwell
|
|
};
|
|
InterestScoring.ScoreCheap(candidate);
|
|
InterestRegistry.Upsert(candidate);
|
|
}
|
|
|
|
/// <summary>Legacy / discovery / harness enqueue path → registry.</summary>
|
|
public static InterestCandidate RegisterDirect(InterestEvent interest)
|
|
{
|
|
if (interest == null || !interest.HasValidPosition)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
Actor unit = interest.FollowUnit;
|
|
long subjectId = SafeId(unit);
|
|
string asset = interest.AssetId ?? "";
|
|
string key = "direct:" + interest.Tier + ":" + subjectId + ":" + (interest.Label ?? "") + ":" + asset;
|
|
InterestLeadKind lead = interest.Tier >= InterestTier.Action
|
|
? InterestLeadKind.EventLed
|
|
: InterestLeadKind.CharacterLed;
|
|
InterestCompletionKind completion = interest.Tier >= InterestTier.Action
|
|
? InterestCompletionKind.FixedDwell
|
|
: InterestCompletionKind.CharacterVignette;
|
|
|
|
if (asset == "live_battle")
|
|
{
|
|
key = "battle:hot:" + Mathf.FloorToInt(interest.Position.x) + ":" + Mathf.FloorToInt(interest.Position.y);
|
|
lead = InterestLeadKind.EventLed;
|
|
completion = InterestCompletionKind.CombatActive;
|
|
}
|
|
|
|
var candidate = new InterestCandidate
|
|
{
|
|
Key = key,
|
|
Urgency = interest.Tier,
|
|
LeadKind = lead,
|
|
Category = asset == "live_battle" ? "Combat" : CategoryForTier(interest.Tier, asset),
|
|
Source = "direct",
|
|
EventStrength = interest.Score > 0f ? interest.Score : 40f + (int)interest.Tier * 15f,
|
|
CharacterSignificance = interest.CharacterSignificance > 0f
|
|
? interest.CharacterSignificance
|
|
: (lead == InterestLeadKind.CharacterLed ? interest.Score * 0.5f : 0f),
|
|
VisualConfidence = unit != null ? 0.75f : 0.4f,
|
|
Position = interest.Position,
|
|
FollowUnit = unit,
|
|
SubjectId = subjectId,
|
|
Label = interest.Label ?? "",
|
|
AssetId = asset,
|
|
SpeciesId = unit?.asset != null ? unit.asset.id : asset,
|
|
ParticipantCount = interest.ParticipantCount,
|
|
NotableParticipantCount = interest.NotableParticipantCount,
|
|
Verb = asset == "live_battle" ? "fighting" : "",
|
|
CreatedAt = interest.CreatedAt > 0f ? interest.CreatedAt : Time.unscaledTime,
|
|
LastSeenAt = Time.unscaledTime,
|
|
ExpiresAt = Time.unscaledTime + 30f,
|
|
MinWatch = interest.Tier <= InterestTier.Curiosity ? 1.2f : 1.5f,
|
|
MaxWatch = interest.Tier >= InterestTier.Epic ? 40f : 25f,
|
|
Completion = completion
|
|
};
|
|
InterestScoring.ScoreCheap(candidate);
|
|
return InterestRegistry.Upsert(candidate);
|
|
}
|
|
|
|
public static InterestCandidate RegisterHarness(
|
|
Actor follow,
|
|
InterestTier urgency,
|
|
string label,
|
|
string keySuffix,
|
|
InterestLeadKind lead,
|
|
InterestCompletionKind completion,
|
|
bool forceActive,
|
|
float eventStrength,
|
|
float maxWatch)
|
|
{
|
|
return RegisterHarness(
|
|
follow,
|
|
urgency,
|
|
label,
|
|
keySuffix,
|
|
lead,
|
|
completion,
|
|
forceActive,
|
|
eventStrength,
|
|
characterSignificance: lead == InterestLeadKind.CharacterLed ? eventStrength : 10f,
|
|
maxWatch,
|
|
ttlSeconds: 60f,
|
|
related: null,
|
|
resumable: true);
|
|
}
|
|
|
|
public static InterestCandidate RegisterHarness(
|
|
Actor follow,
|
|
InterestTier urgency,
|
|
string label,
|
|
string keySuffix,
|
|
InterestLeadKind lead,
|
|
InterestCompletionKind completion,
|
|
bool forceActive,
|
|
float eventStrength,
|
|
float characterSignificance,
|
|
float maxWatch,
|
|
float ttlSeconds,
|
|
Actor related,
|
|
bool resumable)
|
|
{
|
|
if (follow == null || !follow.isAlive())
|
|
{
|
|
return null;
|
|
}
|
|
|
|
long id = SafeId(follow);
|
|
string key = "harness:" + (keySuffix ?? urgency.ToString()) + ":" + id;
|
|
float now = Time.unscaledTime;
|
|
var candidate = new InterestCandidate
|
|
{
|
|
Key = key,
|
|
Urgency = urgency,
|
|
LeadKind = lead,
|
|
Category = CategoryForTier(urgency, ""),
|
|
Source = "harness",
|
|
EventStrength = eventStrength,
|
|
CharacterSignificance = characterSignificance,
|
|
VisualConfidence = 0.9f,
|
|
Position = follow.current_position,
|
|
FollowUnit = follow,
|
|
RelatedUnit = related,
|
|
SubjectId = id,
|
|
Label = label ?? ("Harness " + urgency),
|
|
AssetId = follow.asset != null ? follow.asset.id : "harness",
|
|
SpeciesId = follow.asset != null ? follow.asset.id : "",
|
|
CreatedAt = now,
|
|
LastSeenAt = now,
|
|
ExpiresAt = now + (ttlSeconds > 0f ? ttlSeconds : 60f),
|
|
MinWatch = 1f,
|
|
MaxWatch = maxWatch > 0f ? maxWatch : 20f,
|
|
Completion = completion,
|
|
ForceActive = forceActive,
|
|
Resumable = resumable
|
|
};
|
|
if (related != null)
|
|
{
|
|
try
|
|
{
|
|
candidate.RelatedId = related.getID();
|
|
}
|
|
catch
|
|
{
|
|
candidate.RelatedId = 0;
|
|
}
|
|
}
|
|
|
|
InterestScoring.ScoreCheap(candidate);
|
|
return InterestRegistry.Upsert(candidate);
|
|
}
|
|
|
|
public static void OnActivityNote(Actor actor, string taskOrBeat, bool hot)
|
|
{
|
|
if (actor == null || !actor.isAlive() || AgentHarness.Busy || !hot)
|
|
{
|
|
return;
|
|
}
|
|
|
|
long id = SafeId(actor);
|
|
string verb = taskOrBeat ?? "activity";
|
|
string key = "activity:" + id + ":" + verb;
|
|
bool combat = actor.has_attack_target;
|
|
var candidate = new InterestCandidate
|
|
{
|
|
Key = key,
|
|
Urgency = InterestTier.Action,
|
|
LeadKind = InterestLeadKind.EventLed,
|
|
Category = combat ? "Combat" : "Work",
|
|
Source = "activity",
|
|
EventStrength = combat ? 75f : 50f,
|
|
VisualConfidence = 0.8f,
|
|
Position = actor.current_position,
|
|
FollowUnit = actor,
|
|
SubjectId = id,
|
|
Label = combat ? ("Fighting: " + SafeName(actor)) : (SafeName(actor) + " · " + verb),
|
|
AssetId = actor.asset != null ? actor.asset.id : "",
|
|
SpeciesId = actor.asset != null ? actor.asset.id : "",
|
|
Verb = verb,
|
|
CityKey = actor.city != null ? (actor.city.name ?? "") : "",
|
|
KingdomKey = actor.kingdom != null ? (actor.kingdom.name ?? "") : "",
|
|
CreatedAt = Time.unscaledTime,
|
|
LastSeenAt = Time.unscaledTime,
|
|
ExpiresAt = Time.unscaledTime + 20f,
|
|
MinWatch = 3f,
|
|
MaxWatch = 30f,
|
|
Completion = combat ? InterestCompletionKind.CombatActive : InterestCompletionKind.ActivityActive
|
|
};
|
|
candidate.ParticipantIds.Add(id);
|
|
InterestScoring.ScoreCheap(candidate);
|
|
InterestRegistry.Upsert(candidate);
|
|
}
|
|
|
|
public static void OnStatusChange(Actor actor, string statusId, bool gained)
|
|
{
|
|
if (actor == null || !actor.isAlive() || !gained || string.IsNullOrEmpty(statusId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (AgentHarness.Busy)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Visible spectacle statuses amplify / extend scenes; crying extends grief.
|
|
bool spectacle = statusId == "burning"
|
|
|| statusId == "possessed"
|
|
|| statusId == "crying"
|
|
|| statusId == "frozen"
|
|
|| statusId == "shocked";
|
|
if (!spectacle)
|
|
{
|
|
return;
|
|
}
|
|
|
|
long id = SafeId(actor);
|
|
if (statusId == "crying")
|
|
{
|
|
// Extend existing grief candidate rather than spawning a second scene.
|
|
string griefPrefix = "happiness:death_";
|
|
ExtendMatching(id, griefPrefix, statusId);
|
|
return;
|
|
}
|
|
|
|
string key = "status:" + statusId + ":" + id;
|
|
var candidate = new InterestCandidate
|
|
{
|
|
Key = key,
|
|
Urgency = InterestTier.Action,
|
|
LeadKind = InterestLeadKind.EventLed,
|
|
Category = "StatusTransformation",
|
|
Source = "status",
|
|
EventStrength = 55f,
|
|
VisualConfidence = 0.85f,
|
|
Position = actor.current_position,
|
|
FollowUnit = actor,
|
|
SubjectId = id,
|
|
StatusId = statusId,
|
|
Label = SafeName(actor) + " · " + statusId,
|
|
AssetId = actor.asset != null ? actor.asset.id : "",
|
|
SpeciesId = actor.asset != null ? actor.asset.id : "",
|
|
CreatedAt = Time.unscaledTime,
|
|
LastSeenAt = Time.unscaledTime,
|
|
ExpiresAt = Time.unscaledTime + 18f,
|
|
MinWatch = 2f,
|
|
MaxWatch = 22f,
|
|
Completion = InterestCompletionKind.StatusPhase
|
|
};
|
|
InterestScoring.ScoreCheap(candidate);
|
|
InterestRegistry.Upsert(candidate);
|
|
}
|
|
|
|
public static void OnChronicleMilestone(Actor subject, string milestoneKey, Actor related, string label)
|
|
{
|
|
if (subject == null || !subject.isAlive() || string.IsNullOrEmpty(milestoneKey))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (AgentHarness.Busy)
|
|
{
|
|
return;
|
|
}
|
|
|
|
long sid = SafeId(subject);
|
|
long rid = SafeId(related);
|
|
// Shared key space with happiness canonical merges.
|
|
string key = "chronicle:" + milestoneKey + ":" + sid + ":" + rid;
|
|
InterestTier urgency = milestoneKey.Contains("kill") || milestoneKey.Contains("death")
|
|
? InterestTier.Action
|
|
: InterestTier.Curiosity;
|
|
var candidate = new InterestCandidate
|
|
{
|
|
Key = key,
|
|
Urgency = urgency,
|
|
LeadKind = InterestLeadKind.EventLed,
|
|
Category = "Relationship",
|
|
Source = "chronicle",
|
|
EventStrength = 60f,
|
|
VisualConfidence = 0.7f,
|
|
Position = subject.current_position,
|
|
FollowUnit = subject,
|
|
RelatedUnit = related,
|
|
SubjectId = sid,
|
|
RelatedId = rid,
|
|
Label = label ?? milestoneKey,
|
|
AssetId = subject.asset != null ? subject.asset.id : "",
|
|
SpeciesId = subject.asset != null ? subject.asset.id : "",
|
|
CreatedAt = Time.unscaledTime,
|
|
LastSeenAt = Time.unscaledTime,
|
|
ExpiresAt = Time.unscaledTime + 25f,
|
|
MinWatch = 3f,
|
|
MaxWatch = 20f,
|
|
Completion = InterestCompletionKind.FixedDwell
|
|
};
|
|
InterestScoring.ScoreCheap(candidate);
|
|
InterestRegistry.Upsert(candidate);
|
|
}
|
|
|
|
private static void DrainHappiness()
|
|
{
|
|
ulong next = HappinessEventRouter.DrainSince(_happinessCursor, HappinessDrain);
|
|
if (HappinessDrain.Count == 0)
|
|
{
|
|
_happinessCursor = next;
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < HappinessDrain.Count; i++)
|
|
{
|
|
IngestHappiness(HappinessDrain[i]);
|
|
}
|
|
|
|
_happinessCursor = next;
|
|
}
|
|
|
|
private static void IngestHappiness(HappinessOccurrence occ)
|
|
{
|
|
if (occ == null || !occ.Applied || occ.SuppressedByPsychopath)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Harness scenarios inject via SourceHook.Harness; ignore live world noise while Busy.
|
|
if (AgentHarness.Busy && occ.SourceHook != HappinessSourceHook.Harness)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (occ.Presentation == HappinessPresentationTier.Ambient)
|
|
{
|
|
// Enrichment only - never owns the camera.
|
|
return;
|
|
}
|
|
|
|
if (occ.Presentation == HappinessPresentationTier.Aggregate)
|
|
{
|
|
if (!occ.Merged || occ.SourceHook == HappinessSourceHook.AggregateSummary)
|
|
{
|
|
string kingdom = "";
|
|
try
|
|
{
|
|
// Prefer civ key fragment.
|
|
string civ = occ.CivEventKey ?? "";
|
|
int pipe = civ.IndexOf('|');
|
|
if (pipe >= 0 && pipe + 1 < civ.Length)
|
|
{
|
|
int pipe2 = civ.IndexOf('|', pipe + 1);
|
|
kingdom = pipe2 > pipe
|
|
? civ.Substring(pipe + 1, pipe2 - pipe - 1)
|
|
: civ.Substring(pipe + 1);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
kingdom = "";
|
|
}
|
|
|
|
StampCivicBoost(kingdom, occ.EffectId, 25f + Mathf.Min(40f, occ.TotalAffectedCount));
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Signal: skip merged duplicates (canonical / continuation already covered).
|
|
if (occ.Merged)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Actor subject = FindUnitById(occ.SubjectId);
|
|
if (subject == null || !subject.isAlive())
|
|
{
|
|
return;
|
|
}
|
|
|
|
Actor related = occ.RelatedId != 0 ? FindUnitById(occ.RelatedId) : null;
|
|
string cat = string.IsNullOrEmpty(occ.InterestCategory) ? "Emotion" : occ.InterestCategory;
|
|
string key = "happiness:" + occ.EffectId + ":" + occ.SubjectId + ":" + occ.RelatedId
|
|
+ ":" + CorrBucket(occ.CorrelationKey);
|
|
bool grief = occ.EffectId != null && occ.EffectId.StartsWith("death_");
|
|
InterestTier urgency = InterestScoring.UrgencyForHappiness(occ.EffectId, subject);
|
|
float strength = InterestScoring.EventStrengthForHappiness(occ.EffectId);
|
|
|
|
var candidate = new InterestCandidate
|
|
{
|
|
Key = key,
|
|
Urgency = urgency,
|
|
LeadKind = InterestLeadKind.EventLed,
|
|
Category = grief ? "FamilyEmotion" : MapHappinessCategory(cat),
|
|
Source = "happiness",
|
|
EventStrength = strength,
|
|
VisualConfidence = 0.75f,
|
|
Position = occ.Position != Vector3.zero ? occ.Position : subject.current_position,
|
|
FollowUnit = subject,
|
|
RelatedUnit = related,
|
|
SubjectId = occ.SubjectId,
|
|
RelatedId = occ.RelatedId,
|
|
Label = !string.IsNullOrEmpty(occ.PlainClause)
|
|
? (occ.SubjectName + " " + occ.PlainClause)
|
|
: (occ.SubjectName + " · " + occ.EffectId),
|
|
AssetId = occ.SubjectSpecies,
|
|
SpeciesId = occ.SubjectSpecies,
|
|
HappinessEffectId = occ.EffectId,
|
|
StatusId = grief ? "crying" : "",
|
|
CorrelationKey = occ.CorrelationKey,
|
|
CreatedAt = Time.unscaledTime,
|
|
LastSeenAt = Time.unscaledTime,
|
|
ExpiresAt = Time.unscaledTime + 35f,
|
|
MinWatch = 4f,
|
|
MaxWatch = 28f,
|
|
Completion = grief
|
|
? InterestCompletionKind.HappinessGrief
|
|
: InterestCompletionKind.FixedDwell
|
|
};
|
|
candidate.ParticipantIds.Add(occ.SubjectId);
|
|
if (occ.RelatedId != 0)
|
|
{
|
|
candidate.ParticipantIds.Add(occ.RelatedId);
|
|
}
|
|
|
|
InterestScoring.ScoreCheap(candidate);
|
|
InterestRegistry.Upsert(candidate);
|
|
}
|
|
|
|
/// <summary>Harness / push path: ingest one occurrence into the registry now.</summary>
|
|
public static void IngestForHarness(HappinessOccurrence occ)
|
|
{
|
|
IngestHappiness(occ);
|
|
if (occ != null && occ.Sequence > _happinessCursor)
|
|
{
|
|
_happinessCursor = occ.Sequence;
|
|
}
|
|
}
|
|
|
|
/// <summary>Direct registry insert for harness happiness signals (skips drain filters).</summary>
|
|
public static InterestCandidate RegisterHappinessSignal(
|
|
Actor subject,
|
|
string effectId,
|
|
string label,
|
|
string relatedName = "")
|
|
{
|
|
if (subject == null || string.IsNullOrEmpty(effectId))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
long id = SafeId(subject);
|
|
Vector3 pos = Vector3.zero;
|
|
try
|
|
{
|
|
pos = subject.current_position;
|
|
}
|
|
catch
|
|
{
|
|
pos = Vector3.zero;
|
|
}
|
|
|
|
bool grief = effectId.StartsWith("death_");
|
|
InterestTier urgency = InterestScoring.UrgencyForHappiness(effectId, subject);
|
|
float strength = InterestScoring.EventStrengthForHappiness(effectId);
|
|
string key = "happiness:" + effectId + ":" + id + ":0:harness";
|
|
bool alive = false;
|
|
try
|
|
{
|
|
alive = subject.isAlive();
|
|
}
|
|
catch
|
|
{
|
|
alive = false;
|
|
}
|
|
|
|
var candidate = new InterestCandidate
|
|
{
|
|
Key = key,
|
|
Urgency = urgency,
|
|
LeadKind = InterestLeadKind.EventLed,
|
|
Category = grief ? "FamilyEmotion" : "LifeChapter",
|
|
Source = "happiness_harness",
|
|
EventStrength = strength,
|
|
VisualConfidence = 0.9f,
|
|
Position = pos != Vector3.zero ? pos : new Vector3(1f, 1f, 0f),
|
|
FollowUnit = alive ? subject : null,
|
|
SubjectId = id,
|
|
Label = !string.IsNullOrEmpty(label)
|
|
? label
|
|
: (SafeName(subject) + " · " + effectId),
|
|
AssetId = subject.asset != null ? subject.asset.id : "",
|
|
SpeciesId = subject.asset != null ? subject.asset.id : "",
|
|
HappinessEffectId = effectId,
|
|
StatusId = grief ? "crying" : "",
|
|
CreatedAt = Time.unscaledTime,
|
|
LastSeenAt = Time.unscaledTime,
|
|
ExpiresAt = Time.unscaledTime + 60f,
|
|
MinWatch = 4f,
|
|
MaxWatch = 28f,
|
|
Completion = grief
|
|
? InterestCompletionKind.HappinessGrief
|
|
: InterestCompletionKind.FixedDwell
|
|
};
|
|
if (!string.IsNullOrEmpty(relatedName))
|
|
{
|
|
candidate.Label = SafeName(subject) + " mourns " + relatedName;
|
|
}
|
|
|
|
InterestScoring.ScoreCheap(candidate);
|
|
return InterestRegistry.Upsert(candidate);
|
|
}
|
|
|
|
private static void TickScanner()
|
|
{
|
|
float now = Time.unscaledTime;
|
|
if (now - _lastScannerAt < ScannerInterval)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_lastScannerAt = now;
|
|
if (AgentHarness.Busy)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Top battle cluster.
|
|
InterestEvent battle = WorldActivityScanner.FindHottestBattle();
|
|
if (battle != null)
|
|
{
|
|
RegisterDirect(battle);
|
|
}
|
|
|
|
// Character-led vignette seeds (not sole one-best monopoly: register best + a few notables).
|
|
WorldActivityScanner.RegisterTopCharacterCandidates(max: 4);
|
|
}
|
|
|
|
private static void ExtendMatching(long subjectId, string keyPrefix, string statusId)
|
|
{
|
|
var pending = new List<InterestCandidate>(32);
|
|
InterestRegistry.CopyPending(pending);
|
|
for (int i = 0; i < pending.Count; i++)
|
|
{
|
|
InterestCandidate c = pending[i];
|
|
if (c == null || c.SubjectId != subjectId)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (c.Key != null && c.Key.StartsWith(keyPrefix))
|
|
{
|
|
c.StatusId = statusId;
|
|
c.LastSeenAt = Time.unscaledTime;
|
|
c.ExpiresAt = Time.unscaledTime + 20f;
|
|
InterestRegistry.Upsert(c);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void StampCivicBoost(string kingdom, string effectId, float amount)
|
|
{
|
|
string key = (kingdom ?? "") + "|" + (effectId ?? "");
|
|
CivicBoostUntil[key] = Time.unscaledTime + 12f;
|
|
// Store amount in a parallel way via Expires encoding isn't ideal; keep simple additive peek.
|
|
CivicBoostUntil[key + "|amt"] = amount;
|
|
}
|
|
|
|
private static float PeekCivicBoost(string kingdom, string assetId)
|
|
{
|
|
// Map worldlog war assets to happiness war effects loosely.
|
|
string effect = "";
|
|
if (assetId != null && assetId.Contains("war"))
|
|
{
|
|
effect = "just_started_war";
|
|
}
|
|
else if (assetId != null && (assetId.Contains("city") || assetId.Contains("kingdom")))
|
|
{
|
|
effect = "conquered_city";
|
|
}
|
|
|
|
string key = (kingdom ?? "") + "|" + effect;
|
|
if (!CivicBoostUntil.TryGetValue(key, out float until) || Time.unscaledTime > until)
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
return CivicBoostUntil.TryGetValue(key + "|amt", out float amt) ? amt : 15f;
|
|
}
|
|
|
|
private static void PruneCivicBoosts()
|
|
{
|
|
if (CivicBoostUntil.Count < 64)
|
|
{
|
|
return;
|
|
}
|
|
|
|
float now = Time.unscaledTime;
|
|
var remove = new List<string>();
|
|
foreach (KeyValuePair<string, float> kv in CivicBoostUntil)
|
|
{
|
|
if (!kv.Key.EndsWith("|amt") && now > kv.Value)
|
|
{
|
|
remove.Add(kv.Key);
|
|
remove.Add(kv.Key + "|amt");
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < remove.Count; i++)
|
|
{
|
|
CivicBoostUntil.Remove(remove[i]);
|
|
}
|
|
}
|
|
|
|
private static string MapHappinessCategory(string cat)
|
|
{
|
|
switch ((cat ?? "").ToLowerInvariant())
|
|
{
|
|
case "grief":
|
|
return "FamilyEmotion";
|
|
case "social":
|
|
return "Relationship";
|
|
case "lifechapter":
|
|
return "LifeChapter";
|
|
case "civic":
|
|
return "Politics";
|
|
default:
|
|
return "FamilyEmotion";
|
|
}
|
|
}
|
|
|
|
private static string CategoryForTier(InterestTier tier, string asset)
|
|
{
|
|
if (asset == "live_battle")
|
|
{
|
|
return "Combat";
|
|
}
|
|
|
|
if (tier >= InterestTier.Epic)
|
|
{
|
|
return "Politics";
|
|
}
|
|
|
|
if (tier == InterestTier.Story)
|
|
{
|
|
return "Settlement";
|
|
}
|
|
|
|
if (tier == InterestTier.Action)
|
|
{
|
|
return "Combat";
|
|
}
|
|
|
|
if (tier == InterestTier.Curiosity)
|
|
{
|
|
return "Discovery";
|
|
}
|
|
|
|
return "CharacterVignette";
|
|
}
|
|
|
|
private static string CorrBucket(string corr)
|
|
{
|
|
if (string.IsNullOrEmpty(corr))
|
|
{
|
|
return "0";
|
|
}
|
|
|
|
int hash = corr.GetHashCode();
|
|
return (hash & 0xFFFF).ToString("x");
|
|
}
|
|
|
|
private static Actor FindUnitById(long id)
|
|
{
|
|
if (id == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic())
|
|
{
|
|
try
|
|
{
|
|
if (actor != null && actor.getID() == id)
|
|
{
|
|
return actor;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// continue
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static long SafeId(Actor actor)
|
|
{
|
|
if (actor == null)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
try
|
|
{
|
|
return actor.getID();
|
|
}
|
|
catch
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
private static string SafeName(Actor actor)
|
|
{
|
|
if (actor == null)
|
|
{
|
|
return "Someone";
|
|
}
|
|
|
|
try
|
|
{
|
|
string n = actor.getName();
|
|
if (!string.IsNullOrEmpty(n))
|
|
{
|
|
return n;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
return actor.asset != null ? actor.asset.id : "creature";
|
|
}
|
|
}
|