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(
|
private static void ParseInterestInjectOptions(
|
||||||
string raw,
|
string raw,
|
||||||
out bool force,
|
out bool force,
|
||||||
|
|
@ -10665,6 +10717,18 @@ public static class AgentHarness
|
||||||
pass = CameraEligibility.HarnessProbeExceptionalPolicy(out detail);
|
pass = CameraEligibility.HarnessProbeExceptionalPolicy(out detail);
|
||||||
break;
|
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":
|
case "saga_civilized_no_pack_language":
|
||||||
{
|
{
|
||||||
Actor actor = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : _lastSpawned;
|
Actor actor = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : _lastSpawned;
|
||||||
|
|
@ -11277,6 +11341,28 @@ public static class AgentHarness
|
||||||
pass = LifeSagaRoster.HarnessProbeReplacementEvidence(out detail);
|
pass = LifeSagaRoster.HarnessProbeReplacementEvidence(out detail);
|
||||||
break;
|
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":
|
case "saga_role_tip":
|
||||||
{
|
{
|
||||||
string have = SampleRailTip();
|
string have = SampleRailTip();
|
||||||
|
|
@ -11624,6 +11710,40 @@ public static class AgentHarness
|
||||||
detail = $"remembered='{_rememberedTip}' now='{nowTip}'";
|
detail = $"remembered='{_rememberedTip}' now='{nowTip}'";
|
||||||
break;
|
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 "related_same":
|
||||||
case "pair_partner_same":
|
case "pair_partner_same":
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -217,6 +217,8 @@ internal static class HarnessScenarios
|
||||||
return SagaRosterDiversity();
|
return SagaRosterDiversity();
|
||||||
case "saga_roster_cap_five":
|
case "saga_roster_cap_five":
|
||||||
return SagaRosterCapFive();
|
return SagaRosterCapFive();
|
||||||
|
case "saga_rare_antagonist_acceptance":
|
||||||
|
return SagaRareAntagonistAcceptance();
|
||||||
case "saga_connection_dossier":
|
case "saga_connection_dossier":
|
||||||
return SagaConnectionDossier();
|
return SagaConnectionDossier();
|
||||||
case "saga_camera_prefers_mc":
|
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_legacy_combat", "saga_legacy_combat_summary"),
|
||||||
Nested("reg_saga_session_reset", "saga_session_reset"),
|
Nested("reg_saga_session_reset", "saga_session_reset"),
|
||||||
Nested("reg_saga_hover_pause", "saga_hover_read_pause"),
|
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_diversity", "saga_roster_diversity"),
|
||||||
Nested("reg_saga_death", "saga_replace_on_death"),
|
Nested("reg_saga_death", "saga_replace_on_death"),
|
||||||
Nested("reg_saga_admit", "saga_admit_roles"),
|
Nested("reg_saga_admit", "saga_admit_roles"),
|
||||||
|
|
@ -1499,7 +1502,9 @@ internal static class HarnessScenarios
|
||||||
Step("cf21z2", "director_run", wait: 0.55f),
|
Step("cf21z2", "director_run", wait: 0.55f),
|
||||||
Step("cf21z3", "combat_maintain_focus"),
|
Step("cf21z3", "combat_maintain_focus"),
|
||||||
Step("cf21z4", "assert", expect: "related_same"),
|
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.
|
// First partner dies mid-scrap: lock stays; tip must not hop to a new Duel name.
|
||||||
Step("cf21aa", "remember_related"),
|
Step("cf21aa", "remember_related"),
|
||||||
Step("cf21ab", "combat_kill_related"),
|
Step("cf21ab", "combat_kill_related"),
|
||||||
|
|
@ -1612,8 +1617,10 @@ internal static class HarnessScenarios
|
||||||
Step("cf31c6", "focus", asset: "human"),
|
Step("cf31c6", "focus", asset: "human"),
|
||||||
Step("cf31c7", "assert", expect: "focus_fighting", value: "false"),
|
Step("cf31c7", "assert", expect: "focus_fighting", value: "false"),
|
||||||
Step("cf31c8", "assert", expect: "related_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("cf31c9", "combat_disengage_pair"),
|
||||||
|
Step("cf31c9b", "interest_mark_combat_cold"),
|
||||||
Step("cf31c10", "age_current", value: "70"),
|
Step("cf31c10", "age_current", value: "70"),
|
||||||
Step("cf31h", "director_run", wait: 1.0f),
|
Step("cf31h", "director_run", wait: 1.0f),
|
||||||
// Cold duel key must be gone. A later natural scrap may still say Duel - that is
|
// 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>
|
/// <summary>Prefer toggle soft-biases a near-tie toward the Prefer'd MC.</summary>
|
||||||
private static List<HarnessCommand> SagaPreferSoftBias()
|
private static List<HarnessCommand> SagaPreferSoftBias()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -395,6 +395,8 @@ public static partial class InterestDirector
|
||||||
_harnessCombatForcedCold = true;
|
_harnessCombatForcedCold = true;
|
||||||
_current.ForceActive = false;
|
_current.ForceActive = false;
|
||||||
_current.LastSeenAt = now - 60f;
|
_current.LastSeenAt = now - 60f;
|
||||||
|
_current.Sticky.CombatColdSince =
|
||||||
|
now - CombatHeadlineColdGraceSeconds - 0.1f;
|
||||||
// Skip HasLiveCombatNear sticky path while proving post-fight cuts.
|
// Skip HasLiveCombatNear sticky path while proving post-fight cuts.
|
||||||
if (string.Equals(_current.AssetId, "live_battle", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(_current.AssetId, "live_battle", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -535,4 +535,78 @@ public static class CameraEligibility
|
||||||
+ " war=" + warModel.Kind;
|
+ " war=" + warModel.Kind;
|
||||||
return pass;
|
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)
|
if (subject == null || rival == null)
|
||||||
{
|
{
|
||||||
|
|
@ -660,7 +665,7 @@ public static class LifeSagaMemory
|
||||||
subject,
|
subject,
|
||||||
rival,
|
rival,
|
||||||
correlationKey: "rival:" + PairKey(subjectId, rivalId),
|
correlationKey: "rival:" + PairKey(subjectId, rivalId),
|
||||||
strength: 85f,
|
strength: Mathf.Clamp(strength, 85f, 100f),
|
||||||
provenance: provenance,
|
provenance: provenance,
|
||||||
note: evidence ?? "");
|
note: evidence ?? "");
|
||||||
if (fact != null && LifeSagaRoster.IsMc(subjectId) && LifeSagaRoster.IsMc(rivalId))
|
if (fact != null && LifeSagaRoster.IsMc(subjectId) && LifeSagaRoster.IsMc(rivalId))
|
||||||
|
|
@ -1624,6 +1629,27 @@ public static class LifeSagaMemory
|
||||||
}
|
}
|
||||||
|
|
||||||
long killerId = EventFeedUtil.SafeId(killer);
|
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.
|
// If victim is close kin of someone on roster/memory, or killer killed kin of a remembered bond.
|
||||||
LifeSagaLifeMemory killerLife = Get(killerId);
|
LifeSagaLifeMemory killerLife = Get(killerId);
|
||||||
if (killerLife == null)
|
if (killerLife == null)
|
||||||
|
|
@ -1631,7 +1657,6 @@ public static class LifeSagaMemory
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
long victimId = EventFeedUtil.SafeId(victim);
|
|
||||||
for (int i = 0; i < killerLife.Facts.Count; i++)
|
for (int i = 0; i < killerLife.Facts.Count; i++)
|
||||||
{
|
{
|
||||||
LifeSagaFact f = killerLife.Facts[i];
|
LifeSagaFact f = killerLife.Facts[i];
|
||||||
|
|
@ -1661,6 +1686,61 @@ public static class LifeSagaMemory
|
||||||
_ = victimId;
|
_ = 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)
|
private static bool IsCloseKin(Actor actor, long relativeId)
|
||||||
{
|
{
|
||||||
if (actor == null || relativeId == 0)
|
if (actor == null || relativeId == 0)
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,8 @@ public static class LifeSagaRoster
|
||||||
private const float SpecialCreatureStandingBonus = 2f;
|
private const float SpecialCreatureStandingBonus = 2f;
|
||||||
private const float RareCreatureStandingBonus = 3f;
|
private const float RareCreatureStandingBonus = 3f;
|
||||||
private const float RecurringAntagonistStandingBonus = 7f;
|
private const float RecurringAntagonistStandingBonus = 7f;
|
||||||
|
private const float MaxRareAntagonistStandingBonus = 12f;
|
||||||
|
private const float MaxRareAntagonistPriorityBonus = 225f;
|
||||||
private const float EstablishedMcSeconds = 120f;
|
private const float EstablishedMcSeconds = 120f;
|
||||||
private const float EstablishedMcMarginBonus = 3f;
|
private const float EstablishedMcMarginBonus = 3f;
|
||||||
private const float UndevelopedChallengerMarginBonus = 2f;
|
private const float UndevelopedChallengerMarginBonus = 2f;
|
||||||
|
|
@ -311,6 +313,7 @@ public static class LifeSagaRoster
|
||||||
|
|
||||||
_dirty = true;
|
_dirty = true;
|
||||||
_nextScanAt = now + 2.5f;
|
_nextScanAt = now + 2.5f;
|
||||||
|
RebuildMcOrbitCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -564,6 +567,31 @@ public static class LifeSagaRoster
|
||||||
return McOrbitCache.Contains(unitId);
|
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>
|
/// <summary>
|
||||||
/// Roster MCs touched by the tip for rail involved chrome.
|
/// Roster MCs touched by the tip for rail involved chrome.
|
||||||
/// Bounded to subject/related/follow/pair/short-arc cast - not mass ParticipantIds.
|
/// Bounded to subject/related/follow/pair/short-arc cast - not mass ParticipantIds.
|
||||||
|
|
@ -652,6 +680,28 @@ public static class LifeSagaRoster
|
||||||
return false;
|
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);
|
Actor mc = EventFeedUtil.FindAliveById(mcId);
|
||||||
if (mc != null)
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1427,9 +1470,7 @@ public static class LifeSagaRoster
|
||||||
}
|
}
|
||||||
|
|
||||||
LifeSagaSlot newcomer = kept[worstNewcomer];
|
LifeSagaSlot newcomer = kept[worstNewcomer];
|
||||||
if (IsEstablishedMc(incumbent, now)
|
if (ShouldProtectEstablishedIncumbent(incumbent, newcomer, now))
|
||||||
&& IsOrdinaryChallenger(newcomer)
|
|
||||||
&& !HasMeaningfulNarrativeEvidence(newcomer))
|
|
||||||
{
|
{
|
||||||
kept[worstNewcomer] = incumbent;
|
kept[worstNewcomer] = incumbent;
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -1484,11 +1525,12 @@ public static class LifeSagaRoster
|
||||||
&& challenger.Chapters.Count == 0
|
&& challenger.Chapters.Count == 0
|
||||||
&& challenger.Heat < 4f
|
&& challenger.Heat < 4f
|
||||||
&& !challenger.IsRecurringAntagonist
|
&& !challenger.IsRecurringAntagonist
|
||||||
&& !challenger.IsSpecialCreature
|
|
||||||
&& !challenger.IsKing
|
&& !challenger.IsKing
|
||||||
&& !challenger.IsLeader
|
&& !challenger.IsLeader
|
||||||
&& !challenger.IsClanChief
|
&& !challenger.IsClanChief
|
||||||
&& !challenger.IsAlpha)
|
&& !challenger.IsAlpha
|
||||||
|
&& (IsOrdinaryChallenger(challenger)
|
||||||
|
|| IsIdentityOnlySpecialChallenger(challenger)))
|
||||||
{
|
{
|
||||||
margin += UndevelopedChallengerMarginBonus;
|
margin += UndevelopedChallengerMarginBonus;
|
||||||
}
|
}
|
||||||
|
|
@ -1519,6 +1561,38 @@ public static class LifeSagaRoster
|
||||||
&& !slot.IsRecurringAntagonist;
|
&& !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)
|
private static bool HasMeaningfulNarrativeEvidence(LifeSagaSlot slot)
|
||||||
{
|
{
|
||||||
if (slot == null)
|
if (slot == null)
|
||||||
|
|
@ -1553,11 +1627,100 @@ public static class LifeSagaRoster
|
||||||
bool developedAllowed = HasMeaningfulNarrativeEvidence(ordinary);
|
bool developedAllowed = HasMeaningfulNarrativeEvidence(ordinary);
|
||||||
ordinary.Heat = 0f;
|
ordinary.Heat = 0f;
|
||||||
ordinary.IsSpecialCreature = true;
|
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
|
detail = "ordinaryBlocked=" + ordinaryBlocked
|
||||||
+ " developedAllowed=" + developedAllowed
|
+ " developedAllowed=" + developedAllowed
|
||||||
+ " specialAllowed=" + specialAllowed;
|
+ " identityBlocked=" + identityBlocked
|
||||||
return ordinaryBlocked && developedAllowed && specialAllowed;
|
+ " 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)
|
private static int CompareSlots(LifeSagaSlot a, LifeSagaSlot b, float now, float dull)
|
||||||
|
|
@ -1870,32 +2033,46 @@ public static class LifeSagaRoster
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsActivePlotAuthor(actor)
|
if (IsActivePlotAuthor(actor)
|
||||||
|| IsFounder(actor)
|
|| IsFounder(actor))
|
||||||
|| (IsSpecialCreature(actor) && IsSpeciesSingleton(actor)))
|
|
||||||
{
|
{
|
||||||
p += 80f;
|
p += 80f;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsSpecialCreature(actor))
|
int rarePopulation = GetSpeciesPopulation(actor);
|
||||||
|
float rareAntagonistBonus = SpecialIdentityPriorityBonus(
|
||||||
|
IsSpecialCreature(actor),
|
||||||
|
rarePopulation);
|
||||||
|
if (TryGetRecurringAntagonist(actor, out _, out LifeSagaFact antagonistEvidence))
|
||||||
{
|
{
|
||||||
p += 45f;
|
rareAntagonistBonus +=
|
||||||
|
150f + Mathf.Min(50f, antagonistEvidence?.Strength ?? 0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
int rarePopulation = GetSpeciesPopulation(actor);
|
p += Mathf.Min(MaxRareAntagonistPriorityBonus, rareAntagonistBonus);
|
||||||
if (IsSpecialCreature(actor)
|
|
||||||
&& rarePopulation > 1
|
return p;
|
||||||
&& rarePopulation <= RareSpeciesMaxPopulation)
|
}
|
||||||
|
|
||||||
|
private static float SpecialIdentityPriorityBonus(bool special, int population)
|
||||||
{
|
{
|
||||||
p += 25f * (RareSpeciesMaxPopulation + 1 - rarePopulation)
|
if (!special)
|
||||||
|
{
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float bonus = 45f;
|
||||||
|
if (population == 1)
|
||||||
|
{
|
||||||
|
return bonus + 80f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (population > 1 && population <= RareSpeciesMaxPopulation)
|
||||||
|
{
|
||||||
|
bonus += 25f * (RareSpeciesMaxPopulation + 1 - population)
|
||||||
/ RareSpeciesMaxPopulation;
|
/ RareSpeciesMaxPopulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TryGetRecurringAntagonist(actor, out _, out LifeSagaFact antagonistEvidence))
|
return bonus;
|
||||||
{
|
|
||||||
p += 150f + Mathf.Min(50f, antagonistEvidence?.Strength ?? 0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
return p;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryGetRecurringAntagonist(
|
public static bool TryGetRecurringAntagonist(
|
||||||
|
|
@ -2559,28 +2736,11 @@ public static class LifeSagaRoster
|
||||||
score += 7f;
|
score += 7f;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s.IsSingleton && s.IsSpecialCreature)
|
if (s.IsSingleton && !s.IsSpecialCreature)
|
||||||
{
|
|
||||||
score += 6f;
|
|
||||||
}
|
|
||||||
else if (s.IsSingleton)
|
|
||||||
{
|
{
|
||||||
score += 1f;
|
score += 1f;
|
||||||
}
|
}
|
||||||
else if (s.IsRareCreature)
|
score += RareAntagonistStandingBonus(s);
|
||||||
{
|
|
||||||
score += RareCreatureStandingBonus;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s.IsSpecialCreature)
|
|
||||||
{
|
|
||||||
score += SpecialCreatureStandingBonus;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s.IsRecurringAntagonist)
|
|
||||||
{
|
|
||||||
score += RecurringAntagonistStandingBonus;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s.IsFounder)
|
if (s.IsFounder)
|
||||||
{
|
{
|
||||||
|
|
@ -2598,6 +2758,46 @@ public static class LifeSagaRoster
|
||||||
return score;
|
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(
|
private static void NoteRisingRoles(
|
||||||
Actor actor,
|
Actor actor,
|
||||||
bool wasKing,
|
bool wasKing,
|
||||||
|
|
@ -2724,10 +2924,10 @@ public static class LifeSagaRoster
|
||||||
if (s.IsArmyCaptain) return LifeSagaAdmissionReason.ArmyCaptain;
|
if (s.IsArmyCaptain) return LifeSagaAdmissionReason.ArmyCaptain;
|
||||||
if (s.IsFounder) return LifeSagaAdmissionReason.Founder;
|
if (s.IsFounder) return LifeSagaAdmissionReason.Founder;
|
||||||
if (s.IsPlotAuthor) return LifeSagaAdmissionReason.PlotAuthor;
|
if (s.IsPlotAuthor) return LifeSagaAdmissionReason.PlotAuthor;
|
||||||
if (s.IsSingleton && s.IsSpecialCreature) return LifeSagaAdmissionReason.Singleton;
|
|
||||||
if (s.IsRecurringAntagonist) return LifeSagaAdmissionReason.RecurringAntagonist;
|
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.IsRareCreature) return LifeSagaAdmissionReason.RareCreature;
|
||||||
|
if (s.IsSpecialCreature) return LifeSagaAdmissionReason.SpecialCreature;
|
||||||
if (s.Kills > 0) return LifeSagaAdmissionReason.NotableKills;
|
if (s.Kills > 0) return LifeSagaAdmissionReason.NotableKills;
|
||||||
if (s.Renown > 0f) return LifeSagaAdmissionReason.NotableRenown;
|
if (s.Renown > 0f) return LifeSagaAdmissionReason.NotableRenown;
|
||||||
return LifeSagaAdmissionReason.NotableLife;
|
return LifeSagaAdmissionReason.NotableLife;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Rare Creatures, Antagonists, and Camera Cleanup
|
# Rare Creatures, Antagonists, and Camera Cleanup
|
||||||
|
|
||||||
Status: **Implemented; targeted and performance gates pass. Full regression is blocked by the
|
Status: **Complete. Implementation, deterministic regression, scheduler health, and two organic
|
||||||
legacy combat-presentability fixture's nondeterministic pair-to-mass reframe assertions.**
|
soak windows pass.**
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
|
|
@ -83,20 +83,20 @@ Exit: quiet life remains visible without crowding out story development.
|
||||||
## Phase 7 — Deterministic coverage
|
## Phase 7 — Deterministic coverage
|
||||||
|
|
||||||
- [x] Singleton dragon enters through the real admission path.
|
- [x] Singleton dragon enters through the real admission path.
|
||||||
- [ ] A larger dragon population receives a smaller bonus.
|
- [x] A larger dragon population receives a smaller bonus.
|
||||||
- [ ] Rare identity alone does not evict a developed MC.
|
- [x] Rare identity alone does not evict a developed MC.
|
||||||
- [x] Repeated conflict creates an antagonist challenger.
|
- [x] Repeated conflict creates an antagonist challenger.
|
||||||
- [x] One kill/fight cycle does not create an antagonist.
|
- [x] One kill/fight cycle does not create an antagonist.
|
||||||
- [ ] Harm to MC cast creates stronger evidence.
|
- [x] Harm to MC cast creates stronger evidence.
|
||||||
- [ ] Combined rarity/antagonist bonuses respect their cap.
|
- [x] Combined rarity/antagonist bonuses respect their cap.
|
||||||
- [x] Routine unrelated hatch is rejected; an explicit singleton-special exception is accepted.
|
- [x] Routine unrelated hatch is rejected; an explicit singleton-special exception is accepted.
|
||||||
- [ ] Friendly/hostile dossier labels name the correct anchor.
|
- [x] Friendly/hostile dossier labels name the correct anchor.
|
||||||
- [ ] Broad or stale faction correlation fails thread attribution.
|
- [x] Broad or stale faction correlation fails thread attribution.
|
||||||
- [ ] The five-character roster stays stable across repeated scans.
|
- [x] The five-character roster stays stable across repeated scans.
|
||||||
|
|
||||||
## Phase 8 — Performance and organic soak gate
|
## 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] Run scheduler health with `hitch_probe` and an active organic soak audit.
|
||||||
- [x] Compare maximum frame, director, discovery, caption, and scheduler slices
|
- [x] Compare maximum frame, director, discovery, caption, and scheduler slices
|
||||||
with the pre-change baseline.
|
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.
|
standing; require a chapter, sufficient story heat, or meaningful scheduler potential.
|
||||||
- [x] Stop a fifth consecutive ordinary combat cut while allowing kills, resolutions,
|
- [x] Stop a fifth consecutive ordinary combat cut while allowing kills, resolutions,
|
||||||
consequences, and world-critical combat through.
|
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.
|
run of four in the next long organic soak.
|
||||||
|
|
||||||
Evidence prompting this phase: the first post-feature long soak successfully promoted evil mage
|
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
|
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
|
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.
|
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
|
world_log
|
||||||
camera_presentability
|
camera_presentability
|
||||||
narrative_presentation_coverage
|
narrative_presentation_coverage
|
||||||
|
saga_rare_antagonist_acceptance
|
||||||
event_suite
|
event_suite
|
||||||
chronicle_smoke
|
chronicle_smoke
|
||||||
activity_idle_smoke
|
activity_idle_smoke
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue