fix(event-catalog): update camera setting for laughing event and refine narrative logic

- Set camera property to false for the laughing event in the event catalog
- Update documentation for StoryDecisionIntentLive to clarify intent handling
- Refactor narrative exposure ledger to improve combat and consequence management
- Enhance interest feeds to prevent premature relationship milestone publishing
- Adjust narrative story store limits for runtime events and characters
This commit is contained in:
DazedAnon 2026-07-24 09:43:50 -05:00
parent c9000531fc
commit 4becf1c5dc
7 changed files with 178 additions and 62 deletions

View file

@ -166,6 +166,7 @@ public static partial class EventCatalog
|| s.Contains("fireworks")
|| s.Contains("put_out_fire")
|| s.Contains("burn_tumors")
|| s.Contains("sexual_reproduction")
|| s.Contains("asexual_reproduction")
|| s.Contains("take_city_item")
|| s.Contains("try_to_steal")

View file

@ -683,6 +683,17 @@ public static class InterestFeeds
long sid = SafeId(subject);
long rid = SafeId(related);
bool relationshipMilestone =
milestoneKey.Equals("milestone_friend", StringComparison.OrdinalIgnoreCase)
|| milestoneKey.Equals("milestone_lover", StringComparison.OrdinalIgnoreCase);
// Chronicle rebuilds can observe one side of a relationship before the partner has
// finished loading. Do not publish a two-person sentence until both visual owners exist.
if (relationshipMilestone
&& (related == null || !related.isAlive() || rid == 0))
{
return;
}
string cameraLabel = label ?? milestoneKey;
if (milestoneKey.Equals("milestone_kill", StringComparison.OrdinalIgnoreCase))
{

View file

@ -2801,7 +2801,8 @@ internal static class HarnessScenarios
}
/// <summary>
/// Live DeferredInterestFeed EmitDecision path cools intent class (not only harness inject).
/// Reproduction intent is not a story beat. The live DeferredInterestFeed path must
/// reject it before registry intake and leave the camera free for confirmed outcomes.
/// </summary>
private static List<HarnessCommand> StoryDecisionIntentLive()
{
@ -2820,20 +2821,11 @@ internal static class HarnessScenarios
Step("sdl9", "interest_end_session"),
Step("sdl10", "interest_variety_clear"),
Step("sdl11", "interest_story_clear"),
Step("sdl20", "decision_emit", expect: "sexual_reproduction_try"),
Step("sdl21", "director_run", wait: 1.2f),
Step("sdl22", "assert", expect: "tip_contains", value: "try to reproduce"),
Step("sdl23", "assert", expect: "beat_cooled", value: "sexual_reproduction_try"),
Step("sdl24", "assert", expect: "beat_cooled", value: "decision_find_lover"),
Step("sdl25", "interest_end_session"),
Step("sdl26", "interest_story_clear"),
Step("sdl30", "decision_emit", expect: "find_lover"),
Step("sdl31", "interest_inject", asset: "human", label: "ClearPeer", tier: "Action",
expect: "sdl_peer", value: "lead=event;evt=70;char=5;force=false;ttl=60"),
Step("sdl32", "director_run", wait: 1.2f),
Step("sdl33", "assert", expect: "tip_contains", value: "ClearPeer"),
Step("sdl34", "assert", expect: "tip_not_contains", value: "reproduce"),
Step("sdl35", "assert", expect: "tip_not_contains", value: "find a lover"),
Step("sdl20", "assert", expect: "decision_worthy",
value: "sexual_reproduction_try", label: "false"),
Step("sdl21", "decision_emit", expect: "sexual_reproduction_try"),
Step("sdl22", "director_run", wait: 1.2f),
Step("sdl23", "assert", expect: "tip_not_contains", value: "reproduce"),
Step("sdl90", "fast_timing", value: "false"),
Step("sdl99", "snapshot"),
};

View file

