- 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
371 lines
12 KiB
C#
371 lines
12 KiB
C#
using System;
|
|
using UnityEngine;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>
|
|
/// Canonical confirmed-observation writer. It owns raw Saga facts and semantic development
|
|
/// projection so callers cannot independently emit the same change twice.
|
|
/// </summary>
|
|
public static class NarrativeEventRecorder
|
|
{
|
|
public static NarrativeEventReceipt BondFormed(
|
|
Actor subject,
|
|
Actor partner,
|
|
string source = "set_lover")
|
|
{
|
|
string developmentId = NarrativeDevelopmentRouter.BondFormedId(subject, partner);
|
|
return Record(
|
|
NarrativeEventKind.BondFormed,
|
|
developmentId,
|
|
subject,
|
|
partner,
|
|
source,
|
|
() => LifeSagaMemory.RecordBondFormedFact(subject, partner, source),
|
|
() => NarrativeDevelopmentRouter.BondFormedExact(
|
|
subject, partner, source, developmentId),
|
|
"set_lover");
|
|
}
|
|
|
|
public static NarrativeEventReceipt BondBroken(
|
|
Actor subject,
|
|
Actor former,
|
|
string source = "clear_lover")
|
|
{
|
|
string developmentId = NarrativeDevelopmentRouter.BondBrokenId(subject, former);
|
|
return Record(
|
|
NarrativeEventKind.BondBroken,
|
|
developmentId,
|
|
subject,
|
|
former,
|
|
source,
|
|
() => LifeSagaMemory.RecordBondBrokenFact(subject, former, source),
|
|
() => NarrativeDevelopmentRouter.BondBrokenExact(
|
|
subject, former, source, developmentId),
|
|
"clear_lover");
|
|
}
|
|
|
|
public static NarrativeEventReceipt ChildBorn(
|
|
Actor parent,
|
|
Actor child,
|
|
string source = "add_child")
|
|
{
|
|
string developmentId = NarrativeDevelopmentRouter.ChildBornId(parent, child);
|
|
return Record(
|
|
NarrativeEventKind.ChildBorn,
|
|
developmentId,
|
|
parent,
|
|
child,
|
|
source,
|
|
() => LifeSagaMemory.RecordParentChildFact(parent, child, source),
|
|
() => NarrativeDevelopmentRouter.ChildBornExact(
|
|
parent, child, source, developmentId),
|
|
"add_child");
|
|
}
|
|
|
|
public static NarrativeEventReceipt FriendFormed(
|
|
Actor subject,
|
|
Actor friend,
|
|
string source = "set_best_friend")
|
|
{
|
|
string developmentId = NarrativeDevelopmentRouter.FriendFormedId(subject, friend);
|
|
return Record(
|
|
NarrativeEventKind.FriendFormed,
|
|
developmentId,
|
|
subject,
|
|
friend,
|
|
source,
|
|
() => LifeSagaMemory.RecordFriendFormedFact(subject, friend, source),
|
|
() => NarrativeDevelopmentRouter.FriendFormedExact(
|
|
subject, friend, source, developmentId),
|
|
eventId: "");
|
|
}
|
|
|
|
public static NarrativeEventReceipt Kill(
|
|
Actor killer,
|
|
Actor victim,
|
|
string source = "newKillAction")
|
|
{
|
|
string developmentId = NarrativeDevelopmentRouter.KillId(killer, victim);
|
|
return Record(
|
|
NarrativeEventKind.Kill,
|
|
developmentId,
|
|
killer,
|
|
victim,
|
|
source,
|
|
() => LifeSagaMemory.RecordKillFact(killer, victim, source),
|
|
() => NarrativeDevelopmentRouter.KillExact(
|
|
killer, victim, source, developmentId),
|
|
eventId: "",
|
|
outcome: "victim died",
|
|
magnitude: 82f);
|
|
}
|
|
|
|
public static NarrativeEventReceipt Death(
|
|
Actor victim,
|
|
Actor killer,
|
|
string attackType,
|
|
string source = "die")
|
|
{
|
|
string developmentId = NarrativeDevelopmentRouter.DeathId(victim);
|
|
return Record(
|
|
NarrativeEventKind.Death,
|
|
developmentId,
|
|
victim,
|
|
killer,
|
|
source,
|
|
() => LifeSagaMemory.RecordDeathFact(victim, killer, attackType, source),
|
|
() => NarrativeDevelopmentRouter.DeathExact(
|
|
victim, killer, attackType, source, developmentId),
|
|
eventId: "",
|
|
newState: "dead",
|
|
outcome: attackType ?? "",
|
|
resolved: true,
|
|
magnitude: 100f);
|
|
}
|
|
|
|
public static NarrativeEventReceipt Role(
|
|
Actor subject,
|
|
string role,
|
|
bool gained,
|
|
string source = "role")
|
|
{
|
|
string developmentId = NarrativeDevelopmentRouter.RoleId(subject, role, gained);
|
|
return Record(
|
|
NarrativeEventKind.RoleChanged,
|
|
developmentId,
|
|
subject,
|
|
null,
|
|
source,
|
|
() => LifeSagaMemory.RecordRoleFact(subject, role, gained, source),
|
|
() => NarrativeDevelopmentRouter.RoleExact(
|
|
subject, role, gained, source, developmentId),
|
|
eventId: "",
|
|
newState: gained ? role ?? "" : "",
|
|
previousState: gained ? "" : role ?? "",
|
|
resolved: !gained,
|
|
magnitude: 78f);
|
|
}
|
|
|
|
public static NarrativeEventReceipt Founding(
|
|
Actor subject,
|
|
string kind,
|
|
string key,
|
|
string source)
|
|
{
|
|
string developmentId = NarrativeDevelopmentRouter.FoundingId(subject, kind, key);
|
|
return Record(
|
|
NarrativeEventKind.Founding,
|
|
developmentId,
|
|
subject,
|
|
null,
|
|
source,
|
|
() => LifeSagaMemory.RecordFoundingFact(subject, kind, key, source),
|
|
() => NarrativeDevelopmentRouter.FoundingExact(
|
|
subject, kind, key, source, developmentId),
|
|
eventId: "",
|
|
familyKey: kind == "family" ? key : "",
|
|
cityKey: kind == "city" ? key : "",
|
|
kingdomKey: kind == "kingdom" ? key : "",
|
|
newState: kind ?? "",
|
|
magnitude: 82f);
|
|
}
|
|
|
|
public static NarrativeEventReceipt Home(
|
|
Actor subject,
|
|
bool gained,
|
|
string source = "home")
|
|
{
|
|
string developmentId = NarrativeDevelopmentRouter.HomeId(subject, gained);
|
|
return Record(
|
|
NarrativeEventKind.HomeChanged,
|
|
developmentId,
|
|
subject,
|
|
null,
|
|
source,
|
|
() => LifeSagaMemory.RecordHomeFact(subject, gained, source),
|
|
() => NarrativeDevelopmentRouter.HomeExact(
|
|
subject, gained, source, developmentId),
|
|
eventId: "",
|
|
previousState: gained ? "" : "home",
|
|
newState: gained ? "home" : "",
|
|
outcome: gained ? "" : "home lost",
|
|
resolved: !gained,
|
|
magnitude: gained ? 64f : 72f);
|
|
}
|
|
|
|
public static NarrativeEventReceipt War(
|
|
Actor subject,
|
|
Actor other,
|
|
string warKey,
|
|
string kingdomKey,
|
|
bool ended,
|
|
string source = "war",
|
|
string note = "")
|
|
{
|
|
string developmentId = NarrativeDevelopmentRouter.WarId(subject, warKey, ended);
|
|
return Record(
|
|
NarrativeEventKind.WarChanged,
|
|
developmentId,
|
|
subject,
|
|
other,
|
|
source,
|
|
() => LifeSagaMemory.RecordWarFact(
|
|
subject, warKey, kingdomKey, ended, source, other, note),
|
|
() => NarrativeDevelopmentRouter.WarExact(
|
|
subject, other, warKey, ended, source, developmentId),
|
|
eventId: "",
|
|
kingdomKey: kingdomKey,
|
|
warKey: warKey,
|
|
outcome: ended ? "war ended" : "",
|
|
note: note,
|
|
resolved: ended,
|
|
magnitude: ended ? 86f : 68f);
|
|
}
|
|
|
|
public static NarrativeEventReceipt Plot(
|
|
Actor subject,
|
|
Actor other,
|
|
string plotKey,
|
|
string plotAsset,
|
|
bool ended,
|
|
string source = "plot")
|
|
{
|
|
string developmentId = NarrativeDevelopmentRouter.PlotId(subject, plotKey, ended);
|
|
return Record(
|
|
NarrativeEventKind.PlotChanged,
|
|
developmentId,
|
|
subject,
|
|
other,
|
|
source,
|
|
() => LifeSagaMemory.RecordPlotFact(
|
|
subject, plotKey, plotAsset, ended, source, other),
|
|
() => NarrativeDevelopmentRouter.PlotExact(
|
|
subject, other, plotKey, ended, source, developmentId),
|
|
eventId: "",
|
|
plotKey: plotKey,
|
|
outcome: ended ? "plot ended" : "",
|
|
note: LifeSagaMemory.PlotDisplay(plotAsset),
|
|
resolved: ended,
|
|
magnitude: ended ? 78f : 64f);
|
|
}
|
|
|
|
private static NarrativeEventReceipt Record(
|
|
NarrativeEventKind kind,
|
|
string developmentId,
|
|
Actor subject,
|
|
Actor other,
|
|
string source,
|
|
Func<LifeSagaFact> recordFact,
|
|
Func<bool> recordDevelopment,
|
|
string eventId,
|
|
string familyKey = "",
|
|
string cityKey = "",
|
|
string kingdomKey = "",
|
|
string warKey = "",
|
|
string plotKey = "",
|
|
string previousState = "",
|
|
string newState = "",
|
|
string outcome = "",
|
|
string note = "",
|
|
bool resolved = false,
|
|
float magnitude = 50f)
|
|
{
|
|
long subjectId = EventFeedUtil.SafeId(subject);
|
|
if (subjectId == 0 || string.IsNullOrEmpty(developmentId))
|
|
{
|
|
return new NarrativeEventReceipt { RejectionReason = "missing_identity" };
|
|
}
|
|
|
|
string id = "event:" + developmentId;
|
|
NarrativeEventRecord existing = NarrativeEventStore.Get(id);
|
|
if (existing != null)
|
|
{
|
|
NarrativePresentationModel duplicatePresentation =
|
|
string.IsNullOrEmpty(eventId)
|
|
? null
|
|
: NarrativePresentationFactory.Relationship(eventId, subject, other);
|
|
StampPresentation(duplicatePresentation, existing.Id, existing.DevelopmentId, existing.Other);
|
|
return new NarrativeEventReceipt
|
|
{
|
|
IsNew = false,
|
|
EventId = existing.Id,
|
|
DevelopmentId = existing.DevelopmentId,
|
|
Presentation = duplicatePresentation
|
|
};
|
|
}
|
|
|
|
LifeSagaFact fact = recordFact != null ? recordFact() : null;
|
|
if (fact == null)
|
|
{
|
|
return new NarrativeEventReceipt { RejectionReason = "fact_rejected" };
|
|
}
|
|
|
|
bool newDevelopment = recordDevelopment != null && recordDevelopment();
|
|
NarrativeDevelopment development = NarrativeStoryStore.Development(developmentId);
|
|
LifeSagaIdentity otherSnapshot = fact.Other.Id != 0
|
|
? fact.Other
|
|
: LifeSagaMemory.Snapshot(other);
|
|
var record = new NarrativeEventRecord
|
|
{
|
|
Id = id,
|
|
DedupeKey = developmentId,
|
|
Kind = kind,
|
|
SubjectId = subjectId,
|
|
Subject = fact.Subject.Id != 0 ? fact.Subject : LifeSagaMemory.Snapshot(subject),
|
|
OtherId = otherSnapshot.Id,
|
|
Other = otherSnapshot,
|
|
DevelopmentId = developmentId,
|
|
CorrelationKey = fact.Key ?? developmentId,
|
|
FamilyKey = familyKey ?? "",
|
|
CityKey = cityKey ?? "",
|
|
KingdomKey = kingdomKey ?? "",
|
|
WarKey = warKey ?? "",
|
|
PlotKey = plotKey ?? "",
|
|
PreviousState = previousState ?? "",
|
|
NewState = newState ?? "",
|
|
Outcome = outcome ?? "",
|
|
Note = note ?? "",
|
|
Resolved = resolved,
|
|
EvidenceSource = source ?? "",
|
|
DateLabel = fact.DateLabel ?? "",
|
|
WorldTime = fact.WorldTime,
|
|
ObservedAt = Time.unscaledTime,
|
|
Magnitude = magnitude,
|
|
Confidence = 1f
|
|
};
|
|
bool newEvent = NarrativeEventStore.Add(record);
|
|
NarrativePresentationModel presentation =
|
|
string.IsNullOrEmpty(eventId)
|
|
? null
|
|
: NarrativePresentationFactory.Relationship(eventId, subject, other);
|
|
StampPresentation(presentation, record.Id, developmentId, otherSnapshot);
|
|
return new NarrativeEventReceipt
|
|
{
|
|
IsNew = newEvent || newDevelopment,
|
|
EventId = record.Id,
|
|
DevelopmentId = development?.Id ?? developmentId,
|
|
Presentation = presentation
|
|
};
|
|
}
|
|
|
|
private static void StampPresentation(
|
|
NarrativePresentationModel presentation,
|
|
string eventId,
|
|
string developmentId,
|
|
LifeSagaIdentity other)
|
|
{
|
|
if (presentation == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
presentation.EventId = eventId ?? "";
|
|
presentation.DevelopmentId = developmentId ?? "";
|
|
if (presentation.OtherId == 0 && other.Id != 0)
|
|
{
|
|
presentation.OtherId = other.Id;
|
|
presentation.Other = other;
|
|
}
|
|
}
|
|
}
|