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:
parent
c9000531fc
commit
4becf1c5dc
7 changed files with 178 additions and 62 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
if (candidate == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (candidate.Completion == InterestCompletionKind.CombatActive
|
||||
|| candidate.Completion == InterestCompletionKind.WarFront
|
||||
|| string.Equals(
|
||||
candidate.AssetId,
|
||||
"live_combat",
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(
|
||||
candidate.AssetId,
|
||||
"live_battle",
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
|| 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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1824,7 +1824,7 @@
|
|||
"id": "laughing",
|
||||
"strength": 34,
|
||||
"category": "StatusAmbient",
|
||||
"camera": true,
|
||||
"camera": false,
|
||||
"extendsGrief": false
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue