using System;
using System.Collections.Generic;
namespace IdleSpectator;
///
/// Adds a concise, evidence-backed explanation when an exact event participant matters
/// because of a current MC. Generic identity (species/job/traits) never belongs in the Beat.
///
public static class BeatSignificance
{
private static readonly List Slots =
new List(LifeSagaRoster.Cap);
///
/// Character grounding is footage, not an event. Its live task already appears in the
/// green task chip, so it must never manufacture an orange Beat.
///
public static bool SuppressBeat(InterestCandidate candidate)
{
return candidate != null
&& (candidate.LeadKind == InterestLeadKind.CharacterLed
|| candidate.Completion == InterestCompletionKind.CharacterVignette);
}
public static string Enrich(
Actor focus,
InterestCandidate candidate,
string beat)
{
if (string.IsNullOrEmpty(beat) || candidate == null || SuppressBeat(candidate))
{
return SuppressBeat(candidate) ? "" : beat ?? "";
}
long focusId = EventFeedUtil.SafeId(focus);
long participantId = ExactOtherParticipant(candidate, focusId);
if (participantId == 0
|| participantId == focusId
|| LifeSagaRoster.IsMc(participantId)
|| SemanticsAlreadyExplainRelation(candidate, beat))
{
return beat;
}
string participantName = ExactOtherName(candidate, participantId);
if (string.IsNullOrEmpty(participantName)
|| !ContainsWholeName(StripRichText(beat), participantName))
{
return beat;
}
if (!TryDescribeMcRelation(
participantId,
out long anchorMcId,
out string anchorName,
out RelationEvidence relation))
{
return beat;
}
string qualifier = FormatQualifier(relation, anchorName, colorName: true);
if (string.IsNullOrEmpty(qualifier)
|| StripRichText(beat).IndexOf(
StripRichText(qualifier),
StringComparison.OrdinalIgnoreCase) >= 0)
{
return beat;
}
// Preserve click ownership on the event's named participant. The qualifier explains
// significance but does not replace the event's subject/other ids.
return beat.TrimEnd() + " (" + qualifier + ")";
}
private static long ExactOtherParticipant(InterestCandidate candidate, long focusId)
{
if (candidate?.NarrativePresentation != null)
{
long semanticOther = candidate.NarrativePresentation.OtherId;
if (semanticOther != 0 && semanticOther != focusId)
{
return semanticOther;
}
}
if (candidate == null)
{
return 0;
}
long[] exact =
{
candidate.RelatedId,
EventFeedUtil.SafeId(candidate.RelatedUnit),
candidate.PairOwnerId == focusId ? candidate.PairPartnerId : 0,
candidate.PairPartnerId == focusId ? candidate.PairOwnerId : 0
};
for (int i = 0; i < exact.Length; i++)
{
if (exact[i] != 0 && exact[i] != focusId)
{
return exact[i];
}
}
return 0;
}
private static string ExactOtherName(InterestCandidate candidate, long participantId)
{
NarrativePresentationModel presentation = candidate?.NarrativePresentation;
if (presentation != null
&& presentation.OtherId == participantId
&& !string.IsNullOrEmpty(presentation.Other.Name))
{
return presentation.Other.Name.Trim();
}
if (candidate?.RelatedUnit != null
&& EventFeedUtil.SafeId(candidate.RelatedUnit) == participantId)
{
string related = EventFeedUtil.SafeName(candidate.RelatedUnit);
if (!string.IsNullOrEmpty(related))
{
return related;
}
}
Actor live = EventFeedUtil.FindAliveById(participantId);
return EventFeedUtil.SafeName(live);
}
private static bool TryDescribeMcRelation(
long participantId,
out long anchorMcId,
out string anchorName,
out RelationEvidence relation)
{
anchorMcId = 0;
anchorName = "";
relation = RelationEvidence.None;
Actor participant = EventFeedUtil.FindAliveById(participantId);
Slots.Clear();
LifeSagaRoster.CopySlots(Slots);
int bestPriority = int.MaxValue;
for (int i = 0; i < Slots.Count; i++)
{
LifeSagaSlot slot = Slots[i];
if (slot == null || slot.UnitId == 0 || slot.UnitId == participantId)
{
continue;
}
Actor mc = EventFeedUtil.FindAliveById(slot.UnitId);
RelationEvidence evidence =
SagaProse.ResolveRelation(slot.UnitId, participantId, mc, participant);
string name = !string.IsNullOrEmpty(slot.DisplayName)
? slot.DisplayName
: EventFeedUtil.SafeName(mc);
string candidateQualifier = FormatQualifier(evidence, name, colorName: false);
if (string.IsNullOrEmpty(candidateQualifier))
{
continue;
}
int priority = evidence.Priority > 0 ? evidence.Priority : 9;
if (anchorMcId != 0 && priority >= bestPriority)
{
continue;
}
anchorMcId = slot.UnitId;
anchorName = name;
relation = evidence;
bestPriority = priority;
}
return anchorMcId != 0 && !string.IsNullOrEmpty(anchorName);
}
private static string FormatQualifier(
RelationEvidence relation,
string anchorName,
bool colorName)
{
if (relation == null
|| relation.Class == RelationClass.None
|| string.IsNullOrEmpty(anchorName))
{
return "";
}
string who = colorName ? ActivityProse.ColorName(anchorName) : anchorName;
switch (relation.Class)
{
case RelationClass.Bond:
return (relation.BondBroken ? "former partner of " : "partner of ") + who;
case RelationClass.Friend:
return (relation.BestFriend ? "best friend of " : "friend of ") + who;
case RelationClass.Kin:
switch (relation.KinKind)
{
case KinKind.Parent: return "parent of " + who;
case KinKind.Child: return "child of " + who;
case KinKind.Heir: return "heir of " + who;
case KinKind.Packmate: return "packmate of " + who;
default: return "kin of " + who;
}
case RelationClass.Foe:
switch (relation.FoeKind)
{
case FoeKind.Blood: return "blood foe of " + who;
case FoeKind.War: return "war enemy of " + who;
case FoeKind.Plot: return "plot enemy of " + who;
default: return "rival of " + who;
}
case RelationClass.PlotPartner:
return "plot ally of " + who;
default:
return "";
}
}
private static bool SemanticsAlreadyExplainRelation(
InterestCandidate candidate,
string beat)
{
NarrativePresentationModel presentation = candidate?.NarrativePresentation;
if (presentation != null
&& presentation.RelationRole != NarrativeRelationRole.None)
{
return true;
}
string plain = StripRichText(beat).ToLowerInvariant();
string[] relationWords =
{
"child", "parent", "lover", "partner", "friend", "kin",
"heir", "rival", "enemy", "foe", "ally"
};
for (int i = 0; i < relationWords.Length; i++)
{
if (plain.IndexOf(relationWords[i], StringComparison.Ordinal) >= 0)
{
return true;
}
}
return false;
}
private static bool ContainsWholeName(string text, string name)
{
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(name))
{
return false;
}
int start = 0;
while (start <= text.Length - name.Length)
{
int at = text.IndexOf(name, start, StringComparison.OrdinalIgnoreCase);
if (at < 0)
{
return false;
}
bool left = at == 0 || !IsNameChar(text[at - 1]);
int end = at + name.Length;
bool right = end >= text.Length || !IsNameChar(text[end]);
if (left && right)
{
return true;
}
start = at + 1;
}
return false;
}
private static bool IsNameChar(char value)
{
return char.IsLetterOrDigit(value) || value == '\'' || value == '-';
}
private static string StripRichText(string value)
{
if (string.IsNullOrEmpty(value) || value.IndexOf('<') < 0)
{
return value ?? "";
}
var chars = new System.Text.StringBuilder(value.Length);
bool tag = false;
for (int i = 0; i < value.Length; i++)
{
char ch = value[i];
if (ch == '<')
{
tag = true;
}
else if (ch == '>')
{
tag = false;
}
else if (!tag)
{
chars.Append(ch);
}
}
return chars.ToString();
}
public static bool HarnessProbePolicy(out string detail)
{
var fill = new InterestCandidate
{
LeadKind = InterestLeadKind.CharacterLed,
Completion = InterestCompletionKind.CharacterVignette,
Label = "Elf",
NarrativePresentation = new NarrativePresentationModel
{
Kind = NarrativePresentationKind.AuthoredObservation,
AuthoredClause = "Elf"
}
};
var child = new RelationEvidence
{
Class = RelationClass.Kin,
KinKind = KinKind.Child,
Priority = 4
};
var rival = new RelationEvidence
{
Class = RelationClass.Foe,
FoeKind = FoeKind.Rematch,
Priority = 2
};
bool fillSuppressed = SuppressBeat(fill);
string childQualifier = FormatQualifier(child, "Norron", colorName: false);
string rivalQualifier = FormatQualifier(rival, "Woof", colorName: false);
bool pass = fillSuppressed
&& childQualifier == "child of Norron"
&& rivalQualifier == "rival of Woof";
detail = "fillSuppressed=" + fillSuppressed
+ " child='" + childQualifier
+ "' rival='" + rivalQualifier
+ "' pass=" + pass;
return pass;
}
public static bool HarnessProbeLiveRelation(
Actor focus,
Actor participant,
out string detail)
{
long participantId = EventFeedUtil.SafeId(participant);
string participantName = EventFeedUtil.SafeName(participant);
var candidate = new InterestCandidate
{
LeadKind = InterestLeadKind.EventLed,
Completion = InterestCompletionKind.FixedDwell,
SubjectId = EventFeedUtil.SafeId(focus),
RelatedId = participantId,
RelatedUnit = participant,
NarrativePresentation = new NarrativePresentationModel
{
Kind = NarrativePresentationKind.AuthoredObservation,
PerspectiveId = EventFeedUtil.SafeId(focus),
Perspective = LifeSagaMemory.Snapshot(focus),
OtherId = participantId,
Other = LifeSagaMemory.Snapshot(participant),
AuthoredClause = "Kills " + participantName
}
};
string raw = "Kills " + participantName;
string enriched = Enrich(focus, candidate, raw);
string focusName = EventFeedUtil.SafeName(focus);
bool pass = focus != null
&& participant != null
&& LifeSagaRoster.IsMc(EventFeedUtil.SafeId(focus))
&& enriched.IndexOf("child of", StringComparison.OrdinalIgnoreCase) >= 0
&& enriched.IndexOf(focusName, StringComparison.OrdinalIgnoreCase) >= 0;
detail = "raw='" + raw + "' enriched='" + enriched + "' pass=" + pass;
return pass;
}
}