@ -21,13 +21,13 @@ public static class NarrativeExposureLedger
new Dictionary<long, Exposure>();
private static readonly Dictionary<string, Exposure> Threads =
new Dictionary<string, Exposure>(StringComparer.Ordinal);
private static readonly List<bool> PresentedCuts = new List<bool>(6);
private static readonly List<bool> PresentedCuts = new List<bool>(9);
private static int _maxCharacterCuts;
private static int _maxThreadCuts;
private static int _combatRun;
private const int PresentedWindow = 6;
private const int CombatWindowBudget = 2;
private const int CombatRunBudget = 2;
private const int PresentedWindow = 9;
private const int CombatWindowBudget = 3;
private const int CombatRunBudget = 3;
public static void Clear()
{
@ -90,9 +90,10 @@ public static class NarrativeExposureLedger
}
/// <summary>
/// Two combat cuts may establish and escalate a conflict; another story family must
/// then receive space. Criticality affects which combat cut wins, not whether combat
/// may exceed the global story-first exposure budget.
/// Three combat cuts may establish, escalate, and resolve a conflict; another story
/// family must then receive space. The rolling window also holds combat to one third
/// of authored cuts. Criticality affects which combat cut wins, not whether combat may
/// exceed the global story-first exposure budget.
/// </summary>
public static bool MayPresent(InterestCandidate candidate)
{
@ -150,20 +151,34 @@ public static class NarrativeExposureLedger
};
NotePresented(combat);
NotePresented(combat);
NotePresented(combat);
bool routineBlocked = !MayPresent(combat);
bool criticalBounded = !MayPresent(criticalCombat);
for (int i = 0; i < 5; i++) NotePresented(family);
for (int i = 0; i < PresentedWindow; i++) NotePresented(family);
bool routineReopened = MayPresent(combat);
Clear();
var activityCombat = new InterestCandidate
{
Completion = InterestCompletionKind.ActivityActive,
Category = "Combat",
Label = "Aro is fighting Bex",
EventStrength = 70f
};
NotePresented(activityCombat);
NotePresented(activityCombat);
NotePresented(activityCombat);
bool activityCombatBounded = !MayPresent(activityCombat);
bool ok = underCovered > prolific
&& repetition > 0f
&& diverse == 0f
&& routineBlocked
&& criticalBounded
&& routineReopened;
&& routineReopened
&& activityCombatBounded;
detail = "debt=" + underCovered.ToString("0.0") + "/" + prolific.ToString("0.0")
+ " repetition=" + repetition.ToString("0.0") + "/" + diverse.ToString("0.0")
+ " combat=" + routineBlocked + "/" + criticalBounded
+ "/" + routineReopened
+ "/" + routineReopened + "/" + activityCombatBounded
+ " pass=" + ok;
Clear();
return ok;
@ -196,18 +211,33 @@ public static class NarrativeExposureLedger
}
}
private static bool IsCombat(InterestCandidate candidate)
internal static bool IsCombat(InterestCandidate candidate)
{
return candidate != null
&& (candidate.Completion == InterestCompletionKind.CombatActive
|| candidate.Completion == InterestCompletionKind.WarFront
|| string.Equals(
candidate.AssetId,
"live_combat",
StringComparison.OrdinalIgnoreCase)
|| string.Equals(
candidate.AssetId,
"live_battle",
StringComparison.OrdinalIgnoreCase));
if (candidate == null)
{
return false;
}
if (candidate.Completion == InterestCompletionKind.CombatActive
|| candidate.Completion == InterestCompletionKind.WarFront
|| string.Equals(candidate.Category, "Combat", StringComparison.OrdinalIgnoreCase)
|| string.Equals(candidate.AssetId, "live_combat", StringComparison.OrdinalIgnoreCase)
|| string.Equals(candidate.AssetId, "live_battle", StringComparison.OrdinalIgnoreCase)
|| (!string.IsNullOrEmpty(candidate.Verb)
&& candidate.Verb.IndexOf("fight", StringComparison.OrdinalIgnoreCase) >= 0))
{
return true;
}
string label = candidate.Label ?? "";
return label.StartsWith("Duel", StringComparison.OrdinalIgnoreCase)
|| label.StartsWith("Skirmish", StringComparison.OrdinalIgnoreCase)
|| label.StartsWith("Battle", StringComparison.OrdinalIgnoreCase)
|| label.StartsWith("Mass", StringComparison.OrdinalIgnoreCase)
|| label.IndexOf(" fighting ", StringComparison.OrdinalIgnoreCase) >= 0
|| label.IndexOf(" in a fight", StringComparison.OrdinalIgnoreCase) >= 0
|| (label.IndexOf(" vs ", StringComparison.OrdinalIgnoreCase) >= 0
&& !label.StartsWith("War -", StringComparison.OrdinalIgnoreCase)
&& !label.StartsWith("Plot -", StringComparison.OrdinalIgnoreCase));
}
}

View file

