- Implement checks for civilized terminology in camera focus events - Update event handling to suppress inappropriate pack language for civilized characters - Enhance narrative presentation to differentiate between family and pack language - Introduce new scenarios to validate significance of beats and relationships - Improve documentation to clarify narrative structure and event significance
812 lines
29 KiB
C#
812 lines
29 KiB
C#
using System;
|
|
using System.Text;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>
|
|
/// Semantic presentation kind. These values describe observed meaning, never final player-facing
|
|
/// English.
|
|
/// </summary>
|
|
public enum NarrativePresentationKind
|
|
{
|
|
Unknown,
|
|
BondFormed,
|
|
BondBroken,
|
|
SeekingLover,
|
|
FriendFormed,
|
|
ParentWelcomesChild,
|
|
ChildBorn,
|
|
FamilyFormed,
|
|
FamilyEnded,
|
|
FamilyPackFormed,
|
|
FamilyPackJoined,
|
|
FamilyPackLeft,
|
|
GiftGiven,
|
|
GiftReceived,
|
|
SharedIntimacy,
|
|
Conversation,
|
|
Gossip,
|
|
GrievesChild,
|
|
GrievesLover,
|
|
GrievesFriend,
|
|
GrievesKin,
|
|
AuthoredObservation,
|
|
EnsembleObservation,
|
|
StatusObservation,
|
|
WorldObservation
|
|
}
|
|
|
|
public enum NarrativeRelationRole
|
|
{
|
|
None,
|
|
Lover,
|
|
FormerLover,
|
|
Friend,
|
|
Parent,
|
|
Child,
|
|
Kin,
|
|
GiftPartner,
|
|
ConversationPartner
|
|
}
|
|
|
|
/// <summary>Complete semantic input for one orange Beat. Contains no final sentence.</summary>
|
|
public sealed class NarrativePresentationModel
|
|
{
|
|
public string EventId = "";
|
|
public string DevelopmentId = "";
|
|
public NarrativePresentationKind Kind;
|
|
public NarrativeDevelopmentKind DevelopmentKind;
|
|
public long PerspectiveId;
|
|
public LifeSagaIdentity Perspective;
|
|
public long OtherId;
|
|
public LifeSagaIdentity Other;
|
|
public NarrativeRelationRole RelationRole;
|
|
public int Ordinal;
|
|
public int Count;
|
|
public string Change = "";
|
|
public string Outcome = "";
|
|
/// <summary>Catalog-authored observation clause used only by the safe typed fallback.</summary>
|
|
public string AuthoredClause = "";
|
|
public string ActionId = "";
|
|
public string Phase = "";
|
|
public string EvidenceSource = "";
|
|
public float Confidence = 1f;
|
|
|
|
public NarrativePresentationModel Clone()
|
|
{
|
|
return new NarrativePresentationModel
|
|
{
|
|
EventId = EventId,
|
|
DevelopmentId = DevelopmentId,
|
|
Kind = Kind,
|
|
DevelopmentKind = DevelopmentKind,
|
|
PerspectiveId = PerspectiveId,
|
|
Perspective = Perspective,
|
|
OtherId = OtherId,
|
|
Other = Other,
|
|
RelationRole = RelationRole,
|
|
Ordinal = Ordinal,
|
|
Count = Count,
|
|
Change = Change,
|
|
Outcome = Outcome,
|
|
AuthoredClause = AuthoredClause,
|
|
ActionId = ActionId,
|
|
Phase = Phase,
|
|
EvidenceSource = EvidenceSource,
|
|
Confidence = Confidence
|
|
};
|
|
}
|
|
}
|
|
|
|
public static class NarrativePresentationFactory
|
|
{
|
|
public static bool HarnessProbeNamedParticipantFallback(out string detail)
|
|
{
|
|
var occurrence = new HappinessOccurrence
|
|
{
|
|
EffectId = "fallen_in_love",
|
|
SubjectId = 11,
|
|
SubjectName = "Joon",
|
|
RelatedId = 0,
|
|
RelatedName = "Omyahel",
|
|
RelatedSpecies = "human"
|
|
};
|
|
LifeSagaIdentity other = OccurrenceOtherIdentity(occurrence, default(LifeSagaIdentity));
|
|
var model = new NarrativePresentationModel
|
|
{
|
|
Kind = NarrativePresentationKind.BondFormed,
|
|
PerspectiveId = occurrence.SubjectId,
|
|
Perspective = new LifeSagaIdentity { Id = 11, Name = "Joon" },
|
|
OtherId = other.Id,
|
|
Other = other
|
|
};
|
|
string beat = NarrativeRenderer.Beat(model);
|
|
bool ok = other.Id == 0
|
|
&& other.Name == "Omyahel"
|
|
&& beat == "Finds love with Omyahel";
|
|
detail = "other=" + other.Id + ":" + other.Name
|
|
+ " beat='" + beat + "' pass=" + ok;
|
|
return ok;
|
|
}
|
|
|
|
public static NarrativePresentationModel Relationship(string eventId, Actor subject, Actor related)
|
|
{
|
|
if (subject == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
string id = (eventId ?? "").Trim().ToLowerInvariant();
|
|
NarrativePresentationKind presentationKind;
|
|
NarrativeDevelopmentKind developmentKind = NarrativeDevelopmentKind.Unknown;
|
|
NarrativeRelationRole relation = NarrativeRelationRole.None;
|
|
switch (id)
|
|
{
|
|
case "set_lover":
|
|
presentationKind = NarrativePresentationKind.BondFormed;
|
|
developmentKind = NarrativeDevelopmentKind.BondFormed;
|
|
relation = NarrativeRelationRole.Lover;
|
|
break;
|
|
case "clear_lover":
|
|
presentationKind = NarrativePresentationKind.BondBroken;
|
|
developmentKind = NarrativeDevelopmentKind.BondBroken;
|
|
relation = NarrativeRelationRole.FormerLover;
|
|
break;
|
|
case "find_lover":
|
|
presentationKind = NarrativePresentationKind.SeekingLover;
|
|
break;
|
|
case "add_child":
|
|
presentationKind = NarrativePresentationKind.ParentWelcomesChild;
|
|
developmentKind = NarrativeDevelopmentKind.ChildBorn;
|
|
relation = NarrativeRelationRole.Child;
|
|
break;
|
|
case "baby_created":
|
|
presentationKind = NarrativePresentationKind.ChildBorn;
|
|
developmentKind = NarrativeDevelopmentKind.ChildBorn;
|
|
relation = NarrativeRelationRole.Parent;
|
|
if (related == null)
|
|
{
|
|
related = FirstParent(subject);
|
|
}
|
|
break;
|
|
case "new_family":
|
|
presentationKind = NarrativePresentationKind.FamilyFormed;
|
|
break;
|
|
case "family_removed":
|
|
presentationKind = NarrativePresentationKind.FamilyEnded;
|
|
break;
|
|
case "family_group_new":
|
|
presentationKind = NarrativePresentationKind.FamilyPackFormed;
|
|
relation = NarrativeRelationRole.Kin;
|
|
break;
|
|
case "family_group_join":
|
|
presentationKind = NarrativePresentationKind.FamilyPackJoined;
|
|
relation = NarrativeRelationRole.Kin;
|
|
break;
|
|
case "family_group_leave":
|
|
presentationKind = NarrativePresentationKind.FamilyPackLeft;
|
|
relation = NarrativeRelationRole.Kin;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
long perspectiveId = EventFeedUtil.SafeId(subject);
|
|
LifeSagaIdentity otherSnapshot = LifeSagaMemory.Snapshot(related);
|
|
NarrativeDevelopment development = developmentKind == NarrativeDevelopmentKind.Unknown
|
|
? null
|
|
: MatchingDevelopment(perspectiveId, developmentKind, otherSnapshot.Id);
|
|
if (otherSnapshot.Id == 0 && development != null && development.OtherId != 0)
|
|
{
|
|
otherSnapshot = development.Other;
|
|
}
|
|
|
|
int ordinal = 0;
|
|
if (presentationKind == NarrativePresentationKind.ParentWelcomesChild)
|
|
{
|
|
ordinal = NarrativeStoryStore.CountConsequences(
|
|
perspectiveId, CharacterConsequenceKind.BecameParent);
|
|
}
|
|
|
|
string developmentId = development?.Id ?? "";
|
|
return new NarrativePresentationModel
|
|
{
|
|
EventId = string.IsNullOrEmpty(developmentId)
|
|
? "observed:" + id + ":" + perspectiveId + ":" + otherSnapshot.Id
|
|
: developmentId,
|
|
DevelopmentId = developmentId,
|
|
Kind = presentationKind,
|
|
DevelopmentKind = developmentKind,
|
|
PerspectiveId = perspectiveId,
|
|
Perspective = LifeSagaMemory.Snapshot(subject),
|
|
OtherId = otherSnapshot.Id,
|
|
Other = otherSnapshot,
|
|
RelationRole = relation,
|
|
Ordinal = ordinal,
|
|
EvidenceSource = "relationship:" + id,
|
|
Confidence = 1f
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Typed presentation for relationship-bearing happiness signals. Ambient effects deliberately
|
|
/// stay on the compatibility path until their semantic catalog migration.
|
|
/// </summary>
|
|
public static NarrativePresentationModel Happiness(
|
|
HappinessOccurrence occurrence,
|
|
Actor subject,
|
|
Actor related)
|
|
{
|
|
if (occurrence == null || subject == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
string id = (occurrence.EffectId ?? "").Trim().ToLowerInvariant();
|
|
NarrativePresentationKind presentationKind;
|
|
NarrativeDevelopmentKind developmentKind = NarrativeDevelopmentKind.Unknown;
|
|
NarrativeRelationRole relation = NarrativeRelationRole.None;
|
|
switch (id)
|
|
{
|
|
case "fallen_in_love":
|
|
presentationKind = NarrativePresentationKind.BondFormed;
|
|
developmentKind = NarrativeDevelopmentKind.BondFormed;
|
|
relation = NarrativeRelationRole.Lover;
|
|
break;
|
|
case "just_made_friend":
|
|
presentationKind = NarrativePresentationKind.FriendFormed;
|
|
developmentKind = NarrativeDevelopmentKind.FriendFormed;
|
|
relation = NarrativeRelationRole.Friend;
|
|
break;
|
|
case "just_had_child":
|
|
presentationKind = NarrativePresentationKind.ParentWelcomesChild;
|
|
developmentKind = NarrativeDevelopmentKind.ChildBorn;
|
|
relation = NarrativeRelationRole.Child;
|
|
break;
|
|
case "just_gave_gift":
|
|
presentationKind = NarrativePresentationKind.GiftGiven;
|
|
relation = NarrativeRelationRole.GiftPartner;
|
|
break;
|
|
case "just_received_gift":
|
|
presentationKind = NarrativePresentationKind.GiftReceived;
|
|
relation = NarrativeRelationRole.GiftPartner;
|
|
break;
|
|
case "just_kissed":
|
|
presentationKind = NarrativePresentationKind.SharedIntimacy;
|
|
relation = NarrativeRelationRole.Lover;
|
|
break;
|
|
case "just_talked":
|
|
presentationKind = NarrativePresentationKind.Conversation;
|
|
relation = NarrativeRelationRole.ConversationPartner;
|
|
break;
|
|
case "just_talked_gossip":
|
|
presentationKind = NarrativePresentationKind.Gossip;
|
|
relation = NarrativeRelationRole.ConversationPartner;
|
|
break;
|
|
case "death_child":
|
|
presentationKind = NarrativePresentationKind.GrievesChild;
|
|
relation = NarrativeRelationRole.Child;
|
|
break;
|
|
case "death_lover":
|
|
presentationKind = NarrativePresentationKind.GrievesLover;
|
|
relation = NarrativeRelationRole.FormerLover;
|
|
break;
|
|
case "death_best_friend":
|
|
presentationKind = NarrativePresentationKind.GrievesFriend;
|
|
relation = NarrativeRelationRole.Friend;
|
|
break;
|
|
case "death_family_member":
|
|
presentationKind = NarrativePresentationKind.GrievesKin;
|
|
relation = NarrativeRelationRole.Kin;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
long perspectiveId = EventFeedUtil.SafeId(subject);
|
|
LifeSagaIdentity otherSnapshot =
|
|
OccurrenceOtherIdentity(occurrence, LifeSagaMemory.Snapshot(related));
|
|
|
|
NarrativeDevelopment development = developmentKind == NarrativeDevelopmentKind.Unknown
|
|
? null
|
|
: MatchingDevelopment(perspectiveId, developmentKind, otherSnapshot.Id);
|
|
if (otherSnapshot.Id == 0 && development != null && development.OtherId != 0)
|
|
{
|
|
otherSnapshot = development.Other;
|
|
}
|
|
|
|
int ordinal = presentationKind == NarrativePresentationKind.ParentWelcomesChild
|
|
? NarrativeStoryStore.CountConsequences(
|
|
perspectiveId, CharacterConsequenceKind.BecameParent)
|
|
: 0;
|
|
string developmentId = development?.Id ?? "";
|
|
string eventId = !string.IsNullOrEmpty(developmentId)
|
|
? developmentId
|
|
: "happiness:" + id + ":" + perspectiveId + ":" + otherSnapshot.Id
|
|
+ ":" + (occurrence.CorrelationKey ?? "");
|
|
return new NarrativePresentationModel
|
|
{
|
|
EventId = eventId,
|
|
DevelopmentId = developmentId,
|
|
Kind = presentationKind,
|
|
DevelopmentKind = developmentKind,
|
|
PerspectiveId = perspectiveId,
|
|
Perspective = LifeSagaMemory.Snapshot(subject),
|
|
OtherId = otherSnapshot.Id,
|
|
Other = otherSnapshot,
|
|
RelationRole = relation,
|
|
Ordinal = ordinal,
|
|
EvidenceSource = "happiness:" + id,
|
|
Confidence = 1f
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Safe typed envelope for catalog/ensemble events that do not create a durable character
|
|
/// development. The renderer owns subject removal and final HUD hygiene.
|
|
/// </summary>
|
|
public static NarrativePresentationModel Candidate(
|
|
string key,
|
|
string source,
|
|
string actionId,
|
|
string authoredClause,
|
|
Actor subject,
|
|
Actor related)
|
|
{
|
|
string normalizedSource = (source ?? "").Trim().ToLowerInvariant();
|
|
NarrativePresentationKind kind;
|
|
if (normalizedSource == "status")
|
|
{
|
|
kind = NarrativePresentationKind.StatusObservation;
|
|
}
|
|
else if (normalizedSource == "worldlog"
|
|
|| normalizedSource == "era"
|
|
|| normalizedSource == "worldlaw"
|
|
|| normalizedSource == "power"
|
|
|| normalizedSource == "biome"
|
|
|| normalizedSource == "earthquake")
|
|
{
|
|
kind = NarrativePresentationKind.WorldObservation;
|
|
}
|
|
else if ((actionId ?? "").StartsWith("live_", StringComparison.OrdinalIgnoreCase)
|
|
|| normalizedSource == "combat"
|
|
|| normalizedSource == "war"
|
|
|| normalizedSource == "plot")
|
|
{
|
|
kind = NarrativePresentationKind.EnsembleObservation;
|
|
}
|
|
else
|
|
{
|
|
kind = NarrativePresentationKind.AuthoredObservation;
|
|
}
|
|
|
|
return new NarrativePresentationModel
|
|
{
|
|
EventId = string.IsNullOrEmpty(key) ? "observed:" + normalizedSource : key,
|
|
Kind = kind,
|
|
PerspectiveId = EventFeedUtil.SafeId(subject),
|
|
Perspective = LifeSagaMemory.Snapshot(subject),
|
|
OtherId = EventFeedUtil.SafeId(related),
|
|
Other = LifeSagaMemory.Snapshot(related),
|
|
AuthoredClause = authoredClause ?? "",
|
|
ActionId = actionId ?? "",
|
|
Phase = normalizedSource,
|
|
EvidenceSource = "catalog:" + normalizedSource,
|
|
Confidence = 1f
|
|
};
|
|
}
|
|
|
|
private static NarrativeDevelopment MatchingDevelopment(
|
|
long perspectiveId,
|
|
NarrativeDevelopmentKind kind,
|
|
long otherId)
|
|
{
|
|
NarrativeDevelopment development = NarrativeStoryStore.LatestFor(perspectiveId, kind, 3f);
|
|
if (development == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (otherId != 0 && development.OtherId != 0 && development.OtherId != otherId)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return development;
|
|
}
|
|
|
|
private static LifeSagaIdentity OccurrenceOtherIdentity(
|
|
HappinessOccurrence occurrence,
|
|
LifeSagaIdentity live)
|
|
{
|
|
if (live.Id != 0 || occurrence == null)
|
|
{
|
|
return live;
|
|
}
|
|
|
|
if (occurrence.RelatedId == 0 && string.IsNullOrEmpty(occurrence.RelatedName))
|
|
{
|
|
return live;
|
|
}
|
|
|
|
return new LifeSagaIdentity
|
|
{
|
|
Id = occurrence.RelatedId,
|
|
Name = occurrence.RelatedName ?? "",
|
|
SpeciesId = occurrence.RelatedSpecies ?? ""
|
|
};
|
|
}
|
|
|
|
private static Actor FirstParent(Actor child)
|
|
{
|
|
foreach (Actor parent in ActorRelation.EnumerateParents(child, 1))
|
|
{
|
|
if (parent != null)
|
|
{
|
|
return parent;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>Single English owner for typed narrative presentation.</summary>
|
|
public static class NarrativeRenderer
|
|
{
|
|
public static string Beat(NarrativePresentationModel model)
|
|
{
|
|
if (model == null || model.Confidence < 0.75f)
|
|
{
|
|
return "";
|
|
}
|
|
|
|
string other = CleanName(model.Other.Name);
|
|
switch (model.Kind)
|
|
{
|
|
case NarrativePresentationKind.BondFormed:
|
|
return string.IsNullOrEmpty(other) ? "Finds love" : "Finds love with " + other;
|
|
case NarrativePresentationKind.BondBroken:
|
|
return string.IsNullOrEmpty(other) ? "Parts from a lover" : "Parts from " + other;
|
|
case NarrativePresentationKind.SeekingLover:
|
|
return "Seeks a lover";
|
|
case NarrativePresentationKind.FriendFormed:
|
|
return string.IsNullOrEmpty(other) ? "Makes a true friend" : "Befriends " + other;
|
|
case NarrativePresentationKind.ParentWelcomesChild:
|
|
{
|
|
string ordinal = model.Ordinal == 1 ? " first" : "";
|
|
return string.IsNullOrEmpty(other)
|
|
? "Welcomes a" + ordinal + " child"
|
|
: "Welcomes" + ordinal + " child " + other;
|
|
}
|
|
case NarrativePresentationKind.ChildBorn:
|
|
return string.IsNullOrEmpty(other) ? "Is born" : "Is born to " + other;
|
|
case NarrativePresentationKind.FamilyFormed:
|
|
return "Founds a family";
|
|
case NarrativePresentationKind.FamilyEnded:
|
|
return "Their family line ends";
|
|
case NarrativePresentationKind.FamilyPackFormed:
|
|
{
|
|
bool pack = LifeSagaRoster.UsesPackLanguage(
|
|
EventFeedUtil.FindAliveById(model.PerspectiveId));
|
|
string group = pack ? "a family pack" : "a family";
|
|
return string.IsNullOrEmpty(other)
|
|
? "Forms " + group
|
|
: "Forms " + group + " with " + other;
|
|
}
|
|
case NarrativePresentationKind.FamilyPackJoined:
|
|
{
|
|
bool pack = LifeSagaRoster.UsesPackLanguage(
|
|
EventFeedUtil.FindAliveById(model.PerspectiveId));
|
|
string group = pack ? "a family pack" : "a family";
|
|
return string.IsNullOrEmpty(other)
|
|
? "Joins " + group
|
|
: "Joins " + group + " with " + other;
|
|
}
|
|
case NarrativePresentationKind.FamilyPackLeft:
|
|
{
|
|
bool pack = LifeSagaRoster.UsesPackLanguage(
|
|
EventFeedUtil.FindAliveById(model.PerspectiveId));
|
|
string group = pack ? "a family pack" : "a family";
|
|
return string.IsNullOrEmpty(other)
|
|
? "Leaves " + group
|
|
: "Leaves " + group + " shared with " + other;
|
|
}
|
|
case NarrativePresentationKind.GiftGiven:
|
|
return string.IsNullOrEmpty(other) ? "Gives a gift" : "Gives " + other + " a gift";
|
|
case NarrativePresentationKind.GiftReceived:
|
|
return string.IsNullOrEmpty(other)
|
|
? "Receives a gift"
|
|
: "Receives a gift from " + other;
|
|
case NarrativePresentationKind.SharedIntimacy:
|
|
return string.IsNullOrEmpty(other)
|
|
? "Shares intimacy with their lover"
|
|
: "Shares intimacy with " + other;
|
|
case NarrativePresentationKind.Conversation:
|
|
return string.IsNullOrEmpty(other)
|
|
? "Finishes a conversation"
|
|
: "Finishes a conversation with " + other;
|
|
case NarrativePresentationKind.Gossip:
|
|
return string.IsNullOrEmpty(other)
|
|
? "Shares gossip"
|
|
: "Shares gossip with " + other;
|
|
case NarrativePresentationKind.GrievesChild:
|
|
return string.IsNullOrEmpty(other) ? "Mourns a child" : "Mourns child " + other;
|
|
case NarrativePresentationKind.GrievesLover:
|
|
return string.IsNullOrEmpty(other) ? "Mourns a lost lover" : "Mourns lover " + other;
|
|
case NarrativePresentationKind.GrievesFriend:
|
|
return string.IsNullOrEmpty(other) ? "Mourns a lost friend" : "Mourns friend " + other;
|
|
case NarrativePresentationKind.GrievesKin:
|
|
return string.IsNullOrEmpty(other) ? "Mourns a family member" : "Mourns kin " + other;
|
|
case NarrativePresentationKind.AuthoredObservation:
|
|
case NarrativePresentationKind.EnsembleObservation:
|
|
case NarrativePresentationKind.StatusObservation:
|
|
case NarrativePresentationKind.WorldObservation:
|
|
return SafeAuthoredBeat(model);
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Safety audit only. Semantic models prevent duplication; this detects regressions in rendered
|
|
/// output and compatibility fallbacks without rewriting their prose.
|
|
/// </summary>
|
|
public static bool HasSemanticRedundancy(string beat, NarrativePresentationModel model = null)
|
|
{
|
|
if (string.IsNullOrEmpty(beat))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string plain = StripRichText(beat).ToLowerInvariant();
|
|
string[] clauses = plain.Split(new[] { " · " }, StringSplitOptions.None);
|
|
if (clauses.Length > 1)
|
|
{
|
|
string left = clauses[0];
|
|
for (int i = 1; i < clauses.Length; i++)
|
|
{
|
|
string right = clauses[i];
|
|
if (RepeatsConcept(left, right, "child")
|
|
|| RepeatsConcept(left, right, "lover")
|
|
|| RepeatsConcept(left, right, "friend")
|
|
|| RepeatsConcept(left, right, "parent")
|
|
|| RepeatsConcept(left, right, "family pack"))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
string other = CleanName(model?.Other.Name);
|
|
return !string.IsNullOrEmpty(other) && CountOccurrences(plain, other.ToLowerInvariant()) > 1;
|
|
}
|
|
|
|
public static bool HarnessProbeRelationshipProse(out string detail)
|
|
{
|
|
var parent = new LifeSagaIdentity { Id = 1, Name = "Joon", SpeciesId = "human" };
|
|
var child = new LifeSagaIdentity { Id = 2, Name = "Omyahel", SpeciesId = "human" };
|
|
var parentBeat = new NarrativePresentationModel
|
|
{
|
|
EventId = "event:child:1:2",
|
|
DevelopmentId = "child:1:2",
|
|
Kind = NarrativePresentationKind.ParentWelcomesChild,
|
|
DevelopmentKind = NarrativeDevelopmentKind.ChildBorn,
|
|
PerspectiveId = parent.Id,
|
|
Perspective = parent,
|
|
OtherId = child.Id,
|
|
Other = child,
|
|
RelationRole = NarrativeRelationRole.Child,
|
|
Ordinal = 1
|
|
};
|
|
var childBeat = new NarrativePresentationModel
|
|
{
|
|
EventId = parentBeat.EventId,
|
|
DevelopmentId = parentBeat.DevelopmentId,
|
|
Kind = NarrativePresentationKind.ChildBorn,
|
|
DevelopmentKind = NarrativeDevelopmentKind.ChildBorn,
|
|
PerspectiveId = child.Id,
|
|
Perspective = child,
|
|
OtherId = parent.Id,
|
|
Other = parent,
|
|
RelationRole = NarrativeRelationRole.Parent
|
|
};
|
|
var loverBeat = new NarrativePresentationModel
|
|
{
|
|
Kind = NarrativePresentationKind.BondFormed,
|
|
PerspectiveId = parent.Id,
|
|
Perspective = parent,
|
|
OtherId = child.Id,
|
|
Other = child,
|
|
RelationRole = NarrativeRelationRole.Lover
|
|
};
|
|
|
|
string parentLine = Beat(parentBeat);
|
|
string childLine = Beat(childBeat);
|
|
string loverLine = Beat(loverBeat);
|
|
bool oldDetected = HasSemanticRedundancy(
|
|
"Gains a child, Omyahel · their child", parentBeat);
|
|
bool ok = parentLine == "Welcomes first child Omyahel"
|
|
&& childLine == "Is born to Joon"
|
|
&& loverLine == "Finds love with Omyahel"
|
|
&& !HasSemanticRedundancy(parentLine, parentBeat)
|
|
&& !HasSemanticRedundancy(childLine, childBeat)
|
|
&& !HasSemanticRedundancy(loverLine, loverBeat)
|
|
&& oldDetected;
|
|
detail = "parent='" + parentLine
|
|
+ "' child='" + childLine
|
|
+ "' lover='" + loverLine
|
|
+ "' oldDetected=" + oldDetected
|
|
+ " pass=" + ok;
|
|
return ok;
|
|
}
|
|
|
|
private static bool RepeatsConcept(string left, string right, string concept)
|
|
{
|
|
return left.IndexOf(concept, StringComparison.Ordinal) >= 0
|
|
&& right.IndexOf(concept, StringComparison.Ordinal) >= 0;
|
|
}
|
|
|
|
private static int CountOccurrences(string value, string needle)
|
|
{
|
|
if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(needle))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
int count = 0;
|
|
int at = 0;
|
|
while ((at = value.IndexOf(needle, at, StringComparison.Ordinal)) >= 0)
|
|
{
|
|
count++;
|
|
at += needle.Length;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
private static string CleanName(string value) => (value ?? "").Trim();
|
|
|
|
private static string SafeAuthoredBeat(NarrativePresentationModel model)
|
|
{
|
|
string beat = StripRichText(model?.AuthoredClause ?? "").Trim();
|
|
string subject = CleanName(model?.Perspective.Name);
|
|
if (!string.IsNullOrEmpty(subject)
|
|
&& beat.StartsWith(subject, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
beat = beat.Substring(subject.Length).TrimStart(' ', '·', '-', ':');
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(beat))
|
|
{
|
|
beat = EventReason.HumanizeId(model?.ActionId ?? "");
|
|
}
|
|
|
|
if (beat.IndexOf('{') >= 0 || beat.IndexOf('}') >= 0)
|
|
{
|
|
beat = beat.Replace("{a}", "").Replace("{b}", "")
|
|
.Replace("{id}", EventReason.HumanizeId(model?.ActionId ?? ""))
|
|
.Replace("{", "").Replace("}", "").Trim();
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(beat))
|
|
{
|
|
beat = char.ToUpperInvariant(beat[0]) + (beat.Length > 1 ? beat.Substring(1) : "");
|
|
}
|
|
|
|
return beat;
|
|
}
|
|
|
|
private static string StripRichText(string value)
|
|
{
|
|
if (string.IsNullOrEmpty(value) || value.IndexOf('<') < 0)
|
|
{
|
|
return value ?? "";
|
|
}
|
|
|
|
var sb = new StringBuilder(value.Length);
|
|
bool tag = false;
|
|
for (int i = 0; i < value.Length; i++)
|
|
{
|
|
char ch = value[i];
|
|
if (ch == '<')
|
|
{
|
|
tag = true;
|
|
continue;
|
|
}
|
|
|
|
if (tag)
|
|
{
|
|
if (ch == '>')
|
|
{
|
|
tag = false;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
sb.Append(ch);
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
}
|
|
|
|
/// <summary>Bounded counters for selected-caption prose provenance and quality.</summary>
|
|
public static class NarrativePresentationCoverage
|
|
{
|
|
public static int IntakeStructuredCount { get; private set; }
|
|
public static int IntakeCompatibilityCount { get; private set; }
|
|
public static int StructuredCount { get; private set; }
|
|
public static int ExactDevelopmentCount { get; private set; }
|
|
public static int CompatibilityCount { get; private set; }
|
|
public static int RedundancyCount { get; private set; }
|
|
public static string LastSource { get; private set; } = "";
|
|
public static string LastEventId { get; private set; } = "";
|
|
public static string LastDevelopmentId { get; private set; } = "";
|
|
|
|
public static string Summary =>
|
|
"intake=" + IntakeStructuredCount + "/" + IntakeCompatibilityCount
|
|
+ " structured=" + StructuredCount
|
|
+ " exact=" + ExactDevelopmentCount
|
|
+ " compat=" + CompatibilityCount
|
|
+ " redundant=" + RedundancyCount
|
|
+ " last=" + LastSource
|
|
+ " event=" + LastEventId
|
|
+ " development=" + LastDevelopmentId;
|
|
|
|
public static void Clear()
|
|
{
|
|
IntakeStructuredCount = 0;
|
|
IntakeCompatibilityCount = 0;
|
|
StructuredCount = 0;
|
|
ExactDevelopmentCount = 0;
|
|
CompatibilityCount = 0;
|
|
RedundancyCount = 0;
|
|
LastSource = "";
|
|
LastEventId = "";
|
|
LastDevelopmentId = "";
|
|
}
|
|
|
|
public static void NoteIntake(InterestCandidate candidate)
|
|
{
|
|
if (candidate?.NarrativePresentation != null)
|
|
{
|
|
IntakeStructuredCount++;
|
|
}
|
|
else
|
|
{
|
|
IntakeCompatibilityCount++;
|
|
}
|
|
}
|
|
|
|
public static void Note(
|
|
string source,
|
|
string beat,
|
|
NarrativePresentationModel presentation,
|
|
bool exactDevelopment)
|
|
{
|
|
bool structured = presentation != null;
|
|
if (structured)
|
|
{
|
|
StructuredCount++;
|
|
if (exactDevelopment)
|
|
{
|
|
ExactDevelopmentCount++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CompatibilityCount++;
|
|
}
|
|
|
|
if (NarrativeRenderer.HasSemanticRedundancy(beat, presentation))
|
|
{
|
|
RedundancyCount++;
|
|
}
|
|
|
|
LastSource = source ?? "";
|
|
LastEventId = presentation?.EventId ?? "";
|
|
LastDevelopmentId = presentation?.DevelopmentId ?? "";
|
|
}
|
|
}
|