worldbox-observer-mod/IdleSpectator/Events/Feeds/RelationshipInterestFeed.cs
DazedAnon ff4b40d00e feat(narrative): enhance narrative probes and presentation coverage.
- Introduce new narrative probes for episode grammar, ensemble balance, and prose redundancy
- Implement narrative presentation coverage tracking for better event visibility
- Update harness scenarios to include new narrative ingestion and persistence tests
- Refactor event emission to include narrative development IDs and presentations
- Expand InterestCandidate to support narrative presentation models and roles
2026-07-23 14:12:23 -05:00

271 lines
8.8 KiB
C#

using UnityEngine;
namespace IdleSpectator;
/// <summary>Registers relationship lifecycle interest candidates under the ownership contract.</summary>
public static class RelationshipInterestFeed
{
private const float FoundingMergeSeconds = 0.35f;
private static float _foundingAt = -999f;
private static long _foundingActorId;
public static void Emit(
string eventId,
Actor subject,
Actor related,
string labelOverride = null,
NarrativeEventReceipt receipt = null)
{
if (!AgentHarness.LiveFeedsAllowed
|| subject == null
|| !subject.isAlive())
{
return;
}
DiscreteEventEntry entry = EventCatalog.Relationship.GetOrFallback(eventId);
if (!EventCatalog.IsCameraWorthy(entry))
{
InterestDropLog.Record("createsInterest=false", "relationship:" + (eventId ?? ""));
return;
}
NarrativePresentationModel presentation = receipt?.Presentation?.Clone()
?? NarrativePresentationFactory.Relationship(eventId, subject, related);
Actor relatedAlive = related != null && related.isAlive() ? related : null;
bool claimsPeer = eventId == "family_group_new"
|| eventId == "family_group_join"
|| eventId == "family_group_leave"
|| eventId == "set_lover"
|| eventId == "add_child";
if (claimsPeer && relatedAlive == null)
{
relatedAlive = ActorRelation.ResolvePackRelated(subject);
}
string label = !string.IsNullOrEmpty(labelOverride)
? labelOverride
: TypedLabel(eventId, subject, relatedAlive);
// BehFamilyGroupNew always calls newFamily first - keep family_group_new on but
// share the founding interest key so one orange reason wins for that beat.
if (eventId == "family_group_new" && IsRecentFounding(subject))
{
string foundingKey = FoundingKey(subject);
if (InterestRegistry.TryGet(foundingKey, out _))
{
return;
}
EventFeedUtil.Register(
foundingKey,
"relationship",
entry.Category,
label,
entry.Id,
entry.EventStrength,
subject.current_position,
subject,
related: relatedAlive,
presentation: presentation);
FamilyInterestFeed.EmitFromActor(subject);
return;
}
if (claimsPeer && relatedAlive == null)
{
string soloKey = "rel:" + entry.Id + ":" + EventFeedUtil.SafeId(subject) + ":solo";
EventFeedUtil.Register(
soloKey,
"relationship",
entry.Category,
label,
entry.Id,
Mathf.Min(entry.EventStrength, 40f),
subject.current_position,
subject,
presentation: presentation);
return;
}
string key = "rel:" + entry.Id + ":" + EventFeedUtil.SafeId(subject) + ":" + EventFeedUtil.SafeId(relatedAlive);
EventFeedUtil.Register(
key,
"relationship",
entry.Category,
label,
entry.Id,
entry.EventStrength,
subject.current_position,
subject,
related: relatedAlive,
presentation: presentation);
// Soft sticky pack scene after real join/form - not from a world family poll.
if (eventId == "family_group_new" || eventId == "family_group_join")
{
FamilyInterestFeed.EmitFromActor(subject);
}
}
public static void EmitFamily(string eventId, object family, Actor anchor)
{
if (!AgentHarness.LiveFeedsAllowed)
{
return;
}
DiscreteEventEntry entry = EventCatalog.Relationship.GetOrFallback(eventId);
Actor follow = anchor != null && anchor.isAlive() ? anchor : null;
if (follow == null)
{
follow = FirstFamilyMember(family);
}
if (follow == null)
{
return;
}
Actor related = ActorRelation.FirstFamilyPeer(follow);
NarrativePresentationModel presentation =
NarrativePresentationFactory.Relationship(eventId, follow, related);
string label = TypedLabel(eventId, follow, related);
string key = "rel:" + entry.Id + ":" + EventFeedUtil.SafeId(follow);
if (eventId == "new_family")
{
NoteFounding(follow);
key = FoundingKey(follow);
}
EventFeedUtil.Register(
key,
"relationship",
entry.Category,
label,
entry.Id,
entry.EventStrength,
follow.current_position,
follow,
related: related,
presentation: presentation);
}
private static void NoteFounding(Actor anchor)
{
_foundingAt = Time.unscaledTime;
_foundingActorId = EventFeedUtil.SafeId(anchor);
}
private static bool IsRecentFounding(Actor subject)
{
if (subject == null)
{
return false;
}
long id = EventFeedUtil.SafeId(subject);
if (id == 0 || id != _foundingActorId)
{
return false;
}
return Time.unscaledTime - _foundingAt <= FoundingMergeSeconds;
}
private static string FoundingKey(Actor anchor) =>
"rel:new_family:" + EventFeedUtil.SafeId(anchor);
private static string TypedLabel(string eventId, Actor subject, Actor related)
{
switch (eventId)
{
case "set_lover":
return EventReason.Lovers(subject, related);
case "clear_lover":
return EventReason.LoverParted(subject);
case "find_lover":
return EventReason.SeekingLover(subject);
case "become_alpha":
return EventReason.BecomeAlpha(subject);
case "add_child":
return EventReason.NewChild(subject, related);
case "baby_created":
return EventReason.BabyBorn(subject);
case "new_family":
return EventReason.NewFamily(subject);
case "family_removed":
return EventReason.FamilyEnded(subject);
case "family_group_new":
return EventReason.FamilyPack(subject, related, "form");
case "family_group_join":
return EventReason.FamilyPack(subject, related, "join");
case "family_group_leave":
return EventReason.FamilyPack(subject, related, "leave");
default:
return EventReason.Apply(
EventCatalog.Relationship.GetOrFallback(eventId).LabelTemplate,
subject,
related,
eventId);
}
}
private static Actor FirstFamilyMember(object family)
{
if (family == null)
{
return null;
}
try
{
foreach (Actor member in ActorRelation.EnumerateFamilyMembers(
ActorRelation.ResolveActor(
family.GetType().GetField("founder")?.GetValue(family)
?? family.GetType().GetProperty("founder")?.GetValue(family, null))
?? null,
max: 1))
{
return member;
}
}
catch
{
// fall through
}
try
{
object list = family.GetType().GetField("units")?.GetValue(family)
?? family.GetType().GetProperty("units")?.GetValue(family, null)
?? family.GetType().GetMethod("getUnits")?.Invoke(family, null)
?? family.GetType().GetMethod("getSimpleList")?.Invoke(family, null);
if (list is System.Collections.IEnumerable enumerable)
{
foreach (object raw in enumerable)
{
Actor a = ActorRelation.ResolveActor(raw);
if (a != null && a.isAlive())
{
return a;
}
}
}
Actor founder = family.GetType().GetField("founder")?.GetValue(family) as Actor
?? family.GetType().GetProperty("founder")?.GetValue(family, null) as Actor;
if (founder != null && founder.isAlive())
{
return founder;
}
}
catch
{
// ignore
}
return null;
}
}