@ -14,9 +14,15 @@ public static class NarrativeStoryStore
private const int DevelopmentsPerThread = 16;
private const int MaxRuntimeEvents = 2200;
private const int MaxRuntimeDevelopments = 2000;
private const int MaxRuntimeThreads = 650;
private const int MaxRuntimeThreads = 512;
private const int MaxRuntimeConsequences = 1200;
private const int MaxRuntimeCharacters = 2000;
// Saga/episode consequences are retained independently. Anonymous history competes
// for this smaller editorial budget so it cannot pin one character per world event.
private const int MaxAmbientRuntimeConsequences = 192;
// Character states referenced by retained story material are always kept. Beyond
// those, retain only a small bench of strong emerging challengers.
private const int MaxEmergingRuntimeCharacters = 64;
private const float RuntimeCompactionIntervalSeconds = 60f;
internal const int SchedulerCandidateLimit = 96;
internal const int SchedulerCopyLimit = 128;
@ -736,6 +742,10 @@ public static class NarrativeStoryStore
AddExistingIds(state.OpenThreadIds, Threads, retainedThreadIds);
AddExistingIds(state.ResolvedThreadIds, Threads, retainedThreadIds);
}
// Snapshot the authored spine before ambient challengers fill the runtime budget.
// Consequences belonging to these threads remain lossless.
var protectedThreadIds =
new HashSet<string>(retainedThreadIds, StringComparer.Ordinal);
ThreadScratch.Clear();
foreach (NarrativeThread thread in Threads.Values)
@ -775,8 +785,10 @@ public static class NarrativeStoryStore
var rankedConsequences = new List<CharacterConsequence>(Consequences.Values);
rankedConsequences.Sort((a, b) =>
{
bool aProtected = a != null && protectedCharacters.Contains(a.CharacterId);
bool bProtected = b != null && protectedCharacters.Contains(b.CharacterId);
bool aProtected = IsProtectedConsequence(
a, protectedCharacters, protectedThreadIds);
bool bProtected = IsProtectedConsequence(
b, protectedCharacters, protectedThreadIds);
if (aProtected != bProtected) return aProtected ? -1 : 1;
int importance = (b?.Importance ?? float.MinValue).CompareTo(
a?.Importance ?? float.MinValue);
@ -786,26 +798,38 @@ public static class NarrativeStoryStore
});
var retainedConsequenceIds = new HashSet<string>(StringComparer.Ordinal);
int protectedConsequences = 0;
int ambientConsequences = 0;
for (int i = 0;
i < rankedConsequences.Count
&& retainedConsequenceIds.Count < MaxRuntimeConsequences;
i++)
{
CharacterConsequence consequence = rankedConsequences[i];
if (consequence == null || string.IsNullOrEmpty(consequence.Id)
|| string.IsNullOrEmpty(consequence.DevelopmentId)
|| !Developments.ContainsKey(consequence.DevelopmentId))
{
continue;
}
if (!retainedDevelopmentIds.Contains(consequence.DevelopmentId)
&& retainedDevelopmentIds.Count >= MaxRuntimeDevelopments)
bool protectedConsequence = IsProtectedConsequence(
consequence, protectedCharacters, protectedThreadIds);
if (!protectedConsequence
&& ambientConsequences >= MaxAmbientRuntimeConsequences)
{
continue;
}
retainedDevelopmentIds.Add(consequence.DevelopmentId);
retainedConsequenceIds.Add(consequence.Id);
if (!TryRetainConsequence(
consequence,
retainedDevelopmentIds,
retainedConsequenceIds))
{
continue;
}
if (protectedConsequence)
{
protectedConsequences++;
}
else
{
ambientConsequences++;
}
}
var rankedDevelopments = new List<NarrativeDevelopment>(Developments.Values);
@ -875,11 +899,6 @@ public static class NarrativeStoryStore
{
if (consequence == null) continue;
if (consequence.CharacterId != 0) retainedCharacterIds.Add(consequence.CharacterId);
if (consequence.OtherId != 0
&& retainedCharacterIds.Count < MaxRuntimeCharacters)
{
retainedCharacterIds.Add(consequence.OtherId);
}
}
var rankedCharacters = new List<CharacterStoryState>(Characters.Values);
@ -892,9 +911,12 @@ public static class NarrativeStoryStore
return (b?.LastMeaningfulChangeAt ?? float.MinValue).CompareTo(
a?.LastMeaningfulChangeAt ?? float.MinValue);
});
int emergingLimit = Math.Min(
MaxRuntimeCharacters,
retainedCharacterIds.Count + MaxEmergingRuntimeCharacters);
for (int i = 0;
i < rankedCharacters.Count
&& retainedCharacterIds.Count < MaxRuntimeCharacters;
&& retainedCharacterIds.Count < emergingLimit;
i++)
{
CharacterStoryState state = rankedCharacters[i];
@ -918,6 +940,7 @@ public static class NarrativeStoryStore
+ " developments=" + beforeDevelopments + "->" + Developments.Count
+ " threads=" + beforeThreads + "->" + Threads.Count
+ " consequences=" + beforeConsequences + "->" + Consequences.Count
+ " consequenceMix=" + protectedConsequences + "/" + ambientConsequences
+ " characters=" + beforeCharacters + "->" + Characters.Count
+ " ms=" + LastCompactionMs.ToString("0.0"));
return true;
@ -954,6 +977,45 @@ public static class NarrativeStoryStore
+ development.Confidence * 5f;
}
private static bool IsProtectedConsequence(
CharacterConsequence consequence,
HashSet<long> protectedCharacters,
HashSet<string> protectedThreadIds)
{
if (consequence == null)
{
return false;
}
return (protectedCharacters != null
&& (protectedCharacters.Contains(consequence.CharacterId)
|| (consequence.OtherId != 0
&& protectedCharacters.Contains(consequence.OtherId))))
|| (protectedThreadIds != null
&& !string.IsNullOrEmpty(consequence.ThreadId)
&& protectedThreadIds.Contains(consequence.ThreadId));
}
private static bool TryRetainConsequence(
CharacterConsequence consequence,
HashSet<string> retainedDevelopmentIds,
HashSet<string> retainedConsequenceIds)
{
if (consequence == null
|| string.IsNullOrEmpty(consequence.Id)
|| string.IsNullOrEmpty(consequence.DevelopmentId)
|| !Developments.ContainsKey(consequence.DevelopmentId)
|| (!retainedDevelopmentIds.Contains(consequence.DevelopmentId)
&& retainedDevelopmentIds.Count >= MaxRuntimeDevelopments))
{
return false;
}
retainedDevelopmentIds.Add(consequence.DevelopmentId);
retainedConsequenceIds.Add(consequence.Id);
return true;
}
private static void AddExistingId<T>(
string id,
Dictionary<string, T> source,
@ -1212,6 +1274,7 @@ public static class NarrativeStoryStore
const int importedDevelopments = 2300;
const int importedThreads = 900;
const int importedConsequences = 1300;
for (int i = 0; i < importedDevelopments; i++)
{
string developmentId = "probe:compact:development:" + i;
@ -1259,6 +1322,18 @@ public static class NarrativeStoryStore
thread.AddCast(i + 1, "protagonist");
ImportThread(thread);
}
if (i < importedConsequences)
{
ImportConsequence(new CharacterConsequence
{
Id = "probe:compact:consequence:" + i,
CharacterId = i + 1,
DevelopmentId = developmentId,
Kind = CharacterConsequenceKind.FoundHome,
OccurredAt = i + 1,
Importance = 50f
});
}
}
bool compacted = CompactRuntimeGraph(force: true);
@ -1278,13 +1353,23 @@ public static class NarrativeStoryStore
&& Threads.Count <= MaxRuntimeThreads
&& Consequences.Count <= MaxRuntimeConsequences
&& Characters.Count <= MaxRuntimeCharacters;
bool leanCharacters =
Characters.Count <= MaxRuntimeThreads
+ MaxAmbientRuntimeConsequences
+ MaxEmergingRuntimeCharacters;
bool leanConsequences =
Consequences.Count <= MaxAmbientRuntimeConsequences;
bool ok = ambientGraphDeferred && authoredBirthProjected && authoredHomeProjected
&& compacted && bounded && graphClosed && newestThreadKept;
&& compacted && bounded && leanCharacters && leanConsequences
&& graphClosed && newestThreadKept;
detail = "events=" + NarrativeEventStore.Count + "/" + MaxRuntimeEvents
+ " developments=" + Developments.Count + "/" + MaxRuntimeDevelopments
+ " threads=" + Threads.Count + "/" + MaxRuntimeThreads
+ " consequences=" + Consequences.Count + "/" + MaxRuntimeConsequences
+ " ambientBudget=" + MaxAmbientRuntimeConsequences
+ " characters=" + Characters.Count + "/" + MaxRuntimeCharacters
+ " leanCharacters=" + leanCharacters
+ " leanConsequences=" + leanConsequences
+ " newest=" + newestThreadKept
+ " closed=" + graphClosed
+ " deferred=" + ambientGraphDeferred

View file

@ -574,10 +574,7 @@ public static class StoryScheduler
private static bool IsCombatShot(InterestCandidate candidate)
{
return candidate != null
&& (candidate.Completion == InterestCompletionKind.CombatActive
|| string.Equals(candidate.AssetId, "live_combat", System.StringComparison.OrdinalIgnoreCase)
|| string.Equals(candidate.AssetId, "live_battle", System.StringComparison.OrdinalIgnoreCase));
return NarrativeExposureLedger.IsCombat(candidate);
}
internal static string SemanticFamily(InterestCandidate candidate)

View file

@ -1824,7 +1824,7 @@
"id": "laughing",
"strength": 34,
"category": "StatusAmbient",
"camera": true,
"camera": false,
"extendsGrief": false
},
{