- 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
243 lines
8.5 KiB
C#
243 lines
8.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>
|
|
/// Bounded authored-cut ledger. Camera hold seconds do not count; only meaningful selections do.
|
|
/// </summary>
|
|
public static class NarrativeExposureLedger
|
|
{
|
|
private sealed class Exposure
|
|
{
|
|
public int Cuts;
|
|
public float LastCutAt = -999f;
|
|
public string LastSemanticFamily = "";
|
|
public int SameFamilyRun;
|
|
}
|
|
|
|
private static readonly Dictionary<long, Exposure> Characters =
|
|
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>(9);
|
|
private static int _maxCharacterCuts;
|
|
private static int _maxThreadCuts;
|
|
private static int _combatRun;
|
|
private const int PresentedWindow = 9;
|
|
private const int CombatWindowBudget = 3;
|
|
private const int CombatRunBudget = 3;
|
|
|
|
public static void Clear()
|
|
{
|
|
Characters.Clear();
|
|
Threads.Clear();
|
|
PresentedCuts.Clear();
|
|
_maxCharacterCuts = 0;
|
|
_maxThreadCuts = 0;
|
|
_combatRun = 0;
|
|
}
|
|
|
|
public static void NoteCut(
|
|
long characterId,
|
|
string threadId,
|
|
string semanticFamily,
|
|
float now)
|
|
{
|
|
if (characterId != 0)
|
|
{
|
|
Exposure exposure = Get(Characters, characterId);
|
|
Update(exposure, semanticFamily, now);
|
|
_maxCharacterCuts = Math.Max(_maxCharacterCuts, exposure.Cuts);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(threadId))
|
|
{
|
|
Exposure exposure = Get(Threads, threadId);
|
|
Update(exposure, semanticFamily, now);
|
|
_maxThreadCuts = Math.Max(_maxThreadCuts, exposure.Cuts);
|
|
}
|
|
}
|
|
|
|
public static float CharacterDebt(long characterId)
|
|
{
|
|
if (characterId == 0) return 0f;
|
|
int cuts = Characters.TryGetValue(characterId, out Exposure exposure)
|
|
? exposure.Cuts : 0;
|
|
return Mathf.Min(12f, Mathf.Max(0, _maxCharacterCuts - cuts) * 1.5f);
|
|
}
|
|
|
|
public static float ThreadDebt(string threadId)
|
|
{
|
|
if (string.IsNullOrEmpty(threadId)) return 0f;
|
|
int cuts = Threads.TryGetValue(threadId, out Exposure exposure)
|
|
? exposure.Cuts : 0;
|
|
return Mathf.Min(10f, Mathf.Max(0, _maxThreadCuts - cuts) * 1.25f);
|
|
}
|
|
|
|
public static float RepetitionPenalty(long characterId, string semanticFamily)
|
|
{
|
|
if (characterId == 0 || string.IsNullOrEmpty(semanticFamily)
|
|
|| !Characters.TryGetValue(characterId, out Exposure exposure)
|
|
|| !string.Equals(
|
|
exposure.LastSemanticFamily, semanticFamily, StringComparison.Ordinal))
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
return Mathf.Min(24f, exposure.SameFamilyRun * 6f);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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)
|
|
{
|
|
if (!IsCombat(candidate))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
int combat = 0;
|
|
for (int i = 0; i < PresentedCuts.Count; i++)
|
|
{
|
|
if (PresentedCuts[i]) combat++;
|
|
}
|
|
|
|
return _combatRun < CombatRunBudget
|
|
&& combat < CombatWindowBudget;
|
|
}
|
|
|
|
public static void NotePresented(InterestCandidate candidate)
|
|
{
|
|
bool combat = IsCombat(candidate);
|
|
PresentedCuts.Add(combat);
|
|
while (PresentedCuts.Count > PresentedWindow)
|
|
{
|
|
PresentedCuts.RemoveAt(0);
|
|
}
|
|
_combatRun = combat ? _combatRun + 1 : 0;
|
|
}
|
|
|
|
public static bool HarnessProbeBalance(out string detail)
|
|
{
|
|
Clear();
|
|
NoteCut(1, "thread:a", "combat", 1f);
|
|
NoteCut(1, "thread:a", "combat", 2f);
|
|
NoteCut(1, "thread:a", "combat", 3f);
|
|
NoteCut(2, "thread:b", "lineage", 4f);
|
|
float underCovered = CharacterDebt(2);
|
|
float prolific = CharacterDebt(1);
|
|
float repetition = RepetitionPenalty(1, "combat");
|
|
float diverse = RepetitionPenalty(1, "lineage");
|
|
var combat = new InterestCandidate
|
|
{
|
|
Completion = InterestCompletionKind.CombatActive,
|
|
EventStrength = 80f
|
|
};
|
|
var criticalCombat = new InterestCandidate
|
|
{
|
|
Completion = InterestCompletionKind.CombatActive,
|
|
EventStrength = 100f
|
|
};
|
|
var family = new InterestCandidate
|
|
{
|
|
AssetId = "add_child",
|
|
EventStrength = 70f
|
|
};
|
|
NotePresented(combat);
|
|
NotePresented(combat);
|
|
NotePresented(combat);
|
|
bool routineBlocked = !MayPresent(combat);
|
|
bool criticalBounded = !MayPresent(criticalCombat);
|
|
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
|
|
&& activityCombatBounded;
|
|
detail = "debt=" + underCovered.ToString("0.0") + "/" + prolific.ToString("0.0")
|
|
+ " repetition=" + repetition.ToString("0.0") + "/" + diverse.ToString("0.0")
|
|
+ " combat=" + routineBlocked + "/" + criticalBounded
|
|
+ "/" + routineReopened + "/" + activityCombatBounded
|
|
+ " pass=" + ok;
|
|
Clear();
|
|
return ok;
|
|
}
|
|
|
|
private static Exposure Get<TKey>(Dictionary<TKey, Exposure> map, TKey key)
|
|
{
|
|
if (!map.TryGetValue(key, out Exposure exposure))
|
|
{
|
|
exposure = new Exposure();
|
|
map[key] = exposure;
|
|
}
|
|
|
|
return exposure;
|
|
}
|
|
|
|
private static void Update(Exposure exposure, string family, float now)
|
|
{
|
|
exposure.Cuts++;
|
|
exposure.LastCutAt = now;
|
|
if (!string.IsNullOrEmpty(family)
|
|
&& string.Equals(exposure.LastSemanticFamily, family, StringComparison.Ordinal))
|
|
{
|
|
exposure.SameFamilyRun++;
|
|
}
|
|
else
|
|
{
|
|
exposure.LastSemanticFamily = family ?? "";
|
|
exposure.SameFamilyRun = 1;
|
|
}
|
|
}
|
|
|
|
internal static bool IsCombat(InterestCandidate candidate)
|
|
{
|
|
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));
|
|
}
|
|
}
|