diff --git a/IdleSpectator/AgentHarness.cs b/IdleSpectator/AgentHarness.cs
index f988ca5..3d45a63 100644
--- a/IdleSpectator/AgentHarness.cs
+++ b/IdleSpectator/AgentHarness.cs
@@ -179,7 +179,7 @@ public static class AgentHarness
float seconds = cmd.wait > 0f ? cmd.wait : ParseFloat(cmd.value, 2f);
InterestDirector.HarnessAgeCurrent(seconds);
_cmdOk++;
- Emit(cmd, ok: true, detail: $"aged={seconds:0.##}s tier={InterestDirector.CurrentTierName}");
+ Emit(cmd, ok: true, detail: $"aged={seconds:0.##}s score={InterestDirector.CurrentScoreLabel}");
break;
}
@@ -1949,7 +1949,7 @@ public static class AgentHarness
ok: true,
detail: peek == null
? "peek=null"
- : $"peek={peek.Key} lead={peek.LeadKind} tier={peek.Urgency} label={peek.Label}");
+ : $"peek={peek.Key} lead={peek.LeadKind} score={peek.TotalScore:0.#} label={peek.Label}");
break;
}
@@ -2337,7 +2337,8 @@ public static class AgentHarness
return;
}
- InterestTier tier = ParseTier(cmd.tier, InterestTier.Curiosity);
+ float tierEvt = EventStrengthFromTierHint(cmd.tier, 40f);
+ InterestLeadKind tierLead = LeadFromTierHint(cmd.tier, InterestLeadKind.CharacterLed);
string resolvedAsset = follow.asset != null ? follow.asset.id : assetId;
string label = string.IsNullOrEmpty(cmd.label)
? $"Harness: {resolvedAsset}"
@@ -2349,8 +2350,7 @@ public static class AgentHarness
InterestEvent interest = new InterestEvent
{
- Tier = tier,
- Score = 50f,
+ Score = tierEvt,
Position = follow.current_position,
FollowUnit = follow,
Label = label,
@@ -2391,37 +2391,49 @@ public static class AgentHarness
return;
}
+ float tierEvt = EventStrengthFromTierHint(cmd.tier, 40f);
+ InterestLeadKind tierLead = LeadFromTierHint(cmd.tier, InterestLeadKind.CharacterLed);
+ string tipLabel = string.IsNullOrEmpty(cmd.label)
+ ? $"Harness {cmd.tier ?? "scene"}"
+ : cmd.label.Replace("{asset}", follow.asset != null ? follow.asset.id : (assetId ?? ""));
+
InterestEvent interest = new InterestEvent
{
- Tier = ParseTier(cmd.tier, InterestTier.Curiosity),
- Score = 40f,
+ Score = tierEvt,
Position = follow.current_position,
FollowUnit = follow,
- Label = string.IsNullOrEmpty(cmd.label)
- ? $"Harness {ParseTier(cmd.tier, InterestTier.Curiosity)}"
- : cmd.label.Replace("{asset}", follow.asset != null ? follow.asset.id : (assetId ?? "")),
+ Label = tipLabel,
CreatedAt = Time.unscaledTime,
AssetId = follow.asset != null ? follow.asset.id : assetId
};
- InterestTier tier = interest.Tier;
+ bool forceCamera = tierLead == InterestLeadKind.EventLed
+ && tierEvt >= InterestScoringConfig.W.noticeScoreMin;
InterestCandidate registered = InterestFeeds.RegisterHarness(
follow,
- tier,
interest.Label,
- keySuffix: (interest.Label ?? tier.ToString()).Replace(" ", "_"),
- lead: tier >= InterestTier.Action ? InterestLeadKind.EventLed : InterestLeadKind.CharacterLed,
+ keySuffix: (interest.Label ?? "scene").Replace(" ", "_"),
+ lead: tierLead,
completion: InterestCompletionKind.FixedDwell,
- forceActive: tier >= InterestTier.Action,
- eventStrength: 40f + (int)tier * 20f,
- maxWatch: tier >= InterestTier.Epic ? 40f : 25f);
+ forceActive: forceCamera,
+ eventStrength: tierEvt,
+ maxWatch: tierEvt >= 95f ? 40f : 25f);
if (registered == null)
{
InterestCollector.EnqueueDirect(interest);
}
+ else if (forceCamera)
+ {
+ // Event-led Action/Epic tips may take the camera; character-led Story/curiosity stay pending.
+ InterestDirector.HarnessForceSession(registered);
+ CameraDirector.Watch(registered.ToInterestEvent());
+ }
_cmdOk++;
- Emit(cmd, ok: true, detail: $"queued tier={tier} label={interest.Label} pending={InterestCollector.PendingCount}");
+ Emit(
+ cmd,
+ ok: true,
+ detail: $"triggered label={tipLabel} evt={tierEvt:0.#} forced={forceCamera}");
}
private static void DoInterestInject(HarnessCommand cmd)
@@ -2446,8 +2458,9 @@ public static class AgentHarness
return;
}
- InterestTier tier = ParseTier(cmd.tier, InterestTier.Action);
- string label = string.IsNullOrEmpty(cmd.label) ? ("Inject " + tier) : cmd.label;
+ float tierEvt = EventStrengthFromTierHint(cmd.tier, 70f);
+ InterestLeadKind tierLead = LeadFromTierHint(cmd.tier, InterestLeadKind.EventLed);
+ string label = string.IsNullOrEmpty(cmd.label) ? ("Inject " + (cmd.tier ?? "scene")) : cmd.label;
string keySuffix = string.IsNullOrEmpty(cmd.expect) ? label.Replace(" ", "_") : cmd.expect;
ParseInterestInjectOptions(
cmd.value,
@@ -2461,13 +2474,10 @@ public static class AgentHarness
out int participants,
out int notables);
- InterestLeadKind lead = leadOpt
- ?? (tier >= InterestTier.Curiosity && tier < InterestTier.Action
- ? InterestLeadKind.CharacterLed
- : InterestLeadKind.EventLed);
+ InterestLeadKind lead = leadOpt ?? tierLead;
if (eventStrength < 0f)
{
- eventStrength = 50f + (int)tier * 15f;
+ eventStrength = tierEvt;
}
if (charSig < 0f)
@@ -2487,7 +2497,6 @@ public static class AgentHarness
InterestCandidate c = InterestFeeds.RegisterHarness(
follow,
- tier,
label,
keySuffix,
lead,
@@ -2533,7 +2542,7 @@ public static class AgentHarness
Emit(
cmd,
ok: true,
- detail: $"injected key={c.Key} tier={c.Urgency} lead={c.LeadKind} evt={c.EventStrength:0.#} char={c.CharacterSignificance:0.#} n={c.ParticipantCount}/{c.NotableParticipantCount} score={c.TotalScore:0.#} pending={InterestRegistry.PendingCount}");
+ detail: $"injected key={c.Key} score={c.TotalScore:0.#} lead={c.LeadKind} evt={c.EventStrength:0.#} char={c.CharacterSignificance:0.#} n={c.ParticipantCount}/{c.NotableParticipantCount} score={c.TotalScore:0.#} pending={InterestRegistry.PendingCount}");
}
private static void ParseInterestInjectOptions(
@@ -2669,19 +2678,24 @@ public static class AgentHarness
return;
}
- InterestTier tier = ParseTier(cmd.tier, InterestTier.Action);
- string label = string.IsNullOrEmpty(cmd.label) ? ("Force " + tier) : cmd.label;
+ float tierEvt = EventStrengthFromTierHint(cmd.tier, 70f);
+ InterestLeadKind tierLead = LeadFromTierHint(cmd.tier, InterestLeadKind.EventLed);
+ string label = string.IsNullOrEmpty(cmd.label) ? ("Force " + (cmd.tier ?? "scene")) : cmd.label;
string keySuffix = string.IsNullOrEmpty(cmd.expect) ? "force_" + label.Replace(" ", "_") : cmd.expect;
InterestCandidate c = InterestFeeds.RegisterHarness(
follow,
- tier,
label,
keySuffix,
- lead: InterestLeadKind.EventLed,
+ lead: tierLead,
completion: InterestCompletionKind.Manual,
forceActive: true,
- eventStrength: 80f + (int)tier * 10f,
+ eventStrength: tierEvt > 0f ? tierEvt : 70f,
maxWatch: 60f);
+ if (c != null && tierLead == InterestLeadKind.CharacterLed)
+ {
+ c.Category = "CharacterVignette";
+ InterestScoring.ScoreCheap(c);
+ }
bool ok = InterestDirector.HarnessForceSession(c);
if (ok)
{
@@ -2692,7 +2706,7 @@ public static class AgentHarness
_cmdFail++;
}
- Emit(cmd, ok, detail: $"key={InterestDirector.CurrentKey} tier={InterestDirector.CurrentTierName} active={InterestDirector.CurrentIsActive}");
+ Emit(cmd, ok, detail: $"key={InterestDirector.CurrentKey} score={InterestDirector.CurrentScoreLabel} active={InterestDirector.CurrentIsActive}");
}
private static void DoSetSetting(HarnessCommand cmd)
@@ -2853,11 +2867,43 @@ public static class AgentHarness
}
case "current_tier":
{
- string want = (cmd.value ?? cmd.tier ?? "").Trim();
- string have = InterestDirector.CurrentTierName;
- pass = !string.IsNullOrEmpty(want)
- && string.Equals(have, want, System.StringComparison.OrdinalIgnoreCase);
- detail = $"tier={have} want={want} label={InterestDirector.CurrentLabel}";
+ // Legacy name: prefer tip/label match; else score band from old tier names.
+ string want = cmd.value ?? "";
+ string label = InterestDirector.CurrentLabel ?? "";
+ string tip = CameraDirector.LastWatchLabel ?? "";
+ float score = InterestDirector.CurrentScore;
+ bool byText = (!string.IsNullOrEmpty(want)
+ && (label.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0
+ || tip.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0));
+ bool byBand = false;
+ string band = want.Trim().ToLowerInvariant();
+ if (band == "ambient")
+ {
+ byBand = InterestScoring.IsFillScore(score);
+ }
+ else if (band == "curiosity")
+ {
+ byBand = score >= 35f && score < InterestScoringConfig.W.noticeScoreMin;
+ }
+ else if (band == "action")
+ {
+ byBand = score >= InterestScoringConfig.W.noticeScoreMin;
+ }
+ else if (band == "story")
+ {
+ byBand = score >= 75f;
+ }
+ else if (band == "epic")
+ {
+ byBand = InterestScoring.IsHotScore(score) || score >= 95f;
+ }
+ else if (band == "none")
+ {
+ byBand = InterestDirector.CurrentCandidate == null;
+ }
+
+ pass = byText || byBand;
+ detail = $"want='{want}' label='{label}' tip='{tip}' score={score:0.#} text={byText} band={byBand}";
break;
}
case "would_accept_curiosity":
@@ -2865,7 +2911,7 @@ public static class AgentHarness
bool want = ParseBool(cmd.value, defaultValue: true);
bool have = InterestDirector.WouldAcceptCuriosity();
pass = have == want;
- detail = $"would_accept={have} want={want} tier={InterestDirector.CurrentTierName}";
+ detail = $"would_accept={have} want={want} score={InterestDirector.CurrentScoreLabel}";
break;
}
case "focus_same":
@@ -5146,7 +5192,7 @@ public static class AgentHarness
private static void DoSnapshot(HarnessCommand cmd)
{
string snap =
- $"idle={SpectatorMode.Active} focus={MoveCamera.hasFocusUnit()} powerBar={CanvasMain.isBottomBarShowing()} unit={FocusLabel()} tip={CameraDirector.LastWatchLabel} tipAsset={CameraDirector.LastWatchAssetId} bad={StateProbe.BadEventCount} tier={InterestDirector.CurrentTierName} discoveryPending={SpeciesDiscovery.PendingCount} caption={WatchCaption.LastHeadline} chronicle={Chronicle.Count}";
+ $"idle={SpectatorMode.Active} focus={MoveCamera.hasFocusUnit()} powerBar={CanvasMain.isBottomBarShowing()} unit={FocusLabel()} tip={CameraDirector.LastWatchLabel} tipAsset={CameraDirector.LastWatchAssetId} bad={StateProbe.BadEventCount} score={InterestDirector.CurrentScoreLabel} discoveryPending={SpeciesDiscovery.PendingCount} caption={WatchCaption.LastHeadline} chronicle={Chronicle.Count}";
_cmdOk++;
Emit(cmd, ok: true, detail: snap);
LogService.LogInfo("[IdleSpectator][SNAP] " + snap);
@@ -5219,8 +5265,7 @@ public static class AgentHarness
InterestEvent interest = new InterestEvent
{
- Tier = ParseTier(cmd.tier, InterestTier.Curiosity),
- Score = 50f,
+ Score = 50f,
Position = near.current_position,
FollowUnit = null,
Label = label,
@@ -5557,21 +5602,53 @@ public static class AgentHarness
return actor.asset != null ? actor.asset.id : "unit";
}
- private static InterestTier ParseTier(string raw, InterestTier fallback)
+
+ private static float EventStrengthFromTierHint(string raw, float fallback)
{
if (string.IsNullOrEmpty(raw))
{
return fallback;
}
- if (Enum.TryParse(raw, ignoreCase: true, out InterestTier tier))
+ switch (raw.Trim().ToLowerInvariant())
{
- return tier;
+ case "ambient":
+ return 25f;
+ case "curiosity":
+ return 40f;
+ case "action":
+ return 70f;
+ case "story":
+ return 80f;
+ case "epic":
+ return 100f;
+ default:
+ return fallback;
+ }
+ }
+
+ private static InterestLeadKind LeadFromTierHint(string raw, InterestLeadKind fallback)
+ {
+ if (string.IsNullOrEmpty(raw))
+ {
+ return fallback;
+ }
+
+ string s = raw.Trim().ToLowerInvariant();
+ if (s == "ambient" || s == "curiosity" || s == "story")
+ {
+ return InterestLeadKind.CharacterLed;
+ }
+
+ if (s == "action" || s == "epic")
+ {
+ return InterestLeadKind.EventLed;
}
return fallback;
}
+
private static int ParseCountExpect(HarnessCommand cmd, int defaultValue)
{
// Prefer explicit value= so Step()'s default count=1 does not override want=0.
diff --git a/IdleSpectator/CameraDirector.cs b/IdleSpectator/CameraDirector.cs
index a6475ae..ab998e8 100644
--- a/IdleSpectator/CameraDirector.cs
+++ b/IdleSpectator/CameraDirector.cs
@@ -84,11 +84,10 @@ public static class CameraDirector
public static string FormatWatchTip(InterestEvent interest)
{
- string tier = interest.Tier.ToString();
string reason = string.IsNullOrEmpty(interest.Label)
? "something interesting"
: interest.Label;
- return $"Watching [{tier}]: {reason}";
+ return $"Watching: {reason}";
}
public static void ClearFollow()
diff --git a/IdleSpectator/HarnessScenarios.cs b/IdleSpectator/HarnessScenarios.cs
index 9af2704..ff6cd9c 100644
--- a/IdleSpectator/HarnessScenarios.cs
+++ b/IdleSpectator/HarnessScenarios.cs
@@ -585,24 +585,20 @@ internal static class HarnessScenarios
Step("dt10", "trigger_interest", asset: "sheep", label: "CurioA", tier: "Curiosity"),
Step("dt11", "age_current", wait: 2f),
Step("dt12", "director_run", wait: 1.2f),
- Step("dt13", "assert", expect: "current_tier", value: "Curiosity"),
- Step("dt14", "assert", expect: "tip_contains", value: "CurioA"),
+ Step("dt13", "assert", expect: "tip_contains", value: "CurioA"),
- // Action interrupts curiosity after high-tier window.
+ // Action interrupts curiosity after settle.
Step("dt20", "trigger_interest", asset: "auto", label: "ActionA", tier: "Action"),
Step("dt21", "age_current", wait: 1.2f),
Step("dt22", "director_run", wait: 1.2f),
- Step("dt23", "assert", expect: "current_tier", value: "Action"),
- Step("dt24", "assert", expect: "tip_contains", value: "ActionA"),
+ Step("dt23", "assert", expect: "tip_contains", value: "ActionA"),
- // Curiosity must not be accepted while Action holds the camera (no director_run:
- // ambient rotate would otherwise steal Action after dwell).
+ // Curiosity must not steal the camera while Action holds (no director_run).
Step("dt30", "trigger_interest", asset: "auto", label: "CurioBlocked", tier: "Curiosity"),
- Step("dt31", "assert", expect: "current_tier", value: "Action"),
- Step("dt32", "assert", expect: "tip_contains", value: "ActionA"),
- Step("dt33", "assert", expect: "would_accept_curiosity", value: "false"),
- Step("dt34", "assert", expect: "health"),
- Step("dt35", "assert", expect: "no_bad"),
+ Step("dt31", "assert", expect: "tip_contains", value: "ActionA"),
+ Step("dt32", "assert", expect: "would_accept_curiosity", value: "false"),
+ Step("dt33", "assert", expect: "health"),
+ Step("dt34", "assert", expect: "no_bad"),
Step("dt90", "fast_timing", value: "false"),
Step("dt99", "snapshot"),
@@ -624,19 +620,19 @@ internal static class HarnessScenarios
// Protected Action: Story waits.
Step("dl10", "interest_force_session", asset: "sheep", label: "HoldAction", tier: "Action", expect: "hold_action"),
- Step("dl11", "assert", expect: "current_tier", value: "Action"),
+ Step("dl11", "assert", expect: "tip_contains", value: "HoldAction"),
Step("dl12", "assert", expect: "session_active", value: "true"),
Step("dl13", "trigger_interest", asset: "sheep", label: "StoryWait", tier: "Story"),
Step("dl14", "age_current", wait: 1.2f),
Step("dl15", "director_run", wait: 1.2f),
- Step("dl16", "assert", expect: "current_tier", value: "Action"),
+ Step("dl16", "assert", expect: "tip_contains", value: "HoldAction"),
Step("dl17", "assert", expect: "tip_contains", value: "HoldAction"),
// Epic preempts Action after settle.
Step("dl20", "trigger_interest", asset: "sheep", label: "EpicWin", tier: "Epic"),
Step("dl21", "age_current", wait: 1.2f),
Step("dl22", "director_run", wait: 1.2f),
- Step("dl23", "assert", expect: "current_tier", value: "Epic"),
+ Step("dl23", "assert", expect: "tip_contains", value: "EpicWin"),
Step("dl24", "assert", expect: "tip_contains", value: "EpicWin"),
Step("dl25", "assert", expect: "no_bad"),
@@ -664,20 +660,20 @@ internal static class HarnessScenarios
Step("ih11", "happiness_apply", value: "just_ate"),
Step("ih12", "age_current", wait: 1.2f),
Step("ih13", "director_run", wait: 1.2f),
- Step("ih14", "assert", expect: "current_tier", value: "Action"),
+ Step("ih14", "assert", expect: "tip_contains", value: "ActionHold"),
Step("ih15", "assert", expect: "tip_contains", value: "ActionHold"),
// Grief Signal registers into registry (force-note + direct register).
Step("ih20", "happiness_force_note", value: "death_child", label: "Ava"),
Step("ih21", "assert", expect: "interest_has_key", value: "death_child"),
- Step("ih22", "assert", expect: "current_tier", value: "Action"),
+ Step("ih22", "assert", expect: "tip_contains", value: "ActionHold"),
Step("ih23", "assert", expect: "happiness_drain_seq"),
// Epic can leave Action; grief may then surface.
Step("ih30", "trigger_interest", asset: "auto", label: "EpicClear", tier: "Epic"),
Step("ih31", "age_current", wait: 1.2f),
Step("ih32", "director_run", wait: 1.2f),
- Step("ih33", "assert", expect: "current_tier", value: "Epic"),
+ Step("ih33", "assert", expect: "tip_contains", value: "EpicClear"),
Step("ih34", "assert", expect: "no_bad"),
Step("ih90", "fast_timing", value: "false"),
@@ -704,16 +700,16 @@ internal static class HarnessScenarios
// --- Epic preempts Story; Story does not preempt itself past Action rules ---
Step("dg10", "interest_force_session", asset: "sheep", label: "HoldStory", tier: "Story", expect: "hold_story"),
- Step("dg11", "assert", expect: "current_tier", value: "Story"),
+ Step("dg11", "assert", expect: "tip_contains", value: "HoldStory"),
Step("dg12", "trigger_interest", asset: "sheep", label: "ActionBlocked", tier: "Action"),
Step("dg13", "age_current", wait: 1.2f),
Step("dg14", "director_run", wait: 1.2f),
- Step("dg15", "assert", expect: "current_tier", value: "Story"),
+ Step("dg15", "assert", expect: "tip_contains", value: "HoldStory"),
Step("dg16", "assert", expect: "tip_contains", value: "HoldStory"),
Step("dg17", "trigger_interest", asset: "sheep", label: "EpicOverStory", tier: "Epic"),
Step("dg18", "age_current", wait: 1.2f),
Step("dg19", "director_run", wait: 1.2f),
- Step("dg20", "assert", expect: "current_tier", value: "Epic"),
+ Step("dg20", "assert", expect: "tip_contains", value: "EpicOverStory"),
Step("dg21", "assert", expect: "tip_contains", value: "EpicOverStory"),
// --- Resume after Epic: Action hold interrupted then restored ---
@@ -725,13 +721,13 @@ internal static class HarnessScenarios
Step("dg35", "trigger_interest", asset: "sheep", label: "EpicInterrupt", tier: "Epic"),
Step("dg36", "age_current", wait: 1.2f),
Step("dg37", "director_run", wait: 1.2f),
- Step("dg38", "assert", expect: "current_tier", value: "Epic"),
+ Step("dg38", "assert", expect: "tip_contains", value: "EpicInterrupt"),
Step("dg39", "assert", expect: "interrupted_key", value: "hold_resume"),
// Max-cap the Epic scene (fast timing caps ~8s) so EndCurrent resumes Action.
Step("dg40", "age_current", wait: 9f),
Step("dg41", "director_run", wait: 1.2f),
Step("dg42", "assert", expect: "tip_contains", value: "HoldResume"),
- Step("dg43", "assert", expect: "current_tier", value: "Action"),
+ Step("dg43", "assert", expect: "tip_contains", value: "HoldResume"),
Step("dg44", "assert", expect: "interrupted_key", value: "none"),
// --- Max cap ends a forced scene ---
@@ -788,7 +784,7 @@ internal static class HarnessScenarios
Step("dg112", "interest_feeds_tick"),
Step("dg113", "age_current", wait: 1.2f),
Step("dg114", "director_run", wait: 1.2f),
- Step("dg115", "assert", expect: "current_tier", value: "Action"),
+ Step("dg115", "assert", expect: "tip_contains", value: "ActionKeep"),
Step("dg116", "assert", expect: "tip_contains", value: "ActionKeep"),
Step("dg117", "assert", expect: "interest_no_key", value: "just_ate"),
@@ -867,9 +863,12 @@ internal static class HarnessScenarios
Step("wr44", "screenshot", value: "hud-watch-reason-singleton.png"),
Step("wr45", "wait", wait: 0.55f),
- // Ambient catch-all always fills the reason row.
- Step("wr46", "interest_force_session", asset: "human", label: "Ambient stroll", tier: "Ambient", expect: "ambient_stroll"),
- Step("wr47", "assert", expect: "dossier_contains", value: "Looking around"),
+ // Character-led fill / ambient → no catch-all reason (row hidden).
+ Step("wr46", "interest_force_session", asset: "human", label: "Ambient stroll", tier: "Ambient", expect: "ambient_stroll",
+ value: "lead=character;evt=20"),
+ Step("wr47", "assert", expect: "dossier_not_contains", value: "Going about their day"),
+ Step("wr47b", "assert", expect: "dossier_not_contains", value: "Curious glance"),
+ Step("wr47c", "assert", expect: "dossier_not_contains", value: "Looking around"),
Step("wr48", "assert", expect: "dossier_not_contains", value: "Live ·"),
Step("wr49", "wait", wait: 0.35f),
@@ -945,7 +944,7 @@ internal static class HarnessScenarios
Step("ap43", "age_current", wait: 1.2f),
Step("ap44", "director_run", wait: 1.2f),
Step("ap45", "assert", expect: "tip_contains", value: "RealFight"),
- Step("ap46", "assert", expect: "current_tier", value: "Action"),
+ Step("ap46", "assert", expect: "tip_contains", value: "RealFight"),
// Cold-boot focus gaps from welcome/dismiss are unrelated to score order.
Step("ap89", "reset_counters"),
@@ -1013,13 +1012,13 @@ internal static class HarnessScenarios
Step("wl10", "trigger_interest", asset: "auto", label: "Story: kingdom_new", tier: "Story"),
Step("wl11", "age_current", wait: 1.2f),
Step("wl12", "director_run", wait: 1.2f),
- Step("wl13", "assert", expect: "current_tier", value: "Story"),
+ Step("wl13", "assert", expect: "tip_contains", value: "kingdom_new"),
Step("wl14", "assert", expect: "tip_contains", value: "kingdom_new"),
Step("wl20", "trigger_interest", asset: "auto", label: "Epic: diplomacy_war_started", tier: "Epic"),
Step("wl21", "age_current", wait: 1.2f),
Step("wl22", "director_run", wait: 1.2f),
- Step("wl23", "assert", expect: "current_tier", value: "Epic"),
+ Step("wl23", "assert", expect: "tip_contains", value: "diplomacy_war_started"),
Step("wl24", "assert", expect: "tip_contains", value: "diplomacy_war_started"),
Step("wl25", "assert", expect: "health"),
Step("wl26", "assert", expect: "no_bad"),
diff --git a/IdleSpectator/InterestCandidate.cs b/IdleSpectator/InterestCandidate.cs
index d724ff8..9c7a298 100644
--- a/IdleSpectator/InterestCandidate.cs
+++ b/IdleSpectator/InterestCandidate.cs
@@ -28,13 +28,12 @@ public enum InterestCompletionKind
}
///
-/// Durable interest scene candidate. Urgency () is preemption class only;
-/// ranks within a class.
+/// Durable interest scene candidate. is the sole ranking signal;
+/// typed fields (lead, completion, combat) gate interrupt policy.
///
public sealed class InterestCandidate
{
public string Key = "";
- public InterestTier Urgency = InterestTier.Ambient;
public InterestLeadKind LeadKind = InterestLeadKind.EventLed;
public string Category = "";
public string Source = "";
@@ -83,13 +82,15 @@ public sealed class InterestCandidate
{
return new InterestEvent
{
- Tier = Urgency,
Score = TotalScore,
Position = Position,
FollowUnit = FollowUnit,
Label = Label,
CreatedAt = CreatedAt,
- AssetId = AssetId
+ AssetId = AssetId,
+ ParticipantCount = ParticipantCount,
+ NotableParticipantCount = NotableParticipantCount,
+ CharacterSignificance = CharacterSignificance
};
}
@@ -98,7 +99,6 @@ public sealed class InterestCandidate
var copy = new InterestCandidate
{
Key = Key,
- Urgency = Urgency,
LeadKind = LeadKind,
Category = Category,
Source = Source,
diff --git a/IdleSpectator/InterestCollector.cs b/IdleSpectator/InterestCollector.cs
index b0e3e4d..35f83da 100644
--- a/IdleSpectator/InterestCollector.cs
+++ b/IdleSpectator/InterestCollector.cs
@@ -20,9 +20,9 @@ public static class InterestCollector
public static int PendingCount => InterestRegistry.PendingCount;
- public static bool HasPendingAtLeast(InterestTier minTier)
+ public static bool HasPendingScoreAtLeast(float minScore)
{
- return InterestRegistry.HasPendingAtLeast(minTier);
+ return InterestRegistry.HasPendingScoreAtLeast(minScore);
}
public static void Clear()
diff --git a/IdleSpectator/InterestDirector.cs b/IdleSpectator/InterestDirector.cs
index df37d22..853999c 100644
--- a/IdleSpectator/InterestDirector.cs
+++ b/IdleSpectator/InterestDirector.cs
@@ -5,8 +5,9 @@ using UnityEngine;
namespace IdleSpectator;
///
-/// Event-centered scene director: registry intake, protected sessions, Epic-only preemption
-/// for Action+ scenes, soft 70/30 variety, completion via .
+/// Event-centered scene director: registry intake, protected sessions, score-margin
+/// preemption with typed combat/vignette rules, soft 70/30 variety, completion via
+/// .
///
public static class InterestDirector
{
@@ -44,8 +45,13 @@ public static class InterestDirector
private static float _inactiveSince = -999f;
private static readonly List PendingScratch = new List(96);
- public static string CurrentTierName =>
- _current == null ? "none" : _current.Urgency.ToString();
+ public static string CurrentTierName => CurrentScoreLabel;
+
+ public static string CurrentScoreLabel =>
+ _current == null ? "none" : _current.TotalScore.ToString("0.#");
+
+ public static float CurrentScore =>
+ _current == null ? 0f : _current.TotalScore;
public static string CurrentLabel =>
_current == null ? "" : (_current.Label ?? "");
@@ -291,7 +297,7 @@ public static class InterestDirector
InterestCandidate next = SelectNext(now);
if (next != null && CanSwitchTo(next, onCurrent, sinceSwitch))
{
- if (next.Urgency >= InterestTier.Action)
+ if (!InterestScoring.IsFillScore(next.TotalScore))
{
_lastInterestingAt = now;
}
@@ -302,8 +308,8 @@ public static class InterestDirector
}
// Queued discovery tips beat ambient fill, but never stall under Action/Story.
- if (InterestRegistry.HasPendingAtLeast(InterestTier.Curiosity)
- && (_current == null || _current.Urgency <= InterestTier.Curiosity))
+ if (InterestRegistry.HasPendingScoreAtLeast(InterestScoringConfig.W.fillScoreMax)
+ && (_current == null || InterestScoring.IsFillScore(_current.TotalScore)))
{
return;
}
@@ -448,7 +454,14 @@ public static class InterestDirector
float onCurrent = Time.unscaledTime - _currentStartedAt;
float sinceSwitch = Time.unscaledTime - _lastSwitchAt;
return CanSwitchTo(
- new InterestCandidate { Urgency = InterestTier.Curiosity, Key = "probe:curiosity" },
+ new InterestCandidate
+ {
+ Key = "probe:curiosity",
+ EventStrength = 40f,
+ LeadKind = InterestLeadKind.CharacterLed,
+ Completion = InterestCompletionKind.CharacterVignette,
+ TotalScore = 45f
+ },
onCurrent,
sinceSwitch);
}
@@ -531,14 +544,24 @@ public static class InterestDirector
return _minDwell;
}
- if (current.MinWatch > 0f)
+ ScoringWeights w = InterestScoringConfig.W;
+ float dwell = InterestScoring.IsHotScore(current.TotalScore)
+ ? w.dwellHot
+ : (InterestScoring.IsFillScore(current.TotalScore) ? w.dwellFill : w.dwellNormal);
+ if (_minDwell < MinDwellSeconds * 0.5f)
{
- return current.Urgency <= InterestTier.Curiosity
- ? Mathf.Min(current.MinWatch, _curiosityDwell)
- : Mathf.Max(current.MinWatch, _minDwell * 0.15f);
+ // Harness fast timing: keep compressed dwells.
+ dwell = InterestScoring.IsFillScore(current.TotalScore) ? _curiosityDwell : _minDwell;
}
- return current.Urgency <= InterestTier.Curiosity ? _curiosityDwell : _minDwell;
+ if (current.MinWatch > 0f)
+ {
+ return InterestScoring.IsFillScore(current.TotalScore)
+ ? Mathf.Min(current.MinWatch, dwell)
+ : Mathf.Max(current.MinWatch, dwell * 0.15f);
+ }
+
+ return dwell;
}
private static float MaxWatchFor(InterestCandidate current)
@@ -603,43 +626,37 @@ public static class InterestDirector
float now = Time.unscaledTime;
bool protectedScene = SessionProtected(now, onCurrent);
- InterestTier cur = _current.Urgency;
- InterestTier next = candidate.Urgency;
+ ScoringWeights w = InterestScoringConfig.W;
+ float curScore = _current.TotalScore;
+ float nextScore = candidate.TotalScore;
+ bool nextFill = InterestScoring.IsFillScore(nextScore);
+ bool curFill = InterestScoring.IsFillScore(curScore);
- // Curiosity never interrupts Action+.
- if (next == InterestTier.Curiosity && cur >= InterestTier.Action)
+ // Fill/curiosity-strength scenes never cut protected non-fill sessions.
+ if (nextFill && !curFill && protectedScene)
{
return false;
}
- if (next == InterestTier.Curiosity
- && cur == InterestTier.Curiosity
- && onCurrent < _curiosityRotate)
+ float fillRotate = _minDwell < MinDwellSeconds * 0.5f ? _curiosityRotate : w.fillRotateSeconds;
+ if (nextFill && curFill && onCurrent < fillRotate)
{
return false;
}
- if (protectedScene && cur >= InterestTier.Action)
+ bool typedCutIn = InterestScoring.IsCombatAction(candidate)
+ && InterestScoring.IsCharacterVignette(_current)
+ && onCurrent >= _settleGrace;
+ bool combatCutsNonCombat = InterestScoring.IsCombatAction(candidate)
+ && !InterestScoring.IsCombatAction(_current)
+ && onCurrent >= _settleGrace
+ && nextScore >= curScore - w.rotateSlack;
+ bool scoreCutIn = onCurrent >= _settleGrace
+ && nextScore >= curScore + w.cutInMargin;
+
+ if (protectedScene && !curFill)
{
- // Epic always may cut in after settle.
- if (next >= InterestTier.Epic && onCurrent >= _settleGrace)
- {
- return true;
- }
-
- // Live combat / battles beat celebrity Story vignettes (actions > characters).
- if (InterestScoring.IsCombatAction(candidate)
- && InterestScoring.IsCharacterVignette(_current)
- && onCurrent >= _settleGrace)
- {
- return true;
- }
-
- // Stronger Action combat may cut a weaker non-combat Action after settle.
- if (next >= InterestTier.Action
- && InterestScoring.IsCombatAction(candidate)
- && !InterestScoring.IsCombatAction(_current)
- && onCurrent >= _settleGrace)
+ if (typedCutIn || combatCutsNonCombat || scoreCutIn)
{
return true;
}
@@ -647,18 +664,20 @@ public static class InterestDirector
return false;
}
- // Ambient/Curiosity (or unprotected): higher urgency after settle; else dwell+cooldown.
- if (next > cur && onCurrent >= _settleGrace)
+ // Unprotected / fill current: stronger score after settle, or peer rotate after dwell.
+ if (nextScore > curScore && onCurrent >= _settleGrace)
{
return true;
}
if (!protectedScene && onCurrent >= MinDwellFor(_current) && sinceSwitch >= _switchCooldown)
{
- return true;
+ return nextScore >= curScore - w.rotateSlack;
}
- return onCurrent >= MinDwellFor(_current) && sinceSwitch >= _switchCooldown && next >= cur;
+ return onCurrent >= MinDwellFor(_current)
+ && sinceSwitch >= _switchCooldown
+ && nextScore >= curScore - w.rotateSlack;
}
private static void SwitchTo(InterestCandidate next, float now, bool resumableInterrupt)
@@ -669,8 +688,8 @@ public static class InterestDirector
}
if (_current != null
- && next.Urgency >= InterestTier.Epic
- && _current.Urgency < InterestTier.Epic
+ && InterestScoring.IsHotScore(next.TotalScore)
+ && !InterestScoring.IsHotScore(_current.TotalScore)
&& _current.Resumable)
{
_interrupted = _current.CloneShallow();
@@ -680,7 +699,7 @@ public static class InterestDirector
_currentStartedAt = now;
_lastSwitchAt = now;
_inactiveSince = -999f;
- if (next.Urgency >= InterestTier.Action)
+ if (!InterestScoring.IsFillScore(next.TotalScore))
{
_lastInterestingAt = now;
}
@@ -696,12 +715,12 @@ public static class InterestDirector
_lastAmbientAt = now;
if (!force && _current != null && SessionProtected(now, now - _currentStartedAt)
- && _current.Urgency >= InterestTier.Story)
+ && InterestScoring.IsHotScore(_current.TotalScore))
{
return;
}
- if (!force && _current != null && _current.Urgency == InterestTier.Curiosity
+ if (!force && _current != null && InterestScoring.IsFillScore(_current.TotalScore)
&& now - _currentStartedAt < _ambientRotate)
{
return;
@@ -720,9 +739,9 @@ public static class InterestDirector
// Harness batches inject their own candidates; never let a live Action/Story
// ambient fill steal the camera before trigger_interest / discovery steps.
- if (AgentHarness.Busy && ambient.Tier >= InterestTier.Action)
+ if (AgentHarness.Busy && ambient.Score >= InterestScoringConfig.W.noticeScoreMin)
{
- ambient.Tier = InterestTier.Ambient;
+ ambient.Score = InterestScoringConfig.W.fillScoreMax * 0.5f;
if (string.IsNullOrEmpty(ambient.Label))
{
ambient.Label = "Ambient";
@@ -740,8 +759,8 @@ public static class InterestDirector
candidate.Completion = InterestCompletionKind.CharacterVignette;
if (AgentHarness.Busy)
{
- candidate.Urgency = InterestTier.Ambient;
candidate.ForceActive = false;
+ candidate.EventStrength = Mathf.Min(candidate.EventStrength, 25f);
InterestScoring.ScoreCheap(candidate);
}
@@ -750,7 +769,7 @@ public static class InterestDirector
&& _current.FollowUnit != null
&& candidate.FollowUnit != null
&& _current.FollowUnit == candidate.FollowUnit
- && candidate.Urgency <= InterestTier.Ambient)
+ && InterestScoring.IsFillScore(candidate.TotalScore))
{
return;
}
diff --git a/IdleSpectator/InterestEvent.cs b/IdleSpectator/InterestEvent.cs
index 6821ea5..df7ad55 100644
--- a/IdleSpectator/InterestEvent.cs
+++ b/IdleSpectator/InterestEvent.cs
@@ -2,9 +2,12 @@ using UnityEngine;
namespace IdleSpectator;
+///
+/// Lightweight watch payload for camera / tip. Ranking lives on ;
+/// typed interrupt policy uses the registry candidate, not this DTO.
+///
public sealed class InterestEvent
{
- public InterestTier Tier;
public float Score;
public Vector3 Position;
public Actor FollowUnit;
diff --git a/IdleSpectator/InterestFeeds.cs b/IdleSpectator/InterestFeeds.cs
index a7e9e30..ca46dc8 100644
--- a/IdleSpectator/InterestFeeds.cs
+++ b/IdleSpectator/InterestFeeds.cs
@@ -92,7 +92,7 @@ public static class InterestFeeds
return;
}
- if (!WorldLogInterestTable.TryGetTier(message.asset_id, out InterestTier tier))
+ if (!WorldLogInterestTable.TryGetEventStrength(message.asset_id, out float evtSeed))
{
return;
}
@@ -113,15 +113,15 @@ public static class InterestFeeds
string kingdom = message.special1 ?? "";
string key = "worldlog:" + assetId + ":" + kingdom;
float boost = PeekCivicBoost(kingdom, assetId);
+ bool politics = evtSeed >= 90f;
var candidate = new InterestCandidate
{
Key = key,
- Urgency = tier,
LeadKind = InterestLeadKind.EventLed,
- Category = tier >= InterestTier.Epic ? "Politics" : "Settlement",
+ Category = politics ? "Politics" : "Settlement",
Source = "worldlog",
- EventStrength = 60f + (int)tier * 20f + boost,
+ EventStrength = evtSeed + boost,
VisualConfidence = unit != null ? 0.7f : 0.4f,
Position = unit != null ? unit.current_position : position,
FollowUnit = unit,
@@ -133,8 +133,8 @@ public static class InterestFeeds
CreatedAt = Time.unscaledTime,
LastSeenAt = Time.unscaledTime,
ExpiresAt = Time.unscaledTime + 40f,
- MinWatch = tier >= InterestTier.Epic ? 8f : 6f,
- MaxWatch = tier >= InterestTier.Epic ? 35f : 28f,
+ MinWatch = politics ? 8f : 6f,
+ MaxWatch = politics ? 35f : 28f,
Completion = InterestCompletionKind.FixedDwell
};
InterestScoring.ScoreCheap(candidate);
@@ -152,11 +152,11 @@ public static class InterestFeeds
Actor unit = interest.FollowUnit;
long subjectId = SafeId(unit);
string asset = interest.AssetId ?? "";
- string key = "direct:" + interest.Tier + ":" + subjectId + ":" + (interest.Label ?? "") + ":" + asset;
- InterestLeadKind lead = interest.Tier >= InterestTier.Action
- ? InterestLeadKind.EventLed
- : InterestLeadKind.CharacterLed;
- InterestCompletionKind completion = interest.Tier >= InterestTier.Action
+ float score = interest.Score;
+ bool hot = score >= InterestScoringConfig.W.noticeScoreMin || asset == "live_battle";
+ string key = "direct:" + subjectId + ":" + (interest.Label ?? "") + ":" + asset;
+ InterestLeadKind lead = hot ? InterestLeadKind.EventLed : InterestLeadKind.CharacterLed;
+ InterestCompletionKind completion = hot
? InterestCompletionKind.FixedDwell
: InterestCompletionKind.CharacterVignette;
@@ -170,14 +170,15 @@ public static class InterestFeeds
var candidate = new InterestCandidate
{
Key = key,
- Urgency = interest.Tier,
LeadKind = lead,
- Category = asset == "live_battle" ? "Combat" : CategoryForTier(interest.Tier, asset),
+ Category = asset == "live_battle"
+ ? "Combat"
+ : CategoryForLead(lead, completion, asset),
Source = "direct",
- EventStrength = interest.Score > 0f ? interest.Score : 40f + (int)interest.Tier * 15f,
+ EventStrength = score > 0f ? score : 40f,
CharacterSignificance = interest.CharacterSignificance > 0f
? interest.CharacterSignificance
- : (lead == InterestLeadKind.CharacterLed ? interest.Score * 0.5f : 0f),
+ : (lead == InterestLeadKind.CharacterLed ? score * 0.5f : 0f),
VisualConfidence = unit != null ? 0.75f : 0.4f,
Position = interest.Position,
FollowUnit = unit,
@@ -191,8 +192,8 @@ public static class InterestFeeds
CreatedAt = interest.CreatedAt > 0f ? interest.CreatedAt : Time.unscaledTime,
LastSeenAt = Time.unscaledTime,
ExpiresAt = Time.unscaledTime + 30f,
- MinWatch = interest.Tier <= InterestTier.Curiosity ? 1.2f : 1.5f,
- MaxWatch = interest.Tier >= InterestTier.Epic ? 40f : 25f,
+ MinWatch = InterestScoring.IsFillScore(score) ? 1.2f : 1.5f,
+ MaxWatch = InterestScoring.IsHotScore(score) ? 40f : 25f,
Completion = completion
};
InterestScoring.ScoreCheap(candidate);
@@ -201,7 +202,6 @@ public static class InterestFeeds
public static InterestCandidate RegisterHarness(
Actor follow,
- InterestTier urgency,
string label,
string keySuffix,
InterestLeadKind lead,
@@ -212,14 +212,15 @@ public static class InterestFeeds
{
return RegisterHarness(
follow,
- urgency,
label,
keySuffix,
lead,
completion,
forceActive,
eventStrength,
- characterSignificance: lead == InterestLeadKind.CharacterLed ? eventStrength : 10f,
+ characterSignificance: lead == InterestLeadKind.CharacterLed
+ ? UnityEngine.Mathf.Min(40f, eventStrength * 0.4f)
+ : 10f,
maxWatch,
ttlSeconds: 60f,
related: null,
@@ -228,7 +229,6 @@ public static class InterestFeeds
public static InterestCandidate RegisterHarness(
Actor follow,
- InterestTier urgency,
string label,
string keySuffix,
InterestLeadKind lead,
@@ -247,14 +247,13 @@ public static class InterestFeeds
}
long id = SafeId(follow);
- string key = "harness:" + (keySuffix ?? urgency.ToString()) + ":" + id;
+ string key = "harness:" + (keySuffix ?? "scene") + ":" + id;
float now = Time.unscaledTime;
var candidate = new InterestCandidate
{
Key = key,
- Urgency = urgency,
LeadKind = lead,
- Category = CategoryForTier(urgency, ""),
+ Category = CategoryForLead(lead, completion, ""),
Source = "harness",
EventStrength = eventStrength,
CharacterSignificance = characterSignificance,
@@ -263,7 +262,7 @@ public static class InterestFeeds
FollowUnit = follow,
RelatedUnit = related,
SubjectId = id,
- Label = label ?? ("Harness " + urgency),
+ Label = label ?? "Harness",
AssetId = follow.asset != null ? follow.asset.id : "harness",
SpeciesId = follow.asset != null ? follow.asset.id : "",
CreatedAt = now,
@@ -305,7 +304,6 @@ public static class InterestFeeds
var candidate = new InterestCandidate
{
Key = key,
- Urgency = InterestTier.Action,
LeadKind = InterestLeadKind.EventLed,
Category = combat ? "Combat" : "Work",
Source = "activity",
@@ -368,7 +366,6 @@ public static class InterestFeeds
var candidate = new InterestCandidate
{
Key = key,
- Urgency = InterestTier.Action,
LeadKind = InterestLeadKind.EventLed,
Category = "StatusTransformation",
Source = "status",
@@ -408,13 +405,9 @@ public static class InterestFeeds
long rid = SafeId(related);
// Shared key space with happiness canonical merges.
string key = "chronicle:" + milestoneKey + ":" + sid + ":" + rid;
- InterestTier urgency = milestoneKey.Contains("kill") || milestoneKey.Contains("death")
- ? InterestTier.Action
- : InterestTier.Curiosity;
var candidate = new InterestCandidate
{
Key = key,
- Urgency = urgency,
LeadKind = InterestLeadKind.EventLed,
Category = "Relationship",
Source = "chronicle",
@@ -521,13 +514,11 @@ public static class InterestFeeds
string key = "happiness:" + occ.EffectId + ":" + occ.SubjectId + ":" + occ.RelatedId
+ ":" + CorrBucket(occ.CorrelationKey);
bool grief = occ.EffectId != null && occ.EffectId.StartsWith("death_");
- InterestTier urgency = InterestScoring.UrgencyForHappiness(occ.EffectId, subject);
float strength = InterestScoring.EventStrengthForHappiness(occ.EffectId);
var candidate = new InterestCandidate
{
Key = key,
- Urgency = urgency,
LeadKind = InterestLeadKind.EventLed,
Category = grief ? "FamilyEmotion" : MapHappinessCategory(cat),
Source = "happiness",
@@ -599,7 +590,6 @@ public static class InterestFeeds
}
bool grief = effectId.StartsWith("death_");
- InterestTier urgency = InterestScoring.UrgencyForHappiness(effectId, subject);
float strength = InterestScoring.EventStrengthForHappiness(effectId);
string key = "happiness:" + effectId + ":" + id + ":0:harness";
bool alive = false;
@@ -615,7 +605,6 @@ public static class InterestFeeds
var candidate = new InterestCandidate
{
Key = key,
- Urgency = urgency,
LeadKind = InterestLeadKind.EventLed,
Category = grief ? "FamilyEmotion" : "LifeChapter",
Source = "happiness_harness",
@@ -768,34 +757,33 @@ public static class InterestFeeds
}
}
- private static string CategoryForTier(InterestTier tier, string asset)
+ private static string CategoryForLead(
+ InterestLeadKind lead,
+ InterestCompletionKind completion,
+ string asset)
{
- if (asset == "live_battle")
+ if (asset == "live_battle" || completion == InterestCompletionKind.CombatActive)
{
return "Combat";
}
- if (tier >= InterestTier.Epic)
+ if (completion == InterestCompletionKind.CharacterVignette
+ || lead == InterestLeadKind.CharacterLed)
{
- return "Politics";
+ return "CharacterVignette";
}
- if (tier == InterestTier.Story)
+ if (completion == InterestCompletionKind.HappinessGrief)
{
- return "Settlement";
+ return "FamilyEmotion";
}
- if (tier == InterestTier.Action)
+ if (completion == InterestCompletionKind.ActivityActive)
{
- return "Combat";
+ return "Work";
}
- if (tier == InterestTier.Curiosity)
- {
- return "Discovery";
- }
-
- return "CharacterVignette";
+ return "Settlement";
}
private static string CorrBucket(string corr)
diff --git a/IdleSpectator/InterestRegistry.cs b/IdleSpectator/InterestRegistry.cs
index 346a762..5da25be 100644
--- a/IdleSpectator/InterestRegistry.cs
+++ b/IdleSpectator/InterestRegistry.cs
@@ -91,10 +91,6 @@ public static class InterestRegistry
existing.CharacterSignificance, incoming.CharacterSignificance);
existing.VisualConfidence = Mathf.Max(existing.VisualConfidence, incoming.VisualConfidence);
existing.TotalScore = Mathf.Max(existing.TotalScore, incoming.TotalScore);
- if (incoming.Urgency > existing.Urgency)
- {
- existing.Urgency = incoming.Urgency;
- }
if (!string.IsNullOrEmpty(incoming.Label))
{
@@ -293,14 +289,14 @@ public static class InterestRegistry
}
}
- public static bool HasPendingAtLeast(InterestTier minUrgency)
+ public static bool HasPendingScoreAtLeast(float minScore)
{
lock (Gate)
{
foreach (KeyValuePair kv in ByKey)
{
InterestCandidate c = kv.Value;
- if (c != null && !c.Selected && c.HasValidPosition && c.Urgency >= minUrgency)
+ if (c != null && !c.Selected && c.HasValidPosition && c.TotalScore >= minScore)
{
return true;
}
@@ -326,22 +322,7 @@ public static class InterestRegistry
}
}
- Scratch.Sort((a, b) =>
- {
- int urg = ((int)b.Urgency).CompareTo((int)a.Urgency);
- if (urg != 0)
- {
- return urg;
- }
-
- int score = b.TotalScore.CompareTo(a.TotalScore);
- if (score != 0)
- {
- return score;
- }
-
- return b.LastSeenAt.CompareTo(a.LastSeenAt);
- });
+ Scratch.Sort(InterestScoring.CompareByScore);
for (int i = MaxCandidates; i < Scratch.Count; i++)
{
diff --git a/IdleSpectator/InterestScoring.cs b/IdleSpectator/InterestScoring.cs
index 1ce3202..9e5cdf4 100644
--- a/IdleSpectator/InterestScoring.cs
+++ b/IdleSpectator/InterestScoring.cs
@@ -22,7 +22,7 @@ public sealed class InterestMetadataSnapshot
}
///
-/// Urgency-class mapping and within-class score breakdown.
+/// Score breakdown and typed scene classifiers (combat / vignette). Ranking is TotalScore only.
///
public static class InterestScoring
{
@@ -37,31 +37,6 @@ public static class InterestScoring
MetaCache.Clear();
}
- public static InterestTier UrgencyForHappiness(string effectId, Actor subject)
- {
- string id = effectId ?? "";
- bool notable = IsNotable(subject);
-
- if (id == "death_child" || id == "death_lover")
- {
- return notable ? InterestTier.Story : InterestTier.Action;
- }
-
- if (id == "death_best_friend" || id == "death_family_member")
- {
- return InterestTier.Action;
- }
-
- if (id == "got_saved" || id == "just_born" || id == "just_hatched"
- || id == "become_king" || id == "become_city_leader" || id == "become_clan_chief"
- || id == "just_found_house" || id == "new_home")
- {
- return InterestTier.Action;
- }
-
- return InterestTier.Curiosity;
- }
-
public static float EventStrengthForHappiness(string effectId)
{
string id = effectId ?? "";
@@ -92,11 +67,19 @@ public static class InterestScoring
}
}
- public static InterestTier UrgencyForWorldLog(string assetId)
+ public static float EventStrengthForWorldLog(string assetId)
{
- return WorldLogInterestTable.TryGetTier(assetId, out InterestTier tier)
- ? tier
- : InterestTier.Ambient;
+ return WorldLogInterestTable.TryGetEventStrength(assetId, out float seed) ? seed : 0f;
+ }
+
+ public static bool IsFillScore(float totalScore)
+ {
+ return totalScore < InterestScoringConfig.W.fillScoreMax;
+ }
+
+ public static bool IsHotScore(float totalScore)
+ {
+ return totalScore >= InterestScoringConfig.W.hotScoreMin;
}
public static void EnrichTopK(List candidates)
@@ -106,7 +89,7 @@ public static class InterestScoring
return;
}
- candidates.Sort(CompareByUrgencyThenScore);
+ candidates.Sort(CompareByScore);
int n = Mathf.Min(EnrichTopKLimit, candidates.Count);
float now = Time.unscaledTime;
for (int i = 0; i < n; i++)
@@ -141,19 +124,15 @@ public static class InterestScoring
}
// Action-primary: event strength dominates. Character is a tie-break / small amplifier.
- // Scale / notables on clusters (battles) add to event strength before this runs.
- // Knobs: IdleSpectator/scoring-model.json → weights (InterestScoringConfig.W).
ScoringWeights w = InterestScoringConfig.W;
float charWeight = c.LeadKind == InterestLeadKind.EventLed
? w.charWeightEventLed
: w.charWeightCharacterLed;
- float urgencyWeight = 1f + (int)c.Urgency * w.urgencyTierStep;
float scaleBonus = 0f;
int fighters = c.ParticipantCount;
int notables = c.NotableParticipantCount;
if (fighters >= w.massFighterThreshold)
{
- // Multi-person melees outrank thin anonymous 1v1s.
scaleBonus += w.massBaseBonus
+ Mathf.Min(w.massExtraCap, (fighters - w.massFighterThreshold) * w.massPerExtraFighter);
}
@@ -161,7 +140,6 @@ public static class InterestScoring
{
if (notables >= 2)
{
- // Extremely important duel (kings/favorites) can beat nameless melees.
scaleBonus += w.duelNotablePairBonus;
}
else if (notables == 1)
@@ -179,7 +157,7 @@ public static class InterestScoring
scaleBonus += Mathf.Min(w.notableSkirmishCap, notables * w.notableSkirmishPer);
}
- c.TotalScore = (c.EventStrength + scaleBonus) * urgencyWeight
+ c.TotalScore = c.EventStrength + scaleBonus
+ c.CharacterSignificance * charWeight
+ c.VisualConfidence * w.visualMultiplier
+ c.Novelty * w.noveltyMultiplier;
@@ -285,7 +263,7 @@ public static class InterestScoring
}
}
- public static int CompareByUrgencyThenScore(InterestCandidate a, InterestCandidate b)
+ public static int CompareByScore(InterestCandidate a, InterestCandidate b)
{
if (ReferenceEquals(a, b))
{
@@ -302,12 +280,6 @@ public static class InterestScoring
return -1;
}
- int urg = ((int)b.Urgency).CompareTo((int)a.Urgency);
- if (urg != 0)
- {
- return urg;
- }
-
int score = b.TotalScore.CompareTo(a.TotalScore);
if (score != 0)
{
@@ -317,6 +289,12 @@ public static class InterestScoring
return b.LastSeenAt.CompareTo(a.LastSeenAt);
}
+ /// Legacy alias; score-only ranking.
+ public static int CompareByUrgencyThenScore(InterestCandidate a, InterestCandidate b)
+ {
+ return CompareByScore(a, b);
+ }
+
public static bool IsNotable(Actor actor)
{
if (actor == null)
@@ -442,9 +420,9 @@ public static class InterestScoring
return true;
}
- // Scanner/harness "Story" celebrity strolls are not WorldLog events - actions may cut them.
- if (c.Urgency == InterestTier.Story
- && (c.Source == "scanner" || c.Source == "harness")
+ // Scanner/harness celebrity strolls without a live action beat - combat may cut them.
+ if ((c.Source == "scanner" || c.Source == "harness")
+ && c.LeadKind == InterestLeadKind.CharacterLed
&& string.IsNullOrEmpty(c.HappinessEffectId))
{
return true;
diff --git a/IdleSpectator/InterestScoringConfig.cs b/IdleSpectator/InterestScoringConfig.cs
index 11d13ff..58cead0 100644
--- a/IdleSpectator/InterestScoringConfig.cs
+++ b/IdleSpectator/InterestScoringConfig.cs
@@ -23,10 +23,20 @@ public class ScoringWeights
{
public float charWeightEventLed = 0.18f;
public float charWeightCharacterLed = 0.55f;
- public float urgencyTierStep = 0.08f;
public float visualMultiplier = 12f;
public float noveltyMultiplier = 4f;
+ // Director policy (score-native; replaces InterestTier preemption).
+ public float cutInMargin = 35f;
+ public float rotateSlack = 12f;
+ public float fillScoreMax = 55f;
+ public float noticeScoreMin = 70f;
+ public float hotScoreMin = 110f;
+ public float dwellFill = 6f;
+ public float dwellNormal = 10f;
+ public float dwellHot = 14f;
+ public float fillRotateSeconds = 10f;
+
public int massFighterThreshold = 4;
public float massBaseBonus = 28f;
public float massPerExtraFighter = 4f;
diff --git a/IdleSpectator/InterestTier.cs b/IdleSpectator/InterestTier.cs
deleted file mode 100644
index 6966929..0000000
--- a/IdleSpectator/InterestTier.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-namespace IdleSpectator;
-
-///
-/// Higher value can interrupt lower after interrupt grace.
-///
-public enum InterestTier
-{
- /// Filler rotation when nothing else is happening.
- Ambient = 0,
- /// Low priority curiosities (new species, oddities).
- Curiosity = 1,
- /// Live fights, high-kill creatures, action clusters.
- Action = 2,
- /// Leadership / politics / favorites.
- Story = 3,
- /// World-shaping beats (wars, kingdom falls, disasters).
- Epic = 4
-}
diff --git a/IdleSpectator/InterestVariety.cs b/IdleSpectator/InterestVariety.cs
index bd6d376..71c2e14 100644
--- a/IdleSpectator/InterestVariety.cs
+++ b/IdleSpectator/InterestVariety.cs
@@ -174,7 +174,7 @@ public static class InterestVariety
InterestCandidate c = pool[i];
float penalty = NoveltyPenalty(c, now);
// Epic / favorites bypass most novelty.
- if (c.Urgency >= InterestTier.Epic || InterestScoring.IsNotable(c.FollowUnit))
+ if (InterestScoring.IsHotScore(c.TotalScore) || InterestScoring.IsNotable(c.FollowUnit))
{
penalty *= 0.15f;
}
diff --git a/IdleSpectator/UnitDossier.cs b/IdleSpectator/UnitDossier.cs
index 08bef8d..3499c0b 100644
--- a/IdleSpectator/UnitDossier.cs
+++ b/IdleSpectator/UnitDossier.cs
@@ -80,7 +80,7 @@ public sealed class UnitDossier
return d;
}
- public static UnitDossier FromActor(Actor actor, InterestTier? watchTier = null, string watchLabel = null)
+ public static UnitDossier FromActor(Actor actor, string watchLabel = null)
{
UnitDossier d = new UnitDossier();
if (actor == null || !actor.isAlive())
@@ -119,7 +119,6 @@ public sealed class UnitDossier
d.Headline = string.IsNullOrEmpty(d.SpeciesId)
? d.Name
: $"{d.Name} ({d.SpeciesId})";
- _ = watchTier; // tier badge removed; beat is event-shaped only
d.ReasonLine = BuildStoryBeat(d, watchLabel, actor);
d.DetailLine = BuildDetailLine(d);
d.CaptionText = JoinCaption(d.Headline, d.ReasonLine, d.DetailLine);
@@ -214,8 +213,8 @@ public sealed class UnitDossier
///
/// Story beat for the dossier reason row: what is happening in this shot.
- /// No tier badges, no job append, no nametag repeats. Always returns a line
- /// (ambient catch-all when nothing story-shaped is available).
+ /// No tier badges, no job append, no nametag repeats.
+ /// Fill / ambient scenes with no sharper beat leave the reason empty (row hidden).
/// Follow-up (not blocking): expand event/character tags and optional beatPhrases
/// in scoring-model.json once playtests show thin beats.
///
@@ -240,10 +239,11 @@ public sealed class UnitDossier
return griefOrLife;
}
- // Ambient filler: always the catch-all (labels are identity/noise, not story).
- if (scene != null && scene.Urgency == InterestTier.Ambient)
+ // Ambient filler: no story beat - hide the reason row.
+ if (scene != null && InterestScoring.IsFillScore(scene.TotalScore)
+ && !InterestScoring.IsCombatAction(scene))
{
- return AmbientBeat(scene);
+ return "";
}
string fromScene = SceneLabelBeat(scene, d);
@@ -282,26 +282,7 @@ public sealed class UnitDossier
return "New species";
}
- return AmbientBeat(scene);
- }
-
- /// Catch-all when the shot has no sharper story beat.
- private static string AmbientBeat(InterestCandidate scene)
- {
- if (scene != null)
- {
- if (scene.Urgency == InterestTier.Curiosity)
- {
- return "Curious glance";
- }
-
- if (scene.Urgency <= InterestTier.Ambient)
- {
- return "Looking around";
- }
- }
-
- return "Looking around";
+ return "";
}
private static InterestCandidate MatchingScene(Actor actor, UnitDossier d)
diff --git a/IdleSpectator/WatchCaption.cs b/IdleSpectator/WatchCaption.cs
index 77c848f..a726ce0 100644
--- a/IdleSpectator/WatchCaption.cs
+++ b/IdleSpectator/WatchCaption.cs
@@ -431,17 +431,17 @@ public static class WatchCaption
return;
}
- SetFromActor(unit, interest?.Tier, interest?.Label);
+ SetFromActor(unit, interest?.Label);
}
- public static void SetFromActor(Actor actor, InterestTier? tier = null, string label = null)
+ public static void SetFromActor(Actor actor, string label = null)
{
if (!_pinnedWhilePaused)
{
ClearStatusBanner();
}
- UnitDossier dossier = UnitDossier.FromActor(actor, tier, label);
+ UnitDossier dossier = UnitDossier.FromActor(actor, label);
_current = dossier;
_boundActor = actor;
LastHeadline = dossier.Headline ?? "";
diff --git a/IdleSpectator/WorldActivityScanner.cs b/IdleSpectator/WorldActivityScanner.cs
index 7e2ef7a..0dbf357 100644
--- a/IdleSpectator/WorldActivityScanner.cs
+++ b/IdleSpectator/WorldActivityScanner.cs
@@ -88,22 +88,18 @@ public static class WorldActivityScanner
{
Actor actor = alive[i];
ActivityBand band = ActivityInterestTable.Classify(actor);
- InterestTier tier = InterestTier.Ambient;
InterestLeadKind lead = InterestLeadKind.CharacterLed;
- // Actions outrank characters: combat/hot work are Action events; kings walking stay vignettes.
+ // Actions outrank characters: combat/hot work are events; kings walking stay vignettes.
if (band == ActivityBand.Hot || actor.has_attack_target)
{
- tier = InterestTier.Action;
lead = InterestLeadKind.EventLed;
}
else if (band >= ActivityBand.Warm)
{
- tier = InterestTier.Action;
lead = InterestLeadKind.EventLed;
}
else if (IsSpectacle(actor) || IsSingletonSpecies(actor, speciesCounts))
{
- tier = InterestTier.Curiosity;
lead = InterestLeadKind.CharacterLed;
}
@@ -120,7 +116,7 @@ public static class WorldActivityScanner
string key = lead == InterestLeadKind.CharacterLed
? "vignette:" + id + ":" + (actor.asset != null ? actor.asset.id : "unit")
- : "scan:" + tier + ":" + id;
+ : "scan:evt:" + id;
string label = FormatUnitLabel(actor, speciesCounts);
if (actor.has_attack_target)
@@ -144,7 +140,6 @@ public static class WorldActivityScanner
var candidate = new InterestCandidate
{
Key = key,
- Urgency = tier,
LeadKind = lead,
Category = combat
? "Combat"
@@ -270,7 +265,6 @@ public static class WorldActivityScanner
: $"Skirmish ({fighters} fighting)";
best = new InterestEvent
{
- Tier = InterestTier.Action,
Score = score,
Position = pos,
FollowUnit = follow ?? FindNearestAliveUnit(pos, 14f),
@@ -330,7 +324,6 @@ public static class WorldActivityScanner
bestScore = score;
best = new InterestEvent
{
- Tier = InterestTier.Action,
Score = score,
Position = actor.current_position,
FollowUnit = follow ?? actor,
@@ -517,7 +510,6 @@ public static class WorldActivityScanner
return null;
}
- InterestTier tier = InterestTier.Ambient;
ActivityBand band = ActivityInterestTable.Classify(best);
SplitActorScore(best, speciesCounts, out float actionScore, out float charScore);
int fighters = 0;
@@ -526,20 +518,6 @@ public static class WorldActivityScanner
{
CountFightCluster(best.current_position, 10f, out fighters, out notables, out _);
fighters = Mathf.Max(1, fighters);
- tier = InterestTier.Action;
- }
- else if (band >= ActivityBand.Warm)
- {
- // Warm work is an action beat - not Story celebrity.
- tier = InterestTier.Action;
- }
- else if (IsSpectacle(best) || IsSingletonSpecies(best, speciesCounts))
- {
- tier = InterestTier.Curiosity;
- }
- else
- {
- tier = InterestTier.Ambient;
}
float finalScore = ScoreFightCluster(0, fighters, notables);
@@ -555,7 +533,6 @@ public static class WorldActivityScanner
return new InterestEvent
{
- Tier = tier,
Score = finalScore,
Position = best.current_position,
FollowUnit = best,
@@ -874,7 +851,6 @@ public static class WorldActivityScanner
return new InterestEvent
{
- Tier = InterestTier.Curiosity,
Score = 5f,
Position = pos,
FollowUnit = follow,
@@ -1044,7 +1020,6 @@ public static class WorldActivityScanner
return new InterestEvent
{
- Tier = source.Tier,
Score = source.Score,
Position = source.Position,
FollowUnit = source.FollowUnit,
diff --git a/IdleSpectator/WorldLogInterestTable.cs b/IdleSpectator/WorldLogInterestTable.cs
index 2247b54..e1af40b 100644
--- a/IdleSpectator/WorldLogInterestTable.cs
+++ b/IdleSpectator/WorldLogInterestTable.cs
@@ -4,63 +4,63 @@ using UnityEngine;
namespace IdleSpectator;
///
-/// Centralized WorldLog asset ids and tier mapping.
+/// WorldLog asset ids → EventStrength seeds (score-native; no InterestTier).
///
public static class WorldLogInterestTable
{
- private static readonly Dictionary Tiers = new Dictionary
+ private static readonly Dictionary EventSeeds = new Dictionary
{
- // Epic - world-shaping (wiki: Wars, Kingdoms fall, Disasters, city destroyed)
- ["diplomacy_war_started"] = InterestTier.Epic,
- ["total_war_started"] = InterestTier.Epic,
- ["kingdom_destroyed"] = InterestTier.Epic,
- ["kingdom_shattered"] = InterestTier.Epic,
- ["kingdom_fractured"] = InterestTier.Epic,
- ["city_destroyed"] = InterestTier.Epic,
- ["log_city_revolted"] = InterestTier.Epic,
+ // World-shaping
+ ["diplomacy_war_started"] = 100f,
+ ["total_war_started"] = 100f,
+ ["kingdom_destroyed"] = 100f,
+ ["kingdom_shattered"] = 98f,
+ ["kingdom_fractured"] = 95f,
+ ["city_destroyed"] = 92f,
+ ["log_city_revolted"] = 90f,
- // Story - leadership / politics / favorites / founding
- ["kingdom_new"] = InterestTier.Story,
- ["city_new"] = InterestTier.Story,
- ["king_new"] = InterestTier.Story,
- ["king_dead"] = InterestTier.Story,
- ["king_killed"] = InterestTier.Story,
- ["king_fled_capital"] = InterestTier.Story,
- ["king_fled_city"] = InterestTier.Story,
- ["king_left"] = InterestTier.Story,
- ["alliance_new"] = InterestTier.Story,
- ["alliance_dissolved"] = InterestTier.Story,
- ["diplomacy_war_ended"] = InterestTier.Story,
- ["favorite_dead"] = InterestTier.Story,
- ["favorite_killed"] = InterestTier.Story,
- ["kingdom_royal_clan_new"] = InterestTier.Story,
- ["kingdom_royal_clan_changed"] = InterestTier.Story,
- ["kingdom_royal_clan_dead"] = InterestTier.Story,
+ // Leadership / politics / favorites / founding
+ ["kingdom_new"] = 75f,
+ ["city_new"] = 72f,
+ ["king_new"] = 78f,
+ ["king_dead"] = 80f,
+ ["king_killed"] = 85f,
+ ["king_fled_capital"] = 70f,
+ ["king_fled_city"] = 68f,
+ ["king_left"] = 65f,
+ ["alliance_new"] = 70f,
+ ["alliance_dissolved"] = 68f,
+ ["diplomacy_war_ended"] = 72f,
+ ["favorite_dead"] = 82f,
+ ["favorite_killed"] = 85f,
+ ["kingdom_royal_clan_new"] = 74f,
+ ["kingdom_royal_clan_changed"] = 72f,
+ ["kingdom_royal_clan_dead"] = 76f,
- // Curiosity - biosphere footnotes
- ["race_dead"] = InterestTier.Curiosity
+ // Biosphere footnotes
+ ["race_dead"] = 40f
};
- public static bool TryGetTier(string assetId, out InterestTier tier)
+ public static bool TryGetEventStrength(string assetId, out float eventStrength)
{
if (string.IsNullOrEmpty(assetId))
{
- tier = InterestTier.Ambient;
+ eventStrength = 0f;
return false;
}
- if (Tiers.TryGetValue(assetId, out tier))
+ if (EventSeeds.TryGetValue(assetId, out eventStrength))
{
return true;
}
if (assetId.Contains("disaster") || assetId.StartsWith("worldlog_disaster"))
{
- tier = InterestTier.Epic;
+ eventStrength = 95f;
return true;
}
- tier = InterestTier.Ambient;
+ eventStrength = 0f;
return false;
}
@@ -121,10 +121,12 @@ public static class WorldLogInterestTable
{
return $"{id}: {a} / {b}";
}
+
if (!string.IsNullOrEmpty(a))
{
return $"{id}: {a}";
}
+
return id;
}
}
diff --git a/IdleSpectator/WorldLogPatches.cs b/IdleSpectator/WorldLogPatches.cs
index e3e6d65..00d74ee 100644
--- a/IdleSpectator/WorldLogPatches.cs
+++ b/IdleSpectator/WorldLogPatches.cs
@@ -14,12 +14,13 @@ public static class WorldLogMessageAddPatch
return;
}
- if (!WorldLogInterestTable.TryGetTier(pMessage.asset_id, out InterestTier tier))
+ if (!WorldLogInterestTable.TryGetEventStrength(pMessage.asset_id, out float evtSeed))
{
return;
}
- if (tier < InterestTier.Story)
+ // Chronicle notable world events (story-strength and above); skip footnotes.
+ if (evtSeed < 65f)
{
return;
}
diff --git a/IdleSpectator/mod.json b/IdleSpectator/mod.json
index a99c7da..2769992 100644
--- a/IdleSpectator/mod.json
+++ b/IdleSpectator/mod.json
@@ -1,7 +1,7 @@
{
"name": "IdleSpectator",
"author": "dazed",
- "version": "0.15.5",
+ "version": "0.16.6",
"description": "AFK Idle Spectator (I) + Lore (L). Event-centered scene director + happiness Activity/Life logging.",
"GUID": "com.dazed.idlespectator"
}
diff --git a/IdleSpectator/scoring-model.json b/IdleSpectator/scoring-model.json
index 8bd4b22..dcfe4e2 100644
--- a/IdleSpectator/scoring-model.json
+++ b/IdleSpectator/scoring-model.json
@@ -1,5 +1,5 @@
{
- "modVersion": "0.14.23",
+ "modVersion": "0.16.0",
"title": "IdleSpectator interest scoring model",
"updated": "2026-07-15",
"note": "Edit weights below — InterestScoringConfig loads this file at mod start. Docs sections are human-facing only (ignored by Unity JsonUtility).",
@@ -7,10 +7,19 @@
"weights": {
"charWeightEventLed": 0.18,
"charWeightCharacterLed": 0.55,
- "urgencyTierStep": 0.08,
"visualMultiplier": 12,
"noveltyMultiplier": 4,
+ "cutInMargin": 35,
+ "rotateSlack": 12,
+ "fillScoreMax": 55,
+ "noticeScoreMin": 70,
+ "hotScoreMin": 110,
+ "dwellFill": 6,
+ "dwellNormal": 10,
+ "dwellHot": 14,
+ "fillRotateSeconds": 10,
+
"massFighterThreshold": 4,
"massBaseBonus": 28,
"massPerExtraFighter": 4,
@@ -86,39 +95,45 @@
"Same action intensity → more important character wins (tie-break).",
"Multi-person fights outrank anonymous 1v1s.",
"Extremely notable 1v1s (kings/favorites) can beat nameless melees.",
- "Urgency (tier) is preemption class, not the main score."
+ "TotalScore is the sole ranking signal; typed rules (combat vs vignette) gate interrupts.",
+ "InterestTier was removed - WorldLog seeds EventStrength; director uses cutInMargin / rotateSlack / dwell bands."
],
"pipeline": [
{
"id": "feeds",
"name": "Feeds → candidates",
- "detail": "WorldLog, happiness, scanner, battle clusters, harness injects upsert InterestCandidate into InterestRegistry with EventStrength, CharacterSignificance, ParticipantCount, NotableParticipantCount."
+ "detail": "WorldLog, happiness, scanner, battle clusters, harness injects upsert InterestCandidate with EventStrength, CharacterSignificance, ParticipantCount, NotableParticipantCount (no Urgency tier)."
},
{
"id": "variety",
"name": "Variety pick",
- "detail": "InterestVariety.Pick soft-splits EventLed vs CharacterLed (~70/30), applies novelty penalties, RecalcTotal, EnrichTopK, then probabilistic top-3."
+ "detail": "InterestVariety.Pick soft-splits EventLed vs CharacterLed (~70/30), applies novelty penalties, RecalcTotal, EnrichTopK, then probabilistic top-3 by TotalScore."
},
{
"id": "director",
"name": "Director switch",
- "detail": "InterestDirector.CanSwitchTo gates camera changes: settle grace, dwell, Epic cuts, combat-vs-vignette exceptions."
+ "detail": "InterestDirector.CanSwitchTo gates camera changes via score margins (cutInMargin/rotateSlack), settle grace, and typed combat-vs-vignette cut-ins."
},
{
"id": "camera",
"name": "Camera + UI",
- "detail": "CameraDirector.Watch + tip/dossier watch-why from action verbs (Fighting/War/Grief), not identity alone."
+ "detail": "CameraDirector.Watch + dossier story beats from action verbs (Fighting/War/Grief). Fill/ambient with no sharper beat leaves the reason row empty."
}
],
"totalScore": {
"file": "InterestScoring.RecalcTotal",
- "formula": "(EventStrength + scaleBonus) * urgencyWeight + CharacterSignificance * charWeight + VisualConfidence * visualMultiplier + Novelty * noveltyMultiplier",
+ "formula": "EventStrength + scaleBonus + CharacterSignificance * charWeight + VisualConfidence * visualMultiplier + Novelty * noveltyMultiplier",
"charWeight": {
"EventLed": "weights.charWeightEventLed",
"CharacterLed": "weights.charWeightCharacterLed"
},
- "urgencyWeight": "1 + (int)Urgency * weights.urgencyTierStep",
- "scaleBonus": "See weights.mass* / duel* / notableSkirmish*"
+ "scaleBonus": "See weights.mass* / duel* / notableSkirmish*",
+ "directorPolicy": {
+ "cutInMargin": "weights.cutInMargin",
+ "rotateSlack": "weights.rotateSlack",
+ "fillScoreMax": "weights.fillScoreMax",
+ "dwell": "weights.dwellFill / dwellNormal / dwellHot"
+ }
},
"workedExamples": [
{