feat(saga): refine rare creature and antagonist admission
- Scale and cap rare-creature and antagonist roster bonuses - Protect developed MCs from identity-only displacement - Promote exact MC-cast harm into strong rival evidence - Verify connection attribution, roster stability, and performance
This commit is contained in:
parent
44999fb653
commit
25a2aa94f1
8 changed files with 613 additions and 68 deletions
|
|
@ -6761,6 +6761,58 @@ public static class AgentHarness
|
|||
}
|
||||
}
|
||||
|
||||
private static Actor FindOtherAliveUnitExcluding(
|
||||
Actor excludeA,
|
||||
Actor excludeB,
|
||||
string preferAsset)
|
||||
{
|
||||
if (World.world?.units == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
long excludeAId = EventFeedUtil.SafeId(excludeA);
|
||||
long excludeBId = EventFeedUtil.SafeId(excludeB);
|
||||
string wantAsset = (preferAsset ?? "").Trim();
|
||||
Actor fallback = null;
|
||||
try
|
||||
{
|
||||
foreach (Actor actor in World.world.units)
|
||||
{
|
||||
long id = EventFeedUtil.SafeId(actor);
|
||||
if (actor == null
|
||||
|| !actor.isAlive()
|
||||
|| id == 0
|
||||
|| id == excludeAId
|
||||
|| id == excludeBId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string assetId = actor.asset != null ? actor.asset.id : "";
|
||||
if (!string.IsNullOrEmpty(wantAsset)
|
||||
&& string.Equals(
|
||||
assetId,
|
||||
wantAsset,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return actor;
|
||||
}
|
||||
|
||||
if (fallback == null)
|
||||
{
|
||||
fallback = actor;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private static void ParseInterestInjectOptions(
|
||||
string raw,
|
||||
out bool force,
|
||||
|
|
@ -10665,6 +10717,18 @@ public static class AgentHarness
|
|||
pass = CameraEligibility.HarnessProbeExceptionalPolicy(out detail);
|
||||
break;
|
||||
}
|
||||
case "camera_connection_attribution":
|
||||
{
|
||||
Actor anchor = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||
Actor friendly = _happinessPartner;
|
||||
Actor hostile = FindOtherAliveUnitExcluding(anchor, friendly, "human");
|
||||
pass = CameraEligibility.HarnessProbeConnectionAttribution(
|
||||
anchor,
|
||||
friendly,
|
||||
hostile,
|
||||
out detail);
|
||||
break;
|
||||
}
|
||||
case "saga_civilized_no_pack_language":
|
||||
{
|
||||
Actor actor = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : _lastSpawned;
|
||||
|
|
@ -11277,6 +11341,28 @@ public static class AgentHarness
|
|||
pass = LifeSagaRoster.HarnessProbeReplacementEvidence(out detail);
|
||||
break;
|
||||
}
|
||||
case "saga_rare_antagonist_policy":
|
||||
{
|
||||
pass = LifeSagaRoster.HarnessProbeRareAntagonistPolicy(out detail);
|
||||
break;
|
||||
}
|
||||
case "saga_cast_harm_evidence":
|
||||
{
|
||||
Actor anchor = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||
Actor castMember = _happinessPartner;
|
||||
Actor attacker = FindOtherAliveUnitExcluding(anchor, castMember, "human");
|
||||
pass = LifeSagaMemory.HarnessProbeMcCastHarm(
|
||||
anchor,
|
||||
castMember,
|
||||
attacker,
|
||||
out detail);
|
||||
break;
|
||||
}
|
||||
case "saga_roster_repeat_stable":
|
||||
{
|
||||
pass = LifeSagaRoster.HarnessProbeRepeatedScanStability(out detail);
|
||||
break;
|
||||
}
|
||||
case "saga_role_tip":
|
||||
{
|
||||
string have = SampleRailTip();
|
||||
|
|
@ -11624,6 +11710,40 @@ public static class AgentHarness
|
|||
detail = $"remembered='{_rememberedTip}' now='{nowTip}'";
|
||||
break;
|
||||
}
|
||||
case "tip_same_or_collective_escalation":
|
||||
{
|
||||
string nowTip = InterestDirector.CurrentCandidate?.Label
|
||||
?? CameraDirector.LastWatchLabel
|
||||
?? "";
|
||||
bool same = !string.IsNullOrEmpty(_rememberedTip)
|
||||
&& string.Equals(
|
||||
nowTip,
|
||||
_rememberedTip,
|
||||
StringComparison.Ordinal);
|
||||
bool wasPair = !string.IsNullOrEmpty(_rememberedTip)
|
||||
&& (_rememberedTip.IndexOf(
|
||||
"Duel",
|
||||
StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| _rememberedTip.IndexOf(
|
||||
" vs ",
|
||||
StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| _rememberedTip.IndexOf(
|
||||
"fighting",
|
||||
StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
bool isCollective = nowTip.StartsWith(
|
||||
"Skirmish -",
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
|| nowTip.StartsWith(
|
||||
"Battle -",
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
|| nowTip.StartsWith(
|
||||
"Mass -",
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
pass = same || (wasPair && isCollective);
|
||||
detail = $"remembered='{_rememberedTip}' now='{nowTip}' same={same} "
|
||||
+ $"pairToCollective={wasPair && isCollective}";
|
||||
break;
|
||||
}
|
||||
case "related_same":
|
||||
case "pair_partner_same":
|
||||
{
|
||||
|
|
|
|||
|
|
@ -217,6 +217,8 @@ internal static class HarnessScenarios
|
|||
return SagaRosterDiversity();
|
||||
case "saga_roster_cap_five":
|
||||
return SagaRosterCapFive();
|
||||
case "saga_rare_antagonist_acceptance":
|
||||
return SagaRareAntagonistAcceptance();
|
||||
case "saga_connection_dossier":
|
||||
return SagaConnectionDossier();
|
||||
case "saga_camera_prefers_mc":
|
||||
|
|
@ -517,6 +519,7 @@ internal static class HarnessScenarios
|
|||
Nested("reg_saga_legacy_combat", "saga_legacy_combat_summary"),
|
||||
Nested("reg_saga_session_reset", "saga_session_reset"),
|
||||
Nested("reg_saga_hover_pause", "saga_hover_read_pause"),
|
||||
Nested("reg_saga_rare_antagonist", "saga_rare_antagonist_acceptance"),
|
||||
Nested("reg_saga_diversity", "saga_roster_diversity"),
|
||||
Nested("reg_saga_death", "saga_replace_on_death"),
|
||||
Nested("reg_saga_admit", "saga_admit_roles"),
|
||||
|
|
@ -1499,7 +1502,9 @@ internal static class HarnessScenarios
|
|||
Step("cf21z2", "director_run", wait: 0.55f),
|
||||
Step("cf21z3", "combat_maintain_focus"),
|
||||
Step("cf21z4", "assert", expect: "related_same"),
|
||||
Step("cf21z5", "assert", expect: "tip_same"),
|
||||
// A real local mob may grow during the aged hold. Preserve the locked pair, but
|
||||
// accept a semantic Duel → Skirmish/Battle/Mass escalation as a valid reframe.
|
||||
Step("cf21z5", "assert", expect: "tip_same_or_collective_escalation"),
|
||||
// First partner dies mid-scrap: lock stays; tip must not hop to a new Duel name.
|
||||
Step("cf21aa", "remember_related"),
|
||||
Step("cf21ab", "combat_kill_related"),
|
||||
|
|
@ -1612,8 +1617,10 @@ internal static class HarnessScenarios
|
|||
Step("cf31c6", "focus", asset: "human"),
|
||||
Step("cf31c7", "assert", expect: "focus_fighting", value: "false"),
|
||||
Step("cf31c8", "assert", expect: "related_fighting", value: "false"),
|
||||
// Re-stamp LastSeenAt cold after the last assert tick; age past MaxWatch → max_cap.
|
||||
// Re-stamp LastSeenAt after the last assert tick. The simulation can immediately
|
||||
// reacquire a target, so freeze the already-proven cold boundary before cleanup.
|
||||
Step("cf31c9", "combat_disengage_pair"),
|
||||
Step("cf31c9b", "interest_mark_combat_cold"),
|
||||
Step("cf31c10", "age_current", value: "70"),
|
||||
Step("cf31h", "director_run", wait: 1.0f),
|
||||
// Cold duel key must be gone. A later natural scrap may still say Duel - that is
|
||||
|
|
@ -3603,6 +3610,39 @@ internal static class HarnessScenarios
|
|||
};
|
||||
}
|
||||
|
||||
private static List<HarnessCommand> SagaRareAntagonistAcceptance()
|
||||
{
|
||||
return new List<HarnessCommand>
|
||||
{
|
||||
Step("sra0", "dismiss_windows"),
|
||||
Step("sra1", "wait_world"),
|
||||
Step("sra2", "set_setting", expect: "enabled", value: "true"),
|
||||
Step("sra3", "fast_timing", value: "true"),
|
||||
Step("sra4", "spawn", asset: "human", count: 3),
|
||||
Step("sra5", "spectator", value: "off"),
|
||||
Step("sra6", "spectator", value: "on"),
|
||||
Step("sra7", "pick_unit", asset: "human"),
|
||||
Step("sra8", "happiness_remember_partner"),
|
||||
Step("sra9", "pick_unit", asset: "human", value: "other"),
|
||||
Step("sra10", "focus", asset: "human"),
|
||||
Step("sra11", "interest_saga_clear"),
|
||||
Step("sra12", "saga_force_admit_focus"),
|
||||
Step("sra13", "happiness_bond_lovers"),
|
||||
Step("sra14", "assert", expect: "saga_rare_antagonist_policy"),
|
||||
Step("sra15", "assert", expect: "saga_cast_harm_evidence"),
|
||||
Step("sra16", "assert", expect: "camera_connection_attribution"),
|
||||
|
||||
// Exercise the real live world-scan path after the pure policy boundaries.
|
||||
Step("sra20", "interest_saga_clear"),
|
||||
Step("sra21", "spawn", asset: "human", count: 6),
|
||||
Step("sra22", "saga_force_admit_six"),
|
||||
Step("sra23", "assert", expect: "saga_roster_count", value: "5"),
|
||||
Step("sra24", "assert", expect: "saga_roster_repeat_stable"),
|
||||
Step("sra90", "fast_timing", value: "false"),
|
||||
Step("sra99", "snapshot")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Prefer toggle soft-biases a near-tie toward the Prefer'd MC.</summary>
|
||||
private static List<HarnessCommand> SagaPreferSoftBias()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -395,6 +395,8 @@ public static partial class InterestDirector
|
|||
_harnessCombatForcedCold = true;
|
||||
_current.ForceActive = false;
|
||||
_current.LastSeenAt = now - 60f;
|
||||
_current.Sticky.CombatColdSince =
|
||||
now - CombatHeadlineColdGraceSeconds - 0.1f;
|
||||
// Skip HasLiveCombatNear sticky path while proving post-fight cuts.
|
||||
if (string.Equals(_current.AssetId, "live_battle", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -535,4 +535,78 @@ public static class CameraEligibility
|
|||
+ " war=" + warModel.Kind;
|
||||
return pass;
|
||||
}
|
||||
|
||||
public static bool HarnessProbeConnectionAttribution(
|
||||
Actor anchorMc,
|
||||
Actor friendly,
|
||||
Actor hostile,
|
||||
out string detail)
|
||||
{
|
||||
long anchorId = EventFeedUtil.SafeId(anchorMc);
|
||||
long friendlyId = EventFeedUtil.SafeId(friendly);
|
||||
long hostileId = EventFeedUtil.SafeId(hostile);
|
||||
string anchorName = EventFeedUtil.SafeName(anchorMc);
|
||||
|
||||
bool friendlyResolved = TryResolveCastConnection(
|
||||
friendlyId,
|
||||
out long friendlyAnchor,
|
||||
out string friendlyKind,
|
||||
out string friendlyLine);
|
||||
bool hostileResolved = TryResolveCastConnection(
|
||||
hostileId,
|
||||
out long hostileAnchor,
|
||||
out string hostileKind,
|
||||
out string hostileLine);
|
||||
|
||||
LifeSagaSlot anchorSlot = LifeSagaRoster.Get(anchorId);
|
||||
var staleFaction = new InterestCandidate
|
||||
{
|
||||
Key = "harness:stale_faction",
|
||||
SubjectId = 991777001,
|
||||
AssetId = "faction_activity",
|
||||
Source = "stale_faction_probe",
|
||||
CityKey = anchorSlot?.CityKey ?? "shared-city",
|
||||
KingdomKey = anchorSlot?.KingdomKey ?? "shared-kingdom",
|
||||
EventStrength = 70f,
|
||||
Novelty = 1f,
|
||||
HasNarrativeFunction = true,
|
||||
NarrativeFunction = NarrativeFunction.Consequence,
|
||||
NarrativeDevelopmentId = "stale_faction_correlation"
|
||||
};
|
||||
CameraEligibilityModel staleModel = Classify(staleFaction);
|
||||
|
||||
bool friendlyCorrect = friendlyResolved
|
||||
&& friendlyAnchor == anchorId
|
||||
&& (friendlyKind == "partner"
|
||||
|| friendlyKind == "child"
|
||||
|| friendlyKind == "parent"
|
||||
|| friendlyKind == "friend"
|
||||
|| friendlyKind == "kin")
|
||||
&& ContainsName(friendlyLine, anchorName);
|
||||
bool hostileCorrect = hostileResolved
|
||||
&& hostileAnchor == anchorId
|
||||
&& hostileKind == "antagonist"
|
||||
&& ContainsName(hostileLine, anchorName);
|
||||
bool staleRejected = staleModel.Kind != CameraEligibilityKind.McThreadContext
|
||||
&& staleModel.Kind != CameraEligibilityKind.McCast
|
||||
&& staleModel.AnchorMcId == 0;
|
||||
detail = "friendly=" + friendlyResolved
|
||||
+ ":" + friendlyAnchor
|
||||
+ ":" + friendlyKind
|
||||
+ ":'" + friendlyLine
|
||||
+ "' hostile=" + hostileResolved
|
||||
+ ":" + hostileAnchor
|
||||
+ ":" + hostileKind
|
||||
+ ":'" + hostileLine
|
||||
+ "' stale=" + staleModel.Kind
|
||||
+ ":" + staleModel.AnchorMcId;
|
||||
return friendlyCorrect && hostileCorrect && staleRejected;
|
||||
}
|
||||
|
||||
private static bool ContainsName(string line, string name)
|
||||
{
|
||||
return !string.IsNullOrEmpty(line)
|
||||
&& !string.IsNullOrEmpty(name)
|
||||
&& line.IndexOf(name, StringComparison.Ordinal) >= 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -646,7 +646,12 @@ public static class LifeSagaMemory
|
|||
}
|
||||
}
|
||||
|
||||
public static void EarnRival(Actor subject, Actor rival, string evidence, string provenance)
|
||||
public static void EarnRival(
|
||||
Actor subject,
|
||||
Actor rival,
|
||||
string evidence,
|
||||
string provenance,
|
||||
float strength = 85f)
|
||||
{
|
||||
if (subject == null || rival == null)
|
||||
{
|
||||
|
|
@ -660,7 +665,7 @@ public static class LifeSagaMemory
|
|||
subject,
|
||||
rival,
|
||||
correlationKey: "rival:" + PairKey(subjectId, rivalId),
|
||||
strength: 85f,
|
||||
strength: Mathf.Clamp(strength, 85f, 100f),
|
||||
provenance: provenance,
|
||||
note: evidence ?? "");
|
||||
if (fact != null && LifeSagaRoster.IsMc(subjectId) && LifeSagaRoster.IsMc(rivalId))
|
||||
|
|
@ -1624,6 +1629,27 @@ public static class LifeSagaMemory
|
|||
}
|
||||
|
||||
long killerId = EventFeedUtil.SafeId(killer);
|
||||
long victimId = EventFeedUtil.SafeId(victim);
|
||||
|
||||
// A confirmed kill of an exact MC cast member is stronger antagonist evidence than
|
||||
// ordinary repeated combat. Resolve against the bounded five-character roster and
|
||||
// never infer this edge from a shared city, kingdom, or faction.
|
||||
if (LifeSagaRoster.TryGetFriendlyMcAnchor(victimId, out long anchorMcId)
|
||||
&& anchorMcId != killerId)
|
||||
{
|
||||
Actor anchor = EventFeedUtil.FindAliveById(anchorMcId);
|
||||
if (anchor != null)
|
||||
{
|
||||
EarnRival(
|
||||
anchor,
|
||||
killer,
|
||||
"mc_cast_slain:" + victimId,
|
||||
"mc_cast_harm",
|
||||
strength: 95f);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If victim is close kin of someone on roster/memory, or killer killed kin of a remembered bond.
|
||||
LifeSagaLifeMemory killerLife = Get(killerId);
|
||||
if (killerLife == null)
|
||||
|
|
@ -1631,7 +1657,6 @@ public static class LifeSagaMemory
|
|||
return;
|
||||
}
|
||||
|
||||
long victimId = EventFeedUtil.SafeId(victim);
|
||||
for (int i = 0; i < killerLife.Facts.Count; i++)
|
||||
{
|
||||
LifeSagaFact f = killerLife.Facts[i];
|
||||
|
|
@ -1661,6 +1686,61 @@ public static class LifeSagaMemory
|
|||
_ = victimId;
|
||||
}
|
||||
|
||||
public static bool HarnessProbeMcCastHarm(
|
||||
Actor anchorMc,
|
||||
Actor castMember,
|
||||
Actor attacker,
|
||||
out string detail)
|
||||
{
|
||||
long anchorId = EventFeedUtil.SafeId(anchorMc);
|
||||
long castId = EventFeedUtil.SafeId(castMember);
|
||||
long attackerId = EventFeedUtil.SafeId(attacker);
|
||||
bool exactCast = anchorId != 0
|
||||
&& castId != 0
|
||||
&& attackerId != 0
|
||||
&& LifeSagaRoster.TryGetFriendlyMcAnchor(
|
||||
castId,
|
||||
out long resolvedAnchor)
|
||||
&& resolvedAnchor == anchorId;
|
||||
if (exactCast)
|
||||
{
|
||||
TryMarkKinKillRival(attacker, castMember);
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
float strength = 0f;
|
||||
string note = "";
|
||||
var facts = FactsFor(anchorId);
|
||||
for (int i = 0; i < facts.Count; i++)
|
||||
{
|
||||
LifeSagaFact fact = facts[i];
|
||||
if (fact == null
|
||||
|| fact.Kind != LifeSagaFactKind.RivalEarned
|
||||
|| fact.OtherId != attackerId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
found = IsCredibleRival(fact);
|
||||
strength = fact.Strength;
|
||||
note = fact.Note ?? "";
|
||||
break;
|
||||
}
|
||||
|
||||
bool strongerThanRematch = strength > 85f;
|
||||
detail = "exactCast=" + exactCast
|
||||
+ " anchor=" + anchorId
|
||||
+ " cast=" + castId
|
||||
+ " attacker=" + attackerId
|
||||
+ " found=" + found
|
||||
+ " strength=" + strength.ToString("0.##")
|
||||
+ " note=" + note;
|
||||
return exactCast
|
||||
&& found
|
||||
&& strongerThanRematch
|
||||
&& note.StartsWith("mc_cast_slain:", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool IsCloseKin(Actor actor, long relativeId)
|
||||
{
|
||||
if (actor == null || relativeId == 0)
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ public static class LifeSagaRoster
|
|||
private const float SpecialCreatureStandingBonus = 2f;
|
||||
private const float RareCreatureStandingBonus = 3f;
|
||||
private const float RecurringAntagonistStandingBonus = 7f;
|
||||
private const float MaxRareAntagonistStandingBonus = 12f;
|
||||
private const float MaxRareAntagonistPriorityBonus = 225f;
|
||||
private const float EstablishedMcSeconds = 120f;
|
||||
private const float EstablishedMcMarginBonus = 3f;
|
||||
private const float UndevelopedChallengerMarginBonus = 2f;
|
||||
|
|
@ -311,6 +313,7 @@ public static class LifeSagaRoster
|
|||
|
||||
_dirty = true;
|
||||
_nextScanAt = now + 2.5f;
|
||||
RebuildMcOrbitCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -564,6 +567,31 @@ public static class LifeSagaRoster
|
|||
return McOrbitCache.Contains(unitId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve a living partner, friend, parent, or child to the exact MC whose cast they
|
||||
/// belong to. Bounded to the five roster slots; antagonists are deliberately excluded.
|
||||
/// </summary>
|
||||
public static bool TryGetFriendlyMcAnchor(long unitId, out long anchorMcId)
|
||||
{
|
||||
anchorMcId = 0;
|
||||
if (unitId == 0 || IsMc(unitId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < Slots.Count; i++)
|
||||
{
|
||||
LifeSagaSlot slot = Slots[i];
|
||||
if (slot != null && IsInFriendlyMcOrbit(slot.UnitId, unitId))
|
||||
{
|
||||
anchorMcId = slot.UnitId;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Roster MCs touched by the tip for rail involved chrome.
|
||||
/// Bounded to subject/related/follow/pair/short-arc cast - not mass ParticipantIds.
|
||||
|
|
@ -652,6 +680,28 @@ public static class LifeSagaRoster
|
|||
return false;
|
||||
}
|
||||
|
||||
if (IsInFriendlyMcOrbit(mcId, otherId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (LifeSagaMemory.TryGetEarnedRival(mcId, out LifeSagaFact rival)
|
||||
&& rival != null
|
||||
&& rival.OtherId == otherId)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsInFriendlyMcOrbit(long mcId, long otherId)
|
||||
{
|
||||
if (mcId == 0 || otherId == 0 || mcId == otherId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Actor mc = EventFeedUtil.FindAliveById(mcId);
|
||||
if (mc != null)
|
||||
{
|
||||
|
|
@ -691,13 +741,6 @@ public static class LifeSagaRoster
|
|||
}
|
||||
}
|
||||
|
||||
if (LifeSagaMemory.TryGetEarnedRival(mcId, out LifeSagaFact rival)
|
||||
&& rival != null
|
||||
&& rival.OtherId == otherId)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -1427,9 +1470,7 @@ public static class LifeSagaRoster
|
|||
}
|
||||
|
||||
LifeSagaSlot newcomer = kept[worstNewcomer];
|
||||
if (IsEstablishedMc(incumbent, now)
|
||||
&& IsOrdinaryChallenger(newcomer)
|
||||
&& !HasMeaningfulNarrativeEvidence(newcomer))
|
||||
if (ShouldProtectEstablishedIncumbent(incumbent, newcomer, now))
|
||||
{
|
||||
kept[worstNewcomer] = incumbent;
|
||||
continue;
|
||||
|
|
@ -1484,11 +1525,12 @@ public static class LifeSagaRoster
|
|||
&& challenger.Chapters.Count == 0
|
||||
&& challenger.Heat < 4f
|
||||
&& !challenger.IsRecurringAntagonist
|
||||
&& !challenger.IsSpecialCreature
|
||||
&& !challenger.IsKing
|
||||
&& !challenger.IsLeader
|
||||
&& !challenger.IsClanChief
|
||||
&& !challenger.IsAlpha)
|
||||
&& !challenger.IsAlpha
|
||||
&& (IsOrdinaryChallenger(challenger)
|
||||
|| IsIdentityOnlySpecialChallenger(challenger)))
|
||||
{
|
||||
margin += UndevelopedChallengerMarginBonus;
|
||||
}
|
||||
|
|
@ -1519,6 +1561,38 @@ public static class LifeSagaRoster
|
|||
&& !slot.IsRecurringAntagonist;
|
||||
}
|
||||
|
||||
private static bool IsIdentityOnlySpecialChallenger(LifeSagaSlot slot)
|
||||
{
|
||||
if (slot == null || !slot.IsSpecialCreature || slot.IsRecurringAntagonist)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !slot.GameFavorite
|
||||
&& !slot.Prefer
|
||||
&& !slot.IsKing
|
||||
&& !slot.IsLeader
|
||||
&& !slot.IsClanChief
|
||||
&& !slot.IsArmyCaptain
|
||||
&& !slot.IsPlotAuthor
|
||||
&& !slot.IsFounder
|
||||
&& (slot.AdmissionReason == LifeSagaAdmissionReason.None
|
||||
|| slot.AdmissionReason == LifeSagaAdmissionReason.Singleton
|
||||
|| slot.AdmissionReason == LifeSagaAdmissionReason.RareCreature
|
||||
|| slot.AdmissionReason == LifeSagaAdmissionReason.SpecialCreature);
|
||||
}
|
||||
|
||||
private static bool ShouldProtectEstablishedIncumbent(
|
||||
LifeSagaSlot incumbent,
|
||||
LifeSagaSlot challenger,
|
||||
float now)
|
||||
{
|
||||
return IsEstablishedMc(incumbent, now)
|
||||
&& (IsOrdinaryChallenger(challenger)
|
||||
|| IsIdentityOnlySpecialChallenger(challenger))
|
||||
&& !HasMeaningfulNarrativeEvidence(challenger);
|
||||
}
|
||||
|
||||
private static bool HasMeaningfulNarrativeEvidence(LifeSagaSlot slot)
|
||||
{
|
||||
if (slot == null)
|
||||
|
|
@ -1553,11 +1627,100 @@ public static class LifeSagaRoster
|
|||
bool developedAllowed = HasMeaningfulNarrativeEvidence(ordinary);
|
||||
ordinary.Heat = 0f;
|
||||
ordinary.IsSpecialCreature = true;
|
||||
bool specialAllowed = !IsOrdinaryChallenger(ordinary);
|
||||
ordinary.IsRareCreature = true;
|
||||
ordinary.IsSingleton = true;
|
||||
ordinary.SpeciesPopulation = 1;
|
||||
ordinary.AdmissionReason = LifeSagaAdmissionReason.Singleton;
|
||||
bool identityBlocked = ShouldProtectEstablishedIncumbent(incumbent, ordinary, now);
|
||||
ordinary.Heat = 4f;
|
||||
bool developedSpecialAllowed =
|
||||
!ShouldProtectEstablishedIncumbent(incumbent, ordinary, now);
|
||||
ordinary.Heat = 0f;
|
||||
ordinary.IsRecurringAntagonist = true;
|
||||
ordinary.AdmissionReason = LifeSagaAdmissionReason.RecurringAntagonist;
|
||||
bool antagonistAllowed =
|
||||
!ShouldProtectEstablishedIncumbent(incumbent, ordinary, now);
|
||||
detail = "ordinaryBlocked=" + ordinaryBlocked
|
||||
+ " developedAllowed=" + developedAllowed
|
||||
+ " specialAllowed=" + specialAllowed;
|
||||
return ordinaryBlocked && developedAllowed && specialAllowed;
|
||||
+ " identityBlocked=" + identityBlocked
|
||||
+ " developedSpecialAllowed=" + developedSpecialAllowed
|
||||
+ " antagonistAllowed=" + antagonistAllowed;
|
||||
return ordinaryBlocked
|
||||
&& developedAllowed
|
||||
&& identityBlocked
|
||||
&& developedSpecialAllowed
|
||||
&& antagonistAllowed;
|
||||
}
|
||||
|
||||
public static bool HarnessProbeRareAntagonistPolicy(out string detail)
|
||||
{
|
||||
float singleton = SpecialIdentityStandingBonus(1);
|
||||
float largerRare = SpecialIdentityStandingBonus(RareSpeciesMaxPopulation);
|
||||
float common = SpecialIdentityStandingBonus(RareSpeciesMaxPopulation + 1);
|
||||
var combined = new LifeSagaSlot
|
||||
{
|
||||
IsSpecialCreature = true,
|
||||
IsRareCreature = true,
|
||||
IsSingleton = true,
|
||||
SpeciesPopulation = 1,
|
||||
IsRecurringAntagonist = true
|
||||
};
|
||||
float combinedBonus = RareAntagonistStandingBonus(combined);
|
||||
float combinedPriority = Mathf.Min(
|
||||
MaxRareAntagonistPriorityBonus,
|
||||
SpecialIdentityPriorityBonus(special: true, population: 1) + 200f);
|
||||
bool scales = singleton > largerRare && largerRare > common;
|
||||
bool capped = combinedBonus <= MaxRareAntagonistStandingBonus + 0.001f
|
||||
&& Mathf.Approximately(
|
||||
combinedBonus,
|
||||
MaxRareAntagonistStandingBonus)
|
||||
&& Mathf.Approximately(
|
||||
combinedPriority,
|
||||
MaxRareAntagonistPriorityBonus);
|
||||
bool replacement = HarnessProbeReplacementEvidence(out string replacementDetail);
|
||||
detail = "identity=" + singleton.ToString("0.##")
|
||||
+ "/" + largerRare.ToString("0.##")
|
||||
+ "/" + common.ToString("0.##")
|
||||
+ " combined=" + combinedBonus.ToString("0.##")
|
||||
+ " cap=" + MaxRareAntagonistStandingBonus.ToString("0.##")
|
||||
+ " priority=" + combinedPriority.ToString("0.##")
|
||||
+ "/" + MaxRareAntagonistPriorityBonus.ToString("0.##")
|
||||
+ " replacement={" + replacementDetail + "}";
|
||||
return scales && capped && replacement;
|
||||
}
|
||||
|
||||
public static bool HarnessProbeRepeatedScanStability(out string detail)
|
||||
{
|
||||
HarnessWorldScan();
|
||||
string first = RosterFingerprint();
|
||||
HarnessWorldScan();
|
||||
string second = RosterFingerprint();
|
||||
HarnessWorldScan();
|
||||
string third = RosterFingerprint();
|
||||
bool pass = Slots.Count == Cap
|
||||
&& string.Equals(first, second, StringComparison.Ordinal)
|
||||
&& string.Equals(second, third, StringComparison.Ordinal);
|
||||
detail = "count=" + Slots.Count
|
||||
+ " first=" + first
|
||||
+ " second=" + second
|
||||
+ " third=" + third;
|
||||
return pass;
|
||||
}
|
||||
|
||||
private static string RosterFingerprint()
|
||||
{
|
||||
var value = new System.Text.StringBuilder(Cap * 16);
|
||||
for (int i = 0; i < Slots.Count; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
value.Append(',');
|
||||
}
|
||||
|
||||
value.Append(Slots[i]?.UnitId ?? 0);
|
||||
}
|
||||
|
||||
return value.ToString();
|
||||
}
|
||||
|
||||
private static int CompareSlots(LifeSagaSlot a, LifeSagaSlot b, float now, float dull)
|
||||
|
|
@ -1870,34 +2033,48 @@ public static class LifeSagaRoster
|
|||
}
|
||||
|
||||
if (IsActivePlotAuthor(actor)
|
||||
|| IsFounder(actor)
|
||||
|| (IsSpecialCreature(actor) && IsSpeciesSingleton(actor)))
|
||||
|| IsFounder(actor))
|
||||
{
|
||||
p += 80f;
|
||||
}
|
||||
|
||||
if (IsSpecialCreature(actor))
|
||||
{
|
||||
p += 45f;
|
||||
}
|
||||
|
||||
int rarePopulation = GetSpeciesPopulation(actor);
|
||||
if (IsSpecialCreature(actor)
|
||||
&& rarePopulation > 1
|
||||
&& rarePopulation <= RareSpeciesMaxPopulation)
|
||||
{
|
||||
p += 25f * (RareSpeciesMaxPopulation + 1 - rarePopulation)
|
||||
/ RareSpeciesMaxPopulation;
|
||||
}
|
||||
|
||||
float rareAntagonistBonus = SpecialIdentityPriorityBonus(
|
||||
IsSpecialCreature(actor),
|
||||
rarePopulation);
|
||||
if (TryGetRecurringAntagonist(actor, out _, out LifeSagaFact antagonistEvidence))
|
||||
{
|
||||
p += 150f + Mathf.Min(50f, antagonistEvidence?.Strength ?? 0f);
|
||||
rareAntagonistBonus +=
|
||||
150f + Mathf.Min(50f, antagonistEvidence?.Strength ?? 0f);
|
||||
}
|
||||
|
||||
p += Mathf.Min(MaxRareAntagonistPriorityBonus, rareAntagonistBonus);
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
private static float SpecialIdentityPriorityBonus(bool special, int population)
|
||||
{
|
||||
if (!special)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
float bonus = 45f;
|
||||
if (population == 1)
|
||||
{
|
||||
return bonus + 80f;
|
||||
}
|
||||
|
||||
if (population > 1 && population <= RareSpeciesMaxPopulation)
|
||||
{
|
||||
bonus += 25f * (RareSpeciesMaxPopulation + 1 - population)
|
||||
/ RareSpeciesMaxPopulation;
|
||||
}
|
||||
|
||||
return bonus;
|
||||
}
|
||||
|
||||
public static bool TryGetRecurringAntagonist(
|
||||
Actor actor,
|
||||
out long anchorMcId,
|
||||
|
|
@ -2559,28 +2736,11 @@ public static class LifeSagaRoster
|
|||
score += 7f;
|
||||
}
|
||||
|
||||
if (s.IsSingleton && s.IsSpecialCreature)
|
||||
{
|
||||
score += 6f;
|
||||
}
|
||||
else if (s.IsSingleton)
|
||||
if (s.IsSingleton && !s.IsSpecialCreature)
|
||||
{
|
||||
score += 1f;
|
||||
}
|
||||
else if (s.IsRareCreature)
|
||||
{
|
||||
score += RareCreatureStandingBonus;
|
||||
}
|
||||
|
||||
if (s.IsSpecialCreature)
|
||||
{
|
||||
score += SpecialCreatureStandingBonus;
|
||||
}
|
||||
|
||||
if (s.IsRecurringAntagonist)
|
||||
{
|
||||
score += RecurringAntagonistStandingBonus;
|
||||
}
|
||||
score += RareAntagonistStandingBonus(s);
|
||||
|
||||
if (s.IsFounder)
|
||||
{
|
||||
|
|
@ -2598,6 +2758,46 @@ public static class LifeSagaRoster
|
|||
return score;
|
||||
}
|
||||
|
||||
private static float RareAntagonistStandingBonus(LifeSagaSlot slot)
|
||||
{
|
||||
if (slot == null)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
float bonus = SpecialIdentityStandingBonus(
|
||||
slot.IsSpecialCreature ? slot.SpeciesPopulation : 0);
|
||||
if (slot.IsRecurringAntagonist)
|
||||
{
|
||||
bonus += RecurringAntagonistStandingBonus;
|
||||
}
|
||||
|
||||
return Mathf.Min(MaxRareAntagonistStandingBonus, bonus);
|
||||
}
|
||||
|
||||
private static float SpecialIdentityStandingBonus(int population)
|
||||
{
|
||||
if (population <= 0)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
float bonus = SpecialCreatureStandingBonus;
|
||||
if (population == 1)
|
||||
{
|
||||
return bonus + 6f;
|
||||
}
|
||||
|
||||
if (population <= RareSpeciesMaxPopulation)
|
||||
{
|
||||
bonus += RareCreatureStandingBonus
|
||||
* (RareSpeciesMaxPopulation + 1 - population)
|
||||
/ RareSpeciesMaxPopulation;
|
||||
}
|
||||
|
||||
return bonus;
|
||||
}
|
||||
|
||||
private static void NoteRisingRoles(
|
||||
Actor actor,
|
||||
bool wasKing,
|
||||
|
|
@ -2724,10 +2924,10 @@ public static class LifeSagaRoster
|
|||
if (s.IsArmyCaptain) return LifeSagaAdmissionReason.ArmyCaptain;
|
||||
if (s.IsFounder) return LifeSagaAdmissionReason.Founder;
|
||||
if (s.IsPlotAuthor) return LifeSagaAdmissionReason.PlotAuthor;
|
||||
if (s.IsSingleton && s.IsSpecialCreature) return LifeSagaAdmissionReason.Singleton;
|
||||
if (s.IsRecurringAntagonist) return LifeSagaAdmissionReason.RecurringAntagonist;
|
||||
if (s.IsSpecialCreature) return LifeSagaAdmissionReason.SpecialCreature;
|
||||
if (s.IsSingleton && s.IsSpecialCreature) return LifeSagaAdmissionReason.Singleton;
|
||||
if (s.IsRareCreature) return LifeSagaAdmissionReason.RareCreature;
|
||||
if (s.IsSpecialCreature) return LifeSagaAdmissionReason.SpecialCreature;
|
||||
if (s.Kills > 0) return LifeSagaAdmissionReason.NotableKills;
|
||||
if (s.Renown > 0f) return LifeSagaAdmissionReason.NotableRenown;
|
||||
return LifeSagaAdmissionReason.NotableLife;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Rare Creatures, Antagonists, and Camera Cleanup
|
||||
|
||||
Status: **Implemented; targeted and performance gates pass. Full regression is blocked by the
|
||||
legacy combat-presentability fixture's nondeterministic pair-to-mass reframe assertions.**
|
||||
Status: **Complete. Implementation, deterministic regression, scheduler health, and two organic
|
||||
soak windows pass.**
|
||||
|
||||
## Goal
|
||||
|
||||
|
|
@ -83,20 +83,20 @@ Exit: quiet life remains visible without crowding out story development.
|
|||
## Phase 7 — Deterministic coverage
|
||||
|
||||
- [x] Singleton dragon enters through the real admission path.
|
||||
- [ ] A larger dragon population receives a smaller bonus.
|
||||
- [ ] Rare identity alone does not evict a developed MC.
|
||||
- [x] A larger dragon population receives a smaller bonus.
|
||||
- [x] Rare identity alone does not evict a developed MC.
|
||||
- [x] Repeated conflict creates an antagonist challenger.
|
||||
- [x] One kill/fight cycle does not create an antagonist.
|
||||
- [ ] Harm to MC cast creates stronger evidence.
|
||||
- [ ] Combined rarity/antagonist bonuses respect their cap.
|
||||
- [x] Harm to MC cast creates stronger evidence.
|
||||
- [x] Combined rarity/antagonist bonuses respect their cap.
|
||||
- [x] Routine unrelated hatch is rejected; an explicit singleton-special exception is accepted.
|
||||
- [ ] Friendly/hostile dossier labels name the correct anchor.
|
||||
- [ ] Broad or stale faction correlation fails thread attribution.
|
||||
- [ ] The five-character roster stays stable across repeated scans.
|
||||
- [x] Friendly/hostile dossier labels name the correct anchor.
|
||||
- [x] Broad or stale faction correlation fails thread attribution.
|
||||
- [x] The five-character roster stays stable across repeated scans.
|
||||
|
||||
## Phase 8 — Performance and organic soak gate
|
||||
|
||||
- [ ] Run the full deterministic regression suite without the legacy combat fixture failure.
|
||||
- [x] Run the full deterministic regression suite without the legacy combat fixture failure.
|
||||
- [x] Run scheduler health with `hitch_probe` and an active organic soak audit.
|
||||
- [x] Compare maximum frame, director, discovery, caption, and scheduler slices
|
||||
with the pre-change baseline.
|
||||
|
|
@ -121,7 +121,7 @@ Exit: editorial acceptance criteria pass with no material performance regression
|
|||
standing; require a chapter, sufficient story heat, or meaningful scheduler potential.
|
||||
- [x] Stop a fifth consecutive ordinary combat cut while allowing kills, resolutions,
|
||||
consequences, and world-critical combat through.
|
||||
- [ ] Confirm reduced roster churn, no repeated lover-search cuts, and a maximum ordinary combat
|
||||
- [x] Confirm reduced roster churn, no repeated lover-search cuts, and a maximum ordinary combat
|
||||
run of four in the next long organic soak.
|
||||
|
||||
Evidence prompting this phase: the first post-feature long soak successfully promoted evil mage
|
||||
|
|
@ -133,3 +133,31 @@ lover-search cuts for Yaaahaona and 13 direct MCs in the developed-world portion
|
|||
Final deterministic probes confirm the fifteen-minute lover gate and ordinary-challenger evidence
|
||||
gate. A mature-save instrumented run settled from one 28.5 ms load/warm-up frame to 17.6 ms and
|
||||
then 16.7 ms windows; scheduler cost was 0.24 ms with 268 developments and 597 character states.
|
||||
|
||||
## Completion evidence — 2026-07-25
|
||||
|
||||
- `saga_rare_antagonist_acceptance` passed all five assertions. Population standing scaled from
|
||||
`8` for a singleton to `2.6` at population five and `2` when common. Combined special/antagonist
|
||||
standing capped at `12`, admission priority capped at `225`, identity-only displacement of a
|
||||
developed incumbent was rejected, and developed-special/recurring-antagonist lanes remained
|
||||
eligible.
|
||||
- Exact MC-cast harm produced rival evidence with strength `95` and the exact MC anchor.
|
||||
Friendly and hostile dossier labels named that same anchor; a city/kingdom-only correlation
|
||||
remained `DiscoveryOnly` with no anchor.
|
||||
- Three consecutive live world scans retained the same ordered five-member fingerprint. The
|
||||
legacy combat-summary probe passed three independent runs, and camera presentability passed
|
||||
five independent runs after accepting a valid duel-to-collective escalation while preserving
|
||||
the principal pair.
|
||||
- The isolated release regression passed `21/21` scenarios. Scheduler health passed at `0.71 ms`
|
||||
with `18/128` candidates.
|
||||
- Two uninterrupted five-minute organic windows passed the log audit. Across 45 cuts there were
|
||||
zero focus/name errors, placeholder tips, identity-filter leaks, unpresentable cuts, object-fall
|
||||
cuts, or fighting/sleeping mismatches. Combat share was `24.1%` then `0%`; the maximum combat run
|
||||
was `3`; no character received a repeated lover-search cut.
|
||||
- The established-roster window retained three of five members while the world lost four living
|
||||
actors; the two replacement slots were filled by stronger role-backed candidates and did not
|
||||
flip across repeated scans. Normal director slices were about `2–9 ms`, scans stayed at or below
|
||||
`2.4 ms`, scheduler slices at or below `0.71 ms`, discovery at or below `2.5 ms`, and caption work
|
||||
at or below `7.1 ms`. One `28.4 ms` director cutover outlier occurred. Whole-frame stalls of
|
||||
`401.2 ms` and `162.3 ms` contained only roughly `2–4 ms` of attributed mod work and therefore
|
||||
were not caused by roster/scheduler scanning.
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ REGRESSION_SCENARIOS=(
|
|||
world_log
|
||||
camera_presentability
|
||||
narrative_presentation_coverage
|
||||
saga_rare_antagonist_acceptance
|
||||
event_suite
|
||||
chronicle_smoke
|
||||
activity_idle_smoke
|
||||
|
|
|
|||
Loading…
Reference in a new issue